mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
fix: compress images over 5MB before sending to Anthropic API
- Add sharp library for image compression - Create image compression utility with progressive quality reduction - Automatically compress images larger than 5MB in selectImages() - Show progress notification during compression - Add comprehensive tests for compression logic - Convert PNG to JPEG for better compression when needed - Resize images as last resort if compression alone fails Fixes #8771
This commit is contained in:
parent
6fa6c3c1e6
commit
429ccd06af
5 changed files with 687 additions and 3 deletions
266
pnpm-lock.yaml
generated
266
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
277
src/integrations/misc/__tests__/image-compression.spec.ts
Normal file
277
src/integrations/misc/__tests__/image-compression.spec.ts
Normal file
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
117
src/integrations/misc/image-compression.ts
Normal file
117
src/integrations/misc/image-compression.ts
Normal file
|
|
@ -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`
|
||||
}
|
||||
|
|
@ -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<string[]> {
|
||||
const options: vscode.OpenDialogOptions = {
|
||||
|
|
@ -20,9 +21,33 @@ export async function selectImages(): Promise<string[]> {
|
|||
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
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue