Repository navigation
[QTI] Image sizes set in the editor are lost when exercises are rendered in Kolibri #6189
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: unstable
Observed behavior
When an image is resized in the new editor (TipTap in the QTI Editor), the size is saved correctly: raw_data has width/height on the <img>. The size is lost after publish, and Kolibri shows the image at full size in both formats:
- QTI package: the published item XML still has
width/height, but Kolibri's QTI viewer removes them when rendering.SafeHTMLsanitizes withFORBID_ATTR = ['width', 'height'](kolibri/packages/kolibri-common/components/SafeHTML/index.js), so every<img>shows at its natural size, capped only bymax-width: 100%/max-height: 80vh. - Perseus archive derived from native QTI items:
_render_imginutils/assessment/qti/html_to_markdown.pydropswidth/heightwhen turning the QTI back into Perseus markdown. With no=WxHsuffix, publish doesn't resize the image and the Perseusimagesmap is empty. Older Kolibri versions, which use this archive, show the image at full size.
Errors and logs
None. The size is dropped silently: no publish warnings and no console errors.
Expected behavior
An image resized in the editor shows at that size in Kolibri, however the exercise is published:
- QTI package: the Kolibri QTI viewer shows the image at the authored size, scaling down to fit narrow screens without distorting the aspect ratio.
- Derived Perseus archive: the image is resized at publish and appears in the Perseus
imagesmap, as it did for legacy Perseus exercises. - Same image at two sizes: each copy shows at its own size.
We still need to decide how the QTI package carries the size:
- Kolibri keeps
width/heightin the QTI viewer. The package ships the full-resolution file. That helps because the QTI viewer's lightbox can show the image at full size even when it's small in the item body. - Studio resizes the image file at publish. This must handle the same image used at several sizes in one item. The native QTI publish path currently rewrites a file's
srcthe same way everywhere it appears, so each size would need its own file and reference. It also means the lightbox only has the reduced image to show.
User-facing consequences
Content authors resize images to fit the layout of a question. Learners see those images at full size instead: large diagrams take up the screen and push the answer choices out of view, and small inline images such as icons appear huge. What authors see in Studio's preview doesn't match what learners get.
Acceptance Criteria
- We have decided and recorded how QTI packages carry image sizes (Kolibri keeps the attributes, Studio resizes at publish, or both)
- An image resized in the editor shows at its authored size in Kolibri's QTI viewer
- The derived Perseus archive for a native QTI node contains a resized copy of each sized image and a filled
imagesmap, and older Kolibri shows it at that size - The same image used at two sizes shows at each size in both formats
- Sized images keep their aspect ratio when scaled down on narrow screens
- Tests cover sized images in the QTI → Perseus markdown conversion and in the derived Perseus publish output
- Kolibri's fix is linked here if the decision needs one
This issue tracks the full Studio → Kolibri image-sizing integration. Close it only after the Studio fixes and the Kolibri fix (if needed) have all landed and been checked end to end.
AI usage
I used Claude Code to trace the image-size path through Studio's publish code and Kolibri's QTI viewer, and to draft this issue. It confirmed the loss in the QTI → Perseus markdown conversion by running it. I checked the findings and edited the issue.

Activity
[-]Image sizes set in the editor are lost when exercises are published[/-][+][QTI] Image sizes set in the editor are lost when exercises are rendered in Kolibri[/+]rtibblesbot commentedon Sep 30, 2026
@rtibbles one decision blocks the QTI half: which option do we take?
width/height. Drop them fromSafeHTML'sFORBID_ATTRand addimg { height: auto }somax-width: 100%scales without distortion. The lightbox keeps full resolution, and the Studio QTI archive needs no change.srcrewrite in_write_qti_media_files, and the lightbox only has the reduced file. No Kolibri change.Is
FORBID_ATTR = ['width', 'height']intentional in Kolibri for a reason that rules out A?Perseus half is settled either way:
_render_imginhtml_to_markdown.pyemitsfrom the attributes.resized_images_maphandling inutils/assessment/base.pythen resizes per size, fillsimagesand deduplicates.html_to_markdownwith sized/unsized images, and derived Perseus publish with one image at two sizes.rtibbles commentedon Sep 30, 2026
This has already been handled by this PR: learningequality/kolibri#15383 - the forbid attrs are for any other element, but img has an exception in the handling thanks to this ^ PR.
rtibbles commentedon Sep 30, 2026
Studio should restore its previous resize at publish behaviour though - the point of that was to make sure we didn't ship enormous image files that were much larger than was needed.
rtibblesbot commentedon Sep 30, 2026
Plan, given Kolibri's
<img>exception (kolibri#15383) and resize at publish in Studio:QTI package
_write_qti_media_filesresizes each<img>that has bothwidthandheightwith the existing_process_single_image, then points that element'ssrcat the resized file.width/heightstay in the XML for Kolibri's display size.srcset,hrefanddatakeep the original file.Derived Perseus archive
_render_imgappends=WxH, and the existingprocess_image_stringsresizes the image and fillsimages.Tests
html_to_markdownwith sized, unsized and partial sizes.Assumption: resize only when both attributes are set, since the editor always writes both.
rtibblesbot commentedon Sep 30, 2026
Plan: An
<img>sized in the QTI editor ships resized, at its authored size, in both the QTI package and the derived Perseus archive.Assumptions — the issue did not settle these; say so if one is wrong.
widthorheightshould resize, those ship full-size files.src; if it should drop the image instead, the fallback test inverts.<img>tags are rewritten; if foreign packages use a namespace prefix, the rewrite needs an lxml element walk.@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
feat: resize QTI images at publish to their authored size
feat: resize QTI images at publish to their authored size
feat: resize QTI images at publish to their authored size
feat: resize QTI images at publish to their authored size
feat: resize QTI images at publish to their authored size
rtibblesbot commentedon Oct 2, 2026
@rtibbles Blocked — these cannot be assumed either way:
74880f01e) before this self-review; local branch81aea94f6holds an unpushed revision adding_lanczos_resize. Where should the self-review fixes go? -_lanczos_resizefile-size regression (self_review.md, base.py:63): only in the unpushed revision, not onunstable. Open a follow-up PR with that revision + palette-preserving quantize, or drop it? -_resize_animated_imageanimated output larger than original (base.py:86): present onunstablenow. File a follow-up issue under [QTI] Backend handling, validation, and publishing of assessment items from frontend and ricecooker #5998 / open a fix PR fromunstable?@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
rtibbles commentedon Oct 2, 2026
Please just include the work in the PR for this issue - I don't know exactly what you're asking, so it will be easier to see it all in the PR you open to judge.