Skip to content

Commit 227853a

Browse files
authored
Merge pull request #658 from embedpdf/fix/support-negative-origin-media-crop-box
Read page boxes to fix annotation positions
2 parents c91236d + 5f96cf2 commit 227853a

10 files changed

Lines changed: 167 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@embedpdf/engines": patch
3+
---
4+
5+
Fix incorrect annotation positions for PDFs with a non-zero MediaBox/CropBox origin (e.g. CAD/technical drawing exports). The engine now reads each page's box origin at open time and applies it in both the PDF-to-CSS and CSS-to-PDF coordinate conversions, so annotations render and round-trip at the position shown by native PDF viewers.

.changeset/models-page-boxes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@embedpdf/models": patch
3+
---
4+
5+
Add the `PdfPageBoxes` interface and an optional `boxes` field on `PdfPageObject`, exposing each page's Media/Crop (always present) and optional Bleed/Trim/Art boxes in unrotated PDF user space.

.changeset/pdfium-get-page-box.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@embedpdf/pdfium": patch
3+
---
4+
5+
Add `EPDF_GetPageBoxByIndex` API (with the `EPDF_PAGE_BOX_TYPE` enum) to read a page's Media/Crop/Bleed/Trim/Art box without loading or parsing the page. MediaBox is resolved through page-tree inheritance (falling back to the default page size), CropBox falls back to MediaBox, and Bleed/Trim/Art return false when absent.

packages/engines/src/lib/pdfium/engine.ts

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
PdfBookmarkObject,
1818
PdfDocumentObject,
1919
PdfPageObject,
20+
PdfPageBoxes,
21+
Box,
2022
PdfActionType,
2123
Rotation,
2224
PDF_FORM_FIELD_FLAG,
@@ -359,6 +361,8 @@ export class PdfiumNative implements IPdfiumExecutor {
359361

360362
const pages: PdfPageObject[] = [];
361363
const sizePtr = this.memoryManager.malloc(8);
364+
// FS_RECTF is { float left, top, right, bottom } = 16 bytes.
365+
const boxPtr = this.memoryManager.malloc(16);
362366
for (let index = 0; index < pageCount; index++) {
363367
// Use normalized size function when normalizeRotation is enabled
364368
const result = normalizeRotation
@@ -373,6 +377,7 @@ export class PdfiumNative implements IPdfiumExecutor {
373377
`${normalizeRotation ? 'EPDF_GetPageSizeByIndexNormalized' : 'FPDF_GetPageSizeByIndexF'} failed with ${lastError}`,
374378
);
375379
this.memoryManager.free(sizePtr);
380+
this.memoryManager.free(boxPtr);
376381
this.pdfiumModule.FPDF_CloseDocument(docPtr);
377382
this.memoryManager.free(filePtr);
378383
this.logger.perf(LOG_SOURCE, LOG_CATEGORY, `OpenDocumentBuffer`, 'End', file.id);
@@ -384,6 +389,7 @@ export class PdfiumNative implements IPdfiumExecutor {
384389

385390
const rotation = this.pdfiumModule.EPDF_GetPageRotationByIndex(docPtr, index) as Rotation;
386391
const objectNumber = this.pdfiumModule.EPDFDoc_GetPageObjectNumberByIndex(docPtr, index);
392+
const boxes = this.readPageBoxes(docPtr, index, boxPtr);
387393

388394
const page = {
389395
index,
@@ -393,11 +399,13 @@ export class PdfiumNative implements IPdfiumExecutor {
393399
},
394400
rotation,
395401
objectNumber,
402+
boxes,
396403
};
397404

398405
pages.push(page);
399406
}
400407
this.memoryManager.free(sizePtr);
408+
this.memoryManager.free(boxPtr);
401409

402410
// Query security state
403411
const isEncrypted = this.pdfiumModule.EPDF_IsEncrypted(docPtr);
@@ -10432,6 +10440,75 @@ export class PdfiumNative implements IPdfiumExecutor {
1043210440
};
1043310441
}
1043410442

10443+
/**
10444+
* Read the page boundary boxes (Media/Crop/Bleed/Trim/Art) for a page without
10445+
* loading it. Reuses a caller-owned 16-byte FS_RECTF scratch buffer.
10446+
* @param docPtr - pointer to the pdf document
10447+
* @param index - page index
10448+
* @param boxPtr - pointer to a 16-byte FS_RECTF scratch buffer
10449+
* @returns the page boxes, or undefined when Media/Crop cannot be resolved
10450+
*
10451+
* @private
10452+
*/
10453+
private readPageBoxes(
10454+
docPtr: number,
10455+
index: number,
10456+
boxPtr: number,
10457+
): PdfPageBoxes | undefined {
10458+
const readBox = (boxType: number): Box | undefined => {
10459+
const ok = this.pdfiumModule.EPDF_GetPageBoxByIndex(docPtr, index, boxType, boxPtr);
10460+
if (!ok) {
10461+
return undefined;
10462+
}
10463+
// FS_RECTF layout: { float left, top, right, bottom }.
10464+
return {
10465+
left: this.pdfiumModule.pdfium.getValue(boxPtr, 'float'),
10466+
top: this.pdfiumModule.pdfium.getValue(boxPtr + 4, 'float'),
10467+
right: this.pdfiumModule.pdfium.getValue(boxPtr + 8, 'float'),
10468+
bottom: this.pdfiumModule.pdfium.getValue(boxPtr + 12, 'float'),
10469+
};
10470+
};
10471+
10472+
const media = readBox(0); // EPDF_PAGE_BOX_MEDIA
10473+
const crop = readBox(1); // EPDF_PAGE_BOX_CROP (falls back to media)
10474+
if (!media || !crop) {
10475+
return undefined;
10476+
}
10477+
10478+
const boxes: PdfPageBoxes = { media, crop };
10479+
const bleed = readBox(2); // EPDF_PAGE_BOX_BLEED
10480+
if (bleed) {
10481+
boxes.bleed = bleed;
10482+
}
10483+
const trim = readBox(3); // EPDF_PAGE_BOX_TRIM
10484+
if (trim) {
10485+
boxes.trim = trim;
10486+
}
10487+
const art = readBox(4); // EPDF_PAGE_BOX_ART
10488+
if (art) {
10489+
boxes.art = art;
10490+
}
10491+
return boxes;
10492+
}
10493+
10494+
/**
10495+
* The effective page origin in PDF user space: the lower-left corner of the
10496+
* crop box (which PDFium also uses for the page size). Used to offset
10497+
* coordinates for PDFs whose MediaBox/CropBox origin is not `(0, 0)`.
10498+
* Returns `(0, 0)` when the page has no box information.
10499+
* @param page - pdf page info
10500+
* @returns the page origin
10501+
*
10502+
* @private
10503+
*/
10504+
private getPageOrigin(page: PdfPageObject): Position {
10505+
const crop = page.boxes?.crop;
10506+
if (!crop) {
10507+
return { x: 0, y: 0 };
10508+
}
10509+
return { x: crop.left, y: crop.bottom };
10510+
}
10511+
1043510512
/**
1043610513
* Convert coordinate of point from device coordinate to page coordinate
1043710514
* @param doc - pdf document object
@@ -10452,24 +10529,28 @@ export class PdfiumNative implements IPdfiumExecutor {
1045210529
// so we must also use 0° for coordinate transformations
1045310530
const r = doc.normalizedRotation ? 0 : page.rotation & 3;
1045410531

10532+
// Recover the point relative to the page box origin, then shift back into
10533+
// PDF user space so non-zero MediaBox/CropBox origins round-trip correctly.
10534+
const origin = this.getPageOrigin(page);
10535+
10536+
let point: Position;
1045510537
if (r === 0) {
1045610538
// 0°
10457-
return { x: position.x, y: DH - position.y };
10458-
}
10459-
if (r === 1) {
10539+
point = { x: position.x, y: DH - position.y };
10540+
} else if (r === 1) {
1046010541
// 90° CW
1046110542
// x_d = sx*y, y_d = sy*x => x = y_d/sy, y = x_d/sx
10462-
return { x: position.y, y: position.x };
10463-
}
10464-
if (r === 2) {
10543+
point = { x: position.y, y: position.x };
10544+
} else if (r === 2) {
1046510545
// 180°
10466-
return { x: DW - position.x, y: position.y };
10467-
}
10468-
{
10546+
point = { x: DW - position.x, y: position.y };
10547+
} else {
1046910548
// 270° CW
1047010549
// x_d = DW - sx*y, y_d = DH - sy*x
10471-
return { x: DH - position.y, y: DW - position.x };
10550+
point = { x: DH - position.y, y: DW - position.x };
1047210551
}
10552+
10553+
return { x: point.x + origin.x, y: point.y + origin.y };
1047310554
}
1047410555

1047510556
/**
@@ -10492,21 +10573,27 @@ export class PdfiumNative implements IPdfiumExecutor {
1049210573
// so we must also use 0° for coordinate transformations
1049310574
const r = doc.normalizedRotation ? 0 : page.rotation & 3;
1049410575

10576+
// Shift the PDF user-space point so it is relative to the page box origin
10577+
// before applying rotation, supporting non-zero MediaBox/CropBox origins.
10578+
const origin = this.getPageOrigin(page);
10579+
const px = position.x - origin.x;
10580+
const py = position.y - origin.y;
10581+
1049510582
if (r === 0) {
1049610583
// 0°
10497-
return { x: position.x, y: DH - position.y };
10584+
return { x: px, y: DH - py };
1049810585
}
1049910586
if (r === 1) {
1050010587
// 90° CW
10501-
return { x: position.y, y: position.x };
10588+
return { x: py, y: px };
1050210589
}
1050310590
if (r === 2) {
1050410591
// 180°
10505-
return { x: DW - position.x, y: position.y };
10592+
return { x: DW - px, y: py };
1050610593
}
1050710594
{
1050810595
// 270° CW
10509-
return { x: DW - position.y, y: DH - position.x };
10596+
return { x: DW - py, y: DH - px };
1051010597
}
1051110598
}
1051210599

packages/models/src/pdf.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,42 @@
1-
import { Size, Rect, Position, Rotation } from './geometry';
1+
import { Size, Rect, Position, Rotation, Box } from './geometry';
22
import { Task, TaskError } from './task';
33

4+
/**
5+
* The five PDF page boundary boxes, in unrotated PDF user space.
6+
*
7+
* `media` and `crop` are always present (`crop` defaults to `media` when the
8+
* PDF omits it, since the viewer always needs an effective crop). `bleed`,
9+
* `trim`, and `art` are present only when the PDF actually declares them.
10+
*
11+
* @public
12+
*/
13+
export interface PdfPageBoxes {
14+
/**
15+
* MediaBox, resolved through page-tree inheritance.
16+
*/
17+
media: Box;
18+
19+
/**
20+
* CropBox, falling back to {@link PdfPageBoxes.media} when absent.
21+
*/
22+
crop: Box;
23+
24+
/**
25+
* BleedBox, only present when the PDF declares it.
26+
*/
27+
bleed?: Box;
28+
29+
/**
30+
* TrimBox, only present when the PDF declares it.
31+
*/
32+
trim?: Box;
33+
34+
/**
35+
* ArtBox, only present when the PDF declares it.
36+
*/
37+
art?: Box;
38+
}
39+
440
/**
541
* Representation of pdf page
642
*
@@ -29,6 +65,14 @@ export interface PdfPageObject {
2965
* dictionary (e.g. XFA pages, which have no `CPDF_Page`).
3066
*/
3167
objectNumber: number;
68+
69+
/**
70+
* The page boundary boxes in unrotated PDF user space. Used to position
71+
* content correctly for PDFs whose MediaBox/CropBox origin is not `(0, 0)`
72+
* (e.g. CAD/technical drawing exports). May be absent for documents created
73+
* in-memory (e.g. via `createDocument`).
74+
*/
75+
boxes?: PdfPageBoxes;
3276
}
3377

3478
/**

packages/pdfium/src/vendor/functions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const functions = {
33
EPDF_GetMetaKeyCount: [["number","boolean"] as const, 'number'] as const,
44
EPDF_GetMetaKeyName: [["number","number","boolean","number","number"] as const, 'number'] as const,
55
EPDF_GetMetaTrapped: [["number"] as const, 'number'] as const,
6+
EPDF_GetPageBoxByIndex: [["number","number","number","number"] as const, 'boolean'] as const,
67
EPDF_GetPageRotationByIndex: [["number","number"] as const, 'number'] as const,
78
EPDF_GetPageSizeByIndexNormalized: [["number","number","number"] as const, 'boolean'] as const,
89
EPDF_HasMetaText: [["number","string"] as const, 'boolean'] as const,

packages/pdfium/src/vendor/pdfium.cjs

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.

packages/pdfium/src/vendor/pdfium.js

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.
789 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)