Every document needs a page, or the viewport becomes the document
TLDR: If a document does not state its own extent, every subsystem that needs an extent will read the viewport instead. The viewport belongs to the camera, not the artwork, so the artwork ends up sized by whatever the user was last looking at. Give the document a page rectangle and make it non optional.
The two rectangles
A canvas editor has a surface and a page.
The surface is infinite and belongs to the camera. Panning, zooming and fitting all rewrite it, and that is its entire job: it answers "what is on screen right now".
The page is finite and belongs to the artwork. It answers a completely different question: "how big is this thing". A drawing has a size whether or not anyone is looking at it.
These are easy to conflate because in the simplest implementation they are the same element. You have one <svg>, you set its viewBox, and the viewBox is both the window and the work. It holds together right up until the moment you have more than one document open, or a camera that moves.
How the confusion gets in
Our editor already had both concepts. A new blank canvas was created with an explicit page rectangle, and four separate subsystems read it: fit to view framed it, the exporter cropped to it, the grid was laid over it, and the send back path measured coordinates against it.
Every one of those had a fallback for the case where no page rectangle existed. Every fallback read the surface.
That fallback was written for imported files, on the reasonable assumption that a loaded SVG describes itself well enough. It does, usually. But "usually" is the wrong strength for something four subsystems depend on, and the day a document arrived that described itself badly, four things were wrong at once and none of them logged anything.
The failure mode is silence
This is worth dwelling on, because it is what makes the class dangerous rather than merely annoying.
A figure mounted at the wrong size still looks like a figure. It has the right shape, the right colours, the right internal proportions if you are lucky. Nothing about it says "I am at 38 percent of my real size".
So nobody checks. The user drags it around, draws over it, measures something on it, and every one of those measurements is wrong by a factor nobody has any reason to suspect. The error is discovered later, by someone comparing an export against a source, and by then it has propagated into whatever else was built on top.
Compare that to a crash. A crash is a bad afternoon. A confident wrong number is a bad quarter.
The fix is an invariant, not a patch
The instinct is to fix the import path: work out the dimension there, write it into the document, move on. That works, and it lasts until the next import path.
We had four. Files dropped in, files loaded from the editor host, a vector document arriving over the cross tool bridge, and figures arriving as artifacts from the PDF extractor. Each one built a document and handed it to the mounter.
So the rule moved to the mounter, because the mounter is the one thing they all pass through:
Every document mounted into this editor owns a page rectangle naming its own dimension.
One function, called from one place, run against every document regardless of where it came from. A blank canvas and an imported figure now get their page from the same six lines of code. That is the part that makes it stick: there is no longer a category of document that can skip the step, because there is no code path that could skip it.
We deleted the page rectangle from the blank canvas template to enforce this. It felt backwards. It means exactly one function in the codebase creates a page, and a future import path gets the behaviour without its author knowing the rule exists.
Deciding the dimension
If a document does not state its size, you have to work it out. We ordered the evidence by how much it deserves to be trusted:
The viewBox. The document telling you directly. Take it, including a non zero origin, because a page that ignores the origin sits next to the artwork rather than on it.
Width and height attributes. The document telling you indirectly. Slightly weaker because these are often presentation hints rather than statements about content.
The bounds of what was actually drawn. The document not telling you at all, so you measure the ink.
That third one has a trap in it. The grid layer is painted across the whole surface by design, so measuring "all children" reports the viewport back as the document size, which is precisely the confusion the pass exists to end. Editor infrastructure has to be excluded explicitly, and the exclusion list is the part to write a test around.
There is deliberately no fourth entry. If nothing states a size and nothing was drawn, we refuse to mount rather than invent one.
Two details that will bite
A dangling filter reference is not a no op. Our page carries a drop shadow via filter="url(#_pageShadow)". If the filter is not defined in that document, the SVG spec does not fall back to "no shadow", it drops the element entirely. Synthesise the page into a document that never had the shadow and the page silently does not render, which looks exactly like the bug you were fixing. The definition has to be injected alongside anything that points at it.
Make it idempotent. The page gets serialised back into the document when the user switches artboards, so on remount the pass sees a page already present and does nothing. Getting that wrong gives you a document that accumulates a new page rectangle on every switch, each slightly different, and the last one wins.
What it costs
One getBBox sweep per mount, and only on the branch where nothing stated a size.
The visible change is that imported documents now sit on a white page rather than floating on the dark surface. That reads as a styling decision and is not one. The page is the assertion that this artwork has a size of its own, and seeing it is the point.
The general shape
Any system with a container and a thing inside it has this bug available. A viewport and a document, a window and a canvas, a stage and a sprite, a plot area and a dataset.
The question to ask is: if the inner thing does not state its extent, what happens? If the answer is that it borrows the outer thing's, then the outer thing's size is now an input to your data, and it is an input that changes whenever someone resizes a window.