Fit vs Fill in image-to-PDF tools: the geometry behind cropped pages
An image-to-PDF converter can preserve an image's aspect ratio and still produce a surprising result: the page looks full, but text near the edges has disappeared. That is not necessarily an encoding bug. It can be the
An image-to-PDF converter can preserve an image's aspect ratio and still produce a surprising result: the page looks full, but text near the edges has disappeared.
That is not necessarily an encoding bug. It can be the expected result of Fill, combined with a clipping rectangle.
In my previous article, I covered memory ownership and browser-side processing. This article focuses on a different boundary: translating image dimensions into a predictable PDF layout.
The examples are based on the page-layout approach used in PixToPaper, an image-to-PDF tool I maintain. The small JavaScript functions below are adaptations, not the complete converter. The worked numbers were checked against its layout implementation; they are calculations, not a printer benchmark.
1. Separate image pixels from page dimensions
There are at least three rectangles to think about:
- The source image: its pixel dimensions after crop and rotation.
- The page: its dimensions in PDF user-space units.
- The content box: the part of the page inside the chosen margins.
For the ordinary, unrotated pages created here, PDF units are points, with 72 points per inch.
A US Letter portrait page is therefore:
8.5 inches Γ 72 = 612 points wide
11 inches Γ 72 = 792 points high
Choose half-inch margins, which are 12.7 mm or 36 points on each side:
Content width = 612 - 2 Γ 36 = 540 points
Content height = 792 - 2 Γ 36 = 720 points
Content origin = (36, 36), measured from the top-left
Now place a 1000 Γ 1000 pixel image inside that 540 Γ 720 point box.
The image's pixel dimensions do not become its PDF dimensions automatically. They determine its aspect ratio; the placement calculation determines its physical size on the page.
2. Fit and Fill differ by one operator
For a source of width w and height h, and a content box of width cw and height ch, calculate two possible scale factors:
Horizontal scale = cw / w
Vertical scale = ch / h
Fit takes the smaller scale. Both image dimensions stay within the content box, so the entire image remains visible.
Fill takes the larger scale. Both content-box dimensions are covered, but part of the image can extend beyond that box.
Here is a geometry-only helper. Its callers must supply positive, finite image and box dimensions, finite box coordinates, and a validated mode of "fit" or "fill". File validation and image decoding belong outside this function.
function placeImage(width, height, box, mode) {
const scale = mode === 'fill'
? Math.max(box.width / width, box.height / height)
: Math.min(box.width / width, box.height / height);
const imageWidth = width * scale;
const imageHeight = height * scale;
return {
x: box.x + (box.width - imageWidth) / 2,
y: box.y + (box.height - imageHeight) / 2,
width: imageWidth,
height: imageHeight,
};
}
This function uses top-origin coordinates and centers the image inside the box. The same scale applies to both dimensions, preserving the aspect ratio.
Run the square-image example:
const box = { x: 36, y: 36, width: 540, height: 720 };
const fit = placeImage(1000, 1000, box, 'fit');
const fill = placeImage(1000, 1000, box, 'fill');
console.table({ fit, fill });
The resulting placement is:
| Placement | Scale | Drawn width | Drawn height | Left position | Top position |
|---|---|---|---|---|---|
| Fit | 0.54 | 540 pt | 540 pt | 36 pt | 126 pt |
| Fill | 0.72 | 720 pt | 720 pt | -54 pt | 36 pt |
With Fit, the 720-point-high content box has 180 points of spare vertical space: 90 above and 90 below the image.
With Fill, the image extends beyond the content box by 90 points on each horizontal side. Its negative left position is valid geometry, not an error to repair by forcing x to zero.
The visible horizontal fraction is:
540 / 720 = 0.75
So, with centered clipping, 75% of the source width is visible. For this 1000-pixel-wide source, that corresponds to excluding 125 source pixels on the left and 125 on the right.
Both modes preserve the aspect ratio. Only Fit promises to keep every edge visible.
An illustration from the converter's guide assets. It demonstrates the edge-clipping behavior; it is not a screenshot of the exact numeric setup above.
3. Scaling is not the same as clipping
Calculating the Fill rectangle is only half the job.
If the renderer draws the oversized image without a content-box clip, the image can cover the margins. The page boundary may hide some overflow, but it does not enforce your inner margin boundary.
In this example, the content box starts at x = 36. The Fill image starts at x = -54. Without clipping, the image covers the left margin between x = 0 and x = 36.
A renderer needs to:
- Save its graphics state.
- Establish a clip matching the content box.
- Draw the image using the calculated placement.
- Restore the previous graphics state.
For an unrotated pdf-lib page and an already embedded PDFImage, the drawing adapter can look like this:
import {
clip,
endPath,
popGraphicsState,
pushGraphicsState,
rectangle,
} from 'pdf-lib';
function drawPlacedImage(page, image, box, placement) {
const pageHeight = page.getHeight();
const bottomY = (top, height) => pageHeight - top - height;
page.pushOperators(
pushGraphicsState(),
rectangle(
box.x,
bottomY(box.y, box.height),
box.width,
box.height,
),
clip(),
endPath(),
);
page.drawImage(image, {
x: placement.x,
y: bottomY(placement.y, placement.height),
width: placement.width,
height: placement.height,
});
page.pushOperators(popGraphicsState());
}
endPath() ends the path after establishing the clip; this rectangle is not being drawn as a visible border. Restoring the graphics state prevents the clip from accidentally affecting subsequent labels or other page content.
This is a drawing adapter, not a complete export routine: image decoding, embedding, page creation, and document serialization still happen elsewhere.
4. Centered layouts can hide a coordinate bug
Canvas-style layout code often measures y downward from the top. In the ordinary PDF coordinate system used here, drawing positions are measured upward from the bottom.
For an axis-aligned rectangle, convert its top position using:
PDF bottom position = page height - top position - rectangle height
For a 792-point-high page, a rectangle starting 72 points below the top and measuring 180 points high needs:
792 - 72 - 180 = 540 points from the bottom
Subtracting only the top position gives 720, which places the rectangle incorrectly.
There is a subtle testing trap: for a vertically centered rectangle, its top-origin and bottom-origin positions are equal. In our Fit example:
792 - 126 - 540 = 126
A drawing implementation that accidentally reuses the top-origin y can therefore look correct for every centered example you try.
If you are checking a coordinate adapter, include at least one non-centered rectangle. Centered-only examples cannot expose that particular mistake.
The adapter above assumes no additional page rotation or custom transformation matrix. If you introduce those, review the transform rather than applying this formula blindly.
5. Use edited dimensions before choosing orientation
The source file's original dimensions are not always the dimensions that matter for placement.
A 1200 Γ 800 image rotated by 90 degrees has an edited aspect ratio of 800 Γ 1200. If automatic orientation still uses the original dimensions, it can choose a landscape page for a portrait result.
Cropping can change the aspect ratio too. A wide photograph can become a tall crop.
A predictable sequence is:
Crop dimensions
β swap width and height for a 90Β° or 270Β° rotation
β choose page orientation
β calculate margins and content box
β calculate Fit or Fill placement
Horizontal and vertical flips do not change the dimensions.
Margin handling also needs an explicit policy. A margin of half the page width would leave no usable content width. PixToPaper bounds its effective margin so the shorter content dimension remains at least one point; rejecting an impossible margin is another possible product choice.
The important part is to make that decision before dividing by image or content dimensions, and to use the same effective margin in preview and export.
6. A visual crop is not redaction
There is a security-relevant distinction between these operations:
- Cropping the raster before embedding: creates an image containing only the retained pixel region.
- Clipping an embedded image while drawing: controls which part is visible on the page.
The second operation does not remove the hidden pixels from the embedded image. If the full rendered image is embedded and then clipped, the PDF can still contain image data outside the visible rectangle.
That is how the Fill drawing path discussed here works. It embeds the rendered image and clips the drawing to the content box.
Do not use Fill as a way to remove sensitive information from the resulting document. For sensitive pixels, remove or replace them in the raster before embedding, and inspect the actual exported document. A clean-looking page preview is not evidence that hidden data is absent.
7. Layout correctness does not guarantee print sharpness
A 1000 Γ 1000 image placed at 540 Γ 540 points occupies 7.5 Γ 7.5 inches.
Its nominal effective resolution, if those 1000 pixels are retained in the embedded raster, is:
1000 pixels / 7.5 inches β 133 pixels per inch
The page geometry can be completely correct while the source lacks enough detail for the intended print size.
Increasing JPEG quality does not create more source pixels. Resizing during export can reduce the embedded pixel count further. Likewise, a layout preview rendered at a different resolution does not reproduce the finished PDF's compression.
Keep these concerns separate:
- Placement: where the image sits and which edges are visible.
- Sampling and encoding: how much image detail the export retains.
- Printing: whether the PDF reader or printer applies another scaling step.
For a practical companion to the geometry, I also wrote a guide to printing images at the correct size.
The takeaway
Fit and Fill are not quality settings. They are different placement contracts:
- Fit: preserve the whole image inside the content box.
- Fill: cover the content box and clip whatever extends beyond it.
A maintainable implementation keeps the page geometry independent of the rendering backend, converts coordinate systems explicitly, and clips against the content box rather than relying on page boundaries.
The useful checks are concrete: verify a known page and margin size, inspect an image with labeled edges, try a non-centered rectangle, rotate a non-square image, and distinguish visible clipping from actual pixel removal.
Those checks explain more than simply asking whether the PDF looks full.
References
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.