learningequality / learningequality/studio
Sized SVG and animated GIF images are lost on .perseus files when publishing exercises
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 191
- Forks
- 307
- Avg merge
- 5d 6h
- Merged PRs (30d)
- 10
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Target branch: Reproducible in production; where to land the fix is TBD.
Observed behavior
When a channel is published, images referenced in exercise markdown with a size suffix —  — go through resize_image in contentcuration/contentcuration/utils/assessment/base.py:25, which uses Pillow. Two formats come out wrong:
- SVG: Pillow cannot open it.
resize_imagereturnsNone,_resize_imagelogs a warning and returnsNone(base.py:232-234), and_process_single_imagefalls back to returning the original filename (base.py:244-260). But nothing is written to the archive on that path —_add_original_imageonly runs when both width and height areNone. The item JSON referencesimages/<checksum>.svg, and that file is not in the zip. - Animated GIF: Pillow opens and resizes it, but
img.save(buffered, format="GIF")is called withoutsave_all=True, so only the first frame is encoded. The file is written, but the animation is gone.
This affects markdown-authored items — legacy Perseus questions and the QTI-derived Perseus proxies. Raw Perseus items write their assets byte-for-byte via _write_raw_perseus_assets (base.py:140), and the QTI archive copies media by reference (qti/archive.py:128), so neither path resizes.
https://github.com/user-attachments/assets/4b13ec6c-943e-4812-a86d-17d3b3e83c13
Errors and logs
Publishing a node with a sized SVG logs two warnings, from resize_image and then _resize_image:
WARNING Error resizing image: cannot identify image file <_io.BytesIO object at 0x102a067f0>
WARNING Failed to resize image 0123456789abcdef0123456789abcdef.svg. Using original image.
Expected behavior
Every image an author places in an exercise appears in the published exercise, at the size the author set, with animation intact. Uploading a format Studio accepts should not change that — if exercise_image allows svg and gif, publishing has to carry them through.
Where resizing genuinely can't be done, publishing should say so in the logs in terms that match what actually happened, rather than reporting a fallback it didn't perform.
User-facing consequences
Learners see a broken image where the author placed one, in the SVG case. The question may become unanswerable if the image carried the information the question depends on — a diagram, a chart, a labelled figure. For animated GIFs, a question that relies on motion to convey its point loses that entirely.
Channel authors get no signal: the image renders correctly in the Studio editor, and the publish reports success. The problem only appears in Kolibri, after the channel ships.
How many published channels are affected is unknown — this was found by code reading while writing follow-up issues for #6095, not from a user report.
Steps to reproduce
- In an exercise question, upload an SVG image (accepted —
exercise_imageallows it). - Resize the image in the editor so the Markdown gains a size suffix:
. - Publish the channel.
- Open the generated Perseus zip: the item JSON references
images/<checksum>.svg, butimages/does not contain it. - Repeat with an animated GIF: the file is present, but only the first frame survives.
An unsized image of either format is copied verbatim and comes through fine — the size suffix is what triggers it.
Context
The attached video was recorded using Studio prod.
Acceptance Criteria
- A published exercise contains every image file its item JSON references, including sized SVGs
- A sized animated GIF keeps its animation in the published exercise
- Sized images in formats Pillow can round-trip continue to publish as they do today
- Unsized images of any accepted format continue to publish verbatim
- Publish-level test covering a sized SVG and a sized animated GIF, asserting every referenced file is present in the generated zip
- Unit coverage on the resize helpers for formats Pillow cannot round-trip
AI usage
I found this while reviewing the Perseus export code path with Claude Code as part of writing follow-up issues for #6095. Claude read the publishing code, ran the resize_image helper against a real SVG and a 3-frame animated GIF to confirm the behavior, and drafted this issue section by section from my answers. I verified the code references and decided the scope and acceptance criteria myself.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in contentcuration/contentcuration/utils/assessment/base.py, especially resize_image, _resize_image, _process_single_image, and _add_original_image; review the publishing path and qti/archive.py for format-specific behavior. Add or update the relevant unit and publish-level tests, then verify that sized SVG and animated GIF assets are present and correct in generated Perseus archives while existing sized and unsized formats still pass.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, testing-qa
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100