learningequality / learningequality/studio

Sized SVG and animated GIF images are lost on .perseus files when publishing exercises

Open
#6,143 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug DEV: backend
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 — ![alt](${☣CONTENTSTORAGE}/<checksum>.<ext> =550x364) — 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_image returns None, _resize_image logs a warning and returns None (base.py:232-234), and _process_single_image falls back to returning the original filename (base.py:244-260). But nothing is written to the archive on that path — _add_original_image only runs when both width and height are None. The item JSON references images/<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 without save_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

  1. In an exercise question, upload an SVG image (accepted — exercise_image allows it).
  2. Resize the image in the editor so the Markdown gains a size suffix: ![](${☣ CONTENTSTORAGE}/<checksum>.svg =550x364).
  3. Publish the channel.
  4. Open the generated Perseus zip: the item JSON references images/<checksum>.svg, but images/ does not contain it.
  5. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.