@jasy/pdf 1.0.0-beta.4 → 1.0.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/api/color.d.ts +1 -24
- package/dist/api/color.js +2 -278
- package/dist/api/content.d.ts +7 -2
- package/dist/api/content.js +17 -7
- package/dist/api/descriptor.js +30 -3
- package/dist/api/flow.d.ts +55 -0
- package/dist/api/flow.js +71 -0
- package/dist/api/gradient.d.ts +1 -53
- package/dist/api/gradient.js +2 -97
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/layout.js +4 -3
- package/dist/api/structure.d.ts +1 -1
- package/dist/api/table.js +3 -1
- package/dist/api/text.d.ts +8 -8
- package/dist/api/text.js +6 -0
- package/dist/canvas/painter.d.ts +2 -2
- package/dist/canvas/painter.js +2 -2
- package/dist/common/color-input.d.ts +24 -0
- package/dist/common/color-input.js +278 -0
- package/dist/common/diagnostics.d.ts +9 -1
- package/dist/common/gradient.d.ts +53 -0
- package/dist/common/gradient.js +97 -0
- package/dist/elements/container-element.d.ts +3 -0
- package/dist/elements/container-element.js +17 -5
- package/dist/elements/image-element.d.ts +37 -26
- package/dist/elements/image-element.js +88 -50
- package/dist/elements/index.d.ts +3 -0
- package/dist/elements/index.js +3 -0
- package/dist/elements/layout/column-break-element.d.ts +17 -0
- package/dist/elements/layout/column-break-element.js +26 -0
- package/dist/elements/layout/deferred-element.d.ts +9 -1
- package/dist/elements/layout/deferred-element.js +18 -3
- package/dist/elements/layout/expanded-element.d.ts +4 -0
- package/dist/elements/layout/expanded-element.js +14 -1
- package/dist/elements/layout/flow-element.d.ts +109 -0
- package/dist/elements/layout/flow-element.js +317 -0
- package/dist/elements/layout/keep-together-element.js +14 -2
- package/dist/elements/layout/padding-element.d.ts +3 -0
- package/dist/elements/layout/padding-element.js +9 -2
- package/dist/elements/layout/page-builder-element.js +2 -2
- package/dist/elements/layout/positioned-element.d.ts +9 -2
- package/dist/elements/layout/positioned-element.js +6 -2
- package/dist/elements/layout/proxy-element.d.ts +4 -0
- package/dist/elements/layout/proxy-element.js +12 -0
- package/dist/elements/layout/region-element.d.ts +53 -0
- package/dist/elements/layout/region-element.js +109 -0
- package/dist/elements/layout/repeating-header-element.d.ts +3 -0
- package/dist/elements/layout/repeating-header-element.js +16 -1
- package/dist/elements/layout/rotated-element.d.ts +2 -0
- package/dist/elements/layout/rotated-element.js +8 -0
- package/dist/elements/pdf-element.d.ts +32 -3
- package/dist/elements/pdf-element.js +50 -1
- package/dist/elements/rectangle-element.d.ts +12 -6
- package/dist/elements/rectangle-element.js +31 -12
- package/dist/elements/row-element.d.ts +6 -12
- package/dist/elements/row-element.js +34 -4
- package/dist/elements/svg-element.js +4 -10
- package/dist/elements/text-element.d.ts +17 -1
- package/dist/elements/text-element.js +63 -40
- package/dist/ir/display-list.d.ts +6 -1
- package/dist/layout/box-constraints.d.ts +12 -0
- package/dist/layout/box-constraints.js +17 -0
- package/dist/layout/collect-images.js +8 -4
- package/dist/layout/flex-item.d.ts +2 -0
- package/dist/layout/flex-item.js +14 -2
- package/dist/layout/fragmentation.d.ts +11 -5
- package/dist/layout/fragmentation.js +27 -7
- package/dist/layout/layout-cache.d.ts +8 -8
- package/dist/layout/layout-cache.js +10 -7
- package/dist/platform/browser-fs.d.ts +1 -2
- package/dist/platform/browser-fs.js +3 -6
- package/dist/platform/node-fs.d.ts +0 -1
- package/dist/platform/node-fs.js +0 -4
- package/dist/renderer/column-break-renderer.d.ts +8 -0
- package/dist/renderer/column-break-renderer.js +15 -0
- package/dist/renderer/emoji-image.js +3 -1
- package/dist/renderer/flow-renderer.d.ts +16 -0
- package/dist/renderer/flow-renderer.js +21 -0
- package/dist/renderer/image-renderer.js +31 -54
- package/dist/renderer/index.d.ts +2 -0
- package/dist/renderer/index.js +2 -0
- package/dist/renderer/page-renderer.js +3 -4
- package/dist/renderer/pdf-backend.js +9 -2
- package/dist/renderer/pdf-document-renderer.js +2 -1
- package/dist/renderer/pdf-renderer.js +9 -0
- package/dist/renderer/positioned-renderer.d.ts +14 -2
- package/dist/renderer/positioned-renderer.js +40 -4
- package/dist/renderer/rectangle-renderer.d.ts +1 -1
- package/dist/renderer/rectangle-renderer.js +11 -6
- package/dist/renderer/rotated-renderer.js +3 -2
- package/dist/renderer/text-renderer.js +25 -28
- package/dist/svg/index.d.ts +1 -1
- package/dist/svg/index.js +26 -5
- package/dist/svg/style.js +1 -1
- package/dist/text/line-breaker.d.ts +23 -0
- package/dist/text/line-breaker.js +142 -65
- package/dist/text/line-metrics.d.ts +2 -0
- package/dist/text/line-metrics.js +5 -1
- package/dist/text/word-splitting.js +11 -5
- package/dist/utils/flex-layout.d.ts +3 -1
- package/dist/utils/flex-layout.js +141 -33
- package/dist/utils/image-helper.d.ts +59 -11
- package/dist/utils/image-helper.js +207 -80
- package/dist/utils/pdf-object-manager.d.ts +6 -3
- package/dist/utils/pdf-object-manager.js +31 -7
- package/dist/utils/renderer-registry.d.ts +6 -0
- package/dist/utils/renderer-registry.js +1 -1
- package/dist/utils/ttf-parser.js +3 -2
- package/package.json +9 -2
|
@@ -8,32 +8,198 @@ DataView.prototype.getUint24 = function (byteOffset, littleEndian = false) {
|
|
|
8
8
|
const b3 = this.getUint8(byteOffset + 2);
|
|
9
9
|
return littleEndian ? (b3 << 16) | (b2 << 8) | b1 : (b1 << 16) | (b2 << 8) | b3;
|
|
10
10
|
};
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The matrix that turns the image's unit square upright, inside it - PDF's image space, y up, the first
|
|
13
|
+
* stored row at the top. Applied before the placement scales it, so the box stays the box.
|
|
14
|
+
*/
|
|
15
|
+
export const ORIENTATION_MATRIX = {
|
|
16
|
+
2: [-1, 0, 0, 1, 1, 0],
|
|
17
|
+
3: [-1, 0, 0, -1, 1, 1],
|
|
18
|
+
4: [1, 0, 0, -1, 0, 1],
|
|
19
|
+
5: [0, -1, -1, 0, 1, 1],
|
|
20
|
+
6: [0, -1, 1, 0, 0, 1],
|
|
21
|
+
7: [0, 1, 1, 0, 0, 0],
|
|
22
|
+
8: [0, 1, -1, 0, 1, 0],
|
|
23
|
+
};
|
|
24
|
+
/** The resolution of a file that declares none: CSS's reference pixel, 1/96 inch. */
|
|
25
|
+
export const DEFAULT_DPI = 96;
|
|
26
|
+
/**
|
|
27
|
+
* An image's own size in points, upright: its pixels at the resolution the file declares, else 96 dpi,
|
|
28
|
+
* turned by its orientation.
|
|
29
|
+
*/
|
|
30
|
+
export function naturalSize(dims) {
|
|
31
|
+
const width = (dims.width * 72) / (dims.dpi?.x ?? DEFAULT_DPI);
|
|
32
|
+
const height = (dims.height * 72) / (dims.dpi?.y ?? DEFAULT_DPI);
|
|
33
|
+
return (dims.orientation ?? 1) >= 5 ? { width: height, height: width } : { width, height };
|
|
34
|
+
}
|
|
35
|
+
/** A resolution only counts if both axes are real numbers above zero; anything else is no declaration. */
|
|
36
|
+
function resolution(x, y) {
|
|
37
|
+
return Number.isFinite(x) && Number.isFinite(y) && x > 0 && y > 0 ? { x, y } : undefined;
|
|
38
|
+
}
|
|
39
|
+
/** PNG `pHYs`: pixels per unit, and only unit 1 (meter) is a physical size - 0 is an aspect ratio. */
|
|
40
|
+
function pngResolution(view) {
|
|
41
|
+
for (let offset = 8; offset + 8 <= view.byteLength;) {
|
|
42
|
+
const length = view.getUint32(offset);
|
|
43
|
+
const type = view.getUint32(offset + 4);
|
|
44
|
+
if (type === 0x49444154)
|
|
45
|
+
return undefined; // IDAT: pHYs must come before it
|
|
46
|
+
if (type === 0x70485973 && length >= 9 && offset + 17 <= view.byteLength) {
|
|
47
|
+
if (view.getUint8(offset + 16) !== 1)
|
|
48
|
+
return undefined;
|
|
49
|
+
return resolution(view.getUint32(offset + 8) * 0.0254, view.getUint32(offset + 12) * 0.0254);
|
|
50
|
+
}
|
|
51
|
+
offset += 12 + length;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** JFIF `APP0`: units 1 = dots per inch, 2 = per centimeter, 0 = an aspect ratio only. */
|
|
56
|
+
function jfifResolution(view, data) {
|
|
57
|
+
if (view.getUint32(data) !== 0x4a464946 || view.getUint8(data + 4) !== 0)
|
|
58
|
+
return undefined; // "JFIF\0"
|
|
59
|
+
const units = view.getUint8(data + 7);
|
|
60
|
+
const scale = units === 1 ? 1 : units === 2 ? 2.54 : 0;
|
|
61
|
+
if (scale === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
return resolution(view.getUint16(data + 8) * scale, view.getUint16(data + 10) * scale);
|
|
64
|
+
}
|
|
65
|
+
/** The first IFD of a TIFF block (EXIF), as tag → the entry's offset in `view`. */
|
|
66
|
+
function tiffEntries(view, tiff) {
|
|
67
|
+
const le = view.getUint16(tiff) === 0x4949; // "II"
|
|
68
|
+
const ifd = tiff + view.getUint32(tiff + 4, le);
|
|
69
|
+
const entries = new Map();
|
|
70
|
+
const count = view.getUint16(ifd, le);
|
|
71
|
+
for (let i = 0; i < count; i++) {
|
|
72
|
+
const entry = ifd + 2 + i * 12;
|
|
73
|
+
entries.set(view.getUint16(entry, le), entry);
|
|
74
|
+
}
|
|
75
|
+
return { entries, le };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* EXIF `APP1`: XResolution / YResolution (rationals) in ResolutionUnit, 2 = inch (the default), 3 = cm;
|
|
79
|
+
* Orientation 1-8.
|
|
80
|
+
*/
|
|
81
|
+
function exifInfo(view, data) {
|
|
82
|
+
if (view.getUint32(data) !== 0x45786966 || view.getUint16(data + 4) !== 0)
|
|
83
|
+
return undefined; // "Exif\0\0"
|
|
84
|
+
const tiff = data + 6;
|
|
85
|
+
const { entries, le } = tiffEntries(view, tiff);
|
|
86
|
+
const rational = (tag) => {
|
|
87
|
+
const entry = entries.get(tag);
|
|
88
|
+
if (entry === undefined)
|
|
89
|
+
return NaN;
|
|
90
|
+
const at = tiff + view.getUint32(entry + 8, le);
|
|
91
|
+
return view.getUint32(at, le) / view.getUint32(at + 4, le);
|
|
92
|
+
};
|
|
93
|
+
const short = (tag) => {
|
|
94
|
+
const entry = entries.get(tag);
|
|
95
|
+
return entry === undefined ? undefined : view.getUint16(entry + 8, le);
|
|
96
|
+
};
|
|
97
|
+
const unit = short(0x0128) ?? 2;
|
|
98
|
+
const scale = unit === 2 ? 1 : unit === 3 ? 2.54 : 0;
|
|
99
|
+
const dpi = scale === 0 ? undefined : resolution(rational(0x011a) * scale, rational(0x011b) * scale);
|
|
100
|
+
const turn = short(0x0112);
|
|
101
|
+
const orientation = turn !== undefined && turn >= 2 && turn <= 8 ? turn : undefined;
|
|
102
|
+
return { ...(dpi ? { dpi } : {}), ...(orientation ? { orientation } : {}) };
|
|
103
|
+
}
|
|
104
|
+
/** What a header declares is metadata: a malformed declaration is none, never a failed image. */
|
|
105
|
+
function tryRead(read) {
|
|
106
|
+
try {
|
|
107
|
+
return read();
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Start-of-frame markers; the rest of 0xFFC0..0xFFCF are tables (DHT C4, JPG C8, DAC CC). */
|
|
114
|
+
const isSof = (marker) => marker >= 0xffc0 &&
|
|
115
|
+
marker <= 0xffcf &&
|
|
116
|
+
marker !== 0xffc4 &&
|
|
117
|
+
marker !== 0xffc8 &&
|
|
118
|
+
marker !== 0xffcc;
|
|
119
|
+
/** Markers without a length: TEM and the restart markers. */
|
|
120
|
+
const isStandalone = (marker) => marker === 0xff01 || (marker >= 0xffd0 && marker <= 0xffd7);
|
|
121
|
+
function jpegHeader(view) {
|
|
122
|
+
// JFIF is the container's own declaration and wins; EXIF is what a camera writes.
|
|
123
|
+
let jfif;
|
|
124
|
+
let exif;
|
|
125
|
+
let adobe = false;
|
|
126
|
+
let offset = 2;
|
|
127
|
+
while (offset + 4 <= view.byteLength) {
|
|
128
|
+
if (view.getUint8(offset + 1) === 0xff) {
|
|
129
|
+
offset++; // a fill byte before the marker
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const marker = view.getUint16(offset);
|
|
133
|
+
offset += 2;
|
|
134
|
+
if (isStandalone(marker))
|
|
135
|
+
continue;
|
|
136
|
+
if (isSof(marker)) {
|
|
137
|
+
const dpi = jfif ?? exif?.dpi;
|
|
138
|
+
const orientation = exif?.orientation;
|
|
139
|
+
return {
|
|
140
|
+
height: view.getUint16(offset + 3),
|
|
141
|
+
width: view.getUint16(offset + 5),
|
|
142
|
+
components: view.getUint8(offset + 7),
|
|
143
|
+
sof: marker,
|
|
144
|
+
adobe,
|
|
145
|
+
...(dpi ? { dpi } : {}),
|
|
146
|
+
...(orientation ? { orientation } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (marker === 0xffe0)
|
|
150
|
+
jfif ?? (jfif = tryRead(() => jfifResolution(view, offset + 2)));
|
|
151
|
+
if (marker === 0xffe1)
|
|
152
|
+
exif ?? (exif = tryRead(() => exifInfo(view, offset + 2)));
|
|
153
|
+
// "Adobe"
|
|
154
|
+
if (marker === 0xffee &&
|
|
155
|
+
view.getUint32(offset + 2) === 0x41646f62 &&
|
|
156
|
+
view.getUint8(offset + 6) === 0x65)
|
|
157
|
+
adobe = true;
|
|
158
|
+
offset += view.getUint16(offset);
|
|
159
|
+
}
|
|
160
|
+
throw new Error("Unsupported image format");
|
|
161
|
+
}
|
|
162
|
+
/** Thrown for a JPEG we cannot embed as it is. Says what it is and what to do. */
|
|
163
|
+
export class JpegFormatError extends Error {
|
|
164
|
+
constructor(message) {
|
|
165
|
+
super(`@jasy/pdf: ${message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Baseline, extended sequential and progressive: the codings a PDF viewer's DCTDecode reads. */
|
|
169
|
+
const DCT_CODINGS = new Set([0xffc0, 0xffc1, 0xffc2]);
|
|
170
|
+
/** The color of a JPEG as it is embedded (`DCTDecode`): the space it was saved in. */
|
|
171
|
+
export function jpegColor(bytes) {
|
|
172
|
+
const header = jpegHeader(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
173
|
+
if (!DCT_CODINGS.has(header.sof)) {
|
|
174
|
+
throw new JpegFormatError("this JPEG is lossless, hierarchical or arithmetic-coded, which PDF viewers cannot decode. " +
|
|
175
|
+
"Save it again as a baseline or progressive JPEG, or as a PNG.");
|
|
176
|
+
}
|
|
177
|
+
switch (header.components) {
|
|
178
|
+
case 1:
|
|
179
|
+
return { space: "gray" };
|
|
180
|
+
case 3:
|
|
181
|
+
return { space: "rgb" };
|
|
182
|
+
case 4:
|
|
183
|
+
return header.adobe ? { space: "cmyk", inverted: true } : { space: "cmyk" };
|
|
184
|
+
default:
|
|
185
|
+
throw new JpegFormatError(`this JPEG has ${header.components} color channels; only gray, RGB and CMYK are embedded.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Read from the header alone, synchronously - layout can ask it. */
|
|
189
|
+
export function readImageDimensions(buffer) {
|
|
12
190
|
// The view's own window: a `Uint8Array` may be a slice of a larger buffer (a Node `Buffer` usually is).
|
|
13
191
|
const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
14
|
-
// Check for JPEG (0xFFD8 is the start of JPEG file)
|
|
15
192
|
if (dataView.getUint16(0) === 0xffd8) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const marker = dataView.getUint16(offset, false);
|
|
19
|
-
offset += 2;
|
|
20
|
-
if (marker === 0xffc0 || marker === 0xffc2) {
|
|
21
|
-
// SOF0 or SOF2
|
|
22
|
-
return {
|
|
23
|
-
height: dataView.getUint16(offset + 3, false),
|
|
24
|
-
width: dataView.getUint16(offset + 5, false),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
else {
|
|
28
|
-
offset += dataView.getUint16(offset, false);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
193
|
+
const { width, height, dpi, orientation } = jpegHeader(dataView);
|
|
194
|
+
return { width, height, ...(dpi ? { dpi } : {}), ...(orientation ? { orientation } : {}) };
|
|
31
195
|
}
|
|
32
196
|
// Check for PNG (0x89504E47 is the PNG signature)
|
|
33
197
|
if (dataView.getUint32(0) === 0x89504e47) {
|
|
198
|
+
const dpi = tryRead(() => pngResolution(dataView));
|
|
34
199
|
return {
|
|
35
200
|
width: dataView.getUint32(16, false),
|
|
36
201
|
height: dataView.getUint32(20, false),
|
|
202
|
+
...(dpi ? { dpi } : {}),
|
|
37
203
|
};
|
|
38
204
|
}
|
|
39
205
|
// Check for BMP (0x424D is the BMP signature)
|
|
@@ -123,75 +289,36 @@ export async function convertImageToGrayscaleBuffer(imagePath) {
|
|
|
123
289
|
const grayscaleBuffer = await image.getBuffer(mimeType);
|
|
124
290
|
return grayscaleBuffer;
|
|
125
291
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
let width, height, offsetX, offsetY;
|
|
130
|
-
if (imageAspectRatio > containerAspectRatio) {
|
|
131
|
-
// The images width is bigger than the containers width
|
|
132
|
-
const scaleFactor = containerWidth / imageWidth;
|
|
133
|
-
width = containerWidth;
|
|
134
|
-
height = imageHeight * scaleFactor;
|
|
135
|
-
offsetX = 0;
|
|
136
|
-
offsetY = (containerHeight - height) / 2; // Center vertically
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
// THe images height is bigge rthan the containers height
|
|
140
|
-
const scaleFactor = containerHeight / imageHeight;
|
|
141
|
-
width = imageWidth * scaleFactor;
|
|
142
|
-
height = containerHeight;
|
|
143
|
-
offsetX = (containerWidth - width) / 2; // Center horizontally
|
|
144
|
-
offsetY = 0;
|
|
145
|
-
}
|
|
292
|
+
const CENTER = { x: 0.5, y: 0.5 };
|
|
293
|
+
/** The one place a fitted image is positioned: what is left over (or overhangs) is split by `align`. */
|
|
294
|
+
function placed(width, height, containerWidth, containerHeight, align) {
|
|
146
295
|
return {
|
|
147
296
|
width,
|
|
148
297
|
height,
|
|
149
|
-
offsetX,
|
|
150
|
-
offsetY,
|
|
298
|
+
offsetX: (containerWidth - width) * align.x,
|
|
299
|
+
offsetY: (containerHeight - height) * align.y,
|
|
151
300
|
};
|
|
152
301
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
width = containerWidth;
|
|
169
|
-
height = imageHeight * scaleFactor;
|
|
170
|
-
offsetX = 0;
|
|
171
|
-
offsetY = (containerHeight - height) / 2; // Center vertically
|
|
172
|
-
}
|
|
173
|
-
return {
|
|
174
|
-
width,
|
|
175
|
-
height,
|
|
176
|
-
offsetX,
|
|
177
|
-
offsetY,
|
|
178
|
-
};
|
|
302
|
+
/** Scaled by the width's or the height's factor; that axis takes the box's extent exactly. */
|
|
303
|
+
function scaled(imageWidth, imageHeight, containerWidth, containerHeight, align, byWidthWins) {
|
|
304
|
+
const byWidth = containerWidth / imageWidth;
|
|
305
|
+
const byHeight = containerHeight / imageHeight;
|
|
306
|
+
return byWidthWins(byWidth, byHeight)
|
|
307
|
+
? placed(containerWidth, imageHeight * byWidth, containerWidth, containerHeight, align)
|
|
308
|
+
: placed(imageWidth * byHeight, containerHeight, containerWidth, containerHeight, align);
|
|
309
|
+
}
|
|
310
|
+
/** Scaled to fit inside the box, whole image visible. */
|
|
311
|
+
export function applyContainFit(imageWidth, imageHeight, containerWidth, containerHeight, align = CENTER) {
|
|
312
|
+
return scaled(imageWidth, imageHeight, containerWidth, containerHeight, align, (w, h) => w < h);
|
|
313
|
+
}
|
|
314
|
+
/** Scaled to cover the box, the overhang cut off. */
|
|
315
|
+
export function applyCoverFit(imageWidth, imageHeight, containerWidth, containerHeight, align = CENTER) {
|
|
316
|
+
return scaled(imageWidth, imageHeight, containerWidth, containerHeight, align, (w, h) => w >= h);
|
|
179
317
|
}
|
|
180
318
|
export function applyFillFit(containerWidth, containerHeight) {
|
|
181
|
-
return {
|
|
182
|
-
width: containerWidth,
|
|
183
|
-
height: containerHeight,
|
|
184
|
-
offsetX: 0,
|
|
185
|
-
offsetY: 0,
|
|
186
|
-
};
|
|
319
|
+
return { width: containerWidth, height: containerHeight, offsetX: 0, offsetY: 0 };
|
|
187
320
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
width: imageWidth, // Hold orignal image size
|
|
193
|
-
height: imageHeight,
|
|
194
|
-
offsetX: offsetX > 0 ? offsetX : 0, // Dont move outside the container...
|
|
195
|
-
offsetY: offsetY > 0 ? offsetY : 0,
|
|
196
|
-
};
|
|
321
|
+
/** At its own size (`naturalSize`), the overhang cut off. */
|
|
322
|
+
export function applyFitNone(imageWidth, imageHeight, containerWidth, containerHeight, align = CENTER) {
|
|
323
|
+
return placed(imageWidth, imageHeight, containerWidth, containerHeight, align);
|
|
197
324
|
}
|
|
@@ -13,6 +13,7 @@ import { AcroFormCollector } from "../forms/acroform.js";
|
|
|
13
13
|
import type { PDFConfig } from "../renderer/pdf-document-class.js";
|
|
14
14
|
import type { FontMetrics } from "./font-metrics.js";
|
|
15
15
|
import { Diagnostics } from "../common/diagnostics.js";
|
|
16
|
+
import { type ImageColor } from "./image-helper.js";
|
|
16
17
|
interface FontIndexes {
|
|
17
18
|
fontIndex: number;
|
|
18
19
|
resourceIndex: number;
|
|
@@ -115,7 +116,7 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
115
116
|
private getCurrentByteLength;
|
|
116
117
|
setParentObjectNumber(number: number): void;
|
|
117
118
|
getParentObjectNumber(): number;
|
|
118
|
-
registerImage(width: number, height: number, imageType: string, imageData: string, smaskData?: string): number;
|
|
119
|
+
registerImage(width: number, height: number, imageType: string, imageData: string, smaskData?: string, color?: ImageColor): number;
|
|
119
120
|
attachFile(name: string, data: Uint8Array, opts?: {
|
|
120
121
|
relationship?: string;
|
|
121
122
|
mimeType?: string;
|
|
@@ -167,7 +168,7 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
167
168
|
*/
|
|
168
169
|
hasGlyph(codePoint: number, fontFamily: string, fontStyle?: FontStyle): boolean;
|
|
169
170
|
/**
|
|
170
|
-
* Whether the document draws this code point through the
|
|
171
|
+
* Whether the document draws this code point through the color-emoji path instead of the text
|
|
171
172
|
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
172
173
|
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
173
174
|
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
@@ -207,7 +208,9 @@ export declare class PDFObjectManager implements FontMetrics {
|
|
|
207
208
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
208
209
|
*/
|
|
209
210
|
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
210
|
-
* Same lookup as the character path; `getKerning` is keyed by glyph id either way.
|
|
211
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. A pair that
|
|
212
|
+
* touches a space is never kerned, as on the character path: the line breaker measures words and
|
|
213
|
+
* spaces apart, so a pair across a space would be drawn but never measured. */
|
|
211
214
|
getGlyphKernPairs(glyphs: readonly number[], fontFamily: string, fontStyle: FontStyle): number[];
|
|
212
215
|
getKernPairs(text: string, fontFamily: string, fontStyle: FontStyle, ligatures?: boolean): number[];
|
|
213
216
|
/**
|
|
@@ -36,6 +36,13 @@ import { AcroFormCollector } from "../forms/acroform.js";
|
|
|
36
36
|
// erased at runtime so it can come from the cyclic module safely.
|
|
37
37
|
import { ColorMode, Orientation } from "../renderer/pdf-config.js";
|
|
38
38
|
import { Diagnostics } from "../common/diagnostics.js";
|
|
39
|
+
import { JpegFormatError } from "./image-helper.js";
|
|
40
|
+
/** The PDF color space of each image color space, and its channel count (for `/Decode`). */
|
|
41
|
+
const IMAGE_COLOR_SPACE = {
|
|
42
|
+
gray: { name: "DeviceGray", channels: 1 },
|
|
43
|
+
rgb: { name: "DeviceRGB", channels: 3 },
|
|
44
|
+
cmyk: { name: "DeviceCMYK", channels: 4 },
|
|
45
|
+
};
|
|
39
46
|
export var FontStyle;
|
|
40
47
|
(function (FontStyle) {
|
|
41
48
|
FontStyle["Normal"] = "normal";
|
|
@@ -396,7 +403,7 @@ export class PDFObjectManager {
|
|
|
396
403
|
return this.parentObjectNumber;
|
|
397
404
|
}
|
|
398
405
|
// Registers an image
|
|
399
|
-
registerImage(width, height, imageType, imageData, smaskData) {
|
|
406
|
+
registerImage(width, height, imageType, imageData, smaskData, color = { space: "rgb" }) {
|
|
400
407
|
// A transparent PNG carries its alpha as a separate DeviceGray /SMask image the viewer composites with.
|
|
401
408
|
let smaskEntry = "";
|
|
402
409
|
if (smaskData) {
|
|
@@ -414,14 +421,23 @@ ${s.body}
|
|
|
414
421
|
endstream`;
|
|
415
422
|
smaskEntry = ` /SMask ${this.addObject(smaskObject)} 0 R\n`;
|
|
416
423
|
}
|
|
424
|
+
// PDF/A: the sRGB output intent forbids DeviceCMYK.
|
|
425
|
+
if (color.space === "cmyk" && this.outputIntent !== undefined) {
|
|
426
|
+
throw new JpegFormatError("a CMYK JPEG in a PDF/A document: its sRGB output intent forbids DeviceCMYK. " +
|
|
427
|
+
"Convert the image to RGB first.");
|
|
428
|
+
}
|
|
429
|
+
const space = IMAGE_COLOR_SPACE[color.space];
|
|
430
|
+
const decode = color.inverted
|
|
431
|
+
? ` /Decode [${Array.from({ length: space.channels }, () => "1 0").join(" ")}]\n`
|
|
432
|
+
: "";
|
|
417
433
|
const img = this.streamPayload(bytesFromLatin1(imageData));
|
|
418
434
|
const imageObject = `<< /Type /XObject
|
|
419
435
|
/Subtype /Image
|
|
420
436
|
/Width ${width}
|
|
421
437
|
/Height ${height}
|
|
422
|
-
/ColorSpace
|
|
438
|
+
/ColorSpace /${space.name}
|
|
423
439
|
/BitsPerComponent 8
|
|
424
|
-
/Filter /${imageType}
|
|
440
|
+
${decode} /Filter /${imageType}
|
|
425
441
|
${smaskEntry} /Length ${img.length} >>
|
|
426
442
|
stream
|
|
427
443
|
${img.body}
|
|
@@ -767,7 +783,7 @@ endstream`;
|
|
|
767
783
|
return isWindows1252(codePoint);
|
|
768
784
|
}
|
|
769
785
|
/**
|
|
770
|
-
* Whether the document draws this code point through the
|
|
786
|
+
* Whether the document draws this code point through the color-emoji path instead of the text
|
|
771
787
|
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
772
788
|
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
773
789
|
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
@@ -898,7 +914,9 @@ endstream`;
|
|
|
898
914
|
const all = ttf.glyphWidths();
|
|
899
915
|
const w = [...used]
|
|
900
916
|
.sort((a, b) => a - b)
|
|
901
|
-
|
|
917
|
+
// Four decimals: a 2048-unit font's widths are not whole thousandths, and rounding them drew every
|
|
918
|
+
// line a fraction wider or narrower than it was measured. A 1000-unit font stays byte-identical.
|
|
919
|
+
.map((g) => `${g} [${Number((all[g] ?? 1000).toFixed(4))}]`)
|
|
902
920
|
.join(" ");
|
|
903
921
|
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
|
|
904
922
|
`/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
|
|
@@ -997,14 +1015,20 @@ endstream`;
|
|
|
997
1015
|
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
998
1016
|
*/
|
|
999
1017
|
/** Kerning between adjacent GLYPHS - what a shaped run needs, and what the backend already holds.
|
|
1000
|
-
* Same lookup as the character path; `getKerning` is keyed by glyph id either way.
|
|
1018
|
+
* Same lookup as the character path; `getKerning` is keyed by glyph id either way. A pair that
|
|
1019
|
+
* touches a space is never kerned, as on the character path: the line breaker measures words and
|
|
1020
|
+
* spaces apart, so a pair across a space would be drawn but never measured. */
|
|
1001
1021
|
getGlyphKernPairs(glyphs, fontFamily, fontStyle) {
|
|
1002
1022
|
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
1003
1023
|
if (!ttf)
|
|
1004
1024
|
return [];
|
|
1025
|
+
const space = ttf.getGlyphIndex(0x20);
|
|
1026
|
+
const noBreakSpace = ttf.getGlyphIndex(0xa0);
|
|
1027
|
+
const isSpace = (g) => g !== 0 && (g === space || g === noBreakSpace); // 0 = no such glyph
|
|
1005
1028
|
const pairs = [];
|
|
1006
1029
|
for (let i = 0; i < glyphs.length - 1; i++) {
|
|
1007
|
-
|
|
1030
|
+
const [a, b] = [glyphs[i], glyphs[i + 1]];
|
|
1031
|
+
pairs.push(isSpace(a) || isSpace(b) ? 0 : ttf.getKerning(a, b));
|
|
1008
1032
|
}
|
|
1009
1033
|
return pairs;
|
|
1010
1034
|
}
|
|
@@ -11,6 +11,12 @@ import type { PDFObjectManager } from "./pdf-object-manager.js";
|
|
|
11
11
|
export interface RenderScope {
|
|
12
12
|
origin: Offset;
|
|
13
13
|
frame: Offset;
|
|
14
|
+
/** Where a `Positioned` with a `zIndex` hands its drawing, for the nearest stacking context to sort.
|
|
15
|
+
* It still RENDERS at its own place, so everything it registers there (its tag's parent) is right. */
|
|
16
|
+
stacked?: {
|
|
17
|
+
zIndex: number;
|
|
18
|
+
nodes: IRNode[];
|
|
19
|
+
}[];
|
|
14
20
|
}
|
|
15
21
|
/** The scope for the children of an element at `offset` (relative to `scope.origin`). */
|
|
16
22
|
export declare function within(scope: RenderScope, offset: {
|
|
@@ -30,7 +30,7 @@ export class ElementReusedError extends Error {
|
|
|
30
30
|
constructor(elementName) {
|
|
31
31
|
super(`@jasy/pdf: the same ${elementName} element is used twice on one page. An element has ONE ` +
|
|
32
32
|
`position, so both copies would be drawn in the same place. Create it twice instead - call the ` +
|
|
33
|
-
`factory again (e.g. a function \`const divider = () => Divider()\`)
|
|
33
|
+
`factory again (e.g. a function \`const divider = () => Divider()\`) or place \`element.clone()\`.`);
|
|
34
34
|
this.elementName = elementName;
|
|
35
35
|
}
|
|
36
36
|
}
|
package/dist/utils/ttf-parser.js
CHANGED
|
@@ -149,11 +149,12 @@ export class TTFParser {
|
|
|
149
149
|
glyphCount() {
|
|
150
150
|
return this.numGlyphs;
|
|
151
151
|
}
|
|
152
|
-
// Advance width of every glyph in PDF glyph space (the /W array of the CIDFont)
|
|
152
|
+
// Advance width of every glyph in PDF glyph space (the /W array of the CIDFont), NOT rounded: layout
|
|
153
|
+
// measures the exact width, so the drawn one must be the same number.
|
|
153
154
|
glyphWidths() {
|
|
154
155
|
const out = [];
|
|
155
156
|
for (let g = 0; g < this.numGlyphs; g++) {
|
|
156
|
-
out.push(
|
|
157
|
+
out.push((this.getAdvanceWidth(g) * 1000) / this.unitsPerEm);
|
|
157
158
|
}
|
|
158
159
|
return out;
|
|
159
160
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jasy/pdf",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
|
+
"description": "Create PDFs in TypeScript: declarative, component-based layout with real pagination, in Node or the browser. Its own engine - no headless browser, no Java. Powers ZUGFeRD / XRechnung e-invoices.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"accessibility",
|
|
7
|
+
"browser",
|
|
6
8
|
"declarative",
|
|
7
9
|
"e-invoice",
|
|
8
10
|
"factur-x",
|
|
9
11
|
"invoice",
|
|
10
12
|
"layout",
|
|
13
|
+
"nodejs",
|
|
11
14
|
"pagination",
|
|
12
15
|
"pdf",
|
|
13
16
|
"pdf-a",
|
|
14
17
|
"pdf-generation",
|
|
18
|
+
"pdf-generator",
|
|
19
|
+
"pdf-ua",
|
|
15
20
|
"typescript",
|
|
16
21
|
"xrechnung",
|
|
17
22
|
"zugferd"
|
|
@@ -62,6 +67,8 @@
|
|
|
62
67
|
"cli": "pnpm --filter @jasy/cli dev",
|
|
63
68
|
"cli:watch": "pnpm --filter @jasy/cli watch",
|
|
64
69
|
"play": "pnpm --filter @jasy/playground play",
|
|
70
|
+
"play:vue": "pnpm --filter @jasy/vue play",
|
|
71
|
+
"play:react": "pnpm --filter @jasy/react play",
|
|
65
72
|
"test": "vitest",
|
|
66
73
|
"test:coverage": "vitest --coverage",
|
|
67
74
|
"build": "tsc && node scripts/fix-dts-ext.mjs && pnpm run copy-assets",
|