Fixed on main, released as 1.1.0. Filed as a public record because the defect silently destroyed image data for every consumer that cropped, and because the fix changes export behaviour.
What was wrong
EditorEngine.artboardDataUrl() pinned multiplier: 1 whenever a crop region was committed:
const multiplier = !this.cropRegion && this.artboard ? this.artboard.width / rect.width : 1;
The crop region lives in scene coordinates, and fitBaseImage() scales the photo down to fit the on-screen canvas — so scene units are display-sized, and a cropped export came out at whatever size the host's dialog happened to be.
A 2400x2400 photo cropped 1:1 in a ~640px modal exported at ~230x230. Nothing in the UI or the API indicated the resolution had been discarded, and once the blob was stored the pixels were gone for good. The full-size pixels were on the FabricImage the whole time; the engine never asked Fabric for more than 1x.
Found in the Angular sibling of this engine (reported by the Hiero CMS team, 2026-07-29, chasing "fuzzy avatar" complaints) and present here identically.
Two related defects found in the same function
- Exports were wrong while the canvas was zoomed or panned. Fabric treats the
left/top/width/height given to toDataURL() as viewport-space; the engine passed scene-space values without neutralising the viewport transform, so a zoomed canvas sampled the wrong area at the wrong scale.
aspectOption(w, h) discarded its dimensions, collapsing to { label, ratio }. Hosts had no way to request a pixel size — one consumer resorted to parsing the dimensions back out of the chip's label text.
What changed in 1.1.0
| Situation |
Exported size |
| No target declared |
region at source-limited resolution (never below its on-screen size) |
exportTarget set |
min(target, source-limited) |
Selected aspect option carries width/height |
those dimensions, while it is selected |
| Source has fewer real pixels than the target |
capped at the source — never upscaled |
| Result would exceed 4096x4096 |
clamped to that area |
<ImageEditor exportTarget={{ width: 1000, height: 1000 }} />
// or per option — aspectOption() now keeps its dimensions
<ImageEditor aspectRatios={[aspectOption(1200, 630), aspectOption(1000, 1000)]} />
// headless
engine.setExportTarget({ width: 1000, height: 1000 });
Also: AspExportTarget is exported; basic mode now renders custom aspectRatios chips (it silently ignored them before, despite the docs); outputSize() reports the delivered export size so the PDF page matches its image.
Release status
The npm publish for 1.1.0 is pending: this package's npm Trusted Publisher is not yet
pointed at this repo's Release workflow, so the OIDC publish returns 404. The code,
tests and docs are on main; the release will be re-run once that is configured.
Upgrade note
Cropped exports are larger now, by default. That is the fix, but it will show up in storage and upload sizes immediately. Declare exportTarget where a flow genuinely wants a small image. Images stored while the bug was live cannot be recovered — the pixels were never uploaded.
Why it shipped, and what now catches it
Export resolution cannot be asserted in jsdom (Fabric has no 2D context), so no test could have caught it. CI now runs a Playwright suite against a real canvas that decodes emitted blobs and asserts pixel size plus a sharpness score — an upscaled blur fails the assertion. The zoom case fails without the viewport fix.
Same defect, same fix, in the Angular sibling this engine was ported from: @ascentsparksoftware/angular-image-editor@22.1.0 (ascentspark/angular-image-editor#23).
Still open
svg export serialises the whole canvas and ignores the crop region. Pre-existing and unrelated to resolution; tracked separately.
Fixed on
main, released as 1.1.0. Filed as a public record because the defect silently destroyed image data for every consumer that cropped, and because the fix changes export behaviour.What was wrong
EditorEngine.artboardDataUrl()pinnedmultiplier: 1whenever a crop region was committed:The crop region lives in scene coordinates, and
fitBaseImage()scales the photo down to fit the on-screen canvas — so scene units are display-sized, and a cropped export came out at whatever size the host's dialog happened to be.A 2400x2400 photo cropped 1:1 in a ~640px modal exported at ~230x230. Nothing in the UI or the API indicated the resolution had been discarded, and once the blob was stored the pixels were gone for good. The full-size pixels were on the
FabricImagethe whole time; the engine never asked Fabric for more than 1x.Found in the Angular sibling of this engine (reported by the Hiero CMS team, 2026-07-29, chasing "fuzzy avatar" complaints) and present here identically.
Two related defects found in the same function
left/top/width/heightgiven totoDataURL()as viewport-space; the engine passed scene-space values without neutralising the viewport transform, so a zoomed canvas sampled the wrong area at the wrong scale.aspectOption(w, h)discarded its dimensions, collapsing to{ label, ratio }. Hosts had no way to request a pixel size — one consumer resorted to parsing the dimensions back out of the chip's label text.What changed in 1.1.0
exportTargetsetmin(target, source-limited)width/heightAlso:
AspExportTargetis exported;basicmode now renders customaspectRatioschips (it silently ignored them before, despite the docs);outputSize()reports the delivered export size so the PDF page matches its image.Release status
The npm publish for 1.1.0 is pending: this package's npm Trusted Publisher is not yet
pointed at this repo's Release workflow, so the OIDC publish returns 404. The code,
tests and docs are on
main; the release will be re-run once that is configured.Upgrade note
Cropped exports are larger now, by default. That is the fix, but it will show up in storage and upload sizes immediately. Declare
exportTargetwhere a flow genuinely wants a small image. Images stored while the bug was live cannot be recovered — the pixels were never uploaded.Why it shipped, and what now catches it
Export resolution cannot be asserted in jsdom (Fabric has no 2D context), so no test could have caught it. CI now runs a Playwright suite against a real canvas that decodes emitted blobs and asserts pixel size plus a sharpness score — an upscaled blur fails the assertion. The zoom case fails without the viewport fix.
Same defect, same fix, in the Angular sibling this engine was ported from:
@ascentsparksoftware/angular-image-editor@22.1.0(ascentspark/angular-image-editor#23).Still open
svgexport serialises the whole canvas and ignores the crop region. Pre-existing and unrelated to resolution; tracked separately.