Skip to content

fix(utils): preserve JPEG format when compressing uploaded images#13585

Merged
tjx666 merged 3 commits into
lobehub:canaryfrom
octo-patch:fix/issue-13485-image-compression-format-preserved
Apr 28, 2026
Merged

fix(utils): preserve JPEG format when compressing uploaded images#13585
tjx666 merged 3 commits into
lobehub:canaryfrom
octo-patch:fix/issue-13485-image-compression-format-preserved

Conversation

@octo-patch

Copy link
Copy Markdown
Contributor

Fixes #13485

Problem

After v2.1.47, images with dimensions exceeding 1920 px (e.g. a 2048×1536 px JPEG shot on a phone at 150 KB) were re-encoded as PNG regardless of the original format. PNG is lossless, so a compressed JPEG blows up to 1 MB+ after this round-trip:

compressImage({ img, maxSize })    // type not passed → undefined
  → canvas.toDataURL(undefined)    // browser defaults to image/png
  → dataUrlToFile(dataUrl, name)   // hardcoded type: 'image/png'

Solution

  1. Pass file.type to compressImage() — so the function knows the original format.
  2. Encode JPEG inputs as JPEG at 0.85 qualitycanvas.toDataURL('image/jpeg', 0.85) produces files that are smaller than PNG for photographic content.
  3. Derive File MIME type from the data URL — instead of hardcoding 'image/png', parse the actual type out of data:<type>;base64,... so content and declared type always match.

PNG and WebP inputs still compress to PNG (lossless, consistent behavior).

Testing

  • Small JPEG (≤ 1920 px, ≤ 5 MB): skipped unchanged — no regression.
  • Large JPEG (> 1920 px): re-encoded as JPEG at 0.85 quality — significantly smaller than PNG.
  • Large PNG: re-encoded as PNG — same behavior as before.
  • All existing unit tests in packages/utils/src/compressImage.test.ts continue to pass.

@vercel

vercel Bot commented Apr 5, 2026

Copy link
Copy Markdown

@octo-patch is attempting to deploy a commit to the LobeHub OSS Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

@lobehubbot

Copy link
Copy Markdown
Member

👋 Assigning reviewers for this PR.

@arvinxx @nekomeowww — this fix touches packages/utils/src/compressImage.ts (JPEG format preservation during image compression). Could you take a look?

Summary of changes:

  • Pass file.type to compressImage() so the original format is preserved
  • Encode JPEG inputs as JPEG at 0.85 quality instead of defaulting to PNG
  • Derive File MIME type from the data URL instead of hardcoding image/png

@tjx666

tjx666 commented Apr 6, 2026

Copy link
Copy Markdown
Member

@octo-patch please add the new tests

Images with dimensions > 1920px were always re-encoded as PNG regardless
of original format, inflating small JPEGs (100–200 KB) to 1 MB+ because
PNG is lossless while JPEG is lossy.

Fix: pass file.type to compressImage(), encode JPEG inputs as JPEG at
0.85 quality (not PNG), and derive File MIME type from the data URL
instead of hardcoding 'image/png'.

PNG and WebP inputs still compress to PNG as before.

Fixes lobehub#13485
@tjx666 tjx666 force-pushed the fix/issue-13485-image-compression-format-preserved branch from 1be925f to 99e8ae5 Compare April 8, 2026 15:38
@codecov

codecov Bot commented Apr 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.11%. Comparing base (9508807) to head (da6f463).
⚠️ Report is 2 commits behind head on canary.

Additional details and impacted files
@@             Coverage Diff             @@
##           canary   #13585       +/-   ##
===========================================
- Coverage   85.99%   68.11%   -17.88%     
===========================================
  Files         621     2271     +1650     
  Lines       51799   194752   +142953     
  Branches     7944    19599    +11655     
===========================================
+ Hits        44545   132665    +88120     
- Misses       7123    61956    +54833     
  Partials      131      131               
Flag Coverage Δ
app 61.64% <ø> (?)
database 92.09% <ø> (ø)
packages/agent-runtime 79.93% <ø> (ø)
packages/context-engine 83.25% <ø> (ø)
packages/conversation-flow 92.40% <ø> (ø)
packages/file-loaders 87.02% <ø> (ø)
packages/memory-user-memory 74.74% <ø> (ø)
packages/model-bank 99.89% <ø> (ø)
packages/model-runtime 84.10% <ø> (ø)
packages/prompts 68.17% <ø> (ø)
packages/python-interpreter 92.90% <ø> (ø)
packages/ssrf-safe-fetch 0.00% <ø> (ø)
packages/utils 88.40% <100.00%> (+0.04%) ⬆️
packages/web-crawler 88.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Store 67.16% <ø> (∅)
Services 53.33% <ø> (∅)
Server 68.53% <ø> (∅)
Libs 53.30% <ø> (∅)
Utils 80.04% <ø> (-13.43%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Per @tjx666's request on lobehub#13585. Adds explicit coverage for the JPEG
format-preservation behaviour:

- compressImage with type='image/jpeg' calls toDataURL with quality 0.85
- compressImage with type='image/png' calls toDataURL without a quality arg
- compressImage with no type defaults to PNG
- compressImageFile preserves JPEG inputs as image/jpeg (regression fence
  for the previously hardcoded 'image/png' MIME type in dataUrlToFile)
- compressImageFile keeps WebP inputs as PNG (documents the fallback)

The existing PNG tests are preserved to guard against regression in the
lossless path.
@octo-patch

Copy link
Copy Markdown
Contributor Author

@tjx666 added a test commit (671ca6e). Coverage now explicitly fences the JPEG format-preservation behaviour:

  • compressImage with type='image/jpeg' calls toDataURL('image/jpeg', 0.85)
  • compressImage with type='image/png' calls toDataURL('image/png') (no quality arg)
  • compressImage with no type defaults to PNG
  • compressImageFile with a large JPEG produces a File with type === 'image/jpeg' (regression fence for the previously hardcoded 'image/png' MIME in dataUrlToFile)
  • compressImageFile with a WebP input still falls back to PNG (documents the deliberate choice)

Existing PNG tests preserved.

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 28, 2026
@tjx666 tjx666 merged commit 65da232 into lobehub:canary Apr 28, 2026
21 of 22 checks passed
@lobehubbot

Copy link
Copy Markdown
Member

❤️ Great PR @octo-patch ❤️

The growth of project is inseparable from user feedback and contribution, thanks for your contribution! If you are interesting with the lobehub developer community, please join our discord and then dm @arvinxx or @canisminor1990. They will invite you to our private developer channel. We are talking about the lobe-chat development or sharing ai newsletter around the world.

sxueck pushed a commit to sxueck/lobehub that referenced this pull request Apr 28, 2026
…behub#13585)

* 🐛 fix(utils): preserve JPEG format when compressing uploaded images

Images with dimensions > 1920px were always re-encoded as PNG regardless
of original format, inflating small JPEGs (100–200 KB) to 1 MB+ because
PNG is lossless while JPEG is lossy.

Fix: pass file.type to compressImage(), encode JPEG inputs as JPEG at
0.85 quality (not PNG), and derive File MIME type from the data URL
instead of hardcoding 'image/png'.

PNG and WebP inputs still compress to PNG as before.

Fixes lobehub#13485

* ✅ test(utils): add tests for JPEG format preservation in compressImage

Per @tjx666's request on lobehub#13585. Adds explicit coverage for the JPEG
format-preservation behaviour:

- compressImage with type='image/jpeg' calls toDataURL with quality 0.85
- compressImage with type='image/png' calls toDataURL without a quality arg
- compressImage with no type defaults to PNG
- compressImageFile preserves JPEG inputs as image/jpeg (regression fence
  for the previously hardcoded 'image/png' MIME type in dataUrlToFile)
- compressImageFile keeps WebP inputs as PNG (documents the fallback)

The existing PNG tests are preserved to guard against regression in the
lossless path.

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
mutoe pushed a commit to mutoe/lobehub that referenced this pull request May 1, 2026
…behub#13585)

* 🐛 fix(utils): preserve JPEG format when compressing uploaded images

Images with dimensions > 1920px were always re-encoded as PNG regardless
of original format, inflating small JPEGs (100–200 KB) to 1 MB+ because
PNG is lossless while JPEG is lossy.

Fix: pass file.type to compressImage(), encode JPEG inputs as JPEG at
0.85 quality (not PNG), and derive File MIME type from the data URL
instead of hardcoding 'image/png'.

PNG and WebP inputs still compress to PNG as before.

Fixes lobehub#13485

* ✅ test(utils): add tests for JPEG format preservation in compressImage

Per @tjx666's request on lobehub#13585. Adds explicit coverage for the JPEG
format-preservation behaviour:

- compressImage with type='image/jpeg' calls toDataURL with quality 0.85
- compressImage with type='image/png' calls toDataURL without a quality arg
- compressImage with no type defaults to PNG
- compressImageFile preserves JPEG inputs as image/jpeg (regression fence
  for the previously hardcoded 'image/png' MIME type in dataUrlToFile)
- compressImageFile keeps WebP inputs as PNG (documents the fallback)

The existing PNG tests are preserved to guard against regression in the
lossless path.

---------

Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: YuTengjing <ytj2713151713@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Version 2.1.47 will increase the file size of uploaded small images.

3 participants