@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.
Files changed (111) hide show
  1. package/README.md +3 -2
  2. package/dist/api/color.d.ts +1 -24
  3. package/dist/api/color.js +2 -278
  4. package/dist/api/content.d.ts +7 -2
  5. package/dist/api/content.js +17 -7
  6. package/dist/api/descriptor.js +30 -3
  7. package/dist/api/flow.d.ts +55 -0
  8. package/dist/api/flow.js +71 -0
  9. package/dist/api/gradient.d.ts +1 -53
  10. package/dist/api/gradient.js +2 -97
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/layout.js +4 -3
  14. package/dist/api/structure.d.ts +1 -1
  15. package/dist/api/table.js +3 -1
  16. package/dist/api/text.d.ts +8 -8
  17. package/dist/api/text.js +6 -0
  18. package/dist/canvas/painter.d.ts +2 -2
  19. package/dist/canvas/painter.js +2 -2
  20. package/dist/common/color-input.d.ts +24 -0
  21. package/dist/common/color-input.js +278 -0
  22. package/dist/common/diagnostics.d.ts +9 -1
  23. package/dist/common/gradient.d.ts +53 -0
  24. package/dist/common/gradient.js +97 -0
  25. package/dist/elements/container-element.d.ts +3 -0
  26. package/dist/elements/container-element.js +17 -5
  27. package/dist/elements/image-element.d.ts +37 -26
  28. package/dist/elements/image-element.js +88 -50
  29. package/dist/elements/index.d.ts +3 -0
  30. package/dist/elements/index.js +3 -0
  31. package/dist/elements/layout/column-break-element.d.ts +17 -0
  32. package/dist/elements/layout/column-break-element.js +26 -0
  33. package/dist/elements/layout/deferred-element.d.ts +9 -1
  34. package/dist/elements/layout/deferred-element.js +18 -3
  35. package/dist/elements/layout/expanded-element.d.ts +4 -0
  36. package/dist/elements/layout/expanded-element.js +14 -1
  37. package/dist/elements/layout/flow-element.d.ts +109 -0
  38. package/dist/elements/layout/flow-element.js +317 -0
  39. package/dist/elements/layout/keep-together-element.js +14 -2
  40. package/dist/elements/layout/padding-element.d.ts +3 -0
  41. package/dist/elements/layout/padding-element.js +9 -2
  42. package/dist/elements/layout/page-builder-element.js +2 -2
  43. package/dist/elements/layout/positioned-element.d.ts +9 -2
  44. package/dist/elements/layout/positioned-element.js +6 -2
  45. package/dist/elements/layout/proxy-element.d.ts +4 -0
  46. package/dist/elements/layout/proxy-element.js +12 -0
  47. package/dist/elements/layout/region-element.d.ts +53 -0
  48. package/dist/elements/layout/region-element.js +109 -0
  49. package/dist/elements/layout/repeating-header-element.d.ts +3 -0
  50. package/dist/elements/layout/repeating-header-element.js +16 -1
  51. package/dist/elements/layout/rotated-element.d.ts +2 -0
  52. package/dist/elements/layout/rotated-element.js +8 -0
  53. package/dist/elements/pdf-element.d.ts +32 -3
  54. package/dist/elements/pdf-element.js +50 -1
  55. package/dist/elements/rectangle-element.d.ts +12 -6
  56. package/dist/elements/rectangle-element.js +31 -12
  57. package/dist/elements/row-element.d.ts +6 -12
  58. package/dist/elements/row-element.js +34 -4
  59. package/dist/elements/svg-element.js +4 -10
  60. package/dist/elements/text-element.d.ts +17 -1
  61. package/dist/elements/text-element.js +63 -40
  62. package/dist/ir/display-list.d.ts +6 -1
  63. package/dist/layout/box-constraints.d.ts +12 -0
  64. package/dist/layout/box-constraints.js +17 -0
  65. package/dist/layout/collect-images.js +8 -4
  66. package/dist/layout/flex-item.d.ts +2 -0
  67. package/dist/layout/flex-item.js +14 -2
  68. package/dist/layout/fragmentation.d.ts +11 -5
  69. package/dist/layout/fragmentation.js +27 -7
  70. package/dist/layout/layout-cache.d.ts +8 -8
  71. package/dist/layout/layout-cache.js +10 -7
  72. package/dist/platform/browser-fs.d.ts +1 -2
  73. package/dist/platform/browser-fs.js +3 -6
  74. package/dist/platform/node-fs.d.ts +0 -1
  75. package/dist/platform/node-fs.js +0 -4
  76. package/dist/renderer/column-break-renderer.d.ts +8 -0
  77. package/dist/renderer/column-break-renderer.js +15 -0
  78. package/dist/renderer/emoji-image.js +3 -1
  79. package/dist/renderer/flow-renderer.d.ts +16 -0
  80. package/dist/renderer/flow-renderer.js +21 -0
  81. package/dist/renderer/image-renderer.js +31 -54
  82. package/dist/renderer/index.d.ts +2 -0
  83. package/dist/renderer/index.js +2 -0
  84. package/dist/renderer/page-renderer.js +3 -4
  85. package/dist/renderer/pdf-backend.js +9 -2
  86. package/dist/renderer/pdf-document-renderer.js +2 -1
  87. package/dist/renderer/pdf-renderer.js +9 -0
  88. package/dist/renderer/positioned-renderer.d.ts +14 -2
  89. package/dist/renderer/positioned-renderer.js +40 -4
  90. package/dist/renderer/rectangle-renderer.d.ts +1 -1
  91. package/dist/renderer/rectangle-renderer.js +11 -6
  92. package/dist/renderer/rotated-renderer.js +3 -2
  93. package/dist/renderer/text-renderer.js +25 -28
  94. package/dist/svg/index.d.ts +1 -1
  95. package/dist/svg/index.js +26 -5
  96. package/dist/svg/style.js +1 -1
  97. package/dist/text/line-breaker.d.ts +23 -0
  98. package/dist/text/line-breaker.js +142 -65
  99. package/dist/text/line-metrics.d.ts +2 -0
  100. package/dist/text/line-metrics.js +5 -1
  101. package/dist/text/word-splitting.js +11 -5
  102. package/dist/utils/flex-layout.d.ts +3 -1
  103. package/dist/utils/flex-layout.js +141 -33
  104. package/dist/utils/image-helper.d.ts +59 -11
  105. package/dist/utils/image-helper.js +207 -80
  106. package/dist/utils/pdf-object-manager.d.ts +6 -3
  107. package/dist/utils/pdf-object-manager.js +31 -7
  108. package/dist/utils/renderer-registry.d.ts +6 -0
  109. package/dist/utils/renderer-registry.js +1 -1
  110. package/dist/utils/ttf-parser.js +3 -2
  111. 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
- export async function getImageDimensions(buffer) {
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
- let offset = 2;
17
- while (offset < buffer.byteLength) {
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
- export function applyContainFit(imageWidth, imageHeight, containerWidth, containerHeight) {
127
- const imageAspectRatio = imageWidth / imageHeight;
128
- const containerAspectRatio = containerWidth / containerHeight;
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
- export function applyCoverFit(imageWidth, imageHeight, containerWidth, containerHeight) {
154
- const imageAspectRatio = imageWidth / imageHeight;
155
- const containerAspectRatio = containerWidth / containerHeight;
156
- let width, height, offsetX, offsetY;
157
- if (imageAspectRatio > containerAspectRatio) {
158
- // The images width is bigger than containers width
159
- const scaleFactor = containerHeight / imageHeight;
160
- width = imageWidth * scaleFactor;
161
- height = containerHeight;
162
- offsetX = (containerWidth - width) / 2; // Center horizontally
163
- offsetY = 0;
164
- }
165
- else {
166
- // The images height is bigger than containers height
167
- const scaleFactor = containerWidth / imageWidth;
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
- export function applyFitNone(imageWidth, imageHeight, containerWidth, containerHeight) {
189
- const offsetX = (containerWidth - imageWidth) / 2; // Center horizontally
190
- const offsetY = (containerHeight - imageHeight) / 2; // Center vertically
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 colour-emoji path instead of the text
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 /DeviceRGB
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 colour-emoji path instead of the text
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
- .map((g) => `${g} [${all[g] ?? 1000}]`)
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
- pairs.push(ttf.getKerning(glyphs[i], glyphs[i + 1]));
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()\`) rather than reusing one value.`);
33
+ `factory again (e.g. a function \`const divider = () => Divider()\`) or place \`element.clone()\`.`);
34
34
  this.elementName = elementName;
35
35
  }
36
36
  }
@@ -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(this.toGlyphSpace(this.getAdvanceWidth(g)));
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",
4
- "description": "Declarative, component-based PDF generation in pure TypeScript - Flutter-style components, real AFM font metrics, a hand-rolled PDF writer. No headless browser, no Java.",
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",