DEV Community

Gaven
Gaven

Posted on

Modeling Margins and Overlap in a Browser-Based PDF Layout Engine

The hardest part of a tiled-poster generator is not cutting an image into rectangles. The hard part is deciding what each rectangle means in physical space.

A browser gives us pixels. A PDF library gives us points. Paper presets are usually described in millimetres or inches. The printer adds a non-printable border. The user may request overlap between neighbouring sheets. The responsive preview then compresses the whole result into a few hundred CSS pixels.

If those measurements leak into one another, the preview can look correct while the exported poster is wrong.

This article explains the layout model I used while building Rasterbator.app, a browser-based tool that turns one image into a printable multi-page PDF. The goal is not to present one implementation as universal. It is to show the decisions that kept margins, overlap, source cropping, and preview rendering consistent.

Start with a physical poster model

A tempting implementation begins with the preview container:

  1. read its pixel width;
  2. divide it into columns and rows;
  3. reuse those rectangles when generating the PDF.

That approach couples print output to responsive UI. Resize the browser, open the tool on a phone, or change the device pixel ratio, and the calculation can change.

Instead, the layout engine should first produce a device-independent model. A simplified version might contain:

type PosterLayout = {
  paperWidthMm: number;
  paperHeightMm: number;
  marginMm: number;
  overlapMm: number;
  columns: number;
  rows: number;
  printableWidthMm: number;
  printableHeightMm: number;
  stepXmm: number;
  stepYmm: number;
  posterWidthMm: number;
  posterHeightMm: number;
};
Enter fullscreen mode Exit fullscreen mode

The preview and PDF exporter both consume this object. Neither is allowed to invent its own geometry.

Printable size and page advance are different

Suppose an A4 portrait page is 210 × 297 millimetres and the user selects a 10-millimetre margin on every side.

The printable dimensions are:

printable width  = 210 - 2 × 10 = 190 mm
printable height = 297 - 2 × 10 = 277 mm
Enter fullscreen mode Exit fullscreen mode

Without overlap, each new column advances 190 millimetres across the final poster.

With a 10-millimetre overlap, the printed region is still 190 millimetres wide, but the next logical page begins only 180 millimetres later:

stepX = printableWidth - overlap
stepY = printableHeight - overlap
Enter fullscreen mode Exit fullscreen mode

That difference is the centre of the model.

The printable region describes how much image appears on one sheet. The page advance describes how much unique poster area that sheet adds. Confusing the two causes one of two failures:

  • duplicated strips make the assembled poster larger than expected;
  • the overlap setting appears visually but does not affect source cropping.

Calculating final poster dimensions

For a grid with columns and rows, the physical extent is not simply columns × printableWidth when overlap is active.

A useful formula is:

posterWidth  = printableWidth  + (columns - 1) × stepX
posterHeight = printableHeight + (rows - 1) × stepY
Enter fullscreen mode Exit fullscreen mode

The first page contributes its complete printable region. Each additional page contributes only the logical advance because part of it repeats the previous page.

This formula also provides a helpful user-facing number. The application can display the assembled poster size before the user generates or prints anything.

Convert units only at boundaries

Internally, I prefer to keep physical layout calculations in millimetres. Paper presets are easy to represent, custom dimensions remain readable, and the values shown in the UI match the values used by the engine.

Conversion to PDF points happens at the export boundary:

const mmToPt = (mm: number) => mm * 72 / 25.4;
Enter fullscreen mode Exit fullscreen mode

The preview has a separate conversion. It receives the poster dimensions in millimetres and computes a display-only scale:

const previewScale = containerWidthPx / posterWidthMm;
const displayX = physicalXmm * previewScale;
Enter fullscreen mode Exit fullscreen mode

CSS pixels never flow back into the physical model.

This separation makes it possible to render the same poster at different preview sizes while generating an identical PDF.

Map page rectangles to source-image coordinates

The poster model describes physical windows. The source image uses pixels. The next task is to map each page’s physical bounds to a crop rectangle in the source image.

Assume the image is fitted to the poster while preserving its aspect ratio. Once the effective poster image dimensions are known, calculate a normalized page window:

normalizedX = pagePhysicalX / posterWidth
normalizedY = pagePhysicalY / posterHeight
normalizedW = printableWidth / posterWidth
normalizedH = printableHeight / posterHeight
Enter fullscreen mode Exit fullscreen mode

Then map those normalized values to image pixels:

sourceX = normalizedX × imageWidthPx
sourceY = normalizedY × imageHeightPx
sourceW = normalizedW × imageWidthPx
sourceH = normalizedH × imageHeightPx
Enter fullscreen mode Exit fullscreen mode

Because pagePhysicalX advances by stepX, neighbouring pages naturally include the repeated overlap strip.

Rounding should be handled carefully. Rounding every intermediate value can create one-pixel seams or drift across many pages. Keep floating-point values through the mapping stage and round only where a Canvas or encoder requires integers.

Partial pages still count as pages

Users may specify a physical width or height rather than an exact grid. If a poster needs 3.1 printable page widths, it requires four sheets.

That means page counts are calculated with Math.ceil, not ordinary rounding:

const columns = Math.ceil(requiredWidthMm / stepXmm);
Enter fullscreen mode Exit fullscreen mode

The final column may use only part of its printable area. The exported PDF page is still a complete physical sheet, but the image content may occupy a smaller window or require controlled clipping.

This is also why a preview should show complete page boundaries rather than presenting the poster as one uninterrupted rectangle.

Margins belong to the page, not the source crop

Margins are physical empty areas around the printable content. They should not reduce the amount of source image represented by the poster model in an accidental way.

A clean export sequence is:

  1. create a PDF page using the full paper dimensions;
  2. define a printable rectangle offset by the margin;
  3. crop or draw the source image for that page window;
  4. place the result inside the printable rectangle;
  5. add crop marks and labels in controlled page coordinates.

In no-effects mode, each tile can be rendered to a temporary Canvas and encoded as JPEG before being embedded. In vector halftone modes, sampled marks can be drawn directly into the printable rectangle.

A clipping mask is useful for large dots or line effects because their geometry may otherwise spill into the margin.

Page labels need a stable coordinate convention

Labels such as A1, B1, and A2 are more than decoration. They are part of the assembly model.

Choose one convention and keep it consistent:

  • columns use spreadsheet-style letters;
  • rows use one-based numbers;
  • the top-left page is A1;
  • columns increase from left to right;
  • rows increase from top to bottom.

For more than 26 columns, a spreadsheet-style conversion avoids arbitrary limits:

function columnLabel(index: number): string {
  let n = index + 1;
  let result = "";
  while (n > 0) {
    n--;
    result = String.fromCharCode(65 + (n % 26)) + result;
    n = Math.floor(n / 26);
  }
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Even if the application’s safety limits make extremely wide grids unlikely, using a complete convention keeps the model predictable.

Validate workload before rendering

A physically valid poster can still be too expensive for a browser. A high-resolution source image expands into an uncompressed pixel buffer. Canvas operations, halftone sampling, JPEG encoding, and PDF assembly can all exist in memory at the same time.

Rasterbator.app estimates both page count and sampled-cell workload before starting expensive work. Desktop and mobile use different practical thresholds. The important design principle is not the exact numbers; it is rejecting an unreasonable job before allocating large buffers.

A good validation error should explain what the user can change:

  • reduce the page grid;
  • use a desktop browser;
  • choose a larger paper size;
  • reduce halftone density;
  • resize the source image.

One model, multiple renderers

The most valuable architectural decision was making the poster layout an explicit intermediate representation.

The preview renderer converts physical units into display pixels. The PDF renderer converts the same physical units into points. The source-crop mapper converts them into image pixels. Labels, crop marks, masks, and overlap all derive from the same page rectangles.

That structure prevents a common class of “almost correct” bugs: a preview computed one way and an export computed another way.

When a web application produces a physical object, model the physical object first. Renderers should translate the model; they should not define it.

Top comments (0)