diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39625d1bdcc3..4a55f3db6321 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -779,6 +779,9 @@ importers: serialize-error: specifier: ^12.0.0 version: 12.0.0 + sharp: + specifier: ^0.34.4 + version: 0.34.4 simple-git: specifier: ^3.27.0 version: 3.27.0 @@ -1652,6 +1655,9 @@ packages: '@emnapi/runtime@1.4.3': resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} + '@emnapi/runtime@1.6.0': + resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} + '@emnapi/wasi-threads@1.0.2': resolution: {integrity: sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA==} @@ -1928,111 +1934,237 @@ packages: '@iconify/utils@2.3.0': resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + '@img/sharp-darwin-arm64@0.33.5': resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] + '@img/sharp-darwin-arm64@0.34.4': + resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + '@img/sharp-darwin-x64@0.33.5': resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] + '@img/sharp-darwin-x64@0.34.4': + resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.0.4': resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} cpu: [arm64] os: [darwin] + '@img/sharp-libvips-darwin-arm64@1.2.3': + resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + cpu: [arm64] + os: [darwin] + '@img/sharp-libvips-darwin-x64@1.0.4': resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} cpu: [x64] os: [darwin] + '@img/sharp-libvips-darwin-x64@1.2.3': + resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + cpu: [x64] + os: [darwin] + '@img/sharp-libvips-linux-arm64@1.0.4': resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linux-arm64@1.2.3': + resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + '@img/sharp-libvips-linux-arm@1.2.3': + resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.3': + resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + cpu: [ppc64] + os: [linux] + '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + '@img/sharp-libvips-linux-s390x@1.2.3': + resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + cpu: [s390x] + os: [linux] + '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linux-x64@1.2.3': + resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + cpu: [x64] + os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + cpu: [arm64] + os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + cpu: [x64] + os: [linux] + '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linux-arm64@0.34.4': + resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + '@img/sharp-linux-arm@0.34.4': + resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.4': + resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + '@img/sharp-linux-s390x@0.34.4': + resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linux-x64@0.34.4': + resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + '@img/sharp-linuxmusl-arm64@0.34.4': + resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + '@img/sharp-linuxmusl-x64@0.34.4': + resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] + '@img/sharp-wasm32@0.34.4': + resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.4': + resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + '@img/sharp-win32-ia32@0.33.5': resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] + '@img/sharp-win32-ia32@0.34.4': + resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + '@img/sharp-win32-x64@0.33.5': resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] + '@img/sharp-win32-x64@0.34.4': + resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/external-editor@1.0.1': resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} engines: {node: '>=18'} @@ -5353,6 +5485,10 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} @@ -8828,6 +8964,10 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.34.4: + resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + engines: {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'} @@ -11180,6 +11320,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.6.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.0.2': dependencies: tslib: 2.8.1 @@ -11414,81 +11559,169 @@ snapshots: transitivePeerDependencies: - supports-color + '@img/colour@1.0.0': {} + '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.0.4 optional: true + '@img/sharp-darwin-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.3 + optional: true + '@img/sharp-darwin-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-darwin-x64': 1.0.4 optional: true + '@img/sharp-darwin-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.3 + optional: true + '@img/sharp-libvips-darwin-arm64@1.0.4': optional: true + '@img/sharp-libvips-darwin-arm64@1.2.3': + optional: true + '@img/sharp-libvips-darwin-x64@1.0.4': optional: true + '@img/sharp-libvips-darwin-x64@1.2.3': + optional: true + '@img/sharp-libvips-linux-arm64@1.0.4': optional: true + '@img/sharp-libvips-linux-arm64@1.2.3': + optional: true + '@img/sharp-libvips-linux-arm@1.0.5': optional: true + '@img/sharp-libvips-linux-arm@1.2.3': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.3': + optional: true + '@img/sharp-libvips-linux-s390x@1.0.4': optional: true + '@img/sharp-libvips-linux-s390x@1.2.3': + optional: true + '@img/sharp-libvips-linux-x64@1.0.4': optional: true + '@img/sharp-libvips-linux-x64@1.2.3': + optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + optional: true + '@img/sharp-libvips-linuxmusl-x64@1.0.4': optional: true + '@img/sharp-libvips-linuxmusl-x64@1.2.3': + optional: true + '@img/sharp-linux-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm64': 1.0.4 optional: true + '@img/sharp-linux-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.3 + optional: true + '@img/sharp-linux-arm@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-arm': 1.0.5 optional: true + '@img/sharp-linux-arm@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.3 + optional: true + + '@img/sharp-linux-ppc64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.3 + optional: true + '@img/sharp-linux-s390x@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-s390x': 1.0.4 optional: true + '@img/sharp-linux-s390x@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.3 + optional: true + '@img/sharp-linux-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linux-x64': 1.0.4 optional: true + '@img/sharp-linux-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.3 + optional: true + '@img/sharp-linuxmusl-arm64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 optional: true + '@img/sharp-linuxmusl-arm64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + optional: true + '@img/sharp-linuxmusl-x64@0.33.5': optionalDependencies: '@img/sharp-libvips-linuxmusl-x64': 1.0.4 optional: true + '@img/sharp-linuxmusl-x64@0.34.4': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + optional: true + '@img/sharp-wasm32@0.33.5': dependencies: '@emnapi/runtime': 1.4.3 optional: true + '@img/sharp-wasm32@0.34.4': + dependencies: + '@emnapi/runtime': 1.6.0 + optional: true + + '@img/sharp-win32-arm64@0.34.4': + optional: true + '@img/sharp-win32-ia32@0.33.5': optional: true + '@img/sharp-win32-ia32@0.34.4': + optional: true + '@img/sharp-win32-x64@0.33.5': optional: true + '@img/sharp-win32-x64@0.34.4': + optional: true + '@inquirer/external-editor@1.0.1(@types/node@24.2.1)': dependencies: chardet: 2.1.0 @@ -13919,7 +14152,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -15106,6 +15339,8 @@ snapshots: detect-libc@2.0.4: {} + detect-libc@2.1.2: {} + detect-node-es@1.1.0: {} devlop@1.1.0: @@ -19283,6 +19518,35 @@ snapshots: '@img/sharp-win32-x64': 0.33.5 optional: true + sharp@0.34.4: + dependencies: + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.4 + '@img/sharp-darwin-x64': 0.34.4 + '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-linux-arm': 0.34.4 + '@img/sharp-linux-arm64': 0.34.4 + '@img/sharp-linux-ppc64': 0.34.4 + '@img/sharp-linux-s390x': 0.34.4 + '@img/sharp-linux-x64': 0.34.4 + '@img/sharp-linuxmusl-arm64': 0.34.4 + '@img/sharp-linuxmusl-x64': 0.34.4 + '@img/sharp-wasm32': 0.34.4 + '@img/sharp-win32-arm64': 0.34.4 + '@img/sharp-win32-ia32': 0.34.4 + '@img/sharp-win32-x64': 0.34.4 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/integrations/misc/__tests__/image-compression.spec.ts b/src/integrations/misc/__tests__/image-compression.spec.ts new file mode 100644 index 000000000000..648efc245187 --- /dev/null +++ b/src/integrations/misc/__tests__/image-compression.spec.ts @@ -0,0 +1,277 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import sharp from "sharp" +import { compressImageIfNeeded, needsCompression, formatFileSize } from "../image-compression" + +vi.mock("sharp") + +describe("image-compression", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + describe("needsCompression", () => { + it("should return false for images under 5MB", () => { + const smallBuffer = Buffer.alloc(3 * 1024 * 1024) // 3MB + expect(needsCompression(smallBuffer)).toBe(false) + }) + + it("should return true for images over 5MB", () => { + const largeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB + expect(needsCompression(largeBuffer)).toBe(true) + }) + + it("should return false for images exactly 5MB", () => { + const exactBuffer = Buffer.alloc(5 * 1024 * 1024) // 5MB + expect(needsCompression(exactBuffer)).toBe(false) + }) + }) + + describe("formatFileSize", () => { + it("should format bytes to MB correctly", () => { + expect(formatFileSize(1024 * 1024)).toBe("1.0 MB") + expect(formatFileSize(5.5 * 1024 * 1024)).toBe("5.5 MB") + expect(formatFileSize(10.25 * 1024 * 1024)).toBe("10.3 MB") + }) + }) + + describe("compressImageIfNeeded", () => { + it("should return original buffer if under 5MB", async () => { + const smallBuffer = Buffer.alloc(3 * 1024 * 1024) // 3MB + const mimeType = "image/jpeg" + + const result = await compressImageIfNeeded(smallBuffer, mimeType) + + expect(result.buffer).toBe(smallBuffer) + expect(result.mimeType).toBe(mimeType) + expect(sharp).not.toHaveBeenCalled() + }) + + it("should compress JPEG images over 5MB", async () => { + const largeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB + const compressedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/jpeg" + + const mockMetadata = { + format: "jpeg", + width: 4000, + height: 3000, + } + + const mockSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(compressedBuffer), + } + + vi.mocked(sharp).mockReturnValue(mockSharp as any) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + expect(sharp).toHaveBeenCalledWith(largeBuffer) + expect(mockSharp.jpeg).toHaveBeenCalledWith({ + quality: expect.any(Number), + mozjpeg: true, + }) + expect(result.buffer).toBe(compressedBuffer) + expect(result.mimeType).toBe("image/jpeg") + }, 30000) // Increase timeout to 30 seconds + + it("should convert PNG to JPEG for better compression", async () => { + const largeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB + const compressedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/png" + + const mockMetadata = { + format: "png", + width: 4000, + height: 3000, + } + + const mockSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(compressedBuffer), + } + + vi.mocked(sharp).mockReturnValue(mockSharp as any) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + expect(mockSharp.jpeg).toHaveBeenCalled() + expect(result.buffer).toBe(compressedBuffer) + expect(result.mimeType).toBe("image/jpeg") // Should be converted to JPEG + }) + + it("should compress WebP images", async () => { + const largeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB + const compressedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/webp" + + const mockMetadata = { + format: "webp", + width: 4000, + height: 3000, + } + + const mockSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + webp: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(compressedBuffer), + } + + vi.mocked(sharp).mockReturnValue(mockSharp as any) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + expect(mockSharp.webp).toHaveBeenCalledWith({ + quality: expect.any(Number), + }) + expect(result.buffer).toBe(compressedBuffer) + expect(result.mimeType).toBe("image/webp") + }) + + it("should progressively reduce quality if initial compression is insufficient", async () => { + const largeBuffer = Buffer.alloc(8 * 1024 * 1024) // 8MB + const stillLargeBuffer = Buffer.alloc(5.5 * 1024 * 1024) // 5.5MB + const compressedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/jpeg" + + const mockMetadata = { + format: "jpeg", + width: 4000, + height: 3000, + } + + const mockSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi + .fn() + .mockResolvedValueOnce(stillLargeBuffer) // First attempt still too large + .mockResolvedValueOnce(compressedBuffer), // Second attempt successful + } + + vi.mocked(sharp).mockReturnValue(mockSharp as any) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + expect(mockSharp.toBuffer).toHaveBeenCalledTimes(2) + expect(result.buffer).toBe(compressedBuffer) + }) + + it("should resize image as last resort if compression alone fails", async () => { + const largeBuffer = Buffer.alloc(15 * 1024 * 1024) // 15MB + const stillLargeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB (still too large) + const resizedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/jpeg" + + const mockMetadata = { + format: "jpeg", + width: 4000, + height: 3000, + } + + // Track the number of sharp calls + let sharpCallCount = 0 + + // Mock for metadata call + const mockMetadataSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + } + + // Mock for compression attempts (will fail) + const mockCompressSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(stillLargeBuffer), // Still too large + } + + // Mock for resize operation + const mockResizeSharp = { + resize: vi.fn().mockReturnThis(), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(resizedBuffer), + } + + vi.mocked(sharp).mockImplementation(() => { + sharpCallCount++ + + // First call is for metadata + if (sharpCallCount === 1) { + return mockMetadataSharp as any + } + // Next 5 calls are compression attempts + else if (sharpCallCount <= 6) { + return mockCompressSharp as any + } + // Last call is for resize + else { + return mockResizeSharp as any + } + }) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + // Verify resize was called + expect(mockResizeSharp.resize).toHaveBeenCalledWith({ + width: expect.any(Number), + height: expect.any(Number), + fit: "inside", + withoutEnlargement: true, + }) + expect(result.buffer).toBe(resizedBuffer) + + // Verify the correct number of sharp calls were made + expect(sharpCallCount).toBe(7) // 1 metadata + 5 compression attempts + 1 resize + }, 30000) // Increase timeout to 30 seconds + + it("should handle compression errors gracefully", async () => { + const largeBuffer = Buffer.alloc(6 * 1024 * 1024) // 6MB + const mimeType = "image/jpeg" + + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + vi.mocked(sharp).mockImplementation(() => { + throw new Error("Sharp processing failed") + }) + + const result = await compressImageIfNeeded(largeBuffer, mimeType) + + expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to compress image:", expect.any(Error)) + expect(result.buffer).toBe(largeBuffer) // Should return original + expect(result.mimeType).toBe(mimeType) + + consoleErrorSpy.mockRestore() + }) + + it("should calculate quality based on compression ratio", async () => { + const largeBuffer = Buffer.alloc(7 * 1024 * 1024) // 7MB + const compressedBuffer = Buffer.alloc(4 * 1024 * 1024) // 4MB + const mimeType = "image/jpeg" + + const mockMetadata = { + format: "jpeg", + width: 4000, + height: 3000, + } + + const mockSharp = { + metadata: vi.fn().mockResolvedValue(mockMetadata), + jpeg: vi.fn().mockReturnThis(), + toBuffer: vi.fn().mockResolvedValue(compressedBuffer), + } + + vi.mocked(sharp).mockReturnValue(mockSharp as any) + + await compressImageIfNeeded(largeBuffer, mimeType) + + // Target size is 90% of 5MB = 4.5MB + // Compression ratio = 4.5MB / 7MB ≈ 0.64 + // Quality = 0.64 * 100 = 64 + expect(mockSharp.jpeg).toHaveBeenCalledWith({ + quality: 64, + mozjpeg: true, + }) + }) + }) +}) diff --git a/src/integrations/misc/image-compression.ts b/src/integrations/misc/image-compression.ts new file mode 100644 index 000000000000..7479f464f6fc --- /dev/null +++ b/src/integrations/misc/image-compression.ts @@ -0,0 +1,117 @@ +import sharp from "sharp" + +/** + * Maximum file size in bytes before compression is applied (5MB) + */ +const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB + +/** + * Compresses an image if it exceeds the maximum file size. + * + * @param buffer - The image buffer to potentially compress + * @param mimeType - The MIME type of the image + * @returns The compressed buffer and updated MIME type, or original if compression not needed + */ +export async function compressImageIfNeeded( + buffer: Buffer, + mimeType: string, +): Promise<{ buffer: Buffer; mimeType: string }> { + // If the image is smaller than 5MB, return as-is + if (buffer.length <= MAX_FILE_SIZE) { + return { buffer, mimeType } + } + + try { + // Use sharp to compress the image + let sharpInstance = sharp(buffer) + + // Get image metadata to determine format + const metadata = await sharpInstance.metadata() + + // Calculate compression quality based on how much we need to reduce + const targetSize = MAX_FILE_SIZE * 0.9 // Target 90% of max size to leave some margin + const compressionRatio = targetSize / buffer.length + + // Start with a quality based on the compression ratio + // Lower quality for larger compression needs + let quality = Math.max(20, Math.min(95, Math.round(compressionRatio * 100))) + + let compressedBuffer: Buffer + let attempts = 0 + const maxAttempts = 5 + + // Try to compress with progressively lower quality until we get under the limit + while (attempts < maxAttempts) { + attempts++ + + // Reset sharp instance for each attempt + sharpInstance = sharp(buffer) + + // Apply format-specific compression + if (mimeType === "image/png" || metadata.format === "png") { + // For PNG, convert to JPEG for better compression if needed + compressedBuffer = await sharpInstance.jpeg({ quality, mozjpeg: true }).toBuffer() + + // If successful, update the mime type + if (compressedBuffer.length <= MAX_FILE_SIZE) { + return { buffer: compressedBuffer, mimeType: "image/jpeg" } + } + } else if (mimeType === "image/webp" || metadata.format === "webp") { + compressedBuffer = await sharpInstance.webp({ quality }).toBuffer() + + if (compressedBuffer.length <= MAX_FILE_SIZE) { + return { buffer: compressedBuffer, mimeType } + } + } else { + // For JPEG and other formats, use JPEG compression + compressedBuffer = await sharpInstance.jpeg({ quality, mozjpeg: true }).toBuffer() + + if (compressedBuffer.length <= MAX_FILE_SIZE) { + return { buffer: compressedBuffer, mimeType: "image/jpeg" } + } + } + + // If still too large, reduce quality for next attempt + quality = Math.max(20, quality - 15) + } + + // If we couldn't compress enough, resize the image as a last resort + const resizeFactor = Math.sqrt(targetSize / buffer.length) + const resizedBuffer = await sharp(buffer) + .resize({ + width: Math.round((metadata.width || 1920) * resizeFactor), + height: Math.round((metadata.height || 1080) * resizeFactor), + fit: "inside", + withoutEnlargement: true, + }) + .jpeg({ quality: 70, mozjpeg: true }) + .toBuffer() + + return { buffer: resizedBuffer, mimeType: "image/jpeg" } + } catch (error) { + // If compression fails for any reason, log the error and return the original + console.error("Failed to compress image:", error) + return { buffer, mimeType } + } +} + +/** + * Checks if an image buffer exceeds the maximum file size + * + * @param buffer - The image buffer to check + * @returns True if the image needs compression + */ +export function needsCompression(buffer: Buffer): boolean { + return buffer.length > MAX_FILE_SIZE +} + +/** + * Gets a human-readable size string + * + * @param bytes - The size in bytes + * @returns A formatted string like "5.2 MB" + */ +export function formatFileSize(bytes: number): string { + const mb = bytes / (1024 * 1024) + return `${mb.toFixed(1)} MB` +} diff --git a/src/integrations/misc/process-images.ts b/src/integrations/misc/process-images.ts index cf3e201538dc..0e43e236788f 100644 --- a/src/integrations/misc/process-images.ts +++ b/src/integrations/misc/process-images.ts @@ -1,6 +1,7 @@ import * as vscode from "vscode" import fs from "fs/promises" import * as path from "path" +import { compressImageIfNeeded, needsCompression, formatFileSize } from "./image-compression" export async function selectImages(): Promise { const options: vscode.OpenDialogOptions = { @@ -20,9 +21,33 @@ export async function selectImages(): Promise { return await Promise.all( fileUris.map(async (uri) => { const imagePath = uri.fsPath - const buffer = await fs.readFile(imagePath) + let buffer = await fs.readFile(imagePath) + let mimeType = getMimeType(imagePath) + + // Check if compression is needed + if (needsCompression(buffer)) { + const originalSize = formatFileSize(buffer.length) + + // Show a progress notification while compressing + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Compressing large image (${originalSize})...`, + cancellable: false, + }, + async () => { + const compressed = await compressImageIfNeeded(buffer, mimeType) + buffer = compressed.buffer + mimeType = compressed.mimeType + + // Show info about compression + const newSize = formatFileSize(buffer.length) + vscode.window.showInformationMessage(`Image compressed from ${originalSize} to ${newSize}`) + }, + ) + } + const base64 = buffer.toString("base64") - const mimeType = getMimeType(imagePath) const dataUrl = `data:${mimeType};base64,${base64}` return dataUrl }), diff --git a/src/package.json b/src/package.json index 5ddb9f3f444f..86720fece49c 100644 --- a/src/package.json +++ b/src/package.json @@ -505,6 +505,7 @@ "sanitize-filename": "^1.6.3", "say": "^0.16.0", "serialize-error": "^12.0.0", + "sharp": "^0.34.4", "simple-git": "^3.27.0", "socket.io-client": "^4.8.1", "sound-play": "^1.1.0",