DEV Community

Cover image for Stop Exporting The Viewport: How Zoomed Image Editors Map Back To Original Pixels
byeval
byeval

Posted on • Originally published at happyimg.com

Stop Exporting The Viewport: How Zoomed Image Editors Map Back To Original Pixels

One of the easiest ways to break an image editor is to confuse the viewport with the image.

The screen needs zooming, centering, and a comfortable interaction scale. The exported file needs exact source-pixel geometry. If those two layers get mixed together, the UI might look fine while the saved result is wrong.

That boundary shows up in tools such as crop editors, screenshot redactors, and browser-side markup tools.

The full companion guide is here:

https://happyimg.com/guides/how-zoomed-image-editors-map-back-to-original-pixels

The viewport is for comfort, not truth

In the editor code, the Fabric canvas is sized to the visible container, then the viewport is transformed to fit the uploaded image:

const scale = Math.min(clientWidth / width, clientHeight / height) * 0.5;
const offsetX = (clientWidth - width * scale) / 2;
const offsetY = (clientHeight - height * scale) / 2;

this.canvas.setZoom(scale);
this.canvas.setViewportTransform([scale, 0, 0, scale, offsetX, offsetY]);
Enter fullscreen mode Exit fullscreen mode

That is exactly what the UI should do. Users should not be forced to edit an 1800-pixel image at a 1:1 screen scale.

The important part is what this transform does not mean. It does not mean the viewport has become the source of truth for export.

Keep objects in image space

The source image and editor objects are still inserted using the original image dimensions:

this.image = new FabricImage(image, {
  left: image.width / 2,
  top: image.height / 2,
  selectable: false,
  evented: false,
});
Enter fullscreen mode Exit fullscreen mode

That means a crop box or redaction region is defined in source-image coordinates even while the visible canvas is zoomed and centered for editing.

Once that boundary is in place, zooming becomes a viewing concern instead of a geometry problem.

Resizing is where geometry bugs usually appear

The subtle failure mode shows up during scaling handles.

If you keep committing transformed width and height on every live scaling event, the numbers start compounding and the box drifts away from the correct geometry.

The safer approach is to read the current temporary size from the base dimensions plus scale:

const width = (this.moveableRect.width ?? 0) * (this.moveableRect.scaleX ?? 1);
const height = (this.moveableRect.height ?? 0) * (this.moveableRect.scaleY ?? 1);
Enter fullscreen mode Exit fullscreen mode

Then normalize the box back into real width, height, left, and top values once the interaction should be committed.

That sounds small, but it is the difference between a stable editor and a resize tool that gets weirder every time the user drags a corner.

Export should rebuild from source pixels

The other common mistake is exporting the visible viewport as if it were the artifact.

For crop export, the editor creates a fresh canvas sized to the normalized crop region and draws directly from the original image:

context.drawImage(
  this.sourceImage,
  Math.round(nextRect.left),
  Math.round(nextRect.top),
  width,
  height,
  0,
  0,
  width,
  height
);
Enter fullscreen mode Exit fullscreen mode

For full markup export, the same idea appears as a clean StaticCanvas at the original image dimensions, with the base image re-added and the current overlay objects replayed onto it.

That is what makes the saved result accurate even if the editor viewport was zoomed out, zoomed in, or centered differently when the user clicked export.

The useful mental model

I think the cleanest rule is:

  • viewport transform is for viewing
  • object geometry is for editing
  • export canvas is for truth

If those three concerns stay separate, the math stays understandable and the output stays aligned.

If they get mixed together, you usually end up debugging "why is the export offset?" bugs that are really just architecture bugs.

More implementation details:

https://happyimg.com/guides/how-zoomed-image-editors-map-back-to-original-pixels

Top comments (0)