How to Position jsPDF Images Using DOM Element Dimensions
Measure a rendered element with getBoundingClientRect(), convert its units, and place the image in jsPDF without distortion.
Direct answer: measure the rendered element with getBoundingClientRect(), convert its pixel dimensions and position into the jsPDF document’s unit, then pass the image data and coordinates to doc.addImage().
const rect = element.getBoundingClientRect();
doc.addImage(imageData, "PNG", x, y, rect.width, rect.height);
This works directly only when x, y, rect.width, and rect.height use the same coordinate unit as the jsPDF document. Browser measurements are CSS pixels. A PDF configured in millimeters or points needs an explicit conversion. jsPDF documents the addImage API, while MDN documents getBoundingClientRect() and its viewport-relative geometry.
1. Measure the element you want to place
Call getBoundingClientRect() after layout has settled and after the element has reached the visual state you want to capture.
const element = document.querySelector(".invoice-logo");
if (!element) throw new Error(".invoice-logo was not found");
const rect = element.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
throw new Error("The element has no rendered size");
}
console.log({
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height
});
The rectangle describes the rendered border box in CSS pixels, including padding and borders but excluding margins. Its left and top values are relative to the viewport, so scrolling changes them. Add window.scrollX or window.scrollY only when you need document-relative coordinates.
2. Choose the box measurement that matches your PDF
| Measurement | Includes | Transforms | Use it when |
|---|---|---|---|
getBoundingClientRect() |
Rendered border box | Yes | The PDF should match what is visibly rendered |
offsetWidth/offsetHeight |
Layout border box, integer pixels | No | You need layout dimensions without visual transforms |
clientWidth/clientHeight |
Content plus padding, excluding borders | No | The PDF should represent the inner content box |
MDN explains these differences in Determining the dimensions of elements. A scale transform changes the rectangle returned by getBoundingClientRect(), while layout dimensions remain unchanged.
3. Convert CSS pixels to jsPDF units
addImage(imageData, format, x, y, width, height) interprets all coordinates and dimensions in the document’s configured base unit. Common units are millimeters, points, and pixels.
Millimeters
Using the CSS convention of 96 pixels per inch and 25.4 millimeters per inch:
const pxToMm = px => px * 25.4 / 96;
const x = pxToMm(rect.left);
const y = pxToMm(rect.top);
const width = pxToMm(rect.width);
const height = pxToMm(rect.height);
This conversion is appropriate only when your DOM coordinate space and PDF page coordinate space have the same origin and the same intended scale. A viewport position is not automatically a PDF position; usually you will choose a PDF margin and place the image relative to that margin.
Points
const pxToPt = px => px * 72 / 96;
const width = pxToPt(rect.width);
const height = pxToPt(rect.height);
Pixels
jsPDF documents pixel units with the px_scaling hotfix. Configure it explicitly and verify the behavior against the jsPDF version you install.
const doc = new jsPDF({
unit: "px",
hotfixes: ["px_scaling"]
});
See jsPDF’s unit and px_scaling notes for the documented configuration.
4. Complete browser example
The following example measures an image element, preserves its aspect ratio, and places it inside an A4 millimeter document. It assumes jsPDF is loaded in the page and the source image is same-origin or has suitable CORS headers.
import { jsPDF } from "jspdf";
async function imageElementToDataUrl(image) {
if (!image.complete) {
await new Promise((resolve, reject) => {
image.addEventListener("load", resolve, { once: true });
image.addEventListener("error", reject, { once: true });
});
}
if (!image.naturalWidth || !image.naturalHeight) {
throw new Error("The image did not load");
}
const canvas = document.createElement("canvas");
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const context = canvas.getContext("2d");
context.drawImage(image, 0, 0);
return canvas.toDataURL("image/png");
}
async function exportLogo() {
const element = document.querySelector(".invoice-logo");
if (!element) throw new Error("Missing .invoice-logo");
const rect = element.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) {
throw new Error("The element is hidden or has zero dimensions");
}
const imageData = await imageElementToDataUrl(element.querySelector("img"));
const doc = new jsPDF({ unit: "mm", format: "a4" });
const pxToMm = px => px * 25.4 / 96;
const width = pxToMm(rect.width);
const height = width * rect.height / rect.width;
const margin = 20;
doc.addImage(imageData, "PNG", margin, margin, width, height);
doc.save("invoice-logo.pdf");
}
exportLogo().catch(console.error);
If the measured element is not itself an image, render it into a canvas with the DOM-to-canvas tool used by your application, then pass that canvas data URL to addImage(). The geometry and unit conversion remain the same.
5. Position relative to a PDF layout
Viewport coordinates are useful for measuring, but PDF placement normally uses a page origin and margins. For example, to place an element 12 mm below a heading:
const headingY = 30;
const gap = 12;
const imageY = headingY + gap;
doc.addImage(imageData, "PNG", 20, imageY, width, height);
If you are mapping two DOM elements into one PDF, subtract the reference element’s rectangle first:
const anchor = document.querySelector(".pdf-anchor").getBoundingClientRect();
const target = document.querySelector(".target").getBoundingClientRect();
const pxToMm = px => px * 25.4 / 96;
const x = 20 + pxToMm(target.left - anchor.left);
const y = 30 + pxToMm(target.top - anchor.top);
This removes the viewport origin and makes the target position relative to the anchor.
6. Preserve the image’s aspect ratio
Supplying unrelated width and height values stretches the image. Calculate one dimension from the source ratio:
const width = 100;
const height = width * sourceHeight / sourceWidth;
doc.addImage(imageData, "PNG", x, y, width, height);
For an image element, rect.width / rect.height gives the rendered ratio. For an image file, prefer naturalWidth / naturalHeight when you want the source ratio rather than a CSS crop or transform. MDN describes the distortion risk in Understanding and setting aspect ratios.
7. Fit an image inside the page
function containSize(sourceWidth, sourceHeight, maxWidth, maxHeight) {
const scale = Math.min(maxWidth / sourceWidth, maxHeight / sourceHeight);
return {
width: sourceWidth * scale,
height: sourceHeight * scale
};
}
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 15;
const size = containSize(
width,
height,
pageWidth - margin * 2,
pageHeight - margin * 2
);
const x = (pageWidth - size.width) / 2;
const y = (pageHeight - size.height) / 2;
doc.addImage(imageData, "PNG", x, y, size.width, size.height);
For images taller than the remaining page space, create a new page or split the source before adding it. jsPDF accepts explicit dimensions; it does not infer your intended page-fit policy.
8. Edge cases that change the result
- Scrolling:
leftandtopare viewport-relative. Add scroll offsets only for document coordinates. - Transforms: a CSS scale or rotation affects the bounding rectangle. Use
offsetWidthandoffsetHeightwhen you need untransformed layout size. - Margins: margins are outside the returned rectangle. Add them yourself if they belong in the PDF layout.
- Empty boxes: hidden elements, collapsed containers, and elements with no border boxes can report zero dimensions.
- Fractional pixels: keep the fractional values until conversion; rounding early can accumulate visible alignment errors.
- Device pixel ratio: CSS pixels are not the same as backing-canvas pixels. If you render to a high-resolution canvas, use the canvas’s actual pixel dimensions for image data and the DOM rectangle for intended layout size.
- Cross-origin images: drawing an image without appropriate CORS permission can taint a canvas and prevent
toDataURL(). - Changing layout: fonts, images, responsive breakpoints, and late-loading content can change the rectangle. Measure after those resources are ready.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Image is too large or too small | CSS pixels passed to a millimeter or point document | Convert dimensions to the configured base unit |
| Image is stretched | Width and height use different aspect ratios | Derive one dimension from the source ratio |
| Image is shifted after scrolling | Viewport coordinates treated as document coordinates | Subtract an anchor rectangle or add scroll offsets as appropriate |
| Image appears at (0, 0) | Element was hidden or measured before layout | Wait for rendering and check for zero dimensions |
| Pixel-unit output is unexpectedly scaled | Missing jsPDF pixel scaling hotfix | Use hotfixes: ["px_scaling"] and confirm your installed version’s docs |
SecurityError from canvas |
Cross-origin image tainted the canvas | Serve the image with CORS headers or use a same-origin asset |
| Blurry output | Source bitmap is smaller than its PDF display size | Capture or render a higher-resolution source; do not enlarge a low-resolution bitmap |
| Content is clipped | Image exceeds page bounds | Apply contain sizing, adjust margins, or add pages |
10. Performance, reliability, and cost
- Measure once after layout settles, then reuse the rectangle instead of repeatedly forcing layout.
- Large PNGs increase memory use and PDF size. Use JPEG for photographic content when lossless transparency is unnecessary.
- Keep source dimensions close to the intended display size. Oversized canvases cost memory without improving a small PDF placement.
- Wait for fonts and images before measuring; otherwise a later reflow can invalidate every coordinate.
- Keep conversion functions centralized so every element uses the same pixels-to-unit rule.
- Log the rectangle, converted dimensions, page size, and final coordinates when diagnosing a production mismatch.
- jsPDF runs in the browser, so the work consumes client CPU and memory. For server-side or repeatable website captures, an API can move browser setup out of your application.
11. Or skip the browser setup
ScreenshotNeo captures a URL with one GET request and returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners, removes more than 60 known consent platforms plus newsletter popups and chat widgets before the shot, and lets you turn each step off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; the response identifies the page verdict and billing status with X-Page-Verdict and X-Billed headers. It also provides an MCP server for Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools.
Read the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));
Free accounts include 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account.
12. FAQ
Should I use getBoundingClientRect() or offsetWidth?
Use getBoundingClientRect() when the PDF should match the visible rendered box, including transforms. Use offset dimensions for untransformed layout size.
Do DOM coordinates automatically become PDF coordinates?
No. DOM positions are viewport-relative CSS pixels; jsPDF coordinates use the document’s configured unit and page origin. Map them deliberately.
Can I pass a DOM element directly to addImage()?
No. Convert the rendered content to supported image data such as a data URL, canvas, or accepted image input first.
Why does a zero-sized element produce a blank result?
The element may be hidden, collapsed, not loaded, or measured before layout. Check its rectangle after rendering and verify both dimensions are positive.
Which jsPDF unit is best?
Choose millimeters or points for print-oriented layouts. Pixels can simplify browser mapping when the documented pixel scaling hotfix is enabled. The right choice depends on where your layout values originate.


