From 81bc7e75902728578f6651a1f9bbcbeb318591a6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 3 May 2024 10:24:02 -0500 Subject: [PATCH 01/19] Reorder README --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1d0048eb..16692993 100644 --- a/README.md +++ b/README.md @@ -104,14 +104,6 @@ Application behavior can be changed by configuring the following environment var - `NEXT_PUBLIC_GRID_ASPECT_RATIO = 1.5` sets aspect ratio for grid tiles (defaults to `1`—setting to `0` removes the constraint) - `NEXT_PUBLIC_OG_TEXT_ALIGNMENT = BOTTOM` keeps OG image text bottom aligned (default is top) -## Alternate database providers (experimental) - -Vercel Postgres can be switched to another Postgres-compatible, pooling provider by updating `POSTGRES_URL`. Some providers only work when SSL is disabled, which can configured by setting `DISABLE_POSTGRES_SSL = 1`. - -### Supabase -1. Ensure connection string is set to "Transaction Mode" via port `6543` -2. Disable SSL by setting `DISABLE_POSTGRES_SSL = 1` - ## Alternate storage providers Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used at a time. Ideally, this is configured before photos are uploaded (see [Issue #34](https://github.com/sambecker/exif-photo-blog/issues/34) for migration considerations). If you have multiple adapters, you can set one as preferred by storing "aws-s3," "cloudflare-r2," or "vercel-blob" in `NEXT_PUBLIC_STORAGE_PREFERENCE`. @@ -200,6 +192,14 @@ Only one storage adapter—Vercel Blob, Cloudflare R2, or AWS S3—can be used a - `AWS_S3_ACCESS_KEY` - `AWS_S3_SECRET_ACCESS_KEY` +## Alternate database providers (experimental) + +Vercel Postgres can be switched to another Postgres-compatible, pooling provider by updating `POSTGRES_URL`. Some providers only work when SSL is disabled, which can configured by setting `DISABLE_POSTGRES_SSL = 1`. + +### Supabase +1. Ensure connection string is set to "Transaction Mode" via port `6543` +2. Disable SSL by setting `DISABLE_POSTGRES_SSL = 1` + FAQ - #### Why are my thumbnails square? From 0a201f0dee3acee2ac5e4e997fd5af136b04ec1f Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 3 May 2024 10:53:22 -0500 Subject: [PATCH 02/19] Improve Vercel Postgres admin config check --- .vscode/settings.json | 1 + src/site/config.ts | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e710f6c..8b5d2aae 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -46,6 +46,7 @@ "upstash", "UsKSGcbt", "Velvia", + "verceldb", "WRHGZC", "wxyz", "zadd", diff --git a/src/site/config.ts b/src/site/config.ts index c7c21026..e5700b66 100644 --- a/src/site/config.ts +++ b/src/site/config.ts @@ -135,8 +135,10 @@ export const HIGH_DENSITY_GRID = GRID_ASPECT_RATIO <= 1; export const CONFIG_CHECKLIST_STATUS = { hasDatabase: HAS_DATABASE, isPostgresSSLEnabled: POSTGRES_SSL_ENABLED, - hasVercelPostgres: /\.vercel-storage\.com\// - .test(process.env.POSTGRES_URL ?? ''), + hasVercelPostgres: ( + /\/verceldb\?/.test(process.env.POSTGRES_URL ?? '') || + /\.vercel-storage\.com\//.test(process.env.POSTGRES_URL ?? '') + ), hasVercelKV: HAS_VERCEL_KV, hasVercelBlobStorage: HAS_VERCEL_BLOB_STORAGE, hasCloudflareR2Storage: HAS_CLOUDFLARE_R2_STORAGE, From 0f7299d891ea351e71637faae56aab23c1565044 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 3 May 2024 18:04:59 -0500 Subject: [PATCH 03/19] Add Jimp-based blur proof-of-concept --- package.json | 1 + pnpm-lock.yaml | 703 ++++++++++++++++++++ src/app/admin/uploads/[uploadPath]/page.tsx | 24 +- src/photo/server.ts | 9 + 4 files changed, 729 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 862423db..5abae3ee 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "framer-motion": "^11.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", + "jimp": "^0.22.12", "nanoid": "^5.0.7", "next": "^14.2.3", "next-auth": "5.0.0-beta.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 322f3c15..de848b78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) + jimp: + specifier: ^0.22.12 + version: 0.22.12 nanoid: specifier: ^5.0.7 version: 5.0.7 @@ -669,6 +672,171 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jimp/bmp@0.22.12': + resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/core@0.22.12': + resolution: {integrity: sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==} + + '@jimp/custom@0.22.12': + resolution: {integrity: sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==} + + '@jimp/gif@0.22.12': + resolution: {integrity: sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/jpeg@0.22.12': + resolution: {integrity: sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-blit@0.22.12': + resolution: {integrity: sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-blur@0.22.12': + resolution: {integrity: sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-circle@0.22.12': + resolution: {integrity: sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-color@0.22.12': + resolution: {integrity: sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-contain@0.22.12': + resolution: {integrity: sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + + '@jimp/plugin-cover@0.22.12': + resolution: {integrity: sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + + '@jimp/plugin-crop@0.22.12': + resolution: {integrity: sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-displace@0.22.12': + resolution: {integrity: sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-dither@0.22.12': + resolution: {integrity: sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-fisheye@0.22.12': + resolution: {integrity: sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-flip@0.22.12': + resolution: {integrity: sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-rotate': '>=0.3.5' + + '@jimp/plugin-gaussian@0.22.12': + resolution: {integrity: sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-invert@0.22.12': + resolution: {integrity: sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-mask@0.22.12': + resolution: {integrity: sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-normalize@0.22.12': + resolution: {integrity: sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-print@0.22.12': + resolution: {integrity: sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + + '@jimp/plugin-resize@0.22.12': + resolution: {integrity: sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-rotate@0.22.12': + resolution: {integrity: sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-scale@0.22.12': + resolution: {integrity: sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-shadow@0.22.12': + resolution: {integrity: sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blur': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-threshold@0.22.12': + resolution: {integrity: sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-color': '>=0.8.0' + '@jimp/plugin-resize': '>=0.8.0' + + '@jimp/plugins@0.22.12': + resolution: {integrity: sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/png@0.22.12': + resolution: {integrity: sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/tiff@0.22.12': + resolution: {integrity: sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/types@0.22.12': + resolution: {integrity: sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/utils@0.22.12': + resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1293,6 +1461,9 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -1348,6 +1519,9 @@ packages: '@types/node-fetch@2.6.11': resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + '@types/node@18.19.31': resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} @@ -1642,6 +1816,9 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1761,10 +1938,16 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -1786,9 +1969,16 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bufferutil@4.0.8: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} @@ -2076,6 +2266,9 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -2287,6 +2480,9 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} @@ -2325,6 +2521,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2428,6 +2628,9 @@ packages: get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} + gifwrap@0.10.1: + resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2449,6 +2652,9 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2534,10 +2740,16 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2620,6 +2832,9 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -2707,6 +2922,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isomorphic-fetch@3.0.0: + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -2876,6 +3094,9 @@ packages: node-notifier: optional: true + jimp@0.22.12: + resolution: {integrity: sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg==} + jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true @@ -2883,6 +3104,9 @@ packages: jose@5.2.4: resolution: {integrity: sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==} + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2974,6 +3198,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + load-bmfont@1.4.1: + resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -3046,10 +3273,18 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3230,6 +3465,9 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3269,10 +3507,25 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + + parse-headers@2.0.5: + resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -3303,6 +3556,10 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -3348,6 +3605,10 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + phin@2.9.3: + resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -3363,10 +3624,22 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + pixelmatch@4.0.2: + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} + hasBin: true + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + + pngjs@6.0.0: + resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} + engines: {node: '>=12.13.0'} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -3474,6 +3747,10 @@ packages: pretty-format@3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3557,6 +3834,14 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3569,6 +3854,9 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3629,6 +3917,9 @@ packages: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} @@ -3783,6 +4074,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -3814,6 +4108,10 @@ packages: strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -3900,6 +4198,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + timm@1.7.1: + resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -3911,6 +4215,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4049,6 +4357,9 @@ packages: resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} engines: {node: '>=6.14.2'} + utif2@4.1.0: + resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4099,6 +4410,9 @@ packages: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + whatwg-fetch@3.6.20: + resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -4173,10 +4487,24 @@ packages: utf-8-validate: optional: true + xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -5215,6 +5543,218 @@ snapshots: '@types/yargs': 17.0.32 chalk: 4.1.2 + '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + bmp-js: 0.1.0 + + '@jimp/core@0.22.12': + dependencies: + '@jimp/utils': 0.22.12 + any-base: 1.1.0 + buffer: 5.7.1 + exif-parser: 0.1.12 + file-type: 16.5.4 + isomorphic-fetch: 3.0.0 + pixelmatch: 4.0.2 + tinycolor2: 1.6.0 + transitivePeerDependencies: + - encoding + + '@jimp/custom@0.22.12': + dependencies: + '@jimp/core': 0.22.12 + transitivePeerDependencies: + - encoding + + '@jimp/gif@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + gifwrap: 0.10.1 + omggif: 1.0.10 + + '@jimp/jpeg@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + jpeg-js: 0.4.4 + + '@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-circle@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + tinycolor2: 1.6.0 + + '@jimp/plugin-contain@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-cover@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-displace@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-dither@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-fisheye@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-flip@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-rotate': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-gaussian@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-invert@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-mask@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-normalize@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-print@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) + '@jimp/utils': 0.22.12 + load-bmfont: 1.4.1 + + '@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + + '@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-shadow@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-blur': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/utils': 0.22.12 + + '@jimp/plugin-threshold@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-color': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/utils': 0.22.12 + + '@jimp/plugins@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-blur': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-circle': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-color': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-contain': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) + '@jimp/plugin-cover': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) + '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-displace': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-dither': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-fisheye': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-flip': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) + '@jimp/plugin-gaussian': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-invert': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-mask': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-normalize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-print': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12)) + '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) + '@jimp/plugin-rotate': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/plugin-shadow': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + '@jimp/plugin-threshold': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) + timm: 1.7.1 + + '@jimp/png@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/utils': 0.22.12 + pngjs: 6.0.0 + + '@jimp/tiff@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/custom': 0.22.12 + utif2: 4.1.0 + + '@jimp/types@0.22.12(@jimp/custom@0.22.12)': + dependencies: + '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12) + '@jimp/custom': 0.22.12 + '@jimp/gif': 0.22.12(@jimp/custom@0.22.12) + '@jimp/jpeg': 0.22.12(@jimp/custom@0.22.12) + '@jimp/png': 0.22.12(@jimp/custom@0.22.12) + '@jimp/tiff': 0.22.12(@jimp/custom@0.22.12) + timm: 1.7.1 + + '@jimp/utils@0.22.12': + dependencies: + regenerator-runtime: 0.13.11 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -5948,6 +6488,8 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@tokenizer/token@0.3.0': {} + '@tootallnate/once@2.0.0': {} '@types/aria-query@5.0.4': {} @@ -6013,6 +6555,8 @@ snapshots: '@types/node': 20.12.7 form-data: 4.0.0 + '@types/node@16.9.1': {} + '@types/node@18.19.31': dependencies: undici-types: 5.26.5 @@ -6350,6 +6894,8 @@ snapshots: ansi-styles@6.2.1: {} + any-base@1.1.0: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -6533,8 +7079,12 @@ snapshots: balanced-match@1.0.2: {} + base64-js@1.5.1: {} + binary-extensions@2.3.0: {} + bmp-js@0.1.0: {} + bowser@2.11.0: {} brace-expansion@1.1.11: @@ -6561,8 +7111,15 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal@0.0.1: {} + buffer-from@1.1.2: {} + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.0 @@ -6827,6 +7384,8 @@ snapshots: dom-accessibility-api@0.6.3: {} + dom-walk@0.1.2: {} + domexception@4.0.0: dependencies: webidl-conversions: 7.0.0 @@ -7191,6 +7750,8 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 + exif-parser@0.1.12: {} + exifr@7.1.3: {} exit@0.1.2: {} @@ -7233,6 +7794,12 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.2 + strtok3: 6.3.0 + token-types: 4.2.1 + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -7330,6 +7897,11 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + gifwrap@0.10.1: + dependencies: + image-q: 4.0.0 + omggif: 1.0.10 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7363,6 +7935,11 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global@4.4.0: + dependencies: + min-document: 2.19.0 + process: 0.11.10 + globals@11.12.0: {} globals@13.24.0: @@ -7447,8 +8024,14 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.1: {} + image-q@4.0.0: + dependencies: + '@types/node': 16.9.1 + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -7528,6 +8111,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-function@1.0.2: {} + is-generator-fn@2.1.0: {} is-generator-function@1.0.10: @@ -7598,6 +8183,13 @@ snapshots: isexe@2.0.0: {} + isomorphic-fetch@3.0.0: + dependencies: + node-fetch: 2.7.0 + whatwg-fetch: 3.6.20 + transitivePeerDependencies: + - encoding + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -7976,10 +8568,21 @@ snapshots: - supports-color - ts-node + jimp@0.22.12: + dependencies: + '@jimp/custom': 0.22.12 + '@jimp/plugins': 0.22.12(@jimp/custom@0.22.12) + '@jimp/types': 0.22.12(@jimp/custom@0.22.12) + regenerator-runtime: 0.13.11 + transitivePeerDependencies: + - encoding + jiti@1.21.0: {} jose@5.2.4: {} + jpeg-js@0.4.4: {} + js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -8080,6 +8683,17 @@ snapshots: lines-and-columns@1.2.4: {} + load-bmfont@1.4.1: + dependencies: + buffer-equal: 0.0.1 + mime: 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + phin: 2.9.3 + xhr: 2.6.0 + xtend: 4.0.2 + locate-character@3.0.0: {} locate-path@5.0.0: @@ -8141,8 +8755,14 @@ snapshots: dependencies: mime-db: 1.52.0 + mime@1.6.0: {} + mimic-fn@2.1.0: {} + min-document@2.19.0: + dependencies: + dom-walk: 0.1.2 + min-indent@1.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -8292,6 +8912,8 @@ snapshots: obuf@1.1.2: {} + omggif@1.0.10: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -8342,10 +8964,23 @@ snapshots: p-try@2.2.0: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-bmfont-ascii@1.0.6: {} + + parse-bmfont-binary@1.0.6: {} + + parse-bmfont-xml@1.1.6: + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + + parse-headers@2.0.5: {} + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.2 @@ -8372,6 +9007,8 @@ snapshots: path-type@4.0.0: {} + peek-readable@4.1.0: {} + periscopic@3.1.0: dependencies: '@types/estree': 1.0.5 @@ -8425,6 +9062,8 @@ snapshots: dependencies: split2: 4.2.0 + phin@2.9.3: {} + picocolors@1.0.0: {} picomatch@2.3.1: {} @@ -8433,10 +9072,18 @@ snapshots: pirates@4.0.6: {} + pixelmatch@4.0.2: + dependencies: + pngjs: 3.4.0 + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 + pngjs@3.4.0: {} + + pngjs@6.0.0: {} + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.38): @@ -8527,6 +9174,8 @@ snapshots: pretty-format@3.8.0: {} + process@0.11.10: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -8602,6 +9251,16 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-web-to-node-stream@3.0.2: + dependencies: + readable-stream: 3.6.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -8621,6 +9280,8 @@ snapshots: globalthis: 1.0.3 which-builtin-type: 1.1.3 + regenerator-runtime@0.13.11: {} + regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.2: @@ -8677,6 +9338,8 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-regex-test@1.0.3: dependencies: call-bind: 1.0.7 @@ -8845,6 +9508,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -8867,6 +9534,11 @@ snapshots: strnum@1.0.5: {} + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + styled-jsx@5.1.1(@babel/core@7.24.4)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -8983,6 +9655,10 @@ snapshots: dependencies: any-promise: 1.3.0 + timm@1.7.1: {} + + tinycolor2@1.6.0: {} + tmpl@1.0.5: {} to-fast-properties@2.0.0: {} @@ -8991,6 +9667,11 @@ snapshots: dependencies: is-number: 7.0.0 + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + totalist@3.0.1: {} tough-cookie@4.1.3: @@ -9131,6 +9812,10 @@ snapshots: node-gyp-build: 4.8.0 optional: true + utif2@4.1.0: + dependencies: + pako: 1.0.11 + util-deprecate@1.0.2: {} uuid@9.0.1: {} @@ -9190,6 +9875,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@3.0.0: {} whatwg-url@11.0.0: @@ -9275,8 +9962,24 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.3 + xhr@2.6.0: + dependencies: + global: 4.4.0 + is-function: 1.0.2 + parse-headers: 2.0.5 + xtend: 4.0.2 + xml-name-validator@4.0.0: {} + xml-parse-from-string@1.0.1: {} + + xml2js@0.5.0: + dependencies: + sax: 1.2.4 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} xtend@4.0.2: {} diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index 1092ed7d..ec9c2f16 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -1,5 +1,5 @@ import { PATH_ADMIN } from '@/site/paths'; -import { extractExifDataFromBlobPath } from '@/photo/server'; +import { blurImage, extractExifDataFromBlobPath } from '@/photo/server'; import { redirect } from 'next/navigation'; import { getUniqueTagsCached } from '@/photo/cache'; import UploadPageClient from '@/photo/UploadPageClient'; @@ -18,6 +18,8 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { photoFormExif, } = await extractExifDataFromBlobPath(uploadPath, true); + const blurBase64 = await blurImage(uploadPath); + if (!photoFormExif) { redirect(PATH_ADMIN); } const uniqueTags = await getUniqueTagsCached(); @@ -27,12 +29,18 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; return ( - + <> + Blur Debug + + ); }; diff --git a/src/photo/server.ts b/src/photo/server.ts index 96fae675..92fa99fb 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -10,6 +10,7 @@ import { import { ExifData, ExifParserFactory } from 'ts-exif-parser'; import { PhotoFormData } from './form'; import { FilmSimulation } from '@/simulation'; +import Jimp from 'jimp'; export const extractExifDataFromBlobPath = async ( blobPath: string, @@ -67,3 +68,11 @@ export const extractExifDataFromBlobPath = async ( }, }; }; + +export const blurImage = async (url: string) => + Jimp.read(decodeURIComponent(url)) + .then(image => { + image.resize(300, Jimp.AUTO); + image.blur(30); + return image.getBase64Async(Jimp.MIME_JPEG); + }); From 0666e5311c4b107567391b561ca0df6ed74eaa13 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 3 May 2024 18:19:43 -0500 Subject: [PATCH 04/19] Declare jimp as external server package --- next.config.js | 3 +++ src/photo/form/PhotoForm.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/next.config.js b/next.config.js index f0ff1b30..405f4137 100644 --- a/next.config.js +++ b/next.config.js @@ -35,6 +35,9 @@ const nextConfig = { .concat(createRemotePattern(HOSTNAME_AWS_S3)), minimumCacheTTL: 31536000, }, + experimental: { + serverComponentsExternalPackages: ['jimp'], + }, }; const withBundleAnalyzer = require('@next/bundle-analyzer')({ diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 7e0e7f26..a354e49a 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -264,7 +264,7 @@ export default function PhotoForm({ url, 640, undefined, - window.location.origin, + typeof window !== 'undefined' ? window.location.origin : undefined, )} width={width} height={height} From df5afa4072dbca351030551db7013c14b7823565 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Fri, 3 May 2024 18:31:28 -0500 Subject: [PATCH 05/19] Shrink image blur size --- src/photo/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/photo/server.ts b/src/photo/server.ts index 92fa99fb..8313b72e 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -72,7 +72,7 @@ export const extractExifDataFromBlobPath = async ( export const blurImage = async (url: string) => Jimp.read(decodeURIComponent(url)) .then(image => { - image.resize(300, Jimp.AUTO); - image.blur(30); + image.resize(100, Jimp.AUTO); + image.blur(20); return image.getBase64Async(Jimp.MIME_JPEG); }); From 1114bec46277d8c3417068a626ce8a4afebe0b12 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Sat, 4 May 2024 11:01:35 -0500 Subject: [PATCH 06/19] Switch jimp to sharp --- next.config.js | 6 +- package.json | 2 +- pnpm-lock.yaml | 967 ++++++++++++-------------------------------- src/photo/server.ts | 18 +- 4 files changed, 278 insertions(+), 715 deletions(-) diff --git a/next.config.js b/next.config.js index 405f4137..51aab061 100644 --- a/next.config.js +++ b/next.config.js @@ -35,9 +35,9 @@ const nextConfig = { .concat(createRemotePattern(HOSTNAME_AWS_S3)), minimumCacheTTL: 31536000, }, - experimental: { - serverComponentsExternalPackages: ['jimp'], - }, + // experimental: { + // serverComponentsExternalPackages: ['jimp'], + // }, }; const withBundleAnalyzer = require('@next/bundle-analyzer')({ diff --git a/package.json b/package.json index 5abae3ee..36948280 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "framer-motion": "^11.1.7", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "jimp": "^0.22.12", "nanoid": "^5.0.7", "next": "^14.2.3", "next-auth": "5.0.0-beta.15", @@ -52,6 +51,7 @@ "react": "18.3.1", "react-dom": "18.3.1", "react-icons": "^5.1.0", + "sharp": "^0.33.3", "sonner": "^1.4.41", "swr": "^2.2.5", "tailwindcss": "3.4.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de848b78..e555d6d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,9 +104,6 @@ importers: jest-environment-jsdom: specifier: ^29.7.0 version: 29.7.0(bufferutil@4.0.8)(utf-8-validate@6.0.3) - jimp: - specifier: ^0.22.12 - version: 0.22.12 nanoid: specifier: ^5.0.7 version: 5.0.7 @@ -137,6 +134,9 @@ importers: react-icons: specifier: ^5.1.0 version: 5.1.0(react@18.3.1) + sharp: + specifier: ^0.33.3 + version: 0.33.3 sonner: specifier: ^1.4.41 version: 1.4.41(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -546,6 +546,9 @@ packages: resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} + '@emnapi/runtime@1.1.1': + resolution: {integrity: sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -594,6 +597,119 @@ packages: '@humanwhocodes/object-schema@2.0.3': resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + '@img/sharp-darwin-arm64@0.33.3': + resolution: {integrity: sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.3': + resolution: {integrity: sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.2': + resolution: {integrity: sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==} + engines: {macos: '>=11', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.2': + resolution: {integrity: sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==} + engines: {macos: '>=10.13', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.2': + resolution: {integrity: sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.2': + resolution: {integrity: sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.2': + resolution: {integrity: sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==} + engines: {glibc: '>=2.28', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.2': + resolution: {integrity: sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==} + engines: {glibc: '>=2.26', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + resolution: {integrity: sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + resolution: {integrity: sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==} + engines: {musl: '>=1.2.2', npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.3': + resolution: {integrity: sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.3': + resolution: {integrity: sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.3': + resolution: {integrity: sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==} + engines: {glibc: '>=2.28', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.3': + resolution: {integrity: sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==} + engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.3': + resolution: {integrity: sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.3': + resolution: {integrity: sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==} + engines: {musl: '>=1.2.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.3': + resolution: {integrity: sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.3': + resolution: {integrity: sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.3': + resolution: {integrity: sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -672,171 +788,6 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jimp/bmp@0.22.12': - resolution: {integrity: sha512-aeI64HD0npropd+AR76MCcvvRaa+Qck6loCOS03CkkxGHN5/r336qTM5HPUdHKMDOGzqknuVPA8+kK1t03z12g==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/core@0.22.12': - resolution: {integrity: sha512-l0RR0dOPyzMKfjUW1uebzueFEDtCOj9fN6pyTYWWOM/VS4BciXQ1VVrJs8pO3kycGYZxncRKhCoygbNr8eEZQA==} - - '@jimp/custom@0.22.12': - resolution: {integrity: sha512-xcmww1O/JFP2MrlGUMd3Q78S3Qu6W3mYTXYuIqFq33EorgYHV/HqymHfXy9GjiCJ7OI+7lWx6nYFOzU7M4rd1Q==} - - '@jimp/gif@0.22.12': - resolution: {integrity: sha512-y6BFTJgch9mbor2H234VSjd9iwAhaNf/t3US5qpYIs0TSbAvM02Fbc28IaDETj9+4YB4676sz4RcN/zwhfu1pg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/jpeg@0.22.12': - resolution: {integrity: sha512-Rq26XC/uQWaQKyb/5lksCTCxXhtY01NJeBN+dQv5yNYedN0i7iYu+fXEoRsfaJ8xZzjoANH8sns7rVP4GE7d/Q==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-blit@0.22.12': - resolution: {integrity: sha512-xslz2ZoFZOPLY8EZ4dC29m168BtDx95D6K80TzgUi8gqT7LY6CsajWO0FAxDwHz6h0eomHMfyGX0stspBrTKnQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-blur@0.22.12': - resolution: {integrity: sha512-S0vJADTuh1Q9F+cXAwFPlrKWzDj2F9t/9JAbUvaaDuivpyWuImEKXVz5PUZw2NbpuSHjwssbTpOZ8F13iJX4uw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-circle@0.22.12': - resolution: {integrity: sha512-SWVXx1yiuj5jZtMijqUfvVOJBwOifFn0918ou4ftoHgegc5aHWW5dZbYPjvC9fLpvz7oSlptNl2Sxr1zwofjTg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-color@0.22.12': - resolution: {integrity: sha512-xImhTE5BpS8xa+mAN6j4sMRWaUgUDLoaGHhJhpC+r7SKKErYDR0WQV4yCE4gP+N0gozD0F3Ka1LUSaMXrn7ZIA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-contain@0.22.12': - resolution: {integrity: sha512-Eo3DmfixJw3N79lWk8q/0SDYbqmKt1xSTJ69yy8XLYQj9svoBbyRpSnHR+n9hOw5pKXytHwUW6nU4u1wegHNoQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - '@jimp/plugin-scale': '>=0.3.5' - - '@jimp/plugin-cover@0.22.12': - resolution: {integrity: sha512-z0w/1xH/v/knZkpTNx+E8a7fnasQ2wHG5ze6y5oL2dhH1UufNua8gLQXlv8/W56+4nJ1brhSd233HBJCo01BXA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-crop': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - '@jimp/plugin-scale': '>=0.3.5' - - '@jimp/plugin-crop@0.22.12': - resolution: {integrity: sha512-FNuUN0OVzRCozx8XSgP9MyLGMxNHHJMFt+LJuFjn1mu3k0VQxrzqbN06yIl46TVejhyAhcq5gLzqmSCHvlcBVw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-displace@0.22.12': - resolution: {integrity: sha512-qpRM8JRicxfK6aPPqKZA6+GzBwUIitiHaZw0QrJ64Ygd3+AsTc7BXr+37k2x7QcyCvmKXY4haUrSIsBug4S3CA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-dither@0.22.12': - resolution: {integrity: sha512-jYgGdSdSKl1UUEanX8A85v4+QUm+PE8vHFwlamaKk89s+PXQe7eVE3eNeSZX4inCq63EHL7cX580dMqkoC3ZLw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-fisheye@0.22.12': - resolution: {integrity: sha512-LGuUTsFg+fOp6KBKrmLkX4LfyCy8IIsROwoUvsUPKzutSqMJnsm3JGDW2eOmWIS/jJpPaeaishjlxvczjgII+Q==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-flip@0.22.12': - resolution: {integrity: sha512-m251Rop7GN8W0Yo/rF9LWk6kNclngyjIJs/VXHToGQ6EGveOSTSQaX2Isi9f9lCDLxt+inBIb7nlaLLxnvHX8Q==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-rotate': '>=0.3.5' - - '@jimp/plugin-gaussian@0.22.12': - resolution: {integrity: sha512-sBfbzoOmJ6FczfG2PquiK84NtVGeScw97JsCC3rpQv1PHVWyW+uqWFF53+n3c8Y0P2HWlUjflEla2h/vWShvhg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-invert@0.22.12': - resolution: {integrity: sha512-N+6rwxdB+7OCR6PYijaA/iizXXodpxOGvT/smd/lxeXsZ/empHmFFFJ/FaXcYh19Tm04dGDaXcNF/dN5nm6+xQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-mask@0.22.12': - resolution: {integrity: sha512-4AWZg+DomtpUA099jRV8IEZUfn1wLv6+nem4NRJC7L/82vxzLCgXKTxvNvBcNmJjT9yS1LAAmiJGdWKXG63/NA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-normalize@0.22.12': - resolution: {integrity: sha512-0So0rexQivnWgnhacX4cfkM2223YdExnJTTy6d06WbkfZk5alHUx8MM3yEzwoCN0ErO7oyqEWRnEkGC+As1FtA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-print@0.22.12': - resolution: {integrity: sha512-c7TnhHlxm87DJeSnwr/XOLjJU/whoiKYY7r21SbuJ5nuH+7a78EW1teOaj5gEr2wYEd7QtkFqGlmyGXY/YclyQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' - - '@jimp/plugin-resize@0.22.12': - resolution: {integrity: sha512-3NyTPlPbTnGKDIbaBgQ3HbE6wXbAlFfxHVERmrbqAi8R3r6fQPxpCauA8UVDnieg5eo04D0T8nnnNIX//i/sXg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/plugin-rotate@0.22.12': - resolution: {integrity: sha512-9YNEt7BPAFfTls2FGfKBVgwwLUuKqy+E8bDGGEsOqHtbuhbshVGxN2WMZaD4gh5IDWvR+emmmPPWGgaYNYt1gA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' - '@jimp/plugin-crop': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - - '@jimp/plugin-scale@0.22.12': - resolution: {integrity: sha512-dghs92qM6MhHj0HrV2qAwKPMklQtjNpoYgAB94ysYpsXslhRTiPisueSIELRwZGEr0J0VUxpUY7HgJwlSIgGZw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - - '@jimp/plugin-shadow@0.22.12': - resolution: {integrity: sha512-FX8mTJuCt7/3zXVoeD/qHlm4YH2bVqBuWQHXSuBK054e7wFRnRnbSLPUqAwSeYP3lWqpuQzJtgiiBxV3+WWwTg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blur': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - - '@jimp/plugin-threshold@0.22.12': - resolution: {integrity: sha512-4x5GrQr1a/9L0paBC/MZZJjjgjxLYrqSmWd+e+QfAEPvmRxdRoQ5uKEuNgXnm9/weHQBTnQBQsOY2iFja+XGAw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-color': '>=0.8.0' - '@jimp/plugin-resize': '>=0.8.0' - - '@jimp/plugins@0.22.12': - resolution: {integrity: sha512-yBJ8vQrDkBbTgQZLty9k4+KtUQdRjsIDJSPjuI21YdVeqZxYywifHl4/XWILoTZsjTUASQcGoH0TuC0N7xm3ww==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/png@0.22.12': - resolution: {integrity: sha512-Mrp6dr3UTn+aLK8ty/dSKELz+Otdz1v4aAXzV5q53UDD2rbB5joKVJ/ChY310B+eRzNxIovbUF1KVrUsYdE8Hg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/tiff@0.22.12': - resolution: {integrity: sha512-E1LtMh4RyJsoCAfAkBRVSYyZDTtLq9p9LUiiYP0vPtXyxX4BiYBUYihTLSBlCQg5nF2e4OpQg7SPrLdJ66u7jg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/types@0.22.12': - resolution: {integrity: sha512-wwKYzRdElE1MBXFREvCto5s699izFHNVvALUv79GXNbsOVqlwlOxlWJ8DuyOGIXoLP4JW/m30YyuTtfUJgMRMA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - - '@jimp/utils@0.22.12': - resolution: {integrity: sha512-yJ5cWUknGnilBq97ZXOyOS0HhsHOyAyjHwYfHxGbSyMTohgQI6sVyE8KPgDwH8HHW/nMKXk8TrSwAE71zt716Q==} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -1461,9 +1412,6 @@ packages: react: ^18.0.0 react-dom: ^18.0.0 - '@tokenizer/token@0.3.0': - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -1519,9 +1467,6 @@ packages: '@types/node-fetch@2.6.11': resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} - '@types/node@16.9.1': - resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - '@types/node@18.19.31': resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} @@ -1816,9 +1761,6 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} - any-base@1.1.0: - resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} - any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1938,16 +1880,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bmp-js@0.1.0: - resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} - bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -1969,16 +1905,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-equal@0.0.1: - resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} - engines: {node: '>=0.4.0'} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bufferutil@4.0.8: resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} engines: {node: '>=6.14.2'} @@ -2093,6 +2022,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2228,6 +2164,10 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -2266,9 +2206,6 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dom-walk@0.1.2: - resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - domexception@4.0.0: resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} engines: {node: '>=12'} @@ -2480,9 +2417,6 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - exif-parser@0.1.12: - resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} - exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} @@ -2521,10 +2455,6 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} - file-type@16.5.4: - resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} - engines: {node: '>=10'} - fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2628,9 +2558,6 @@ packages: get-tsconfig@4.7.3: resolution: {integrity: sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==} - gifwrap@0.10.1: - resolution: {integrity: sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2652,9 +2579,6 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - global@4.4.0: - resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} - globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -2740,16 +2664,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} - image-q@4.0.0: - resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} - import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} engines: {node: '>=6'} @@ -2787,6 +2705,9 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.0.0: resolution: {integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==} engines: {node: '>= 0.4'} @@ -2832,9 +2753,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-function@1.0.2: - resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} - is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -2922,9 +2840,6 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-fetch@3.0.0: - resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -3094,9 +3009,6 @@ packages: node-notifier: optional: true - jimp@0.22.12: - resolution: {integrity: sha512-R5jZaYDnfkxKJy1dwLpj/7cvyjxiclxU3F4TrI/J4j2rS0niq6YDUMoPn5hs8GDpO+OZGo7Ky057CRtWesyhfg==} - jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true @@ -3104,9 +3016,6 @@ packages: jose@5.2.4: resolution: {integrity: sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==} - jpeg-js@0.4.4: - resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3198,9 +3107,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - load-bmfont@1.4.1: - resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} - locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -3273,18 +3179,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - min-document@2.19.0: - resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} - min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -3465,9 +3363,6 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} - omggif@1.0.10: - resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3507,25 +3402,10 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-bmfont-ascii@1.0.6: - resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} - - parse-bmfont-binary@1.0.6: - resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} - - parse-bmfont-xml@1.1.6: - resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} - - parse-headers@2.0.5: - resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -3556,10 +3436,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - peek-readable@4.1.0: - resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} - engines: {node: '>=8'} - periscopic@3.1.0: resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} @@ -3605,10 +3481,6 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - phin@2.9.3: - resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -3624,22 +3496,10 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - pixelmatch@4.0.2: - resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} - hasBin: true - pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - pngjs@3.4.0: - resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} - engines: {node: '>=4.0.0'} - - pngjs@6.0.0: - resolution: {integrity: sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==} - engines: {node: '>=12.13.0'} - possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -3747,10 +3607,6 @@ packages: pretty-format@3.8.0: resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -3834,14 +3690,6 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readable-web-to-node-stream@3.0.2: - resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} - engines: {node: '>=8'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -3854,9 +3702,6 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3917,9 +3762,6 @@ packages: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-regex-test@1.0.3: resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} engines: {node: '>= 0.4'} @@ -3970,6 +3812,10 @@ packages: resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} engines: {node: '>= 0.4'} + sharp@0.33.3: + resolution: {integrity: sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==} + engines: {libvips: '>=8.15.2', node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3989,6 +3835,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -4074,9 +3923,6 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4108,10 +3954,6 @@ packages: strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} - strtok3@6.3.0: - resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} - engines: {node: '>=10'} - styled-jsx@5.1.1: resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} engines: {node: '>= 12.0.0'} @@ -4198,12 +4040,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - timm@1.7.1: - resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} - - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -4215,10 +4051,6 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - token-types@4.2.1: - resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} - engines: {node: '>=10'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4357,9 +4189,6 @@ packages: resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} engines: {node: '>=6.14.2'} - utif2@4.1.0: - resolution: {integrity: sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -4410,9 +4239,6 @@ packages: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} - whatwg-fetch@3.6.20: - resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} - whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -4487,24 +4313,10 @@ packages: utf-8-validate: optional: true - xhr@2.6.0: - resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} - xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} - xml-parse-from-string@1.0.1: - resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} - - xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -5308,6 +5120,11 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} + '@emnapi/runtime@1.1.1': + dependencies: + tslib: 2.6.2 + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.0)': dependencies: eslint: 8.57.0 @@ -5362,6 +5179,81 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} + '@img/sharp-darwin-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.2 + optional: true + + '@img/sharp-darwin-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.2': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.2': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.2': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.2': + optional: true + + '@img/sharp-linux-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.2 + optional: true + + '@img/sharp-linux-arm@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.2 + optional: true + + '@img/sharp-linux-s390x@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.2 + optional: true + + '@img/sharp-linux-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + optional: true + + '@img/sharp-wasm32@0.33.3': + dependencies: + '@emnapi/runtime': 1.1.1 + optional: true + + '@img/sharp-win32-ia32@0.33.3': + optional: true + + '@img/sharp-win32-x64@0.33.3': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -5543,218 +5435,6 @@ snapshots: '@types/yargs': 17.0.32 chalk: 4.1.2 - '@jimp/bmp@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - bmp-js: 0.1.0 - - '@jimp/core@0.22.12': - dependencies: - '@jimp/utils': 0.22.12 - any-base: 1.1.0 - buffer: 5.7.1 - exif-parser: 0.1.12 - file-type: 16.5.4 - isomorphic-fetch: 3.0.0 - pixelmatch: 4.0.2 - tinycolor2: 1.6.0 - transitivePeerDependencies: - - encoding - - '@jimp/custom@0.22.12': - dependencies: - '@jimp/core': 0.22.12 - transitivePeerDependencies: - - encoding - - '@jimp/gif@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - gifwrap: 0.10.1 - omggif: 1.0.10 - - '@jimp/jpeg@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - jpeg-js: 0.4.4 - - '@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-circle@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - tinycolor2: 1.6.0 - - '@jimp/plugin-contain@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-cover@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-displace@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-dither@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-fisheye@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-flip@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-rotate': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-gaussian@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-invert@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-mask@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-normalize@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-print@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) - '@jimp/utils': 0.22.12 - load-bmfont: 1.4.1 - - '@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - - '@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-shadow@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-blur': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/utils': 0.22.12 - - '@jimp/plugin-threshold@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-color': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/utils': 0.22.12 - - '@jimp/plugins@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugin-blit': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-blur': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-circle': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-color': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-contain': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) - '@jimp/plugin-cover': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-scale@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) - '@jimp/plugin-crop': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-displace': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-dither': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-fisheye': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-flip': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-rotate@0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12))) - '@jimp/plugin-gaussian': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-invert': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-mask': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-normalize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-print': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12)) - '@jimp/plugin-resize': 0.22.12(@jimp/custom@0.22.12) - '@jimp/plugin-rotate': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blit@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-crop@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/plugin-scale': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/plugin-shadow': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-blur@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - '@jimp/plugin-threshold': 0.22.12(@jimp/custom@0.22.12)(@jimp/plugin-color@0.22.12(@jimp/custom@0.22.12))(@jimp/plugin-resize@0.22.12(@jimp/custom@0.22.12)) - timm: 1.7.1 - - '@jimp/png@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/utils': 0.22.12 - pngjs: 6.0.0 - - '@jimp/tiff@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/custom': 0.22.12 - utif2: 4.1.0 - - '@jimp/types@0.22.12(@jimp/custom@0.22.12)': - dependencies: - '@jimp/bmp': 0.22.12(@jimp/custom@0.22.12) - '@jimp/custom': 0.22.12 - '@jimp/gif': 0.22.12(@jimp/custom@0.22.12) - '@jimp/jpeg': 0.22.12(@jimp/custom@0.22.12) - '@jimp/png': 0.22.12(@jimp/custom@0.22.12) - '@jimp/tiff': 0.22.12(@jimp/custom@0.22.12) - timm: 1.7.1 - - '@jimp/utils@0.22.12': - dependencies: - regenerator-runtime: 0.13.11 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -6488,8 +6168,6 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tokenizer/token@0.3.0': {} - '@tootallnate/once@2.0.0': {} '@types/aria-query@5.0.4': {} @@ -6555,8 +6233,6 @@ snapshots: '@types/node': 20.12.7 form-data: 4.0.0 - '@types/node@16.9.1': {} - '@types/node@18.19.31': dependencies: undici-types: 5.26.5 @@ -6894,8 +6570,6 @@ snapshots: ansi-styles@6.2.1: {} - any-base@1.1.0: {} - any-promise@1.3.0: {} anymatch@3.1.3: @@ -7079,12 +6753,8 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - binary-extensions@2.3.0: {} - bmp-js@0.1.0: {} - bowser@2.11.0: {} brace-expansion@1.1.11: @@ -7111,15 +6781,8 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-equal@0.0.1: {} - buffer-from@1.1.2: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.0 @@ -7238,6 +6901,16 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -7356,6 +7029,8 @@ snapshots: dequal@2.0.3: {} + detect-libc@2.0.3: {} + detect-newline@3.1.0: {} detect-node-es@1.1.0: {} @@ -7384,8 +7059,6 @@ snapshots: dom-accessibility-api@0.6.3: {} - dom-walk@0.1.2: {} - domexception@4.0.0: dependencies: webidl-conversions: 7.0.0 @@ -7750,8 +7423,6 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exif-parser@0.1.12: {} - exifr@7.1.3: {} exit@0.1.2: {} @@ -7794,12 +7465,6 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-type@16.5.4: - dependencies: - readable-web-to-node-stream: 3.0.2 - strtok3: 6.3.0 - token-types: 4.2.1 - fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -7897,11 +7562,6 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - gifwrap@0.10.1: - dependencies: - image-q: 4.0.0 - omggif: 1.0.10 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -7935,11 +7595,6 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 - global@4.4.0: - dependencies: - min-document: 2.19.0 - process: 0.11.10 - globals@11.12.0: {} globals@13.24.0: @@ -8024,14 +7679,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.1: {} - image-q@4.0.0: - dependencies: - '@types/node': 16.9.1 - import-fresh@3.3.0: dependencies: parent-module: 1.0.1 @@ -8070,6 +7719,8 @@ snapshots: is-arrayish@0.2.1: {} + is-arrayish@0.3.2: {} + is-async-function@2.0.0: dependencies: has-tostringtag: 1.0.2 @@ -8111,8 +7762,6 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-function@1.0.2: {} - is-generator-fn@2.1.0: {} is-generator-function@1.0.10: @@ -8183,13 +7832,6 @@ snapshots: isexe@2.0.0: {} - isomorphic-fetch@3.0.0: - dependencies: - node-fetch: 2.7.0 - whatwg-fetch: 3.6.20 - transitivePeerDependencies: - - encoding - istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -8568,21 +8210,10 @@ snapshots: - supports-color - ts-node - jimp@0.22.12: - dependencies: - '@jimp/custom': 0.22.12 - '@jimp/plugins': 0.22.12(@jimp/custom@0.22.12) - '@jimp/types': 0.22.12(@jimp/custom@0.22.12) - regenerator-runtime: 0.13.11 - transitivePeerDependencies: - - encoding - jiti@1.21.0: {} jose@5.2.4: {} - jpeg-js@0.4.4: {} - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -8683,17 +8314,6 @@ snapshots: lines-and-columns@1.2.4: {} - load-bmfont@1.4.1: - dependencies: - buffer-equal: 0.0.1 - mime: 1.6.0 - parse-bmfont-ascii: 1.0.6 - parse-bmfont-binary: 1.0.6 - parse-bmfont-xml: 1.1.6 - phin: 2.9.3 - xhr: 2.6.0 - xtend: 4.0.2 - locate-character@3.0.0: {} locate-path@5.0.0: @@ -8755,14 +8375,8 @@ snapshots: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} - mimic-fn@2.1.0: {} - min-document@2.19.0: - dependencies: - dom-walk: 0.1.2 - min-indent@1.0.1: {} mini-svg-data-uri@1.4.4: {} @@ -8912,8 +8526,6 @@ snapshots: obuf@1.1.2: {} - omggif@1.0.10: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -8964,23 +8576,10 @@ snapshots: p-try@2.2.0: {} - pako@1.0.11: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 - parse-bmfont-ascii@1.0.6: {} - - parse-bmfont-binary@1.0.6: {} - - parse-bmfont-xml@1.1.6: - dependencies: - xml-parse-from-string: 1.0.1 - xml2js: 0.5.0 - - parse-headers@2.0.5: {} - parse-json@5.2.0: dependencies: '@babel/code-frame': 7.24.2 @@ -9007,8 +8606,6 @@ snapshots: path-type@4.0.0: {} - peek-readable@4.1.0: {} - periscopic@3.1.0: dependencies: '@types/estree': 1.0.5 @@ -9062,8 +8659,6 @@ snapshots: dependencies: split2: 4.2.0 - phin@2.9.3: {} - picocolors@1.0.0: {} picomatch@2.3.1: {} @@ -9072,18 +8667,10 @@ snapshots: pirates@4.0.6: {} - pixelmatch@4.0.2: - dependencies: - pngjs: 3.4.0 - pkg-dir@4.2.0: dependencies: find-up: 4.1.0 - pngjs@3.4.0: {} - - pngjs@6.0.0: {} - possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.38): @@ -9174,8 +8761,6 @@ snapshots: pretty-format@3.8.0: {} - process@0.11.10: {} - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -9251,16 +8836,6 @@ snapshots: dependencies: pify: 2.3.0 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - - readable-web-to-node-stream@3.0.2: - dependencies: - readable-stream: 3.6.2 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -9280,8 +8855,6 @@ snapshots: globalthis: 1.0.3 which-builtin-type: 1.1.3 - regenerator-runtime@0.13.11: {} - regenerator-runtime@0.14.1: {} regexp.prototype.flags@1.5.2: @@ -9338,8 +8911,6 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 - safe-buffer@5.2.1: {} - safe-regex-test@1.0.3: dependencies: call-bind: 1.0.7 @@ -9390,6 +8961,32 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + sharp@0.33.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.0 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.3 + '@img/sharp-darwin-x64': 0.33.3 + '@img/sharp-libvips-darwin-arm64': 1.0.2 + '@img/sharp-libvips-darwin-x64': 1.0.2 + '@img/sharp-libvips-linux-arm': 1.0.2 + '@img/sharp-libvips-linux-arm64': 1.0.2 + '@img/sharp-libvips-linux-s390x': 1.0.2 + '@img/sharp-libvips-linux-x64': 1.0.2 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.2 + '@img/sharp-libvips-linuxmusl-x64': 1.0.2 + '@img/sharp-linux-arm': 0.33.3 + '@img/sharp-linux-arm64': 0.33.3 + '@img/sharp-linux-s390x': 0.33.3 + '@img/sharp-linux-x64': 0.33.3 + '@img/sharp-linuxmusl-arm64': 0.33.3 + '@img/sharp-linuxmusl-x64': 0.33.3 + '@img/sharp-wasm32': 0.33.3 + '@img/sharp-win32-ia32': 0.33.3 + '@img/sharp-win32-x64': 0.33.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9407,6 +9004,10 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.25 @@ -9508,10 +9109,6 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -9534,11 +9131,6 @@ snapshots: strnum@1.0.5: {} - strtok3@6.3.0: - dependencies: - '@tokenizer/token': 0.3.0 - peek-readable: 4.1.0 - styled-jsx@5.1.1(@babel/core@7.24.4)(react@18.3.1): dependencies: client-only: 0.0.1 @@ -9655,10 +9247,6 @@ snapshots: dependencies: any-promise: 1.3.0 - timm@1.7.1: {} - - tinycolor2@1.6.0: {} - tmpl@1.0.5: {} to-fast-properties@2.0.0: {} @@ -9667,11 +9255,6 @@ snapshots: dependencies: is-number: 7.0.0 - token-types@4.2.1: - dependencies: - '@tokenizer/token': 0.3.0 - ieee754: 1.2.1 - totalist@3.0.1: {} tough-cookie@4.1.3: @@ -9812,10 +9395,6 @@ snapshots: node-gyp-build: 4.8.0 optional: true - utif2@4.1.0: - dependencies: - pako: 1.0.11 - util-deprecate@1.0.2: {} uuid@9.0.1: {} @@ -9875,8 +9454,6 @@ snapshots: dependencies: iconv-lite: 0.6.3 - whatwg-fetch@3.6.20: {} - whatwg-mimetype@3.0.0: {} whatwg-url@11.0.0: @@ -9962,24 +9539,8 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 6.0.3 - xhr@2.6.0: - dependencies: - global: 4.4.0 - is-function: 1.0.2 - parse-headers: 2.0.5 - xtend: 4.0.2 - xml-name-validator@4.0.0: {} - xml-parse-from-string@1.0.1: {} - - xml2js@0.5.0: - dependencies: - sax: 1.2.4 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - xmlchars@2.2.0: {} xtend@4.0.2: {} diff --git a/src/photo/server.ts b/src/photo/server.ts index 8313b72e..f05a6cb7 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -10,7 +10,7 @@ import { import { ExifData, ExifParserFactory } from 'ts-exif-parser'; import { PhotoFormData } from './form'; import { FilmSimulation } from '@/simulation'; -import Jimp from 'jimp'; +import sharp from 'sharp'; export const extractExifDataFromBlobPath = async ( blobPath: string, @@ -69,10 +69,12 @@ export const extractExifDataFromBlobPath = async ( }; }; -export const blurImage = async (url: string) => - Jimp.read(decodeURIComponent(url)) - .then(image => { - image.resize(100, Jimp.AUTO); - image.blur(20); - return image.getBase64Async(Jimp.MIME_JPEG); - }); +export const blurImage = async (url: string) => { + const image = await fetch(decodeURIComponent(url)) + .then(res => res.arrayBuffer()); + return sharp(image) + .resize(200) + .blur(20) + .toBuffer() + .then(data => `data:image/png;base64,${data.toString('base64')}`); +}; From d448c36445305cca6427f4e236de6923c1196dcf Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 00:19:42 -0500 Subject: [PATCH 07/19] Build tooling around server-side blur generation --- README.md | 3 + next.config.js | 3 - src/app/admin/photos/[photoId]/edit/page.tsx | 7 ++ src/app/admin/uploads/[uploadPath]/page.tsx | 34 +++-- src/components/CanvasBlurCapture.tsx | 126 ------------------- src/components/CommandKClient.tsx | 9 ++ src/components/ImageBlurFallback.tsx | 18 ++- src/components/ImageLarge.tsx | 5 +- src/components/ImageSmall.tsx | 5 +- src/components/ImageTiny.tsx | 3 + src/components/PageSpinner.tsx | 2 +- src/photo/PhotoEditPageClient.tsx | 7 +- src/photo/PhotoLarge.tsx | 2 + src/photo/PhotoSmall.tsx | 3 +- src/photo/PhotoTiny.tsx | 3 +- src/photo/UpdateBlurDataButton.tsx | 36 ++++++ src/photo/UploadPageClient.tsx | 7 +- src/photo/actions.ts | 15 ++- src/photo/ai/AiButton.tsx | 2 +- src/photo/ai/useAiImageQueries.ts | 9 +- src/photo/form/PhotoForm.tsx | 86 +++---------- src/photo/form/index.ts | 11 +- src/photo/form/usePhotoFormParent.ts | 13 +- src/photo/index.ts | 4 + src/photo/server.ts | 72 +++++++++-- src/state/AppState.ts | 2 + src/state/AppStateProvider.tsx | 4 + 27 files changed, 235 insertions(+), 256 deletions(-) delete mode 100644 src/components/CanvasBlurCapture.tsx create mode 100644 src/photo/UpdateBlurDataButton.tsx diff --git a/README.md b/README.md index 16692993..eacb861c 100644 --- a/README.md +++ b/README.md @@ -219,3 +219,6 @@ FAQ #### Why do my images appear flipped/rotated incorrectly? > For a number of reasons, only EXIF orientations: 1, 3, 6, and 8 are supported. Orientations 2, 4, 5, and 7—which make use of mirroring—are not supported. + +#### Why does my image placeholder blur look different from photo to photo? +> Earlier template versions generated blur data on the client, which varied visually from browser to browser. Data is now generated consistently on the server. If you wish to update blur data for a particular photo: go to the photo editor, click the refresh icon next to the "Blur Data" field, and click "Update." diff --git a/next.config.js b/next.config.js index 51aab061..f0ff1b30 100644 --- a/next.config.js +++ b/next.config.js @@ -35,9 +35,6 @@ const nextConfig = { .concat(createRemotePattern(HOSTNAME_AWS_S3)), minimumCacheTTL: 31536000, }, - // experimental: { - // serverComponentsExternalPackages: ['jimp'], - // }, }; const withBundleAnalyzer = require('@next/bundle-analyzer')({ diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index c9a5c68c..4ca6dcb6 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -3,6 +3,8 @@ import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache'; import { PATH_ADMIN } from '@/site/paths'; import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; +import { resizeImageFromUrl } from '@/photo/server'; +import { getNextImageUrlForRequest } from '@/services/next-image'; export default async function PhotoEditPage({ params: { photoId }, @@ -16,12 +18,17 @@ export default async function PhotoEditPage({ const uniqueTags = await getUniqueTagsCached(); const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; + + const imageThumbnailBase64 = await resizeImageFromUrl( + getNextImageUrlForRequest(photo.url, 640), + ); return ( ); }; diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index ec9c2f16..d52d7a10 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -1,5 +1,5 @@ import { PATH_ADMIN } from '@/site/paths'; -import { blurImage, extractExifDataFromBlobPath } from '@/photo/server'; +import { extractImageDataFromBlobPath } from '@/photo/server'; import { redirect } from 'next/navigation'; import { getUniqueTagsCached } from '@/photo/cache'; import UploadPageClient from '@/photo/UploadPageClient'; @@ -16,11 +16,14 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { const { blobId, photoFormExif, - } = await extractExifDataFromBlobPath(uploadPath, true); + imageResizedBase64: imageThumbnailBase64, + } = await extractImageDataFromBlobPath( uploadPath, { + includeInitialPhotoFields: true, + generateBlurData: true, + generateResizedImage: true, + }); - const blurBase64 = await blurImage(uploadPath); - - if (!photoFormExif) { redirect(PATH_ADMIN); } + if (!photoFormExif || !imageThumbnailBase64) { redirect(PATH_ADMIN); } const uniqueTags = await getUniqueTagsCached(); @@ -29,18 +32,13 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { const textFieldsToAutoGenerate = AI_TEXT_AUTO_GENERATED_FIELDS; return ( - <> - Blur Debug - - + ); }; diff --git a/src/components/CanvasBlurCapture.tsx b/src/components/CanvasBlurCapture.tsx deleted file mode 100644 index cce1ed31..00000000 --- a/src/components/CanvasBlurCapture.tsx +++ /dev/null @@ -1,126 +0,0 @@ -'use client'; - -import { useEffect, useRef } from 'react'; - -const RETRY_DELAY = 2000; - -export default function CanvasBlurCapture({ - imageUrl, - onLoad, - onCapture, - onError, - width, - height, - hidden = true, - edgeCompensation = 10, - scale = 0.5, - quality = 0.9, -}: { - imageUrl: string - onLoad?: (imageData: string) => void - onCapture: (imageData: string) => void - onError?: (error: string) => void - width: number - height: number - hidden?: boolean - edgeCompensation?: number - scale?: number - quality?: number -}) { - const refCanvas = useRef(null); - const refImage = useRef(typeof Image !== 'undefined' ? new Image() : null); - const refTimeouts = useRef([]); - const refShouldCapture = useRef(true); - - useEffect(() => { - refShouldCapture.current = true; - - const capture = () => { - if (refShouldCapture.current) { - if ( - refCanvas.current && - refImage.current?.complete - ) { - const canvas = refCanvas.current; - canvas.width = width * scale; - canvas.height = height * scale; - canvas.style.width = `${width}px`; - canvas.style.height = `${height}px`; - const context = refCanvas.current?.getContext('2d'); - if (context) { - // Draw scaled image - context.scale(scale, scale); - context.drawImage( - refImage.current, - -edgeCompensation, - -edgeCompensation, - width + edgeCompensation * 2, - width * refImage.current.height / refImage.current.width + - edgeCompensation * 2, - ); - onLoad?.(canvas.toDataURL('image/jpeg', quality)); - // Draw blurred image - context.filter = - 'contrast(1.2) saturate(1.2) ' + - `blur(${scale * 10}px)`; - context.drawImage( - refImage.current, - -edgeCompensation, - -edgeCompensation, - width + edgeCompensation * 2, - width * refImage.current.height / refImage.current.width + - edgeCompensation * 2, - ); - onCapture(canvas.toDataURL('image/jpeg', quality)); - onError?.(''); - refTimeouts.current.forEach(clearTimeout); - refShouldCapture.current = false; - } else { - console.error('Cannot get 2d context ... retrying'); - onError?.('Cannot get 2d context ... retrying'); - // Retry capture in case canvas is not available - refTimeouts.current.push(setTimeout(capture, RETRY_DELAY)); - } - } else { - // eslint-disable-next-line max-len - console.error('Cannot generate blur data: canvas/image not ready ... retrying'); - // eslint-disable-next-line max-len - onError?.('Cannot generate blur data: canvas/image not ready ... retrying'); - // Retry capture in case canvas is not available - refTimeouts.current.push(setTimeout(capture, RETRY_DELAY)); - } - } - }; - - if (refImage.current) { - refImage.current.crossOrigin = 'anonymous'; - refImage.current.src = imageUrl; - refImage.current.onload = capture; - } - - // Attempt delayed capture in case image.onload never fires - refTimeouts.current.push(setTimeout(capture, RETRY_DELAY)); - - // Store timeout ref to ensure it's closed over - // in cleanup function (recommended by exhaustive-deps) - const timeouts = refTimeouts.current; - return () => { - refShouldCapture.current = false; - timeouts.forEach(clearTimeout); - }; - }, [ - imageUrl, - onCapture, - onLoad, - onError, - width, - height, - edgeCompensation, - scale, - quality, - ]); - - return ( - - ); -} diff --git a/src/components/CommandKClient.tsx b/src/components/CommandKClient.tsx index 5286062a..7c566112 100644 --- a/src/components/CommandKClient.tsx +++ b/src/components/CommandKClient.tsx @@ -36,6 +36,7 @@ import { TbPhoto } from 'react-icons/tb'; import { getKeywordsForPhoto, titleForPhoto } from '@/photo'; import PhotoDate from '@/photo/PhotoDate'; import PhotoTiny from '@/photo/PhotoTiny'; +import { FaCheck } from 'react-icons/fa6'; const LISTENER_KEYDOWN = 'keydown'; const MINIMUM_QUERY_LENGTH = 2; @@ -69,9 +70,12 @@ export default function CommandKClient({ isUserSignedIn, setUserEmail, isCommandKOpen: isOpen, + shouldShowBaselineGrid, + shouldDebugBlur, setIsCommandKOpen: setIsOpen, setShouldRespondToKeyboardCommands, setShouldShowBaselineGrid, + setShouldDebugBlur, } = useAppState(); const isOpenRef = useRef(isOpen); @@ -193,8 +197,13 @@ export default function CommandKClient({ heading: 'Debug Tools', accessory: , items: [{ + label: 'Toggle Blur Debug', + action: () => setShouldDebugBlur?.(prev => !prev), + annotation: shouldDebugBlur ? : undefined, + }, { label: 'Toggle Baseline Grid', action: () => setShouldShowBaselineGrid?.(prev => !prev), + annotation: shouldShowBaselineGrid ? : undefined, }], }); } diff --git a/src/components/ImageBlurFallback.tsx b/src/components/ImageBlurFallback.tsx index 6f5072b7..beffe029 100644 --- a/src/components/ImageBlurFallback.tsx +++ b/src/components/ImageBlurFallback.tsx @@ -2,18 +2,24 @@ /* eslint-disable jsx-a11y/alt-text */ import { BLUR_ENABLED } from '@/site/config'; +import { useAppState } from '@/state/AppState'; import { clsx} from 'clsx/lite'; import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; -export default function ImageBlurFallback(props: ImageProps) { +export default function ImageBlurFallback(props: ImageProps & { + blurCompatibilityMode?: boolean +}) { const { className, priority, blurDataURL, + blurCompatibilityMode, ...rest } = props; + const { shouldDebugBlur } = useAppState(); + const [wasCached, setWasCached] = useState(true); const [isLoading, setIsLoading] = useState(true); const [didError, setDidError] = useState(false); @@ -55,13 +61,13 @@ export default function ImageBlurFallback(props: ImageProps) { 'flex relative', )} > - {showPlaceholder && + {showPlaceholder || shouldDebugBlur &&
{(BLUR_ENABLED && props.blurDataURL) ? :
diff --git a/src/components/ImageSmall.tsx b/src/components/ImageSmall.tsx index d13cb0e7..830a767a 100644 --- a/src/components/ImageSmall.tsx +++ b/src/components/ImageSmall.tsx @@ -7,6 +7,7 @@ export default function ImageSmall({ alt, aspectRatio, blurData, + blurCompatibilityMode, priority, }: { className?: string @@ -14,6 +15,7 @@ export default function ImageSmall({ alt: string aspectRatio: number blurData?: string + blurCompatibilityMode?: boolean priority?: boolean }) { return ( @@ -21,8 +23,9 @@ export default function ImageSmall({ className, src, alt, - priority, blurDataURL: blurData, + blurCompatibilityMode, + priority, width: IMAGE_SMALL_WIDTH, height: Math.round(IMAGE_SMALL_WIDTH / aspectRatio), }} /> diff --git a/src/components/ImageTiny.tsx b/src/components/ImageTiny.tsx index 5e976775..cf57eda8 100644 --- a/src/components/ImageTiny.tsx +++ b/src/components/ImageTiny.tsx @@ -7,12 +7,14 @@ export default function ImageTiny({ alt, aspectRatio, blurData, + blurCompatibilityMode, }: { className?: string src: string alt: string aspectRatio: number blurData?: string + blurCompatibilityMode?: boolean }) { return ( diff --git a/src/components/PageSpinner.tsx b/src/components/PageSpinner.tsx index cd4c99af..630f5821 100644 --- a/src/components/PageSpinner.tsx +++ b/src/components/PageSpinner.tsx @@ -1,4 +1,4 @@ -import clsx from 'clsx/lite'; +import { clsx } from 'clsx/lite'; import Spinner from './Spinner'; import SiteGrid from './SiteGrid'; diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 218d35a4..94561417 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -21,10 +21,12 @@ export default function PhotoEditPageClient({ photo, uniqueTags, hasAiTextGeneration, + imageThumbnailBase64, }: { photo: Photo uniqueTags: TagsWithMeta hasAiTextGeneration: boolean + imageThumbnailBase64: string }) { const seedExifData = { url: photo.url }; @@ -48,7 +50,10 @@ export default function PhotoEditPageClient({ hasTextContent, setHasTextContent, aiContent, - } = usePhotoFormParent({ photoForm }); + } = usePhotoFormParent({ + photoForm, + imageThumbnailBase64, + }); return ( } diff --git a/src/photo/PhotoSmall.tsx b/src/photo/PhotoSmall.tsx index d8d0a7b6..a379d2cc 100644 --- a/src/photo/PhotoSmall.tsx +++ b/src/photo/PhotoSmall.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Photo, altTextForPhoto } from '.'; +import { Photo, altTextForPhoto, doesPhotoNeedBlurCompatibility } from '.'; import ImageSmall from '@/components/ImageSmall'; import Link from 'next/link'; import { clsx } from 'clsx/lite'; @@ -49,6 +49,7 @@ export default function PhotoSmall({ src={photo.url} aspectRatio={photo.aspectRatio} blurData={photo.blurData} + blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)} className="w-full" alt={altTextForPhoto(photo)} priority={priority} diff --git a/src/photo/PhotoTiny.tsx b/src/photo/PhotoTiny.tsx index 5cb9b9c3..797d8ccb 100644 --- a/src/photo/PhotoTiny.tsx +++ b/src/photo/PhotoTiny.tsx @@ -1,4 +1,4 @@ -import { Photo, altTextForPhoto } from '.'; +import { Photo, altTextForPhoto, doesPhotoNeedBlurCompatibility } from '.'; import ImageTiny from '@/components/ImageTiny'; import Link from 'next/link'; import { clsx } from 'clsx/lite'; @@ -44,6 +44,7 @@ export default function PhotoTiny({ src={photo.url} aspectRatio={photo.aspectRatio} blurData={photo.blurData} + blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)} alt={altTextForPhoto(photo)} /> diff --git a/src/photo/UpdateBlurDataButton.tsx b/src/photo/UpdateBlurDataButton.tsx new file mode 100644 index 00000000..104baa39 --- /dev/null +++ b/src/photo/UpdateBlurDataButton.tsx @@ -0,0 +1,36 @@ +import { clsx } from 'clsx/lite'; +import { FiRotateCcw } from 'react-icons/fi'; +import { getImageBlurAction } from './actions'; +import { useState } from 'react'; +import Spinner from '@/components/Spinner'; + +export default function UpdateBlurDataButton({ + photoUrl, + onUpdatedBlurData, +}: { + photoUrl?: string + onUpdatedBlurData: (blurData: string) => void +}) { + const [isLoading, setIsLoading] = useState(false); + + return ( + + ); +} diff --git a/src/photo/UploadPageClient.tsx b/src/photo/UploadPageClient.tsx index 3a070d55..912b840f 100644 --- a/src/photo/UploadPageClient.tsx +++ b/src/photo/UploadPageClient.tsx @@ -16,12 +16,14 @@ export default function UploadPageClient({ uniqueTags, hasAiTextGeneration, textFieldsToAutoGenerate, + imageThumbnailBase64, }: { blobId?: string photoFormExif: Partial uniqueTags: TagsWithMeta hasAiTextGeneration?: boolean textFieldsToAutoGenerate?: AiAutoGeneratedField[], + imageThumbnailBase64: string }) { const { pending, @@ -31,7 +33,10 @@ export default function UploadPageClient({ hasTextContent, setHasTextContent, aiContent, - } = usePhotoFormParent({ textFieldsToAutoGenerate }); + } = usePhotoFormParent({ + textFieldsToAutoGenerate, + imageThumbnailBase64, + }); const initialPhotoForm = useMemo(() => ({ ...photoFormExif, diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 21dcc7f4..58caab53 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -33,7 +33,7 @@ import { PATH_ROOT, pathForPhoto, } from '@/site/paths'; -import { extractExifDataFromBlobPath } from './server'; +import { blurImageFromUrl, extractImageDataFromBlobPath } from './server'; import { TAG_FAVS, isTagFavs } from '@/tag'; import { convertPhotoToPhotoDbInsert } from '.'; import { safelyRunAdminServerAction } from '@/auth'; @@ -154,7 +154,10 @@ export async function getExifDataAction( return safelyRunAdminServerAction(async () => { const { url } = photoFormPrevious; if (url) { - const { photoFormExif } = await extractExifDataFromBlobPath(url); + const { photoFormExif } = await extractImageDataFromBlobPath( + url, { + generateBlurData: true, + }); if (photoFormExif) { return photoFormExif; } @@ -169,7 +172,10 @@ export async function syncPhotoExifDataAction(formData: FormData) { if (photoId) { const photo = await getPhoto(photoId); if (photo) { - const { photoFormExif } = await extractExifDataFromBlobPath(photo.url); + const { photoFormExif } = await extractImageDataFromBlobPath( + photo.url, { + generateBlurData: true, + }); if (photoFormExif) { const photoFormDbInsert = convertFormDataToPhotoDbInsert({ ...convertPhotoToFormData(photo), @@ -209,6 +215,9 @@ export const getPhotosAction = async ( ) => getPhotos({ offset, includeHidden, limit }); +export const getImageBlurAction = async (url: string) => + blurImageFromUrl(url); + export const queryPhotosByTitleAction = async (query: string) => (await getPhotos({ query, limit: 10 })) .filter(({ title }) => Boolean(title)); diff --git a/src/photo/ai/AiButton.tsx b/src/photo/ai/AiButton.tsx index b88d1fb0..f7fbdbe7 100644 --- a/src/photo/ai/AiButton.tsx +++ b/src/photo/ai/AiButton.tsx @@ -56,7 +56,7 @@ export default function AiButton({ e.preventDefault(); } }} - disabled={!aiContent.isReady || isLoading} + disabled={isLoading} > {isLoading ? : } diff --git a/src/photo/ai/useAiImageQueries.ts b/src/photo/ai/useAiImageQueries.ts index 3071dc64..618af005 100644 --- a/src/photo/ai/useAiImageQueries.ts +++ b/src/photo/ai/useAiImageQueries.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import useAiImageQuery from './useAiImageQuery'; import useTitleCaptionAiImageQuery from './useTitleCaptionAiImageQuery'; import { ALL_AI_AUTO_GENERATED_FIELDS, AiAutoGeneratedField } from '.'; @@ -7,11 +7,8 @@ export type AiContent = ReturnType; export default function useAiImageQueries( textFieldsToAutoGenerate: AiAutoGeneratedField[] = [], + imageData: string, ) { - const [imageData, setImageData] = useState(); - - const isReady = Boolean(imageData); - const [ requestTitleCaption, _title, @@ -115,12 +112,10 @@ export default function useAiImageQueries( caption, tags, semanticDescription, - isReady, isLoading, isLoadingTitle, isLoadingCaption, isLoadingTags, isLoadingSemantic, - setImageData, }; } diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index a354e49a..f1b81c2e 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { FORM_METADATA_ENTRIES, PhotoFormData, @@ -15,21 +15,18 @@ import { createPhotoAction, updatePhotoAction } from '../actions'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import Link from 'next/link'; import { clsx } from 'clsx/lite'; -import CanvasBlurCapture from '@/components/CanvasBlurCapture'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths'; import { toastSuccess, toastWarning } from '@/toast'; import { getDimensionsFromSize } from '@/utility/size'; import ImageBlurFallback from '@/components/ImageBlurFallback'; -import { BLUR_ENABLED } from '@/site/config'; import { TagsWithMeta, sortTagsObjectWithoutFavs } from '@/tag'; import { formatCount, formatCountDescriptive } from '@/utility/string'; import { AiContent } from '../ai/useAiImageQueries'; import AiButton from '../ai/AiButton'; import Spinner from '@/components/Spinner'; -import { getNextImageUrlForRequest } from '@/services/next-image'; -import useDelay from '@/utility/useDelay'; import usePreventNavigation from '@/utility/usePreventNavigation'; import { useAppState } from '@/state/AppState'; +import UpdateBlurDataButton from '../UpdateBlurDataButton'; const THUMBNAIL_SIZE = 300; @@ -39,7 +36,6 @@ export default function PhotoForm({ type = 'create', uniqueTags, aiContent, - debugBlur, onTitleChange, onTextContentChange, onFormStatusChange, @@ -49,8 +45,6 @@ export default function PhotoForm({ type?: 'create' | 'edit' uniqueTags?: TagsWithMeta aiContent?: AiContent - setImageData?: (imageData: string) => void - debugBlur?: boolean onTitleChange?: (updatedTitle: string) => void onTextContentChange?: (hasContent: boolean) => void, onFormStatusChange?: (pending: boolean) => void @@ -59,11 +53,8 @@ export default function PhotoForm({ useState>(initialPhotoForm); const [formErrors, setFormErrors] = useState(getFormErrors(initialPhotoForm)); - const [blurError, setBlurError] = - useState(); - const [hasBlurData, setHasBlurData] = useState(false); - const { invalidateSwr } = useAppState(); + const { invalidateSwr, shouldDebugBlur } = useAppState(); const changedFormKeys = useMemo(() => getChangedFormFields(initialPhotoForm, formData), @@ -79,17 +70,6 @@ export default function PhotoForm({ (type === 'create' || formHasChanged) && isFormValid(formData) && !aiContent?.isLoading; - - const didLoad1000msAgo = useDelay(1000); - - // Show image loading status when necessary for - // blur data or AI analysis - const showImageLoadingStatus = - !hasBlurData && - didLoad1000msAgo && ( - (BLUR_ENABLED && !formData.blurData) || - aiContent !== undefined - ); // Update form when EXIF data // is refreshed by parent @@ -130,16 +110,6 @@ export default function PhotoForm({ const url = formData.url ?? ''; - const updateBlurData = useCallback((blurData: string) => { - if (BLUR_ENABLED) { - setFormData(data => ({ - ...data, - blurData, - })); - } - setHasBlurData(true); - }, []); - useEffect(() => setFormData(data => aiContent?.title ? { ...data, title: aiContent?.title } @@ -183,7 +153,7 @@ export default function PhotoForm({ } }; - const aiButtonForField = (key: keyof PhotoFormData) => { + const accessoryForField = (key: keyof PhotoFormData) => { if (aiContent) { switch (key) { case 'title': @@ -213,16 +183,20 @@ export default function PhotoForm({ requestFields={['semantic']} shouldConfirm={Boolean(formData.semanticDescription)} />; + case 'blurData': + return type === 'edit' + ? + setFormData(data => ({ ...data, blurData }))} + /> + : null; } } }; return (
- {debugBlur && blurError && -
- {blurError} -
}
Analyzing image
- - {debugBlur && formData.blurData && - blur}
)}
{/* Actions */} diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 5c772333..afed10fc 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -42,7 +42,7 @@ type FormMeta = { label: string note?: string required?: boolean - virtual?: boolean + excludeFromInsert?: boolean readOnly?: boolean validate?: (value?: string) => string | undefined validateStringMaxLength?: number @@ -55,6 +55,7 @@ type FormMeta = { selectOptions?: { value: string, label: string }[] selectOptionsDefaultLabel?: string tagOptions?: AnnotatedTag[] + nullOverride?: boolean }; const STRING_MAX_LENGTH_SHORT = 255; @@ -97,6 +98,7 @@ const FORM_METADATA = ( required: BLUR_ENABLED, hideIfEmpty: !BLUR_ENABLED, loadingMessage: 'Generating blur data ...', + nullOverride: !BLUR_ENABLED, }, url: { label: 'url', readOnly: true }, extension: { label: 'extension', readOnly: true }, @@ -121,7 +123,7 @@ const FORM_METADATA = ( takenAt: { label: 'taken at' }, takenAtNaive: { label: 'taken at (naive)' }, priorityOrder: { label: 'priority order' }, - favorite: { label: 'favorite', type: 'checkbox', virtual: true }, + favorite: { label: 'favorite', type: 'checkbox', excludeFromInsert: true }, hidden: { label: 'hidden', type: 'checkbox' }, }); @@ -242,12 +244,15 @@ export const convertFormDataToPhotoDbInsert = ( // - remove server action ID // - remove empty strings Object.keys(photoForm).forEach(key => { + const meta = FORM_METADATA()[key as keyof PhotoFormData]; if ( key.startsWith('$ACTION_ID_') || (photoForm as any)[key] === '' || - FORM_METADATA()[key as keyof PhotoFormData]?.virtual + meta?.excludeFromInsert ) { delete (photoForm as any)[key]; + } else if (meta?.nullOverride) { + (photoForm as any)[key] = null; } }); diff --git a/src/photo/form/usePhotoFormParent.ts b/src/photo/form/usePhotoFormParent.ts index bf3fe83a..f7766347 100644 --- a/src/photo/form/usePhotoFormParent.ts +++ b/src/photo/form/usePhotoFormParent.ts @@ -6,16 +6,21 @@ import { AiAutoGeneratedField } from '../ai'; export default function usePhotoFormParent({ photoForm, textFieldsToAutoGenerate, + imageThumbnailBase64, }: { - photoForm?: Partial, - textFieldsToAutoGenerate?: AiAutoGeneratedField[], -} = {}) { + photoForm?: Partial + textFieldsToAutoGenerate?: AiAutoGeneratedField[] + imageThumbnailBase64: string, +}) { const [pending, setIsPending] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); const [hasTextContent, setHasTextContent] = useState(photoForm ? formHasTextContent(photoForm) : false); - const aiContent = useAiImageQueries(textFieldsToAutoGenerate); + const aiContent = useAiImageQueries( + textFieldsToAutoGenerate, + imageThumbnailBase64, + ); return { pending, diff --git a/src/photo/index.ts b/src/photo/index.ts index 2af0a08f..d93af5b3 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -11,6 +11,7 @@ import { formatFocalLength, } from '@/utility/exif'; import camelcaseKeys from 'camelcase-keys'; +import { isAfter } from 'date-fns'; import type { Metadata } from 'next'; // ROOT PAGE @@ -276,3 +277,6 @@ export const isNextImageReadyBasedOnPhotos = async (photos: Photo[]) => photos.length > 0 && fetch(getNextImageUrlForRequest(photos[0].url, 640)) .then(response => response.ok) .catch(() => false); + +export const doesPhotoNeedBlurCompatibility = (photo: Photo) => + isAfter(photo.updatedAt, new Date('2024-05-07')); diff --git a/src/photo/server.ts b/src/photo/server.ts index f05a6cb7..2f0bc5b9 100644 --- a/src/photo/server.ts +++ b/src/photo/server.ts @@ -10,15 +10,29 @@ import { import { ExifData, ExifParserFactory } from 'ts-exif-parser'; import { PhotoFormData } from './form'; import { FilmSimulation } from '@/simulation'; -import sharp from 'sharp'; +import sharp, { Sharp } from 'sharp'; -export const extractExifDataFromBlobPath = async ( +const IMAGE_WIDTH_RESIZE = 200; +const IMAGE_WIDTH_BLUR = 200; + +export const extractImageDataFromBlobPath = async ( blobPath: string, - includeInitialPhotoFields?: boolean, + options?: { + includeInitialPhotoFields?: boolean + generateBlurData?: boolean + generateResizedImage?: boolean + }, ): Promise<{ blobId?: string photoFormExif?: Partial + imageResizedBase64?: string }> => { + const { + includeInitialPhotoFields, + generateBlurData, + generateResizedImage, + } = options ?? {}; + const url = decodeURIComponent(blobPath); const blobId = getIdFromStorageUrl(url); @@ -26,12 +40,13 @@ export const extractExifDataFromBlobPath = async ( const extension = getExtensionFromStorageUrl(url); const fileBytes = blobPath - ? await fetch(url) - .then(res => res.arrayBuffer()) + ? await fetch(url).then(res => res.arrayBuffer()) : undefined; let exifData: ExifData | undefined; let filmSimulation: FilmSimulation | undefined; + let blurData: string | undefined; + let imageResizedBase64: string | undefined; if (fileBytes) { const parser = ExifParserFactory.create(Buffer.from(fileBytes)); @@ -51,6 +66,14 @@ export const extractExifDataFromBlobPath = async ( filmSimulation = getFujifilmSimulationFromMakerNote(makerNote); } } + + if (generateBlurData) { + blurData = await blurImage(fileBytes); + } + + if (generateResizedImage) { + imageResizedBase64 = await resizeImage(fileBytes); + } } return { @@ -63,18 +86,41 @@ export const extractExifDataFromBlobPath = async ( extension, url, }, + ...generateBlurData && { blurData }, ...convertExifToFormData(exifData, filmSimulation), }, }, + imageResizedBase64, }; }; -export const blurImage = async (url: string) => { - const image = await fetch(decodeURIComponent(url)) - .then(res => res.arrayBuffer()); - return sharp(image) - .resize(200) - .blur(20) +const generateBase64 = async ( + image: ArrayBuffer, + middleware: (sharp: Sharp) => Sharp, +) => + middleware(sharp(image)) + .toFormat('jpeg', { quality: 90 }) .toBuffer() - .then(data => `data:image/png;base64,${data.toString('base64')}`); -}; + .then(data => `data:image/jpeg;base64,${data.toString('base64')}`); + +const resizeImage = async (image: ArrayBuffer) => + generateBase64(image, sharp => sharp + .resize(IMAGE_WIDTH_RESIZE) + ); + +const blurImage = async (image: ArrayBuffer) => + generateBase64(image, sharp => sharp + .resize(IMAGE_WIDTH_BLUR) + .modulate({ saturation: 1.15 }) + .blur(4) + ); + +export const resizeImageFromUrl = async (url: string) => + fetch(decodeURIComponent(url)) + .then(res => res.arrayBuffer()) + .then(buffer => resizeImage(buffer)); + +export const blurImageFromUrl = async (url: string) => + fetch(decodeURIComponent(url)) + .then(res => res.arrayBuffer()) + .then(buffer => blurImage(buffer)); diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 94d90b49..83f3ceb4 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -20,6 +20,8 @@ export interface AppStateContext { registerAdminUpdate?: () => void shouldShowBaselineGrid?: boolean setShouldShowBaselineGrid?: Dispatch> + shouldDebugBlur?: boolean + setShouldDebugBlur?: Dispatch> clearNextPhotoAnimation?: () => void } diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index ac4ecf5b..57c24a14 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -29,6 +29,8 @@ export default function AppStateProvider({ const [adminUpdateTimes, setAdminUpdateTimes] = useState([]); const [shouldShowBaselineGrid, setShouldShowBaselineGrid] = useState(false); + const [shouldDebugBlur, setShouldDebugBlur] = + useState(false); const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []); @@ -63,6 +65,8 @@ export default function AppStateProvider({ adminUpdateTimes, registerAdminUpdate, shouldShowBaselineGrid, + shouldDebugBlur, + setShouldDebugBlur, setShouldShowBaselineGrid, clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined), }} From 333ea9006f35d7b5c854f5c8c4048af5e59d2186 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 08:54:56 -0500 Subject: [PATCH 08/19] Adjust blur fallback logic --- src/app/admin/uploads/[uploadPath]/page.tsx | 2 +- src/components/ImageBlurFallback.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index d52d7a10..3fecf27c 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -17,7 +17,7 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { blobId, photoFormExif, imageResizedBase64: imageThumbnailBase64, - } = await extractImageDataFromBlobPath( uploadPath, { + } = await extractImageDataFromBlobPath(uploadPath, { includeInitialPhotoFields: true, generateBlurData: true, generateResizedImage: true, diff --git a/src/components/ImageBlurFallback.tsx b/src/components/ImageBlurFallback.tsx index beffe029..7c2aa474 100644 --- a/src/components/ImageBlurFallback.tsx +++ b/src/components/ImageBlurFallback.tsx @@ -61,13 +61,13 @@ export default function ImageBlurFallback(props: ImageProps & { 'flex relative', )} > - {showPlaceholder || shouldDebugBlur && + {(showPlaceholder || shouldDebugBlur) &&
{(BLUR_ENABLED && props.blurDataURL) ? Date: Mon, 6 May 2024 09:19:13 -0500 Subject: [PATCH 09/19] Stop generating blur data when clicking EXIF button --- src/photo/actions.ts | 99 ++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 58caab53..2e0d01d8 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -40,8 +40,10 @@ import { safelyRunAdminServerAction } from '@/auth'; import { AI_IMAGE_QUERIES, AiImageQuery } from './ai'; import { streamOpenAiImageQuery } from '@/services/openai'; -export async function createPhotoAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +// Private actions + +export const createPhotoAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { const photo = convertFormDataToPhotoDbInsert(formData, true); const updatedUrl = await convertUploadToPhoto(photo.url, photo.id); @@ -54,10 +56,9 @@ export async function createPhotoAction(formData: FormData) { redirect(PATH_ADMIN_PHOTOS); }); -} -export async function updatePhotoAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +export const updatePhotoAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { const photo = convertFormDataToPhotoDbInsert(formData); await sqlUpdatePhoto(photo); @@ -66,13 +67,12 @@ export async function updatePhotoAction(formData: FormData) { redirect(PATH_ADMIN_PHOTOS); }); -} -export async function toggleFavoritePhotoAction( +export const toggleFavoritePhotoAction = async ( photoId: string, shouldRedirect?: boolean, -) { - return safelyRunAdminServerAction(async () => { +) => + safelyRunAdminServerAction(async () => { const photo = await getPhoto(photoId); if (photo) { const { tags } = photo; @@ -86,33 +86,30 @@ export async function toggleFavoritePhotoAction( } } }); -} -export async function deletePhotoAction( +export const deletePhotoAction = async ( photoId: string, photoUrl: string, shouldRedirect?: boolean, -) { - return safelyRunAdminServerAction(async () => { +) => + safelyRunAdminServerAction(async () => { await sqlDeletePhoto(photoId).then(() => deleteStorageUrl(photoUrl)); revalidateAllKeysAndPaths(); if (shouldRedirect) { redirect(PATH_ROOT); } }); -}; -export async function deletePhotoFormAction(formData: FormData) { - return safelyRunAdminServerAction(async () => +export const deletePhotoFormAction = async (formData: FormData) => + safelyRunAdminServerAction(() => deletePhotoAction( formData.get('id') as string, formData.get('url') as string, ) ); -}; -export async function deletePhotoTagGloballyAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +export const deletePhotoTagGloballyAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { const tag = formData.get('tag') as string; await sqlDeletePhotoTagGlobally(tag); @@ -120,10 +117,9 @@ export async function deletePhotoTagGloballyAction(formData: FormData) { revalidatePhotosKey(); revalidateAdminPaths(); }); -} -export async function renamePhotoTagGloballyAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +export const renamePhotoTagGloballyAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { const tag = formData.get('tag') as string; const updatedTag = formData.get('updatedTag') as string; @@ -134,10 +130,9 @@ export async function renamePhotoTagGloballyAction(formData: FormData) { redirect(PATH_ADMIN_TAGS); } }); -} -export async function deleteBlobPhotoAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +export const deleteBlobPhotoAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { await deleteStorageUrl(formData.get('url') as string); revalidateAdminPaths(); @@ -146,28 +141,27 @@ export async function deleteBlobPhotoAction(formData: FormData) { redirect(PATH_ADMIN_PHOTOS); } }); -} -export async function getExifDataAction( +// Accessed from admin photo edit page +// will not update blur data +export const getExifDataAction = async ( photoFormPrevious: Partial, -): Promise> { - return safelyRunAdminServerAction(async () => { +): Promise> => + safelyRunAdminServerAction(async () => { const { url } = photoFormPrevious; if (url) { - const { photoFormExif } = await extractImageDataFromBlobPath( - url, { - generateBlurData: true, - }); + const { photoFormExif } = await extractImageDataFromBlobPath(url); if (photoFormExif) { return photoFormExif; } } return {}; }); -} -export async function syncPhotoExifDataAction(formData: FormData) { - return safelyRunAdminServerAction(async () => { +// Accessed from admin photo table +// will update blur data +export const syncPhotoExifDataAction = async (formData: FormData) => + safelyRunAdminServerAction(async () => { const photoId = formData.get('id') as string; if (photoId) { const photo = await getPhoto(photoId); @@ -187,26 +181,21 @@ export async function syncPhotoExifDataAction(formData: FormData) { } } }); -} -export async function syncCacheAction() { - return safelyRunAdminServerAction(revalidateAllKeysAndPaths); -} +export const syncCacheAction = async () => + safelyRunAdminServerAction(revalidateAllKeysAndPaths); -export async function streamAiImageQueryAction( +export const streamAiImageQueryAction = async ( imageBase64: string, query: AiImageQuery, -) { - return safelyRunAdminServerAction(async () => - streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query])); -} - -export const getPhotosCachedAction = async ( - offset: number, - limit: number, - includeHidden?: boolean, ) => - getPhotosCachedCached({ offset, includeHidden, limit }); + safelyRunAdminServerAction(() => + streamOpenAiImageQuery(imageBase64, AI_IMAGE_QUERIES[query])); + +export const getImageBlurAction = async (url: string) => + safelyRunAdminServerAction(() => blurImageFromUrl(url)); + +// Public actions export const getPhotosAction = async ( offset: number, @@ -215,8 +204,12 @@ export const getPhotosAction = async ( ) => getPhotos({ offset, includeHidden, limit }); -export const getImageBlurAction = async (url: string) => - blurImageFromUrl(url); +export const getPhotosCachedAction = async ( + offset: number, + limit: number, + includeHidden?: boolean, +) => + getPhotosCachedCached({ offset, includeHidden, limit }); export const queryPhotosByTitleAction = async (query: string) => (await getPhotos({ query, limit: 10 })) From a493619bcadf7c3890210b1d05fb0d76978a1c14 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 10:30:32 -0500 Subject: [PATCH 10/19] Only generate image thumbnails when AI is enabled --- src/app/admin/photos/[photoId]/edit/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index 4ca6dcb6..b89549d5 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -19,9 +19,10 @@ export default async function PhotoEditPage({ const hasAiTextGeneration = AI_TEXT_GENERATION_ENABLED; - const imageThumbnailBase64 = await resizeImageFromUrl( - getNextImageUrlForRequest(photo.url, 640), - ); + // Only generate image thumbnails when AI generation is enabled + const imageThumbnailBase64 = AI_TEXT_GENERATION_ENABLED + ? await resizeImageFromUrl(getNextImageUrlForRequest(photo.url, 640)) + : ''; return ( Date: Mon, 6 May 2024 10:34:51 -0500 Subject: [PATCH 11/19] Rename admin tables --- .../{AdminPhotoTable.tsx => AdminPhotosTable.tsx} | 2 +- ...leInfinite.tsx => AdminPhotosTableInfinite.tsx} | 6 +++--- .../{StorageUrls.tsx => AdminUploadsTable.tsx} | 2 +- src/app/admin/photos/page.tsx | 14 +++++++------- src/app/admin/uploads/page.tsx | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) rename src/admin/{AdminPhotoTable.tsx => AdminPhotosTable.tsx} (98%) rename src/admin/{AdminPhotoTableInfinite.tsx => AdminPhotosTableInfinite.tsx} (82%) rename src/admin/{StorageUrls.tsx => AdminUploadsTable.tsx} (98%) diff --git a/src/admin/AdminPhotoTable.tsx b/src/admin/AdminPhotosTable.tsx similarity index 98% rename from src/admin/AdminPhotoTable.tsx rename to src/admin/AdminPhotosTable.tsx index 4354bf8c..a0802161 100644 --- a/src/admin/AdminPhotoTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -21,7 +21,7 @@ import { import { useAppState } from '@/state/AppState'; import { RevalidatePhoto } from '@/photo/InfinitePhotoScroll'; -export default function AdminPhotoTable({ +export default function AdminPhotosTable({ photos, onLastPhotoVisible, revalidatePhoto, diff --git a/src/admin/AdminPhotoTableInfinite.tsx b/src/admin/AdminPhotosTableInfinite.tsx similarity index 82% rename from src/admin/AdminPhotoTableInfinite.tsx rename to src/admin/AdminPhotosTableInfinite.tsx index ab8bf1e9..16975bb7 100644 --- a/src/admin/AdminPhotoTableInfinite.tsx +++ b/src/admin/AdminPhotosTableInfinite.tsx @@ -3,9 +3,9 @@ import InfinitePhotoScroll, { InfinitePhotoScrollExternalProps, } from '../photo/InfinitePhotoScroll'; -import AdminPhotoTable from './AdminPhotoTable'; +import AdminPhotosTable from './AdminPhotosTable'; -export default function AdminPhotoTableInfinite({ +export default function AdminPhotosTableInfinite({ initialOffset, itemsPerPage, }: InfinitePhotoScrollExternalProps) { @@ -18,7 +18,7 @@ export default function AdminPhotoTableInfinite({ includeHiddenPhotos > {({ photos, onLastPhotoVisible, revalidatePhoto }) => - -
}
- + {photosCount > photos.length && - } diff --git a/src/app/admin/uploads/page.tsx b/src/app/admin/uploads/page.tsx index ce1273c3..e4fecd0f 100644 --- a/src/app/admin/uploads/page.tsx +++ b/src/app/admin/uploads/page.tsx @@ -1,4 +1,4 @@ -import StorageUrls from '@/admin/StorageUrls'; +import AdminUploadsTable from '@/admin/AdminUploadsTable'; import { getStorageUploadUrlsNoStore } from '@/services/storage/cache'; import SiteGrid from '@/components/SiteGrid'; @@ -6,7 +6,7 @@ export default async function AdminUploadsPage() { const storageUrls = await getStorageUploadUrlsNoStore(); return ( } + contentMain={} /> ); } From afd0e23a67ece409b3c8f67421637196725b86d5 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 12:00:46 -0500 Subject: [PATCH 12/19] Show loading indicator when adding uploads --- src/admin/AddButton.tsx | 24 ++++----- src/admin/AdminPhotosTable.tsx | 4 +- src/admin/AdminUploadsTable.tsx | 2 +- src/admin/ClearCacheButton.tsx | 2 +- src/admin/DeleteButton.tsx | 7 ++- src/admin/LoaderButton.tsx | 54 +++++++++++++++++++ src/components/PathLoaderButton.tsx | 66 +++++++++++++++++++++++ src/components/SubmitButtonWithStatus.tsx | 30 ++++------- src/site/globals.css | 3 +- 9 files changed, 150 insertions(+), 42 deletions(-) create mode 100644 src/admin/LoaderButton.tsx create mode 100644 src/components/PathLoaderButton.tsx diff --git a/src/admin/AddButton.tsx b/src/admin/AddButton.tsx index b1bdede6..f776b6d7 100644 --- a/src/admin/AddButton.tsx +++ b/src/admin/AddButton.tsx @@ -1,23 +1,17 @@ -import Link from 'next/link'; import { BiImageAdd } from 'react-icons/bi'; +import PathLoaderButton from '@/components/PathLoaderButton'; -export default function AddButton ({ - href, - label = 'Add', +export default function AddButton({ + path, }: { - href: string, - label?: string, + path: string, }) { return ( - } > - - - {label} - - + Add + ); } diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index a0802161..e501c963 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -91,7 +91,9 @@ export default function AdminPhotosTable({ > } + icon={} onFormSubmitToastMessage={` "${titleForPhoto(photo)}" EXIF data synced `} diff --git a/src/admin/AdminUploadsTable.tsx b/src/admin/AdminUploadsTable.tsx index 316f7222..1427f594 100644 --- a/src/admin/AdminUploadsTable.tsx +++ b/src/admin/AdminUploadsTable.tsx @@ -49,7 +49,7 @@ export default function AdminUploadsTable({ 'flex flex-nowrap', 'gap-2 sm:gap-3 items-center', )}> - + } + icon={} onFormSubmit={invalidateSwr} > Clear Cache diff --git a/src/admin/DeleteButton.tsx b/src/admin/DeleteButton.tsx index 7c570903..7c048beb 100644 --- a/src/admin/DeleteButton.tsx +++ b/src/admin/DeleteButton.tsx @@ -14,6 +14,7 @@ export default function DeleteButton ( const { onFormSubmit: onFormSubmitProps, clearLocalState, + className, ...rest } = props; @@ -30,11 +31,13 @@ export default function DeleteButton ( return } + icon={} spinnerColor="text" className={clsx( - 'text-red-500 dark:text-red-600', + className, + '!text-red-500 dark:!text-red-600', 'active:!bg-red-100/50 active:dark:!bg-red-950/50', + 'disabled:!bg-red-100/50 disabled:dark:!bg-red-950/50', '!border-red-200 hover:!border-red-300', 'dark:!border-red-900/75 dark:hover:!border-red-900', )} diff --git a/src/admin/LoaderButton.tsx b/src/admin/LoaderButton.tsx new file mode 100644 index 00000000..b6c55329 --- /dev/null +++ b/src/admin/LoaderButton.tsx @@ -0,0 +1,54 @@ +import Spinner, { SpinnerColor } from '@/components/Spinner'; +import { clsx } from 'clsx/lite'; +import { ButtonHTMLAttributes, ReactNode } from 'react'; + +export default function LoaderButton(props: { + children?: ReactNode + isLoading?: boolean + icon?: JSX.Element + spinnerColor?: SpinnerColor + styleAsLink?: boolean +} & ButtonHTMLAttributes) { + const { + children, + isLoading, + icon, + spinnerColor, + styleAsLink, + type = 'button', + disabled, + className, + ...rest + } = props; + return ( + + ); +} diff --git a/src/components/PathLoaderButton.tsx b/src/components/PathLoaderButton.tsx new file mode 100644 index 00000000..ba121f4d --- /dev/null +++ b/src/components/PathLoaderButton.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { ReactNode, useEffect, useState, useTransition } from 'react'; +import { SpinnerColor } from './Spinner'; +import LoaderButton from '@/admin/LoaderButton'; + +export default function PathLoaderButton({ + path, + icon, + prefetch, + loaderDelay = 100, + shouldScroll = true, + shouldReplace, + spinnerColor, + children, +}: { + path: string + icon?: JSX.Element + prefetch?: boolean + loaderDelay?: number + shouldScroll?: boolean + shouldReplace?: boolean + spinnerColor?: SpinnerColor + children?: ReactNode +}) { + const router = useRouter(); + + const [isPending, startTransition] = useTransition(); + + const [shouldShowLoader, setShouldShowLoader] = useState(false); + + useEffect(() => { + if (isPending) { + const timeout = setTimeout(() => { + setShouldShowLoader(true); + }, loaderDelay); + return () => clearTimeout(timeout); + } else { + setShouldShowLoader(false); + } + }, [isPending, loaderDelay]); + + useEffect(() => { + if (prefetch) { + router.prefetch(path); + } + }, [prefetch, router, path]); + + return ( + startTransition(() => { + if (shouldReplace) { + router.replace(path, { scroll: shouldScroll }); + } else { + router.push(path, { scroll: shouldScroll }); + } + })} + isLoading={shouldShowLoader} + spinnerColor={spinnerColor} + > + {children} + + ); +} diff --git a/src/components/SubmitButtonWithStatus.tsx b/src/components/SubmitButtonWithStatus.tsx index 514b138d..3cd6a248 100644 --- a/src/components/SubmitButtonWithStatus.tsx +++ b/src/components/SubmitButtonWithStatus.tsx @@ -2,9 +2,10 @@ import { HTMLProps, useEffect, useRef } from 'react'; import { useFormStatus } from 'react-dom'; -import Spinner, { SpinnerColor } from './Spinner'; +import { SpinnerColor } from './Spinner'; import { clsx } from 'clsx/lite'; import { toastSuccess } from '@/toast'; +import LoaderButton from '@/admin/LoaderButton'; interface Props extends HTMLProps { icon?: JSX.Element @@ -49,34 +50,21 @@ export default function SubmitButtonWithStatus({ }, [onFormStatusChange, pending]); return ( - + {children} + ); }; diff --git a/src/site/globals.css b/src/site/globals.css index 5052b267..f0fdf1a1 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -77,12 +77,13 @@ cursor-pointer hover:no-underline inline-flex gap-2 items-center - px-4 + px-3 text-base shadow-sm active:bg-gray-100 dark:active:bg-gray-900 hover:border-gray-300 dark:hover:border-gray-600 disabled:cursor-not-allowed + disabled:text-dim disabled:bg-gray-100 dark:disabled:bg-gray-900 disabled:border-gray-200 disabled:dark:border-gray-700 } From af0b004a79b06c801c579dea1834483a347abb75 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 21:34:04 -0500 Subject: [PATCH 13/19] Auto-generate blur data when editing photos --- src/admin/EditButton.tsx | 19 ++++++------------- src/app/admin/photos/[photoId]/edit/page.tsx | 11 ++++++++--- src/photo/PhotoEditPageClient.tsx | 3 +++ src/photo/form/PhotoForm.tsx | 17 +++++++++++++---- src/services/next-image.ts | 5 +++++ 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/admin/EditButton.tsx b/src/admin/EditButton.tsx index 113aec65..112e5fd9 100644 --- a/src/admin/EditButton.tsx +++ b/src/admin/EditButton.tsx @@ -1,24 +1,17 @@ -import Link from 'next/link'; +import PathLoaderButton from '@/components/PathLoaderButton'; import { FaRegEdit } from 'react-icons/fa'; export default function EditButton ({ href, - label = 'Edit', }: { href: string, - label?: string, }) { return ( - } > - - - {label} - - + Edit + ); } diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index b89549d5..38d9fa0c 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -3,8 +3,8 @@ import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache'; import { PATH_ADMIN } from '@/site/paths'; import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; -import { resizeImageFromUrl } from '@/photo/server'; -import { getNextImageUrlForRequest } from '@/services/next-image'; +import { blurImageFromUrl, resizeImageFromUrl } from '@/photo/server'; +import { getNextImageUrlForManipulation } from '@/services/next-image'; export default async function PhotoEditPage({ params: { photoId }, @@ -21,15 +21,20 @@ export default async function PhotoEditPage({ // Only generate image thumbnails when AI generation is enabled const imageThumbnailBase64 = AI_TEXT_GENERATION_ENABLED - ? await resizeImageFromUrl(getNextImageUrlForRequest(photo.url, 640)) + ? await resizeImageFromUrl(getNextImageUrlForManipulation(photo.url)) : ''; + const blurData = await blurImageFromUrl( + getNextImageUrlForManipulation(photo.url) + ); + return ( ); }; diff --git a/src/photo/PhotoEditPageClient.tsx b/src/photo/PhotoEditPageClient.tsx index 94561417..77b83492 100644 --- a/src/photo/PhotoEditPageClient.tsx +++ b/src/photo/PhotoEditPageClient.tsx @@ -22,11 +22,13 @@ export default function PhotoEditPageClient({ uniqueTags, hasAiTextGeneration, imageThumbnailBase64, + blurData, }: { photo: Photo uniqueTags: TagsWithMeta hasAiTextGeneration: boolean imageThumbnailBase64: string + blurData: string }) { const seedExifData = { url: photo.url }; @@ -86,6 +88,7 @@ export default function PhotoEditPageClient({ updatedExifData={hasExifDataBeenFound ? updatedExifData : undefined} + updatedBlurData={blurData} uniqueTags={uniqueTags} aiContent={hasAiTextGeneration ? aiContent : undefined} onTitleChange={setUpdatedTitle} diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index f1b81c2e..674c8ff1 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -27,22 +27,25 @@ import Spinner from '@/components/Spinner'; import usePreventNavigation from '@/utility/usePreventNavigation'; import { useAppState } from '@/state/AppState'; import UpdateBlurDataButton from '../UpdateBlurDataButton'; +import { getNextImageUrlForManipulation } from '@/services/next-image'; const THUMBNAIL_SIZE = 300; export default function PhotoForm({ + type = 'create', initialPhotoForm, updatedExifData, - type = 'create', + updatedBlurData, uniqueTags, aiContent, onTitleChange, onTextContentChange, onFormStatusChange, }: { + type?: 'create' | 'edit' initialPhotoForm: Partial updatedExifData?: Partial - type?: 'create' | 'edit' + updatedBlurData?: string uniqueTags?: TagsWithMeta aiContent?: AiContent onTitleChange?: (updatedTitle: string) => void @@ -110,6 +113,12 @@ export default function PhotoForm({ const url = formData.url ?? ''; + useEffect(() => + setFormData(data => updatedBlurData + ? { ...data, blurData: updatedBlurData } + : data) + , [updatedBlurData]); + useEffect(() => setFormData(data => aiContent?.title ? { ...data, title: aiContent?.title } @@ -184,9 +193,9 @@ export default function PhotoForm({ shouldConfirm={Boolean(formData.semanticDescription)} />; case 'blurData': - return type === 'edit' + return shouldDebugBlur && type === 'edit' && formData.url ? setFormData(data => ({ ...data, blurData }))} /> diff --git a/src/services/next-image.ts b/src/services/next-image.ts index 9b8cc116..13ce7ffe 100644 --- a/src/services/next-image.ts +++ b/src/services/next-image.ts @@ -23,3 +23,8 @@ export const getNextImageUrlForRequest = ( return url.toString(); }; + +// Generate small, low-bandwidth images for quick manipulations such as +// generating blur data or image thumbnails for AI text generation +export const getNextImageUrlForManipulation = (imageUrl: string) => + getNextImageUrlForRequest(imageUrl, 640, 90); From 547def17215b862bb9fe760b4ac01125552a8f94 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 6 May 2024 23:30:45 -0500 Subject: [PATCH 14/19] Tweak footer spacing --- src/site/Footer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/site/Footer.tsx b/src/site/Footer.tsx index f598ec38..f874bc62 100644 --- a/src/site/Footer.tsx +++ b/src/site/Footer.tsx @@ -34,10 +34,10 @@ export default function Footer() { ? [
-
+
{isPathAdmin(pathname) ? <> {userEmail === undefined && From 2e4208e7e1b22451092650079e04c976ce54e0f1 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 7 May 2024 00:03:28 -0500 Subject: [PATCH 15/19] Streamline primitive components --- src/admin/AddButton.tsx | 2 +- src/admin/AdminPhotosTable.tsx | 2 +- src/admin/AdminTagTable.tsx | 2 +- src/admin/EditButton.tsx | 8 +-- src/components/IconButton.tsx | 52 -------------- src/components/IconPathButton.tsx | 69 ------------------- src/components/ShareButton.tsx | 28 +++++--- src/components/SubmitButtonWithStatus.tsx | 2 +- .../primitives}/LoaderButton.tsx | 4 +- .../{ => primitives}/PathLoaderButton.tsx | 10 ++- src/photo/PhotoLarge.tsx | 4 ++ src/photo/PhotoSetHeader.tsx | 8 ++- src/site/SiteChecklistClient.tsx | 13 ++-- src/site/globals.css | 2 +- 14 files changed, 55 insertions(+), 151 deletions(-) delete mode 100644 src/components/IconButton.tsx delete mode 100644 src/components/IconPathButton.tsx rename src/{admin => components/primitives}/LoaderButton.tsx (92%) rename src/components/{ => primitives}/PathLoaderButton.tsx (85%) diff --git a/src/admin/AddButton.tsx b/src/admin/AddButton.tsx index f776b6d7..cba511a6 100644 --- a/src/admin/AddButton.tsx +++ b/src/admin/AddButton.tsx @@ -1,5 +1,5 @@ import { BiImageAdd } from 'react-icons/bi'; -import PathLoaderButton from '@/components/PathLoaderButton'; +import PathLoaderButton from '@/components/primitives/PathLoaderButton'; export default function AddButton({ path, diff --git a/src/admin/AdminPhotosTable.tsx b/src/admin/AdminPhotosTable.tsx index e501c963..f4effb3f 100644 --- a/src/admin/AdminPhotosTable.tsx +++ b/src/admin/AdminPhotosTable.tsx @@ -80,7 +80,7 @@ export default function AdminPhotosTable({ 'flex flex-nowrap', 'gap-2 sm:gap-3 items-center', )}> - + - + } > Edit diff --git a/src/components/IconButton.tsx b/src/components/IconButton.tsx deleted file mode 100644 index b446752f..00000000 --- a/src/components/IconButton.tsx +++ /dev/null @@ -1,52 +0,0 @@ -'use client'; - -import { clsx } from 'clsx/lite'; -import Spinner, { SpinnerColor } from './Spinner'; - -export default function IconButton({ - icon, - onClick, - isLoading, - className, - spinnerColor, - spinnerSize, -}: { - icon: JSX.Element - onClick?: () => void - isLoading?: boolean - className?: string - spinnerColor?: SpinnerColor - spinnerSize?: number -}) { - return ( - - {!isLoading - ? - : - - } - - ); -} diff --git a/src/components/IconPathButton.tsx b/src/components/IconPathButton.tsx deleted file mode 100644 index 4108d89d..00000000 --- a/src/components/IconPathButton.tsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client'; - -import { useRouter } from 'next/navigation'; -import IconButton from './IconButton'; -import { useEffect, useState, useTransition } from 'react'; -import { clsx } from 'clsx/lite'; -import { SpinnerColor } from './Spinner'; - -export default function IconPathButton({ - icon, - path, - prefetch, - loaderDelay = 250, - shouldScroll = true, - shouldReplace, - spinnerColor, -}: { - icon: JSX.Element - path: string - prefetch?: boolean - loaderDelay?: number - shouldScroll?: boolean - shouldReplace?: boolean - spinnerColor?: SpinnerColor -}) { - const router = useRouter(); - - const [isPending, startTransition] = useTransition(); - - const [shouldShowLoader, setShouldShowLoader] = useState(false); - - useEffect(() => { - if (isPending) { - const timeout = setTimeout(() => { - setShouldShowLoader(true); - }, loaderDelay); - return () => clearTimeout(timeout); - } else { - setShouldShowLoader(false); - } - }, [isPending, loaderDelay]); - - useEffect(() => { - if (prefetch) { - router.prefetch(path); - } - }, [prefetch, router, path]); - - return ( - startTransition(() => { - if (shouldReplace) { - router.replace(path, { scroll: shouldScroll }); - } else { - router.push(path, { scroll: shouldScroll }); - } - })} - isLoading={shouldShowLoader} - className={clsx( - 'translate-y-[-0.5px]', - 'active:translate-y-[1px]', - 'text-medium', - 'active:text-gray-600 dark:active:text-gray-300', - )} - spinnerColor={spinnerColor ?? 'text'} - /> - ); -} diff --git a/src/components/ShareButton.tsx b/src/components/ShareButton.tsx index 02057aac..b0af3664 100644 --- a/src/components/ShareButton.tsx +++ b/src/components/ShareButton.tsx @@ -1,27 +1,33 @@ import { TbPhotoShare } from 'react-icons/tb'; -import IconPathButton from '@/components/IconPathButton'; +import PathLoaderButton from './primitives/PathLoaderButton'; +import clsx from 'clsx'; export default function ShareButton({ path, prefetch, shouldScroll, dim, + className, }: { path: string prefetch?: boolean shouldScroll?: boolean dim?: boolean + className?: string }) { return ( - , - prefetch, - shouldScroll, - shouldReplace: true, - spinnerColor: 'dim', - }} /> + } + spinnerColor="dim" + prefetch={prefetch} + shouldScroll={shouldScroll} + shouldReplace + styleAsLink + /> ); } diff --git a/src/components/SubmitButtonWithStatus.tsx b/src/components/SubmitButtonWithStatus.tsx index 3cd6a248..502abb22 100644 --- a/src/components/SubmitButtonWithStatus.tsx +++ b/src/components/SubmitButtonWithStatus.tsx @@ -5,7 +5,7 @@ import { useFormStatus } from 'react-dom'; import { SpinnerColor } from './Spinner'; import { clsx } from 'clsx/lite'; import { toastSuccess } from '@/toast'; -import LoaderButton from '@/admin/LoaderButton'; +import LoaderButton from '@/components/primitives/LoaderButton'; interface Props extends HTMLProps { icon?: JSX.Element diff --git a/src/admin/LoaderButton.tsx b/src/components/primitives/LoaderButton.tsx similarity index 92% rename from src/admin/LoaderButton.tsx rename to src/components/primitives/LoaderButton.tsx index b6c55329..1abc4993 100644 --- a/src/admin/LoaderButton.tsx +++ b/src/components/primitives/LoaderButton.tsx @@ -26,7 +26,9 @@ export default function LoaderButton(props: { type={type} className={clsx( className, - styleAsLink ? 'link h-4' : 'h-9', + styleAsLink + ? 'link h-4 hover:text-dim active:text-medium' + : 'h-9', 'inline-flex items-center gap-2 self-start', )} disabled={isLoading || disabled} diff --git a/src/components/PathLoaderButton.tsx b/src/components/primitives/PathLoaderButton.tsx similarity index 85% rename from src/components/PathLoaderButton.tsx rename to src/components/primitives/PathLoaderButton.tsx index ba121f4d..dc61947a 100644 --- a/src/components/PathLoaderButton.tsx +++ b/src/components/primitives/PathLoaderButton.tsx @@ -2,8 +2,8 @@ import { useRouter } from 'next/navigation'; import { ReactNode, useEffect, useState, useTransition } from 'react'; -import { SpinnerColor } from './Spinner'; -import LoaderButton from '@/admin/LoaderButton'; +import { SpinnerColor } from '../Spinner'; +import LoaderButton from '@/components/primitives/LoaderButton'; export default function PathLoaderButton({ path, @@ -13,6 +13,8 @@ export default function PathLoaderButton({ shouldScroll = true, shouldReplace, spinnerColor, + styleAsLink, + className, children, }: { path: string @@ -22,6 +24,8 @@ export default function PathLoaderButton({ shouldScroll?: boolean shouldReplace?: boolean spinnerColor?: SpinnerColor + styleAsLink?: boolean + className?: string children?: ReactNode }) { const router = useRouter(); @@ -50,6 +54,7 @@ export default function PathLoaderButton({ return ( startTransition(() => { if (shouldReplace) { router.replace(path, { scroll: shouldScroll }); @@ -59,6 +64,7 @@ export default function PathLoaderButton({ })} isLoading={shouldShowLoader} spinnerColor={spinnerColor} + styleAsLink={styleAsLink} > {children} diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index c094696d..a745b8ab 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -170,6 +170,10 @@ export default function PhotoLarge({ {photo.takenAtNaiveFormatted}
- - } + } ; const renderCopyButton = (label: string, text: string, subtle?: boolean) => - } - className={clsx(subtle && 'text-gray-300 dark:text-gray-700')} + className={clsx( + 'translate-y-[2px]', + subtle && 'text-gray-300 dark:text-gray-700', + )} onClick={() => { navigator.clipboard.writeText(text); toastSuccess(`${label} copied to clipboard`); }} + styleAsLink />; const renderEnvVar = ( @@ -235,11 +239,12 @@ export default function SiteChecklistClient({ {secret}
{renderCopyButton('Secret', secret)} - } onClick={refreshSecret} isLoading={isPendingSecret} spinnerColor="text" + styleAsLink />
diff --git a/src/site/globals.css b/src/site/globals.css index f0fdf1a1..c4b8cf43 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -112,7 +112,7 @@ button.link { @apply p-0 min-h-0 - border-none bg-transparent active:bg-transparent shadow-none + border-none bg-transparent active:bg-transparent shadow-none rounded-none } a, .link { @apply From bbc0a4e23945cec8cff724416b5d83dca85589c9 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 7 May 2024 00:15:40 -0500 Subject: [PATCH 16/19] Prevent server-side blur generation when disabled --- src/app/admin/photos/[photoId]/edit/page.tsx | 10 ++++++---- src/photo/form/PhotoForm.tsx | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/app/admin/photos/[photoId]/edit/page.tsx b/src/app/admin/photos/[photoId]/edit/page.tsx index 38d9fa0c..8affc888 100644 --- a/src/app/admin/photos/[photoId]/edit/page.tsx +++ b/src/app/admin/photos/[photoId]/edit/page.tsx @@ -2,7 +2,7 @@ import { redirect } from 'next/navigation'; import { getPhotoNoStore, getUniqueTagsCached } from '@/photo/cache'; import { PATH_ADMIN } from '@/site/paths'; import PhotoEditPageClient from '@/photo/PhotoEditPageClient'; -import { AI_TEXT_GENERATION_ENABLED } from '@/site/config'; +import { AI_TEXT_GENERATION_ENABLED, BLUR_ENABLED } from '@/site/config'; import { blurImageFromUrl, resizeImageFromUrl } from '@/photo/server'; import { getNextImageUrlForManipulation } from '@/services/next-image'; @@ -24,9 +24,11 @@ export default async function PhotoEditPage({ ? await resizeImageFromUrl(getNextImageUrlForManipulation(photo.url)) : ''; - const blurData = await blurImageFromUrl( - getNextImageUrlForManipulation(photo.url) - ); + const blurData = BLUR_ENABLED + ? await blurImageFromUrl( + getNextImageUrlForManipulation(photo.url) + ) + : ''; return ( - setFormData(data => updatedBlurData - ? { ...data, blurData: updatedBlurData } - : data) - , [updatedBlurData]); + useEffect(() => { + if (updatedBlurData) { + setFormData(data => updatedBlurData + ? { ...data, blurData: updatedBlurData } + : data); + } + }, [updatedBlurData]); useEffect(() => setFormData(data => aiContent?.title From 97830c50aeb4e2eaaf7b6f364616ff19ff55f9b8 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 7 May 2024 09:36:46 -0500 Subject: [PATCH 17/19] Improve blur data form handling --- src/app/admin/uploads/[uploadPath]/page.tsx | 12 +++++++++--- src/photo/UploadPageClient.tsx | 2 +- src/photo/actions.ts | 3 ++- src/photo/ai/useAiImageQueries.ts | 2 +- src/photo/form/PhotoForm.tsx | 17 +++++++++++++---- src/photo/form/index.ts | 12 +----------- src/photo/form/usePhotoFormParent.ts | 2 +- 7 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/app/admin/uploads/[uploadPath]/page.tsx b/src/app/admin/uploads/[uploadPath]/page.tsx index 3fecf27c..e9e252b0 100644 --- a/src/app/admin/uploads/[uploadPath]/page.tsx +++ b/src/app/admin/uploads/[uploadPath]/page.tsx @@ -6,6 +6,7 @@ import UploadPageClient from '@/photo/UploadPageClient'; import { AI_TEXT_AUTO_GENERATED_FIELDS, AI_TEXT_GENERATION_ENABLED, + BLUR_ENABLED, } from '@/site/config'; interface Params { @@ -19,11 +20,16 @@ export default async function UploadPage({ params: { uploadPath } }: Params) { imageResizedBase64: imageThumbnailBase64, } = await extractImageDataFromBlobPath(uploadPath, { includeInitialPhotoFields: true, - generateBlurData: true, - generateResizedImage: true, + generateBlurData: BLUR_ENABLED, + generateResizedImage: AI_TEXT_GENERATION_ENABLED, }); - if (!photoFormExif || !imageThumbnailBase64) { redirect(PATH_ADMIN); } + if ( + !photoFormExif || + (AI_TEXT_GENERATION_ENABLED && !imageThumbnailBase64) + ) { + redirect(PATH_ADMIN); + } const uniqueTags = await getUniqueTagsCached(); diff --git a/src/photo/UploadPageClient.tsx b/src/photo/UploadPageClient.tsx index 912b840f..cbd106f1 100644 --- a/src/photo/UploadPageClient.tsx +++ b/src/photo/UploadPageClient.tsx @@ -23,7 +23,7 @@ export default function UploadPageClient({ uniqueTags: TagsWithMeta hasAiTextGeneration?: boolean textFieldsToAutoGenerate?: AiAutoGeneratedField[], - imageThumbnailBase64: string + imageThumbnailBase64?: string }) { const { pending, diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 2e0d01d8..f2d190dc 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -39,6 +39,7 @@ import { convertPhotoToPhotoDbInsert } from '.'; import { safelyRunAdminServerAction } from '@/auth'; import { AI_IMAGE_QUERIES, AiImageQuery } from './ai'; import { streamOpenAiImageQuery } from '@/services/openai'; +import { BLUR_ENABLED } from '@/site/config'; // Private actions @@ -168,7 +169,7 @@ export const syncPhotoExifDataAction = async (formData: FormData) => if (photo) { const { photoFormExif } = await extractImageDataFromBlobPath( photo.url, { - generateBlurData: true, + generateBlurData: BLUR_ENABLED, }); if (photoFormExif) { const photoFormDbInsert = convertFormDataToPhotoDbInsert({ diff --git a/src/photo/ai/useAiImageQueries.ts b/src/photo/ai/useAiImageQueries.ts index 618af005..8c78ab7c 100644 --- a/src/photo/ai/useAiImageQueries.ts +++ b/src/photo/ai/useAiImageQueries.ts @@ -7,7 +7,7 @@ export type AiContent = ReturnType; export default function useAiImageQueries( textFieldsToAutoGenerate: AiAutoGeneratedField[] = [], - imageData: string, + imageData?: string, ) { const [ requestTitleCaption, diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index 43a6a984..ce69fd7f 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -28,6 +28,8 @@ import usePreventNavigation from '@/utility/usePreventNavigation'; import { useAppState } from '@/state/AppState'; import UpdateBlurDataButton from '../UpdateBlurDataButton'; import { getNextImageUrlForManipulation } from '@/services/next-image'; +import { BLUR_ENABLED } from '@/site/config'; +import { PhotoDbInsert } from '..'; const THUMBNAIL_SIZE = 300; @@ -118,6 +120,8 @@ export default function PhotoForm({ setFormData(data => updatedBlurData ? { ...data, blurData: updatedBlurData } : data); + } else if (!BLUR_ENABLED) { + setFormData(data => ({ ...data, blurData: '' })); } }, [updatedBlurData]); @@ -206,6 +210,14 @@ export default function PhotoForm({ } }; + const shouldHideField = ( + key: keyof PhotoDbInsert | 'favorite', + hideIfEmpty?: boolean, + shouldHide?: (formData: Partial) => boolean, + ) => + (hideIfEmpty && !formData[key]) || + shouldHide?.(formData); + return (
@@ -278,10 +290,7 @@ export default function PhotoForm({ loadingMessage, type, }]) => - ( - (!hideIfEmpty || formData[key]) && - !shouldHide?.(formData) - ) && + !shouldHideField(key, hideIfEmpty, shouldHide) && textFieldsToAutoGenerate?: AiAutoGeneratedField[] - imageThumbnailBase64: string, + imageThumbnailBase64?: string, }) { const [pending, setIsPending] = useState(false); const [updatedTitle, setUpdatedTitle] = useState(''); From 7b4fe756b34bc1838f7cd2635d85e7066092c7d6 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 7 May 2024 10:26:26 -0500 Subject: [PATCH 18/19] Refine blur compatibility behavior --- src/components/ImageBlurFallback.tsx | 19 +++++++++++++------ src/components/ImageLarge.tsx | 2 +- src/components/ImageSmall.tsx | 2 +- src/components/ImageTiny.tsx | 2 +- src/photo/form/PhotoForm.tsx | 14 +++++++++++--- src/photo/index.ts | 4 ++-- 6 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/components/ImageBlurFallback.tsx b/src/components/ImageBlurFallback.tsx index 7c2aa474..acdd58fc 100644 --- a/src/components/ImageBlurFallback.tsx +++ b/src/components/ImageBlurFallback.tsx @@ -8,13 +8,13 @@ import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; export default function ImageBlurFallback(props: ImageProps & { - blurCompatibilityMode?: boolean + blurCompatibilityLevel?: 'none' | 'low' | 'high'; }) { const { className, priority, blurDataURL, - blurCompatibilityMode, + blurCompatibilityLevel = 'low', ...rest } = props; @@ -54,6 +54,16 @@ export default function ImageBlurFallback(props: ImageProps & { !wasCached && !hideBlurPlaceholder; + const getBlurClass = () => { + switch (blurCompatibilityLevel) { + case 'high': + // Fix poorly blurred placeholder data generated on client + return 'blur-[4px] @xs:blue-md scale-[1.05]'; + case 'low': + return 'blur-[2px] @xs:blue-md scale-[1.01]'; + } + }; + return (
:
diff --git a/src/photo/form/PhotoForm.tsx b/src/photo/form/PhotoForm.tsx index ce69fd7f..e180b6e2 100644 --- a/src/photo/form/PhotoForm.tsx +++ b/src/photo/form/PhotoForm.tsx @@ -214,9 +214,16 @@ export default function PhotoForm({ key: keyof PhotoDbInsert | 'favorite', hideIfEmpty?: boolean, shouldHide?: (formData: Partial) => boolean, - ) => - (hideIfEmpty && !formData[key]) || - shouldHide?.(formData); + ) => { + if (key === 'blurData' && type === 'create' && !shouldDebugBlur) { + return true; + } else { + return ( + (hideIfEmpty && !formData[key]) || + shouldHide?.(formData) + ); + } + }; return (
@@ -230,6 +237,7 @@ export default function PhotoForm({ 'border-gray-200 dark:border-gray-700', )} blurDataURL={formData.blurData} + blurCompatibilityLevel="none" width={width} height={height} priority diff --git a/src/photo/index.ts b/src/photo/index.ts index d93af5b3..903fb8bd 100644 --- a/src/photo/index.ts +++ b/src/photo/index.ts @@ -11,7 +11,7 @@ import { formatFocalLength, } from '@/utility/exif'; import camelcaseKeys from 'camelcase-keys'; -import { isAfter } from 'date-fns'; +import { isBefore } from 'date-fns'; import type { Metadata } from 'next'; // ROOT PAGE @@ -279,4 +279,4 @@ export const isNextImageReadyBasedOnPhotos = async (photos: Photo[]) => .catch(() => false); export const doesPhotoNeedBlurCompatibility = (photo: Photo) => - isAfter(photo.updatedAt, new Date('2024-05-07')); + isBefore(photo.updatedAt, new Date('2024-05-07')); From 8483b0524882a56c6594ea711957f51e7fe675ff Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Tue, 7 May 2024 10:39:32 -0500 Subject: [PATCH 19/19] Update README blur question --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eacb861c..6389b757 100644 --- a/README.md +++ b/README.md @@ -221,4 +221,4 @@ FAQ > For a number of reasons, only EXIF orientations: 1, 3, 6, and 8 are supported. Orientations 2, 4, 5, and 7—which make use of mirroring—are not supported. #### Why does my image placeholder blur look different from photo to photo? -> Earlier template versions generated blur data on the client, which varied visually from browser to browser. Data is now generated consistently on the server. If you wish to update blur data for a particular photo: go to the photo editor, click the refresh icon next to the "Blur Data" field, and click "Update." +> Earlier versions of this template generated blur data on the client, which varied visually from browser to browser. Data is now generated consistently on the server. If you wish to update blur data for a particular photo, edit the photo in question, make no changes, and choose "Update."