Opened 7 weeks ago
Closed 5 weeks ago
#65708 closed defect (bug) (fixed)
Media: HEIC to JPG conversion can create an orphaned full size JPG file and incorrectly versioned files
| Reported by: | ianmjones | Owned by: | adamsilverstein |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.1 |
| Component: | Media | Version: | |
| Severity: | normal | Keywords: | has-patch has-unit-tests commit |
| Cc: | Focuses: |
Description
When adding a large HEIC image to an Image block, the client side conversion can result in a "-scaled.jpg" file as the "original_image", but also "-scaled-1.jpg" as the "file", along with the thumbnails all having the "-1" suffix.
Along with this, a JPG with the same base file name as the HEIC is created, but not added to the "_wp_attachment_metadata" options record, meaning it is effectively orphaned.
When you "Delete Permanently" the Media Library item, the JPG without "-scaled" is left on the server.
This can be demonstrated by adding the "shelf-christmas-decoration.heic" image from https://heic.digital/samples/ to an Image block.
I'll attached some screenshots that show the metadata, generated files, and orphaned file after delete of the Media Library item.
Attachments (3)
Change History (11)
This ticket was mentioned in PR #12689 on WordPress/wordpress-develop by @khokansardar.
7 weeks ago
#2
- Keywords has-patch has-unit-tests added
When an attachment is uploaded with generate_sub_sizes set to false, the client generates all derivatives itself — including the scaled full-size image it later supplies through the sideload endpoint. create_item() already suppressed thumbnail generation and EXIF rotation in this case, but not the "big image" downscaling gated by the big_image_size_threshold filter.
What the problem was:
- A large client-converted image (e.g. a HEIC converted to JPEG in the browser) was still scaled server-side to a
-scaledfile, which became the attached file while the untouched upload was recorded asoriginal_image. - The client's subsequent
scaledsideload then collided with that-scaledfile and was renamed-scaled-1; the thumbnails inherited the numbered name. - The server-generated full-size file was left orphaned on disk and remained after the Media Library item was permanently deleted.
What the fix does:
- Disables
big_image_size_thresholdalongside the existing client-side processing filters increate_item(), and removes it again inremove_client_side_media_processing_filters(). - The uploaded full-size image is stored untouched, so the client's scaled sideload records it as
original_imageand keeps the plain-scaledname.
Approach and why:
generate_sub_sizes = falseis the contract that the client owns all derivative generation, which includes the scaled full-size image. Server-side scaling in that mode duplicates the client's work and causes the collision. Suppressing it is the minimal, consistent change — it sits directly beside the three filters already applied for the same purpose.
Trac ticket: https://core.trac.wordpress.org/ticket/65708
## Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 4.8
Used for: Ticket analysis, code implementation, and tests. All changes were reviewed and validated by me.
#3
@
6 weeks ago
- Owner set to
- Status new → assigned
Thanks for the detailed test and bug report @ianmjones - I'll take a look. This probably needs a fix in Gutenberg as well as core.
@adamsilverstein commented on PR #12689:
6 weeks ago
#4
Thanks for the PR @itzmekhokan - I'll take a look.
#5
@
6 weeks ago
- Keywords commit added
- Milestone Awaiting Review → 7.1
I dug into this and can confirm the diagnosis, plus the reason it shows up on Core but not with the Gutenberg plugin active.
When the client handles media processing it uploads the full-size image with generate_sub_sizes set to false, which is the contract that says the client owns every derivative - including the scaled-down full-size copy it sideloads afterwards. WP_REST_Attachments_Controller::create_item() already suppresses sub-size generation and EXIF rotation for that case, but not the "big image" downscaling in wp_create_image_subsizes(). So the server scales the upload anyway, and then:
- the server's
-scaledfile becomesfileand the untouched upload becomesoriginal_image - the client's own scaled sideload collides with that name and lands as
-scaled-1 original_imageis rewritten to point at the server's-scaledfile, so the plain.jpgis no longer referenced by the metadata and is orphaned on disk- the sub-sizes inherit the numbered name, because
filter_wp_unique_filename()only strips the numeric suffix when the un-suffixed name is free, and it is not
That last one is the mechanism behind the -1 thumbnails you saw @ianmjones.
The Gutenberg plugin has suppressed this since gutenberg#75817, which is why the bug only reproduces on Core. So this is really Core catching up to the plugin.
@itzmekhokan's patch in PR 12689 does exactly that, and I verified it locally - the test fails on trunk and passes with the fix, and the full WP_Test_REST_Attachments_Controller suite stays green. I have approved it and suggested one extra test covering the two symptoms the current test does not: the numbered sub-size names and the orphaned file surviving "Delete Permanently".
I opened gutenberg#81061 alongside it. The plugin's fix was untested (every existing test in this area used an image below the threshold), so that PR adds PHP coverage plus an end-to-end test that uploads a large image in the editor and asserts the exact file names. With the plugin's filter removed the e2e test fails on -scaled-1.jpeg, so it will catch a regression here in either project.
Worth noting for anyone testing: this is not HEIC specific. Any image over the threshold hits it once the client owns the sub-sizes, so a large JPEG reproduces it too and is easier to test with, since Chromium has no native HEIC decode.
Expected result after the fix, matching what you laid out in comment:1:
source_image: shelf-christmas-decoration.heic original_image: shelf-christmas-decoration.jpg file: 2026/07/shelf-christmas-decoration-scaled.jpg thumbnail: shelf-christmas-decoration-150x150.jpg
@adamsilverstein commented on PR #12689:
6 weeks ago
#6
Adding testing steps for anyone picking this up.
[[Image(https://img.shields.io/badge/Test_in-WP_Playground-blue?style=for-the-badge&logo=wordpress)]]
Note that this one needs client-side media processing to be active, which means Chrome 137+ and a cross-origin isolated page. I have not confirmed Playground gives you that, so a local build is the reliable way to see it.
Automated, fails on trunk and passes with the patch:
npm run test:php -- --filter test_create_item_skips_big_image_scaling_when_client_generates_sub_sizes
Manually, with the site on https or localhost so client-side processing kicks in:
- Add an Image block and upload a JPEG larger than 2560px on its longest side.
- Check the metadata:
wp post meta get <id> _wp_attachment_metadata. - Before the patch,
fileis<name>-scaled-1.jpg,original_imageis<name>-scaled.jpg, and the sub-sizes are<name>-WxH-1.jpg. After the patch,fileis<name>-scaled.jpg,original_imageis<name>.jpg, and the sub-sizes are<name>-WxH.jpg. - Delete the attachment permanently and look in
wp-content/uploads. Before the patch<name>.jpgis left behind; after, nothing is.
Step 4 is the orphan @ianmjones reported. Step 3 is worth eyeballing in the Media Library too, since the -1 names are what a user actually notices.
The ticket describes this with a HEIC upload but it is not HEIC specific - any image over the threshold hits it once the client owns the sub-sizes, so a large JPEG is easier to test with.
@khokansardar commented on PR #12689:
6 weeks ago
#7
Thanks for the review @adamsilverstein - I have lifted your test, adapted to the Core test class (enable_client_side_media_processing(), 33772.jpg with the threshold forced to 1000, self::$test_file for the scaled sideload). Pushed in 292c57879d.
I checked each symptom lands on its own by temporarily relaxing the earlier assertions: on trunk the thumbnail comes back as big-photo-150x150-1.jpg, the flow writes four files instead of three (big-photo-150x150-1.jpg, big-photo-scaled-1.jpg, big-photo-scaled.jpg, big-photo.jpg), and big-photo.jpg is still on disk after wp_delete_attachment( $id, true ). All three pass with the patch, and the full WP_Test_REST_Attachments_Controller suite is green at 190 tests.
Good catch on the incomplete backport from #11015 - that explains why the sideload half was already in place.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
I'm guessing that what should happen for this example, is ...
Giving ...
source_image: shelf-christmas-decoration.heic
original_image: shelf-christmas-decoration.jpg
file: 2026/07/shelf-christmas-decoration-scaled.jpg
thumbnail: shelf-christmas-decoration-150x150.jpg
Etc.