From 1c49e29030ec28853df037a567f928ba85b08fe0 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Wed, 5 Oct 2022 19:51:00 -0700 Subject: [PATCH] Index the number of transparent pixels --- packages/skin-database/data/SkinModel.ts | 17 ++-- packages/skin-database/data/skins.ts | 15 +++- packages/skin-database/package.json | 1 + packages/skin-database/transparency.ts | 98 ++++++++++++++++++++++++ yarn.lock | 12 +++ 5 files changed, 130 insertions(+), 13 deletions(-) create mode 100644 packages/skin-database/transparency.ts diff --git a/packages/skin-database/data/SkinModel.ts b/packages/skin-database/data/SkinModel.ts index c1d69951..9ed5b310 100644 --- a/packages/skin-database/data/SkinModel.ts +++ b/packages/skin-database/data/SkinModel.ts @@ -19,7 +19,7 @@ import fetch from "node-fetch"; import JSZip from "jszip"; import fs from "fs/promises"; import path from "path"; -import regionParser from "../regionParser"; +import { getTransparentAreaSize } from "../transparency"; export const IS_README = /(file_id\.diz)|(\.txt)$/i; // Skinning Updates.txt ? @@ -340,18 +340,17 @@ export default class SkinModel { // Has transparency async hasTransparency(): Promise { + const size = await this.transparentAreaSize(); + return size > 0; + } + + async transparentPixels(): Promise { const region = await this._getFile("region", "txt"); if (region == null) { - return false; + return 0; } const text = await region.getTextContent(); - if (text == null) { - return false; - } - - const regions = regionParser(text); - - return Object.values(regions).some((region) => region.length > 0); + return getTransparentAreaSize(text); } async hasAni(): Promise { diff --git a/packages/skin-database/data/skins.ts b/packages/skin-database/data/skins.ts index c976aec3..7393124f 100644 --- a/packages/skin-database/data/skins.ts +++ b/packages/skin-database/data/skins.ts @@ -9,6 +9,7 @@ import * as CloudFlare from "../CloudFlare"; import SkinModel from "./SkinModel"; import UserContext from "./UserContext"; import TweetModel from "./TweetModel"; +import { TweetStatus } from "../types"; export const SKIN_TYPE = { CLASSIC: 1, @@ -158,17 +159,20 @@ export async function getUploadStatuses( type SearchIndex = { objectID: string; md5: string; + // TODO: Can be deprecated in favor of tweetStatus nsfw: boolean; + tweetStatus: TweetStatus; fileName: string; twitterLikes: number; equalizer: boolean; playlist: boolean; browser: boolean; + media: boolean; general: boolean; video: boolean; cur: boolean; ani: boolean; - transparency: boolean; + transparentPixels: number; mikro: boolean; vidamp: boolean; avs: boolean; @@ -186,6 +190,9 @@ async function getSearchIndexes( return Promise.all( skins.map(async (skin) => { + if (skin.getSkinType() !== "CLASSIC") { + throw new Error("Only classic skins are supported"); + } const readmeText = await skin.getReadme(); const tweets = await skin.getTweets(); const likes = tweets.reduce((acc: number, tweet: TweetModel) => { @@ -195,17 +202,19 @@ async function getSearchIndexes( objectID: skin.getMd5(), md5: skin.getMd5(), nsfw: await skin.getIsNsfw(), + tweetStatus: await skin.getTweetStatus(), readmeText: readmeText ? truncate(readmeText, 4800) : null, fileName: await skin.getFileName(), twitterLikes: likes, equalizer: await skin.hasEqualizer(), + media: await skin.hasMediaLibrary(), playlist: await skin.hasPlaylist(), browser: await skin.hasBrowser(), general: await skin.hasGeneral(), video: await skin.hasVideo(), cur: await skin.hasCur(), ani: await skin.hasAni(), - transparency: await skin.hasTransparency(), + transparentPixels: await skin.transparentPixels(), mikro: await skin.hasMikro(), vidamp: await skin.hasVidamp(), avs: await skin.hasAVS(), @@ -219,8 +228,6 @@ export async function updateSearchIndexs( md5s: string[] ): Promise { const skinIndexes = await getSearchIndexes(ctx, md5s); - console.log(skinIndexes); - return; const results = await searchIndex.partialUpdateObjects(skinIndexes, { createIfNotExists: true, diff --git a/packages/skin-database/package.json b/packages/skin-database/package.json index 0a634cd5..102ad21d 100644 --- a/packages/skin-database/package.json +++ b/packages/skin-database/package.json @@ -34,6 +34,7 @@ "lru-cache": "^6.0.0", "md5": "^2.2.1", "node-fetch": "^2.6.7", + "polygon-clipping": "^0.15.3", "puppeteer": "^13.3.2", "sharp": "^0.28.3", "sqlite3": "^5.0.2", diff --git a/packages/skin-database/transparency.ts b/packages/skin-database/transparency.ts new file mode 100644 index 00000000..42bde5af --- /dev/null +++ b/packages/skin-database/transparency.ts @@ -0,0 +1,98 @@ +import polygonClipping, { + Polygon, + MultiPolygon, + Pair, + Ring, +} from "polygon-clipping"; +import regionParser from "./regionParser"; + +const HEIGHT = 116; +const SHADE_HEIGHT = 14; +const WIDTH = 275; +const largeWindowBounds = [[makeBox(WIDTH, HEIGHT)]] as MultiPolygon; +const shadeWindowBounds = [[makeBox(WIDTH, SHADE_HEIGHT)]] as MultiPolygon; + +const maxBounds: { [name: string]: MultiPolygon } = { + normal: largeWindowBounds, + equalizer: largeWindowBounds, + windowshade: shadeWindowBounds, + equalizerws: shadeWindowBounds, +}; + +export function getTransparentAreaSize(regionTxt: string): number { + const regions = regionParser(regionTxt); + + let totalTransparentArea = 0; + for (const _name of Object.keys(regions)) { + const name = _name.toLocaleLowerCase(); + const region = regions[name]; + if (region == null || region.length === 0) { + continue; + } + const max = maxBounds[name]; + if (max == null) { + // This is an invalid region. + continue; + } + + const opaque = regionStringsToPolygons(region); + const transparent = polygonClipping.difference(max, opaque); + const transparentArea = getMultipolygonArea(transparent); + totalTransparentArea += transparentArea; + } + return totalTransparentArea; +} + +// Compute the area of a multi-polygon. +// Note: If there are overlaps they might be computed multiple times. +function getMultipolygonArea(multipolygon: MultiPolygon) { + let area = 0; + for (const polygon of multipolygon) { + for (const ring of polygon) { + area += getArea(ring); + } + } + return area; +} + +// Make a box polygon with the given width and height. +function makeBox(width: number, height: number): Ring { + return [ + [0, 0], + [width, 0], + [width, height], + [0, height], + [0, 0], + ]; +} + +// Given a parsed list of region.txt polygon strings, return a list of polygons +// which are closed (end where they begin). +function regionStringsToPolygons(region: string[]): Polygon[] { + return region.map((p) => { + const points: Ring = p.split(" ").map((p) => { + return p.split(",").map((n) => parseInt(n, 10)) as Pair; + }); + // Ensure these close properly. + points.push(points[0]); + return [points]; + }); +} + +// Get the area of a single Ring (simple polygon). +// Assumes the polygon is closed (ends where it begins). +function getArea(points: Ring) { + let det = 0; + + const l = points.length; + if (l < 3) { + return 0; + } + + for (let i = 1; i < l; i++) { + const p1 = points[i - 1]; + const p2 = points[i]; + det += p1[0] * p2[1] - p1[1] * p2[0]; + } + return Math.abs(det) / 2; +} diff --git a/yarn.lock b/yarn.lock index a5deabba..80b8ee9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20023,6 +20023,13 @@ pngparse@^2.0.1: resolved "https://registry.yarnpkg.com/pngparse/-/pngparse-2.0.1.tgz#86852de4de349f4efb1e852e7525655e5ac5dfb8" integrity sha512-RyB1P0BBwt3CNIZ5wT53lR1dT3CUtopnMOuP8xZdHjPhI/uXNNRnkx1yQb/3MMMyyMeo6p19fiIRHcLopWIkxA== +polygon-clipping@^0.15.3: + version "0.15.3" + resolved "https://registry.yarnpkg.com/polygon-clipping/-/polygon-clipping-0.15.3.tgz#0215840438470ba2e9e6593625e4ea5c1087b4b7" + integrity sha512-ho0Xx5DLkgxRx/+n4O74XyJ67DcyN3Tu9bGYKsnTukGAW6ssnuak6Mwcyb1wHy9MZc9xsUWqIoiazkZB5weECg== + dependencies: + splaytree "^3.1.0" + portfinder@^1.0.13, portfinder@^1.0.26: version "1.0.26" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" @@ -23631,6 +23638,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +splaytree@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/splaytree/-/splaytree-3.1.1.tgz#e1bc8e68e64ef5a9d5f09d36e6d9f3621795a438" + integrity sha512-9FaQ18FF0+sZc/ieEeXHt+Jw2eSpUgUtTLDYB/HXKWvhYVyOc7h1hzkn5MMO3GPib9MmXG1go8+OsBBzs/NMww== + split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"