@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.11
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/LICENSE +21 -0
- package/README.md +35 -21
- package/dist/api/args.d.ts +1 -1
- package/dist/api/args.js +2 -5
- package/dist/api/color.d.ts +4 -4
- package/dist/api/color.js +11 -17
- package/dist/api/content.d.ts +20 -13
- package/dist/api/content.js +44 -29
- package/dist/api/descriptor.d.ts +2 -2
- package/dist/api/descriptor.js +121 -31
- package/dist/api/dimension.d.ts +76 -0
- package/dist/api/dimension.js +43 -0
- package/dist/api/font-url.d.ts +28 -0
- package/dist/api/font-url.js +134 -0
- package/dist/api/forms.d.ts +347 -0
- package/dist/api/forms.js +189 -0
- package/dist/api/gradient.d.ts +53 -0
- package/dist/api/gradient.js +97 -0
- package/dist/api/index.d.ts +13 -8
- package/dist/api/index.js +13 -24
- package/dist/api/insets.d.ts +19 -12
- package/dist/api/insets.js +19 -12
- package/dist/api/layout.d.ts +149 -25
- package/dist/api/layout.js +182 -51
- package/dist/api/page-builder.d.ts +40 -0
- package/dist/api/page-builder.js +44 -0
- package/dist/api/structure.d.ts +107 -13
- package/dist/api/structure.js +187 -88
- package/dist/api/table.d.ts +5 -5
- package/dist/api/table.js +38 -26
- package/dist/api/text.d.ts +90 -4
- package/dist/api/text.js +116 -31
- package/dist/assets/font-data.d.ts +2 -0
- package/dist/assets/font-data.js +21 -0
- package/dist/assets/font-data.ts +37 -0
- package/dist/common/color.js +1 -5
- package/dist/constants/page-sizes.js +3 -6
- package/dist/constants/pdf-parts.js +1 -4
- package/dist/crypto/legacy-handler.d.ts +43 -0
- package/dist/crypto/legacy-handler.js +145 -0
- package/dist/crypto/security-handler.d.ts +62 -0
- package/dist/crypto/security-handler.js +155 -0
- package/dist/crypto/webcrypto.d.ts +11 -0
- package/dist/crypto/webcrypto.js +62 -0
- package/dist/edit/acroform-reader.d.ts +70 -0
- package/dist/edit/acroform-reader.js +126 -0
- package/dist/edit/appearance.d.ts +47 -0
- package/dist/edit/appearance.js +171 -0
- package/dist/edit/document.d.ts +145 -0
- package/dist/edit/document.js +642 -0
- package/dist/edit/fill.d.ts +65 -0
- package/dist/edit/fill.js +424 -0
- package/dist/edit/flatten.d.ts +54 -0
- package/dist/edit/flatten.js +462 -0
- package/dist/edit/index.d.ts +20 -0
- package/dist/edit/index.js +18 -0
- package/dist/edit/lexer.d.ts +30 -0
- package/dist/edit/lexer.js +285 -0
- package/dist/edit/objects.d.ts +68 -0
- package/dist/edit/objects.js +83 -0
- package/dist/edit/writer.d.ts +60 -0
- package/dist/edit/writer.js +283 -0
- package/dist/elements/container-element.d.ts +50 -5
- package/dist/elements/container-element.js +100 -39
- package/dist/elements/forms/checkbox-element.d.ts +51 -0
- package/dist/elements/forms/checkbox-element.js +48 -0
- package/dist/elements/forms/choice-element.d.ts +53 -0
- package/dist/elements/forms/choice-element.js +59 -0
- package/dist/elements/forms/push-button-element.d.ts +56 -0
- package/dist/elements/forms/push-button-element.js +49 -0
- package/dist/elements/forms/radio-element.d.ts +52 -0
- package/dist/elements/forms/radio-element.js +49 -0
- package/dist/elements/forms/signature-element.d.ts +54 -0
- package/dist/elements/forms/signature-element.js +47 -0
- package/dist/elements/forms/text-field-element.d.ts +61 -0
- package/dist/elements/forms/text-field-element.js +54 -0
- package/dist/elements/image-element.d.ts +58 -6
- package/dist/elements/image-element.js +145 -111
- package/dist/elements/index.d.ts +26 -10
- package/dist/elements/index.js +26 -28
- package/dist/elements/layout/anchor-element.d.ts +23 -0
- package/dist/elements/layout/anchor-element.js +27 -0
- package/dist/elements/layout/bookmark-element.d.ts +27 -0
- package/dist/elements/layout/bookmark-element.js +30 -0
- package/dist/elements/layout/default-text-style-element.d.ts +30 -0
- package/dist/elements/layout/default-text-style-element.js +47 -0
- package/dist/elements/layout/deferred-element.d.ts +3 -3
- package/dist/elements/layout/deferred-element.js +4 -8
- package/dist/elements/layout/expanded-element.d.ts +6 -4
- package/dist/elements/layout/expanded-element.js +22 -17
- package/dist/elements/layout/keep-together-element.d.ts +42 -0
- package/dist/elements/layout/keep-together-element.js +85 -0
- package/dist/elements/layout/link-element.d.ts +33 -0
- package/dist/elements/layout/link-element.js +39 -0
- package/dist/elements/layout/padding-element.d.ts +10 -5
- package/dist/elements/layout/padding-element.js +25 -16
- package/dist/elements/layout/page-break-element.d.ts +17 -0
- package/dist/elements/layout/page-break-element.js +24 -0
- package/dist/elements/layout/page-builder-element.d.ts +32 -0
- package/dist/elements/layout/page-builder-element.js +45 -0
- package/dist/elements/layout/positioned-element.d.ts +47 -0
- package/dist/elements/layout/positioned-element.js +73 -0
- package/dist/elements/layout/repeating-header-element.d.ts +3 -3
- package/dist/elements/layout/repeating-header-element.js +8 -12
- package/dist/elements/layout/rotated-box-element.d.ts +35 -0
- package/dist/elements/layout/rotated-box-element.js +54 -0
- package/dist/elements/layout/rotated-element.d.ts +32 -0
- package/dist/elements/layout/rotated-element.js +41 -0
- package/dist/elements/layout/sized-container-element.d.ts +2 -2
- package/dist/elements/layout/sized-container-element.js +6 -11
- package/dist/elements/layout/struct-group.d.ts +28 -0
- package/dist/elements/layout/struct-group.js +42 -0
- package/dist/elements/line-element.d.ts +3 -3
- package/dist/elements/line-element.js +5 -10
- package/dist/elements/page-element.d.ts +26 -6
- package/dist/elements/page-element.js +72 -27
- package/dist/elements/pdf-document-element.d.ts +10 -4
- package/dist/elements/pdf-document-element.js +11 -10
- package/dist/elements/pdf-element.d.ts +150 -4
- package/dist/elements/pdf-element.js +138 -20
- package/dist/elements/rectangle-element.d.ts +52 -11
- package/dist/elements/rectangle-element.js +129 -36
- package/dist/elements/row-element.d.ts +45 -4
- package/dist/elements/row-element.js +66 -18
- package/dist/elements/text-element.d.ts +139 -11
- package/dist/elements/text-element.js +234 -57
- package/dist/forms/acroform.d.ts +63 -0
- package/dist/forms/acroform.js +427 -0
- package/dist/forms/appearance.d.ts +58 -0
- package/dist/forms/appearance.js +204 -0
- package/dist/forms/field.d.ts +164 -0
- package/dist/forms/field.js +1 -0
- package/dist/forms/pdf.d.ts +19 -0
- package/dist/forms/pdf.js +28 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -19
- package/dist/ir/display-list.d.ts +197 -8
- package/dist/ir/display-list.js +8 -2
- package/dist/layout/alignment.d.ts +10 -0
- package/dist/layout/alignment.js +6 -0
- package/dist/layout/box-constraints.d.ts +61 -0
- package/dist/layout/box-constraints.js +88 -6
- package/dist/layout/collect-images.d.ts +9 -0
- package/dist/layout/collect-images.js +34 -0
- package/dist/layout/fragmentation.d.ts +20 -1
- package/dist/layout/fragmentation.js +74 -18
- package/dist/layout/insets.d.ts +28 -0
- package/dist/layout/insets.js +20 -0
- package/dist/platform/browser-fs.d.ts +2 -0
- package/dist/platform/browser-fs.js +9 -0
- package/dist/platform/browser-image.d.ts +5 -0
- package/dist/platform/browser-image.js +13 -0
- package/dist/platform/node-fs.d.ts +2 -0
- package/dist/platform/node-fs.js +10 -0
- package/dist/platform/node-image.d.ts +5 -0
- package/dist/platform/node-image.js +9 -0
- package/dist/renderer/anchor-renderer.d.ts +6 -0
- package/dist/renderer/anchor-renderer.js +11 -0
- package/dist/renderer/bookmark-renderer.d.ts +6 -0
- package/dist/renderer/bookmark-renderer.js +11 -0
- package/dist/renderer/container-renderer.d.ts +3 -3
- package/dist/renderer/container-renderer.js +11 -28
- package/dist/renderer/default-text-style-renderer.d.ts +6 -0
- package/dist/renderer/default-text-style-renderer.js +10 -0
- package/dist/renderer/deferred-renderer.d.ts +3 -3
- package/dist/renderer/deferred-renderer.js +8 -23
- package/dist/renderer/emoji-image.d.ts +9 -0
- package/dist/renderer/emoji-image.js +68 -0
- package/dist/renderer/expanded-renderer.d.ts +3 -3
- package/dist/renderer/expanded-renderer.js +6 -21
- package/dist/renderer/form-field-renderer.d.ts +20 -0
- package/dist/renderer/form-field-renderer.js +12 -0
- package/dist/renderer/image-renderer.d.ts +4 -4
- package/dist/renderer/image-renderer.js +86 -75
- package/dist/renderer/index.d.ts +10 -10
- package/dist/renderer/index.js +10 -26
- package/dist/renderer/keep-together-renderer.d.ts +10 -0
- package/dist/renderer/keep-together-renderer.js +12 -0
- package/dist/renderer/line-renderer.d.ts +3 -3
- package/dist/renderer/line-renderer.js +13 -28
- package/dist/renderer/link-renderer.d.ts +6 -0
- package/dist/renderer/link-renderer.js +11 -0
- package/dist/renderer/padding-renderer.d.ts +3 -3
- package/dist/renderer/padding-renderer.js +6 -21
- package/dist/renderer/page-break-renderer.d.ts +14 -0
- package/dist/renderer/page-break-renderer.js +15 -0
- package/dist/renderer/page-builder-renderer.d.ts +6 -0
- package/dist/renderer/page-builder-renderer.js +11 -0
- package/dist/renderer/page-renderer.d.ts +2 -2
- package/dist/renderer/page-renderer.js +115 -77
- package/dist/renderer/pdf-backend.d.ts +31 -4
- package/dist/renderer/pdf-backend.js +287 -49
- package/dist/renderer/pdf-config.js +4 -7
- package/dist/renderer/pdf-document-class.d.ts +5 -5
- package/dist/renderer/pdf-document-class.js +27 -41
- package/dist/renderer/pdf-document-renderer.d.ts +19 -10
- package/dist/renderer/pdf-document-renderer.js +135 -91
- package/dist/renderer/pdf-renderer.d.ts +2 -2
- package/dist/renderer/pdf-renderer.js +165 -90
- package/dist/renderer/positioned-renderer.d.ts +6 -0
- package/dist/renderer/positioned-renderer.js +10 -0
- package/dist/renderer/rectangle-renderer.d.ts +3 -3
- package/dist/renderer/rectangle-renderer.js +49 -44
- package/dist/renderer/repeating-header-renderer.d.ts +3 -3
- package/dist/renderer/repeating-header-renderer.js +10 -26
- package/dist/renderer/rotated-renderer.d.ts +7 -0
- package/dist/renderer/rotated-renderer.js +29 -0
- package/dist/renderer/row-renderer.d.ts +3 -3
- package/dist/renderer/row-renderer.js +11 -28
- package/dist/renderer/struct-group-renderer.d.ts +10 -0
- package/dist/renderer/struct-group-renderer.js +24 -0
- package/dist/renderer/text-renderer.d.ts +9 -5
- package/dist/renderer/text-renderer.js +437 -89
- package/dist/text/advance.d.ts +34 -0
- package/dist/text/advance.js +45 -0
- package/dist/text/arabic.d.ts +20 -0
- package/dist/text/arabic.js +101 -0
- package/dist/text/bidi.d.ts +33 -0
- package/dist/text/bidi.js +94 -0
- package/dist/text/emoji-codepoints.d.ts +1 -0
- package/dist/text/emoji-codepoints.js +29 -0
- package/dist/text/font-fallback.d.ts +25 -0
- package/dist/text/font-fallback.js +38 -0
- package/dist/text/glyph-coverage.d.ts +15 -0
- package/dist/text/glyph-coverage.js +77 -0
- package/dist/text/line-breaker.d.ts +50 -12
- package/dist/text/line-breaker.js +200 -54
- package/dist/text/line-metrics.d.ts +49 -0
- package/dist/text/line-metrics.js +57 -0
- package/dist/text/orphans-widows.d.ts +27 -0
- package/dist/text/orphans-widows.js +53 -0
- package/dist/text/shape.d.ts +29 -0
- package/dist/text/shape.js +67 -0
- package/dist/text/text-decoration.d.ts +50 -0
- package/dist/text/text-decoration.js +62 -0
- package/dist/text/text-style.d.ts +60 -0
- package/dist/text/text-style.js +79 -0
- package/dist/text/whitespace.d.ts +4 -0
- package/dist/text/whitespace.js +43 -0
- package/dist/utils/afm-parser.d.ts +34 -0
- package/dist/utils/afm-parser.js +76 -17
- package/dist/utils/bytes.d.ts +24 -0
- package/dist/utils/bytes.js +76 -0
- package/dist/utils/dest-registry.d.ts +22 -0
- package/dist/utils/dest-registry.js +32 -0
- package/dist/utils/flex-layout.d.ts +36 -9
- package/dist/utils/flex-layout.js +207 -30
- package/dist/utils/font-metrics.d.ts +26 -1
- package/dist/utils/font-metrics.js +1 -2
- package/dist/utils/font-path.js +3 -6
- package/dist/utils/gsub.d.ts +36 -0
- package/dist/utils/gsub.js +178 -0
- package/dist/utils/image-helper.d.ts +6 -5
- package/dist/utils/image-helper.js +101 -111
- package/dist/utils/inflate.d.ts +16 -0
- package/dist/utils/inflate.js +52 -0
- package/dist/utils/md5.d.ts +4 -0
- package/dist/utils/md5.js +79 -0
- package/dist/utils/outline.d.ts +21 -0
- package/dist/utils/outline.js +79 -0
- package/dist/utils/pdf-object-manager.d.ts +127 -8
- package/dist/utils/pdf-object-manager.js +615 -141
- package/dist/utils/renderer-registry.d.ts +13 -1
- package/dist/utils/renderer-registry.js +23 -6
- package/dist/utils/struct-tree.d.ts +44 -0
- package/dist/utils/struct-tree.js +129 -0
- package/dist/utils/ttf-parser.d.ts +134 -2
- package/dist/utils/ttf-parser.js +873 -50
- package/dist/utils/ttf-subsetter.d.ts +1 -1
- package/dist/utils/ttf-subsetter.js +40 -42
- package/dist/utils/ua-xmp.d.ts +9 -0
- package/dist/utils/ua-xmp.js +32 -0
- package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
- package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
- package/dist/utils/woff.d.ts +10 -0
- package/dist/utils/woff.js +116 -0
- package/dist/validators/element-validator.d.ts +2 -2
- package/dist/validators/element-validator.js +31 -26
- package/package.json +22 -2
|
@@ -1,57 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.PDFObjectManager = exports.FontStyle = void 0;
|
|
37
|
-
const page_sizes_1 = require("../constants/page-sizes");
|
|
38
|
-
const fs = __importStar(require("fs"));
|
|
39
|
-
const path = __importStar(require("path"));
|
|
40
|
-
const crypto_1 = require("crypto");
|
|
41
|
-
const zlib_1 = require("zlib");
|
|
42
|
-
const afm_parser_1 = require("./afm-parser");
|
|
43
|
-
const ttf_parser_1 = require("./ttf-parser");
|
|
44
|
-
const ttf_subsetter_1 = require("./ttf-subsetter");
|
|
1
|
+
import { pageFormats, PageSize } from "../constants/page-sizes.js";
|
|
2
|
+
import { STANDARD_AFM } from "../assets/font-data.js";
|
|
3
|
+
import { md5, md5Hex } from "./md5.js";
|
|
4
|
+
import { zlibSync } from "fflate";
|
|
5
|
+
import { bytesFromLatin1, latin1FromBytes } from "./bytes.js";
|
|
6
|
+
import { AFMParser } from "./afm-parser.js";
|
|
7
|
+
import { mergeSpans, TTFParser } from "./ttf-parser.js";
|
|
8
|
+
import { isWoff, woffToSfnt } from "./woff.js";
|
|
9
|
+
import { subsetTTF } from "./ttf-subsetter.js";
|
|
10
|
+
import { shapeRun } from "../text/shape.js";
|
|
11
|
+
import { getArrayBuffer, isWindows1252 } from "./utf8-to-windows1252-encoder.js";
|
|
12
|
+
import { isEmojiCodePoint } from "../text/emoji-codepoints.js";
|
|
13
|
+
import { StructTree } from "./struct-tree.js";
|
|
14
|
+
import { OutlineBuilder } from "./outline.js";
|
|
15
|
+
import { DestRegistry } from "./dest-registry.js";
|
|
16
|
+
import { AcroFormCollector } from "../forms/acroform.js";
|
|
45
17
|
// Enums come from the leaf config module (never in a cycle); the config type is
|
|
46
18
|
// erased at runtime so it can come from the cyclic module safely.
|
|
47
|
-
|
|
48
|
-
var FontStyle;
|
|
19
|
+
import { ColorMode, Orientation } from "../renderer/pdf-config.js";
|
|
20
|
+
export var FontStyle;
|
|
49
21
|
(function (FontStyle) {
|
|
50
22
|
FontStyle["Normal"] = "normal";
|
|
51
23
|
FontStyle["Bold"] = "bold";
|
|
52
24
|
FontStyle["Italic"] = "italic";
|
|
53
25
|
FontStyle["BoldItalic"] = "boldItalic";
|
|
54
|
-
})(FontStyle || (
|
|
26
|
+
})(FontStyle || (FontStyle = {}));
|
|
27
|
+
// Escapes a string into a valid PDF /Name token. A raw space or delimiter in a /Name (e.g. a font
|
|
28
|
+
// family like "Great Vibes" or "Times New Roman") breaks the dictionary, so such chars become #XX.
|
|
29
|
+
const pdfName = (s) => s.replace(/[^\x21-\x7e]|[#()<>[\]{}/%]/g, (c) => "#" + c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0"));
|
|
55
30
|
// Suffix appended to an embedded font's PDF /BaseFont so each style variant of a family keeps
|
|
56
31
|
// a distinct name (the variants are otherwise selected by the font resource, not the name).
|
|
57
32
|
const STYLE_SUFFIX = {
|
|
@@ -152,7 +127,19 @@ class FontManager {
|
|
|
152
127
|
return `${fontName}-${fontStyle}`;
|
|
153
128
|
}
|
|
154
129
|
}
|
|
155
|
-
class PDFObjectManager {
|
|
130
|
+
export class PDFObjectManager {
|
|
131
|
+
get struct() {
|
|
132
|
+
return this._struct;
|
|
133
|
+
}
|
|
134
|
+
get outline() {
|
|
135
|
+
return this._outline;
|
|
136
|
+
}
|
|
137
|
+
get dests() {
|
|
138
|
+
return this._dests;
|
|
139
|
+
}
|
|
140
|
+
get acroform() {
|
|
141
|
+
return this._acroform;
|
|
142
|
+
}
|
|
156
143
|
constructor(pageSize) {
|
|
157
144
|
this.objects = [];
|
|
158
145
|
this.objectPositions = [];
|
|
@@ -160,24 +147,64 @@ class PDFObjectManager {
|
|
|
160
147
|
this.fonts = new FontManager(); // Stores the fonts
|
|
161
148
|
this.images = new ImageManager(); // Stores the images (object numbers and names)
|
|
162
149
|
this.extGStates = new ExtGStateManager(); // Transparency states
|
|
163
|
-
this.
|
|
150
|
+
this.shadings = new Map(); // gradient resource name -> object number
|
|
151
|
+
this.pageFormat = pageFormats[PageSize.A4];
|
|
164
152
|
this.afmParsers = [];
|
|
165
153
|
// Embedded TrueType fonts, keyed by the name the user registers them under. When a font
|
|
166
154
|
// name is in here the metric/emission paths take the TTF branch instead of the AFM one.
|
|
155
|
+
// Nested by family, then by style. The flat `${name}-${style}` key it replaced had to be BUILT on
|
|
156
|
+
// every lookup, and the lookups happen once per character - the string allocation alone was a third
|
|
157
|
+
// of a custom-font render. `customFontEmit` below keeps the flat key: it is touched once per face.
|
|
167
158
|
this.customFonts = new Map();
|
|
159
|
+
// Memoised getFontVerticals answers, family -> style -> metrics. Cleared when a font registers.
|
|
160
|
+
this.verticalsCache = new Map();
|
|
161
|
+
// Same, for getFontDecoration.
|
|
162
|
+
this.decorationCache = new Map();
|
|
168
163
|
// Per-registered face: the reserved font-object numbers + the glyph ids actually used. The font
|
|
169
164
|
// program is subsetted and the objects filled in by finalizeCustomFonts(), after the render pass.
|
|
170
165
|
this.customFontEmit = new Map();
|
|
166
|
+
// Shaped runs, keyed by font + text; `null` means "asked, and there was nothing to shape".
|
|
167
|
+
this.shapeCache = new Map();
|
|
168
|
+
// Per font, glyph -> the code points it stands for. A shaped glyph is often absent from the cmap (a
|
|
169
|
+
// ligature) or maps back to a presentation form, so `ToUnicode` cannot come from the cmap alone.
|
|
170
|
+
this.shapedOrigins = new Map();
|
|
171
171
|
// Embedded file attachments (their /Filespec object numbers + display names). Referenced from
|
|
172
172
|
// the catalog's /Names/EmbeddedFiles + /AF. Drives ZUGFeRD's embedded factur-x.xml.
|
|
173
173
|
this.attachments = [];
|
|
174
174
|
// PDF header version (PDF/A-3 needs 1.7) and whether to write a trailer /ID (PDF/A needs one).
|
|
175
|
-
// Both default to the pre-PDF/A
|
|
175
|
+
// Both default to the pre-PDF/A behavior, so a normal document stays byte-identical.
|
|
176
176
|
this.pdfVersion = "1.4";
|
|
177
177
|
this.documentId = false;
|
|
178
178
|
this.compress = false;
|
|
179
|
+
this.overflowPolicy = "error";
|
|
180
|
+
this.encJobs = [];
|
|
181
|
+
this.strJobs = [];
|
|
182
|
+
// Accessible (PDF/UA) tagging, off by default (byte-identical output); the API turns it on.
|
|
183
|
+
this._struct = new StructTree();
|
|
184
|
+
// Document outline (bookmarks). Empty unless a `Bookmark` was placed; then finalize() emits /Outlines.
|
|
185
|
+
this._outline = new OutlineBuilder();
|
|
186
|
+
// Named destinations (internal-link jump targets). Empty unless an `Anchor` was placed; then it emits
|
|
187
|
+
// a /Names /Dests tree.
|
|
188
|
+
this._dests = new DestRegistry();
|
|
189
|
+
// Interactive form fields (AcroForm). Empty unless a form field was placed; then finalize() emits the
|
|
190
|
+
// catalog /AcroForm dict and each field's widget lands in its page /Annots.
|
|
191
|
+
this._acroform = new AcroFormCollector();
|
|
192
|
+
// Overflow policy for unbreakable content taller than a page region (default "error"); the renderer
|
|
193
|
+
// seeds it into the layout-context root so packChildren can act on it.
|
|
194
|
+
/**
|
|
195
|
+
* Code points no font in the document could draw, so they were removed rather than emitted as
|
|
196
|
+
* `.notdef` (which PDF/A forbids). Collected here because this object is the one thing threaded
|
|
197
|
+
* through BOTH passes; the caller gets them back with the bytes, so an application can say which
|
|
198
|
+
* character went missing instead of a server log nobody reads.
|
|
199
|
+
*/
|
|
200
|
+
this.missingGlyphs = new Set();
|
|
201
|
+
// Whether to kern. Document-level: measuring (via `runAdvance`, which reads this off the metrics)
|
|
202
|
+
// and drawing (the backend emits a `TJ`) both consult it, so they can never disagree. OFF while the
|
|
203
|
+
// embedded-font path (kern/GPOS) is unbuilt - a standard-14-only kerning would set the 14 system
|
|
204
|
+
// fonts finer than a user's own font, which is worse than none. Flip to on when GPOS lands.
|
|
205
|
+
this.kerningEnabled = false;
|
|
179
206
|
if (pageSize)
|
|
180
|
-
this.pageFormat =
|
|
207
|
+
this.pageFormat = pageFormats[pageSize];
|
|
181
208
|
this.fillConfigWithStandardValues();
|
|
182
209
|
}
|
|
183
210
|
// Adds an object and returns its number
|
|
@@ -198,40 +225,108 @@ class PDFObjectManager {
|
|
|
198
225
|
setCompress(on) {
|
|
199
226
|
this.compress = on;
|
|
200
227
|
}
|
|
228
|
+
reportMissingGlyph(codePoints) {
|
|
229
|
+
for (const cp of codePoints)
|
|
230
|
+
this.missingGlyphs.add(cp);
|
|
231
|
+
}
|
|
232
|
+
/** The dropped code points, in the order first seen. Empty for a document that hit none. */
|
|
233
|
+
getMissingGlyphs() {
|
|
234
|
+
return [...this.missingGlyphs];
|
|
235
|
+
}
|
|
236
|
+
setOverflowPolicy(policy) {
|
|
237
|
+
this.overflowPolicy = policy;
|
|
238
|
+
}
|
|
239
|
+
getOverflowPolicy() {
|
|
240
|
+
return this.overflowPolicy;
|
|
241
|
+
}
|
|
242
|
+
// A unique, binary-safe placeholder for a stream's bytes, swapped for ciphertext in finalizeEncryption().
|
|
243
|
+
encToken(id) {
|
|
244
|
+
return `JENC:${id}`;
|
|
245
|
+
}
|
|
246
|
+
// The same trick for a literal STRING. A separate marker because the two are swapped differently: a
|
|
247
|
+
// stream keeps its delimiters and only the payload changes, a string is replaced whole by a hex one.
|
|
248
|
+
strToken(id) {
|
|
249
|
+
return `\x00\x02JSTR:${id}\x02\x00`;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* The single choke-point every literal string in an OBJECT goes through - a field's `/T` and `/V`, a
|
|
253
|
+
* bookmark `/Title`, a link `/URI`, an `/Alt` text, a destination name.
|
|
254
|
+
*
|
|
255
|
+
* Without a security handler this is the escaped literal we have always written, so output stays
|
|
256
|
+
* byte-identical. With one, the raw bytes are registered for the finalize pass and a placeholder takes
|
|
257
|
+
* their place: ISO 32000-1 7.6.2 requires ALL strings and streams to be encrypted, and the ones missed
|
|
258
|
+
* here were exactly the values a password is supposed to protect (ISSUE-7).
|
|
259
|
+
*
|
|
260
|
+
* A string INSIDE a content stream needs none of this - the stream is enciphered as a whole.
|
|
261
|
+
*/
|
|
262
|
+
pdfString(text) {
|
|
263
|
+
if (!this.security) {
|
|
264
|
+
return `(${text.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)")})`;
|
|
265
|
+
}
|
|
266
|
+
// Encrypt the bytes the unencrypted path would have written, so a viewer decrypts to the same value.
|
|
267
|
+
// No escaping is applied: the result is emitted as a HEX string, which has no escapes to get wrong.
|
|
268
|
+
const bytes = new Uint8Array(getArrayBuffer(text));
|
|
269
|
+
const id = this.strJobs.push(bytes) - 1;
|
|
270
|
+
return this.strToken(id);
|
|
271
|
+
}
|
|
272
|
+
// The single encryption choke-point every stream emitter routes through (stream(), registerImage). Without
|
|
273
|
+
// a security handler: the bytes as a latin1 string + their length. With one: register the raw bytes for the
|
|
274
|
+
// finalize pass and return a placeholder + the pre-computable encrypted length (16-byte IV + PKCS#7 padding).
|
|
275
|
+
streamPayload(bytes) {
|
|
276
|
+
if (!this.security)
|
|
277
|
+
return { body: latin1FromBytes(bytes), length: bytes.length };
|
|
278
|
+
const id = this.encJobs.push(bytes) - 1;
|
|
279
|
+
return { body: this.encToken(id), length: 16 + (Math.floor(bytes.length / 16) + 1) * 16 };
|
|
280
|
+
}
|
|
201
281
|
// Builds a stream object body: `<< extraDict /Length n >> stream … endstream`, FlateDecode-compressed
|
|
202
|
-
// when enabled AND it actually helps (tiny streams aren't inflated).
|
|
203
|
-
//
|
|
282
|
+
// when enabled AND it actually helps (tiny streams aren't inflated). The payload rides through
|
|
283
|
+
// streamPayload, so it is encrypted uniformly when a security handler is set.
|
|
204
284
|
stream(extraDict, data) {
|
|
205
285
|
const head = extraDict ? extraDict + " " : "";
|
|
286
|
+
let body = data;
|
|
287
|
+
let filter = "";
|
|
206
288
|
if (this.compress) {
|
|
207
|
-
const z = (
|
|
289
|
+
const z = zlibSync(data);
|
|
208
290
|
if (z.length < data.length) {
|
|
209
|
-
|
|
291
|
+
body = z;
|
|
292
|
+
filter = "/Filter /FlateDecode ";
|
|
210
293
|
}
|
|
211
294
|
}
|
|
212
|
-
|
|
295
|
+
const p = this.streamPayload(body);
|
|
296
|
+
return `<< ${head}${filter}/Length ${p.length} >>\nstream\n${p.body}\nendstream`;
|
|
213
297
|
}
|
|
214
298
|
// Adds a page content stream (compressed when enabled). The caller passes the raw operator string.
|
|
299
|
+
// Bytes go through the Windows-1252 encoder (NOT a latin1 low-byte cast): a Tj literal may carry a
|
|
300
|
+
// CP1252 char like "…"/"—"/"€" whose codepoint is > 0xFF, and latin1 would mangle it (… -> "&").
|
|
301
|
+
// For every char <= 0xFF the encoder passes the byte through, so existing streams stay byte-identical.
|
|
215
302
|
addContentStream(content) {
|
|
216
|
-
return this.addObject(this.stream("",
|
|
303
|
+
return this.addObject(this.stream("", new Uint8Array(getArrayBuffer(content))));
|
|
304
|
+
}
|
|
305
|
+
// A Form XObject: a self-contained content stream with its own /BBox (+ optional /Resources). Used for
|
|
306
|
+
// form-field appearance streams (/AP). Routes through the same choke-point as page content, so
|
|
307
|
+
// compression and encryption apply. Returns the object number.
|
|
308
|
+
addFormXObject(bbox, content, resources) {
|
|
309
|
+
const dict = `/Type /XObject /Subtype /Form /BBox ${bbox}` +
|
|
310
|
+
(resources ? ` /Resources << ${resources} >>` : "");
|
|
311
|
+
return this.addObject(this.stream(dict, new Uint8Array(getArrayBuffer(content))));
|
|
217
312
|
}
|
|
218
313
|
changePDFConfig(config) {
|
|
219
|
-
this.pdfConfig =
|
|
314
|
+
this.pdfConfig = { ...this.pdfConfig, ...config };
|
|
220
315
|
}
|
|
221
316
|
getPDFConfig() {
|
|
222
317
|
return this.pdfConfig;
|
|
223
318
|
}
|
|
224
319
|
fillConfigWithStandardValues() {
|
|
225
320
|
this.pdfConfig = {
|
|
226
|
-
orientation:
|
|
321
|
+
orientation: Orientation.portrait,
|
|
227
322
|
defaultFont: {
|
|
228
323
|
fontFamily: "Helvetica",
|
|
229
324
|
fontSize: 12,
|
|
230
325
|
fontStyle: FontStyle.Normal,
|
|
231
326
|
},
|
|
232
|
-
pageSize:
|
|
327
|
+
pageSize: PageSize.A4,
|
|
233
328
|
margin: { left: 72, top: 72, bottom: 72, right: 72 },
|
|
234
|
-
colorMode:
|
|
329
|
+
colorMode: ColorMode.color,
|
|
235
330
|
};
|
|
236
331
|
}
|
|
237
332
|
// Calculates the total length of the document in bytes (for XRef)
|
|
@@ -240,10 +335,8 @@ class PDFObjectManager {
|
|
|
240
335
|
for (let i = 0; i < this.objects.length; i++) {
|
|
241
336
|
const obj = this.objects[i];
|
|
242
337
|
const objectContent = `${i + 1} 0 obj\n${obj}\nendobj\n`;
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
// Add the actual byte length to the total length
|
|
246
|
-
length += encodedContent.length;
|
|
338
|
+
// The body is a latin1/binary string (every char <= 0xFF), so its length IS its byte length.
|
|
339
|
+
length += objectContent.length;
|
|
247
340
|
}
|
|
248
341
|
return length;
|
|
249
342
|
}
|
|
@@ -256,7 +349,25 @@ class PDFObjectManager {
|
|
|
256
349
|
return this.parentObjectNumber;
|
|
257
350
|
}
|
|
258
351
|
// Registers an image
|
|
259
|
-
registerImage(width, height, imageType, imageData) {
|
|
352
|
+
registerImage(width, height, imageType, imageData, smaskData) {
|
|
353
|
+
// A transparent PNG carries its alpha as a separate DeviceGray /SMask image the viewer composites with.
|
|
354
|
+
let smaskEntry = "";
|
|
355
|
+
if (smaskData) {
|
|
356
|
+
const s = this.streamPayload(bytesFromLatin1(smaskData));
|
|
357
|
+
const smaskObject = `<< /Type /XObject
|
|
358
|
+
/Subtype /Image
|
|
359
|
+
/Width ${width}
|
|
360
|
+
/Height ${height}
|
|
361
|
+
/ColorSpace /DeviceGray
|
|
362
|
+
/BitsPerComponent 8
|
|
363
|
+
/Filter /FlateDecode
|
|
364
|
+
/Length ${s.length} >>
|
|
365
|
+
stream
|
|
366
|
+
${s.body}
|
|
367
|
+
endstream`;
|
|
368
|
+
smaskEntry = ` /SMask ${this.addObject(smaskObject)} 0 R\n`;
|
|
369
|
+
}
|
|
370
|
+
const img = this.streamPayload(bytesFromLatin1(imageData));
|
|
260
371
|
const imageObject = `<< /Type /XObject
|
|
261
372
|
/Subtype /Image
|
|
262
373
|
/Width ${width}
|
|
@@ -264,9 +375,9 @@ class PDFObjectManager {
|
|
|
264
375
|
/ColorSpace /DeviceRGB
|
|
265
376
|
/BitsPerComponent 8
|
|
266
377
|
/Filter /${imageType}
|
|
267
|
-
/Length ${
|
|
378
|
+
${smaskEntry} /Length ${img.length} >>
|
|
268
379
|
stream
|
|
269
|
-
${
|
|
380
|
+
${img.body}
|
|
270
381
|
endstream`;
|
|
271
382
|
// Add the image and its object number to the image manager - return the object number
|
|
272
383
|
const imageObjectNumber = this.addObject(imageObject);
|
|
@@ -278,13 +389,11 @@ endstream`;
|
|
|
278
389
|
// `relationship` is the /AFRelationship (e.g. "Data" for the ZUGFeRD factur-x.xml). Binary bytes
|
|
279
390
|
// ride as a latin1 string, like images/fonts (the final encoder passes 0x00-0xFF through).
|
|
280
391
|
attachFile(name, data, opts = {}) {
|
|
281
|
-
|
|
282
|
-
const subtype = ((_a = opts.mimeType) !== null && _a !== void 0 ? _a : "application/octet-stream").replace(/\//g, "#2F");
|
|
392
|
+
const subtype = (opts.mimeType ?? "application/octet-stream").replace(/\//g, "#2F");
|
|
283
393
|
const embedded = this.addObject(this.stream(`/Type /EmbeddedFile /Subtype /${subtype} /Params << /Size ${data.length} >>`, data));
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
`/AFRelationship /${(_b = opts.relationship) !== null && _b !== void 0 ? _b : "Unspecified"}${desc} ` +
|
|
394
|
+
const desc = opts.description ? ` /Desc ${this.pdfString(opts.description)}` : "";
|
|
395
|
+
const filespec = this.addObject(`<< /Type /Filespec /F ${this.pdfString(name)} /UF ${this.pdfString(name)} ` +
|
|
396
|
+
`/AFRelationship /${opts.relationship ?? "Unspecified"}${desc} ` +
|
|
288
397
|
`/EF << /F ${embedded} 0 R /UF ${embedded} 0 R >> >>`);
|
|
289
398
|
this.attachments.push({ name, filespec });
|
|
290
399
|
}
|
|
@@ -302,11 +411,10 @@ endstream`;
|
|
|
302
411
|
// Embeds an ICC profile and a PDF/A /OutputIntent that points at it (catalog /OutputIntents).
|
|
303
412
|
// `icc` are the raw profile bytes (an RGB profile, /N 3 - e.g. sRGB).
|
|
304
413
|
setOutputIntent(icc, opts = {}) {
|
|
305
|
-
var _a, _b;
|
|
306
414
|
const profile = this.addObject(this.stream("/N 3", icc));
|
|
307
415
|
this.outputIntent = this.addObject(`<< /Type /OutputIntent /S /GTS_PDFA1 ` +
|
|
308
|
-
`/OutputConditionIdentifier
|
|
309
|
-
`/Info
|
|
416
|
+
`/OutputConditionIdentifier ${this.pdfString(opts.identifier ?? "sRGB")} ` +
|
|
417
|
+
`/Info ${this.pdfString(opts.info ?? "sRGB IEC61966-2.1")} /DestOutputProfile ${profile} 0 R >>`);
|
|
310
418
|
}
|
|
311
419
|
getOutputIntent() {
|
|
312
420
|
return this.outputIntent;
|
|
@@ -331,6 +439,43 @@ endstream`;
|
|
|
331
439
|
enableDocumentId() {
|
|
332
440
|
this.documentId = true;
|
|
333
441
|
}
|
|
442
|
+
// Turn on encryption: the handler encrypts every stream and supplies the /Encrypt dict. Forces a trailer
|
|
443
|
+
// /ID (encrypted PDFs require one) and PDF 2.0 (AES-256 R6). PDF/A forbids encryption - guarded at finalize.
|
|
444
|
+
setSecurityHandler(handler) {
|
|
445
|
+
this.security = handler;
|
|
446
|
+
this.documentId = true;
|
|
447
|
+
this.setPdfVersion("2.0");
|
|
448
|
+
}
|
|
449
|
+
// One async pass at the very end: refuse to encrypt a PDF/A document, then encrypt every registered stream
|
|
450
|
+
// (swap each placeholder for its ciphertext) and add the /Encrypt object the trailer references.
|
|
451
|
+
async finalizeEncryption() {
|
|
452
|
+
if (!this.security)
|
|
453
|
+
return;
|
|
454
|
+
if (this.outputIntent !== undefined || this.attachments.length > 0) {
|
|
455
|
+
throw new Error("@jasy/pdf: cannot encrypt a PDF/A document - PDF/A forbids encryption.");
|
|
456
|
+
}
|
|
457
|
+
for (let id = 0; id < this.encJobs.length; id++) {
|
|
458
|
+
const ciphertext = latin1FromBytes(await this.security.encrypt(this.encJobs[id]));
|
|
459
|
+
const token = this.encToken(id);
|
|
460
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
461
|
+
// Function replacement: a string replacement would interpret `$&`/`$\``/... in the random ciphertext.
|
|
462
|
+
if (idx >= 0)
|
|
463
|
+
this.objects[idx] = this.objects[idx].replace(token, () => ciphertext);
|
|
464
|
+
}
|
|
465
|
+
// Strings: the same idea, but a string is replaced WHOLE - `(text)` becomes `<ciphertext>`. Hex,
|
|
466
|
+
// because the ciphertext is binary and a hex string has no escape rules to get wrong.
|
|
467
|
+
for (let id = 0; id < this.strJobs.length; id++) {
|
|
468
|
+
const cipher = await this.security.encrypt(this.strJobs[id]);
|
|
469
|
+
const hex = Array.from(cipher, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
|
470
|
+
const token = this.strToken(id);
|
|
471
|
+
const idx = this.objects.findIndex((o) => o.includes(token));
|
|
472
|
+
if (idx >= 0)
|
|
473
|
+
this.objects[idx] = this.objects[idx].replace(token, () => `<${hex}>`);
|
|
474
|
+
}
|
|
475
|
+
// Added LAST on purpose: the /Encrypt dictionary's own strings must stay in the clear (7.6.2), and
|
|
476
|
+
// they would otherwise be swept up by the pass above.
|
|
477
|
+
this.encryptObjNum = this.addObject(`<< ${this.security.encryptDict()} >>`);
|
|
478
|
+
}
|
|
334
479
|
// Registers (or reuses) a transparency graphics state and returns its resource name
|
|
335
480
|
// (e.g. "GS1"). `fillAlpha` -> /ca, `strokeAlpha` -> /CA. Deduped by the alpha pair.
|
|
336
481
|
registerExtGState(fillAlpha, strokeAlpha) {
|
|
@@ -349,25 +494,73 @@ endstream`;
|
|
|
349
494
|
getAllExtGStatesRaw() {
|
|
350
495
|
return this.extGStates.getAll();
|
|
351
496
|
}
|
|
497
|
+
// Registers a gradient as a PDF shading (+ its color-stop function) and returns the resource name
|
|
498
|
+
// (e.g. "Sh1"). Used to fill a color-glyph layer with a COLR v1 gradient. Not deduped: each layer
|
|
499
|
+
// carries page-absolute coordinates, so two calls rarely match.
|
|
500
|
+
registerShading(g) {
|
|
501
|
+
const fn = this.buildShadingFunction(g.stops);
|
|
502
|
+
const n = (x) => x.toFixed(3);
|
|
503
|
+
const coords = g.type === "linear"
|
|
504
|
+
? `${n(g.x0)} ${n(g.y0)} ${n(g.x1)} ${n(g.y1)}`
|
|
505
|
+
: `${n(g.x0)} ${n(g.y0)} ${n(g.r0)} ${n(g.x1)} ${n(g.y1)} ${n(g.r1)}`;
|
|
506
|
+
const shadingType = g.type === "linear" ? 2 : 3;
|
|
507
|
+
// PDF shadings extend by clamping the end colors; "repeat"/"reflect" would need a tiling pattern,
|
|
508
|
+
// so we approximate them as clamp (pad) for now - correct for the common pad case.
|
|
509
|
+
const objectNumber = this.addObject(`<< /ShadingType ${shadingType} /ColorSpace /DeviceRGB /Coords [${coords}] ` +
|
|
510
|
+
`/Function ${fn} 0 R /Extend [true true] >>`);
|
|
511
|
+
const name = `Sh${this.shadings.size + 1}`;
|
|
512
|
+
this.shadings.set(name, objectNumber);
|
|
513
|
+
return name;
|
|
514
|
+
}
|
|
515
|
+
// Builds the PDF function mapping t in [0,1] to a color along the stops. Two stops collapse to one
|
|
516
|
+
// exponential (linear) function; more stops stitch one linear piece per interval (FunctionType 3).
|
|
517
|
+
buildShadingFunction(stops) {
|
|
518
|
+
const c = (i) => stops[i].color.toPDFColorString(); // "r g b" in 0..1
|
|
519
|
+
// Malformed / empty color line: fall back to opaque black instead of indexing an empty array.
|
|
520
|
+
if (stops.length === 0) {
|
|
521
|
+
return this.addObject("<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [0 0 0] /N 1 >>");
|
|
522
|
+
}
|
|
523
|
+
if (stops.length <= 2) {
|
|
524
|
+
const c0 = c(0);
|
|
525
|
+
const c1 = c(stops.length - 1);
|
|
526
|
+
return this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c0}] /C1 [${c1}] /N 1 >>`);
|
|
527
|
+
}
|
|
528
|
+
const pieces = [];
|
|
529
|
+
for (let i = 0; i < stops.length - 1; i++) {
|
|
530
|
+
pieces.push(this.addObject(`<< /FunctionType 2 /Domain [0 1] /C0 [${c(i)}] /C1 [${c(i + 1)}] /N 1 >>`));
|
|
531
|
+
}
|
|
532
|
+
const functions = pieces.map((p) => `${p} 0 R`).join(" ");
|
|
533
|
+
const bounds = stops
|
|
534
|
+
.slice(1, -1)
|
|
535
|
+
.map((s) => s.offset.toFixed(4))
|
|
536
|
+
.join(" ");
|
|
537
|
+
const encode = pieces.map(() => "0 1").join(" ");
|
|
538
|
+
return this.addObject(`<< /FunctionType 3 /Domain [0 1] /Functions [${functions}] /Bounds [${bounds}] /Encode [${encode}] >>`);
|
|
539
|
+
}
|
|
540
|
+
// Returns all registered gradient shadings (name -> object number) for the page /Resources.
|
|
541
|
+
getAllShadingsRaw() {
|
|
542
|
+
return this.shadings;
|
|
543
|
+
}
|
|
352
544
|
// Registers a font
|
|
353
545
|
registerFont(fontName, fontStyle = FontStyle.Normal, fullName = fontName) {
|
|
354
546
|
if (this.fonts.hasFont(fontName, fontStyle)) {
|
|
355
547
|
return this.fonts.getFont(fontName, fontStyle); // Already exists? Return it!
|
|
356
548
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
549
|
+
this.verticalsCache.delete(fontName);
|
|
550
|
+
this.decorationCache.delete(fontName);
|
|
551
|
+
const data = STANDARD_AFM[fullName];
|
|
552
|
+
if (data !== undefined) {
|
|
360
553
|
this.afmParsers.push({
|
|
361
554
|
fontName,
|
|
362
555
|
fontStyle,
|
|
363
556
|
fullFontName: fullName,
|
|
364
|
-
parser: new
|
|
557
|
+
parser: new AFMParser(data),
|
|
365
558
|
});
|
|
366
559
|
}
|
|
367
560
|
const resourceNumber = this.objects.length + 1; // New resource number
|
|
368
561
|
const fontNumber = this.fonts.getLastFontIndex() + 1; // New font index number
|
|
369
562
|
this.fonts.addFont(fontName, fontNumber, resourceNumber, fontStyle); // Store it
|
|
370
|
-
const fontObject = `<</BaseFont/${fullName}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
|
|
563
|
+
const fontObject = `<</BaseFont/${pdfName(fullName)}/Type/Font\n/Encoding/WinAnsiEncoding\n/Subtype/Type1>>`;
|
|
371
564
|
this.addObject(fontObject);
|
|
372
565
|
return {
|
|
373
566
|
fontIndex: fontNumber,
|
|
@@ -380,10 +573,19 @@ endstream`;
|
|
|
380
573
|
// metrics and emits its PDF font objects. All variants share the family `name`; bold/italic are
|
|
381
574
|
// separate .ttf files registered under the same name with a different style.
|
|
382
575
|
registerCustomFont(name, data, style = FontStyle.Normal) {
|
|
383
|
-
|
|
384
|
-
if (
|
|
576
|
+
let byStyle = this.customFonts.get(name);
|
|
577
|
+
if (!byStyle) {
|
|
578
|
+
byStyle = new Map();
|
|
579
|
+
this.customFonts.set(name, byStyle);
|
|
580
|
+
}
|
|
581
|
+
if (byStyle.has(style))
|
|
385
582
|
return;
|
|
386
|
-
|
|
583
|
+
// A WOFF is the same sfnt behind a small wrapper, so it is unpacked HERE - the one place every
|
|
584
|
+
// font path meets (a file, raw bytes, a URL, a browser upload) - and TTFParser never learns a
|
|
585
|
+
// second container format.
|
|
586
|
+
byStyle.set(style, new TTFParser(isWoff(data) ? woffToSfnt(data) : data));
|
|
587
|
+
this.verticalsCache.delete(name); // this family now answers from the .ttf, not from an AFM
|
|
588
|
+
this.decorationCache.delete(name);
|
|
387
589
|
// Emission (the PDF font objects + page resource) is DEFERRED to first use via ensureEmitted(),
|
|
388
590
|
// so a registered-but-never-rendered face - e.g. a bundled family the document doesn't touch -
|
|
389
591
|
// costs nothing in the output. The metrics above are still available for layout immediately.
|
|
@@ -406,6 +608,7 @@ endstream`;
|
|
|
406
608
|
this.fonts.addFont(name, this.fonts.getLastFontIndex() + 1, type0, style, name, true);
|
|
407
609
|
this.customFontEmit.set(key, {
|
|
408
610
|
pdfName: name + STYLE_SUFFIX[style],
|
|
611
|
+
ttf: this.customFonts.get(name).get(style), // `style` is already resolved, so this exists
|
|
409
612
|
fontFile,
|
|
410
613
|
descriptor,
|
|
411
614
|
cidFont,
|
|
@@ -418,20 +621,18 @@ endstream`;
|
|
|
418
621
|
// tagged "ABCDEF+" as PDF/A requires for subsets. Call once, after the render pass, before output.
|
|
419
622
|
finalizeCustomFonts() {
|
|
420
623
|
for (const [key, e] of this.customFontEmit) {
|
|
421
|
-
const ttf =
|
|
624
|
+
const ttf = e.ttf;
|
|
422
625
|
const base = `${this.subsetTag(e.pdfName, e.used)}+${e.pdfName}`;
|
|
423
626
|
this.replaceObject(e.fontFile, this.buildFontFile2(ttf, e.used));
|
|
424
627
|
this.replaceObject(e.descriptor, this.buildFontDescriptor(base, ttf, e.fontFile));
|
|
425
628
|
this.replaceObject(e.cidFont, this.buildCIDFont(base, ttf, e.descriptor, e.used));
|
|
426
|
-
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used));
|
|
629
|
+
this.replaceObject(e.toUnicode, this.buildToUnicode(ttf, e.used, this.shapedOrigins.get(key)));
|
|
427
630
|
this.replaceObject(e.type0, this.buildType0(base, e.cidFont, e.toUnicode));
|
|
428
631
|
}
|
|
429
632
|
}
|
|
430
633
|
// A deterministic 6-uppercase-letter subset tag (PDF/A wants the "TAG+FontName" form).
|
|
431
634
|
subsetTag(pdfName, used) {
|
|
432
|
-
const h = (
|
|
433
|
-
.update(pdfName + [...used].sort((a, b) => a - b).join(","))
|
|
434
|
-
.digest();
|
|
635
|
+
const h = md5(new TextEncoder().encode(pdfName + [...used].sort((a, b) => a - b).join(",")));
|
|
435
636
|
let tag = "";
|
|
436
637
|
for (let i = 0; i < 6; i++)
|
|
437
638
|
tag += String.fromCharCode(65 + (h[i] % 26));
|
|
@@ -443,22 +644,91 @@ endstream`;
|
|
|
443
644
|
// The variant to actually use for (name, style): the requested style if registered, else the
|
|
444
645
|
// family's Normal as a clean fallback (e.g. bold chosen but no bold file), else undefined when
|
|
445
646
|
// `name` is not a custom family at all.
|
|
647
|
+
//
|
|
648
|
+
// This runs once per CHARACTER (getCharWidth -> getCustomFont), and every paragraph is measured
|
|
649
|
+
// several times (layout, pagination, drawing), so it is the hottest function in the whole renderer.
|
|
650
|
+
// The size check is what keeps a standard-14 document from paying for a feature it never uses: with
|
|
651
|
+
// no custom font registered, neither lookup could ever hit, so there is nothing to look up.
|
|
446
652
|
resolveCustomStyle(name, style = FontStyle.Normal) {
|
|
447
|
-
if (!name)
|
|
653
|
+
if (this.customFonts.size === 0 || !name)
|
|
654
|
+
return undefined;
|
|
655
|
+
const byStyle = this.customFonts.get(name);
|
|
656
|
+
if (!byStyle)
|
|
448
657
|
return undefined;
|
|
449
|
-
if (
|
|
658
|
+
if (byStyle.has(style))
|
|
450
659
|
return style;
|
|
451
|
-
|
|
452
|
-
return FontStyle.Normal;
|
|
453
|
-
return undefined;
|
|
660
|
+
return byStyle.has(FontStyle.Normal) ? FontStyle.Normal : undefined;
|
|
454
661
|
}
|
|
662
|
+
// The same lookup as above, but returning the font itself in ONE map walk instead of resolving the
|
|
663
|
+
// style and then looking the font up again under a freshly built key.
|
|
455
664
|
getCustomFont(name, style = FontStyle.Normal) {
|
|
456
|
-
|
|
457
|
-
|
|
665
|
+
if (this.customFonts.size === 0 || !name)
|
|
666
|
+
return undefined;
|
|
667
|
+
const byStyle = this.customFonts.get(name);
|
|
668
|
+
if (!byStyle)
|
|
669
|
+
return undefined;
|
|
670
|
+
return byStyle.get(style) ?? byStyle.get(FontStyle.Normal);
|
|
458
671
|
}
|
|
459
672
|
isCustomFont(name, style = FontStyle.Normal) {
|
|
460
673
|
return !!this.resolveCustomStyle(name, style);
|
|
461
674
|
}
|
|
675
|
+
// The parsed font for a color-capable custom family (COLR/CPAL), for drawing emoji as vector
|
|
676
|
+
// layers. Returns undefined for standard-14 fonts, unknown names, or fonts with no color glyphs -
|
|
677
|
+
// the caller then draws normal monochrome text.
|
|
678
|
+
getColorFont(name, style = FontStyle.Normal) {
|
|
679
|
+
const ttf = this.getCustomFont(name, style);
|
|
680
|
+
return ttf?.hasColorGlyphs() ? ttf : undefined;
|
|
681
|
+
}
|
|
682
|
+
// The document's color-emoji fallback font: a code point the current text font can't color-render
|
|
683
|
+
// is drawn from this font's color glyphs instead (so `Text("Hallo 😅")` works in one string). Set
|
|
684
|
+
// via `Document({ emoji })`; must name a registered color font.
|
|
685
|
+
setEmojiFont(name) {
|
|
686
|
+
this.emojiFontName = name;
|
|
687
|
+
}
|
|
688
|
+
getEmojiFont() {
|
|
689
|
+
return this.emojiFontName;
|
|
690
|
+
}
|
|
691
|
+
// Alternative color-emoji source: raster images (a CDN like Twemoji, react-pdf style). The renderer
|
|
692
|
+
// fetches `${url}${hexCodePoint}.${format}` per emoji and embeds it. The font source is preferred
|
|
693
|
+
// (native vector); this exists for parity + as an escape hatch.
|
|
694
|
+
setEmojiImageSource(url, format) {
|
|
695
|
+
this.emojiImage = { url, format };
|
|
696
|
+
}
|
|
697
|
+
getEmojiImageSource() {
|
|
698
|
+
return this.emojiImage;
|
|
699
|
+
}
|
|
700
|
+
// Whether `ttf` has a COLOR glyph for a code point (i.e. it would render it as color emoji).
|
|
701
|
+
/**
|
|
702
|
+
* Whether a family can draw this code point at all. An embedded face answers from its `cmap`; a
|
|
703
|
+
* standard-14 one can only draw what Windows-1252 encodes, which is what its `/Widths` array covers.
|
|
704
|
+
* Used to pick a family out of a fallback stack, per code point.
|
|
705
|
+
*/
|
|
706
|
+
hasGlyph(codePoint, fontFamily, fontStyle = FontStyle.Normal) {
|
|
707
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
708
|
+
if (ttf)
|
|
709
|
+
return ttf.getGlyphIndex(codePoint) !== 0;
|
|
710
|
+
return isWindows1252(codePoint);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Whether the document draws this code point through the colour-emoji path instead of the text
|
|
714
|
+
* font - a fallback emoji FONT, or an image source (`Document({ emoji })`). Such a code point is
|
|
715
|
+
* missing from the text font ON PURPOSE, so glyph coverage must leave it alone. Mirrors exactly
|
|
716
|
+
* what `getCharWidth` measures it as, or a dropped emoji would have been measured and not drawn.
|
|
717
|
+
*/
|
|
718
|
+
rendersAsEmoji(codePoint, fontFamily, fontStyle) {
|
|
719
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
720
|
+
if (ttf && this.colorRenders(ttf, codePoint))
|
|
721
|
+
return true;
|
|
722
|
+
if (this.emojiFontName && this.emojiFontName !== fontFamily) {
|
|
723
|
+
const emoji = this.getColorFont(this.emojiFontName, fontStyle);
|
|
724
|
+
if (emoji && this.colorRenders(emoji, codePoint))
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
return this.emojiImage !== undefined && isEmojiCodePoint(codePoint);
|
|
728
|
+
}
|
|
729
|
+
colorRenders(ttf, codePoint) {
|
|
730
|
+
return ttf.getColorGlyph(ttf.getGlyphIndex(codePoint)) !== null;
|
|
731
|
+
}
|
|
462
732
|
// The page font resource for the resolved variant - same resolution as getCustomFont, so the
|
|
463
733
|
// selected Type0 object and the emitted glyph ids always come from the SAME font file.
|
|
464
734
|
getCustomFontResource(name, style = FontStyle.Normal) {
|
|
@@ -468,6 +738,53 @@ endstream`;
|
|
|
468
738
|
this.ensureEmitted(name, resolved);
|
|
469
739
|
return this.fonts.getFont(name, resolved);
|
|
470
740
|
}
|
|
741
|
+
/** How many glyphs a run draws - its code-point count unless a ligature merged some. `letterSpacing`
|
|
742
|
+
* is per glyph (`Tc`), so this is what it multiplies. */
|
|
743
|
+
shapedGlyphCount(text, fontFamily, fontStyle) {
|
|
744
|
+
return this.shapeText(text, fontFamily, fontStyle)?.length;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The shaped glyphs of a run, or undefined when nothing needed shaping. The ONE place shaping is
|
|
748
|
+
* decided, so measuring and drawing cannot disagree. Memoised: a paragraph is measured several
|
|
749
|
+
* times (layout, pagination, drawing).
|
|
750
|
+
*/
|
|
751
|
+
shapeText(text, fontFamily, fontStyle = FontStyle.Normal) {
|
|
752
|
+
const resolved = this.resolveCustomStyle(fontFamily, fontStyle);
|
|
753
|
+
if (!resolved)
|
|
754
|
+
return undefined;
|
|
755
|
+
const key = `${this.customKey(fontFamily, resolved)}\u0000${text}`;
|
|
756
|
+
const cached = this.shapeCache.get(key);
|
|
757
|
+
if (cached !== undefined)
|
|
758
|
+
return cached ?? undefined;
|
|
759
|
+
const ttf = this.customFonts.get(fontFamily).get(resolved);
|
|
760
|
+
const shaped = shapeRun([...text].map((c) => c.codePointAt(0)), ttf) ?? null;
|
|
761
|
+
this.shapeCache.set(key, shaped);
|
|
762
|
+
if (shaped) {
|
|
763
|
+
// Recorded at shaping time - the only moment the glyph and its code points are known together.
|
|
764
|
+
const fontKey = this.customKey(fontFamily, resolved);
|
|
765
|
+
let origins = this.shapedOrigins.get(fontKey);
|
|
766
|
+
if (!origins)
|
|
767
|
+
this.shapedOrigins.set(fontKey, (origins = new Map()));
|
|
768
|
+
for (const g of shaped)
|
|
769
|
+
origins.set(g.glyph, g.codePoints);
|
|
770
|
+
}
|
|
771
|
+
return shaped ?? undefined;
|
|
772
|
+
}
|
|
773
|
+
/** Hex Identity-H for glyphs the producer already resolved (a shaped run), which cannot be given as
|
|
774
|
+
* text. Recording each keeps it in the subset. */
|
|
775
|
+
registerGlyphs(name, glyphs, style = FontStyle.Normal) {
|
|
776
|
+
const resolved = this.resolveCustomStyle(name, style);
|
|
777
|
+
if (!resolved)
|
|
778
|
+
return "";
|
|
779
|
+
this.ensureEmitted(name, resolved);
|
|
780
|
+
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
781
|
+
let out = "";
|
|
782
|
+
for (const g of glyphs) {
|
|
783
|
+
emit.used.add(g);
|
|
784
|
+
out += g.toString(16).padStart(4, "0").toUpperCase();
|
|
785
|
+
}
|
|
786
|
+
return out;
|
|
787
|
+
}
|
|
471
788
|
// Encodes text as a hex Identity-H string for an embedded font's Tj operator: each codepoint
|
|
472
789
|
// becomes its 2-byte glyph id (CID == GID under /CIDToGIDMap /Identity).
|
|
473
790
|
encodeCustomText(name, text, style = FontStyle.Normal) {
|
|
@@ -475,27 +792,38 @@ endstream`;
|
|
|
475
792
|
if (!resolved)
|
|
476
793
|
return "";
|
|
477
794
|
this.ensureEmitted(name, resolved);
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
const used =
|
|
795
|
+
// Once per text run, not per character: the flat key is fine here.
|
|
796
|
+
const emit = this.customFontEmit.get(this.customKey(name, resolved));
|
|
797
|
+
const { ttf, used } = emit;
|
|
798
|
+
const hex4 = (gid) => gid.toString(16).padStart(4, "0").toUpperCase();
|
|
799
|
+
// The SAME shaped run the measuring path used, so the glyphs drawn are the glyphs measured.
|
|
800
|
+
const shaped = this.shapeText(text, name, style);
|
|
801
|
+
if (shaped) {
|
|
802
|
+
let out = "";
|
|
803
|
+
for (const g of shaped) {
|
|
804
|
+
used.add(g.glyph);
|
|
805
|
+
out += hex4(g.glyph);
|
|
806
|
+
}
|
|
807
|
+
return out;
|
|
808
|
+
}
|
|
481
809
|
let hex = "";
|
|
482
810
|
for (const ch of text) {
|
|
483
811
|
const gid = ttf.getGlyphIndex(ch.codePointAt(0));
|
|
484
812
|
used.add(gid); // record the glyph so the subset keeps it
|
|
485
|
-
hex += gid
|
|
813
|
+
hex += hex4(gid);
|
|
486
814
|
}
|
|
487
815
|
return hex;
|
|
488
816
|
}
|
|
489
817
|
// The SUBSET font program (only the used glyphs' outlines). Binary bytes survive as a latin1
|
|
490
818
|
// string - the final Windows-1252 encoder passes 0x00-0xFF through unchanged (see getArrayBuffer).
|
|
491
819
|
buildFontFile2(ttf, used) {
|
|
492
|
-
const bytes =
|
|
820
|
+
const bytes = subsetTTF(ttf.getData(), used);
|
|
493
821
|
// /Length1 is the UNCOMPRESSED font-program length (required even when FlateDecode'd).
|
|
494
822
|
return this.stream(`/Length1 ${bytes.length}`, bytes);
|
|
495
823
|
}
|
|
496
824
|
buildFontDescriptor(name, ttf, fontFile) {
|
|
497
825
|
const [x0, y0, x1, y1] = ttf.bbox;
|
|
498
|
-
return (`<< /Type /FontDescriptor /FontName /${name} /Flags 4 ` +
|
|
826
|
+
return (`<< /Type /FontDescriptor /FontName /${pdfName(name)} /Flags 4 ` +
|
|
499
827
|
`/FontBBox [${x0} ${y0} ${x1} ${y1}] /ItalicAngle 0 ` +
|
|
500
828
|
`/Ascent ${ttf.ascent} /Descent ${ttf.descent} /CapHeight ${ttf.ascent} /StemV 80 ` +
|
|
501
829
|
`/FontFile2 ${fontFile} 0 R >>`);
|
|
@@ -506,26 +834,33 @@ endstream`;
|
|
|
506
834
|
const all = ttf.glyphWidths();
|
|
507
835
|
const w = [...used]
|
|
508
836
|
.sort((a, b) => a - b)
|
|
509
|
-
.map((g) =>
|
|
837
|
+
.map((g) => `${g} [${all[g] ?? 1000}]`)
|
|
510
838
|
.join(" ");
|
|
511
|
-
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${name} ` +
|
|
512
|
-
`/CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> ` +
|
|
839
|
+
return (`<< /Type /Font /Subtype /CIDFontType2 /BaseFont /${pdfName(name)} ` +
|
|
840
|
+
`/CIDSystemInfo << /Registry ${this.pdfString("Adobe")} /Ordering ${this.pdfString("Identity")} /Supplement 0 >> ` +
|
|
513
841
|
`/FontDescriptor ${descriptor} 0 R /CIDToGIDMap /Identity ` +
|
|
514
842
|
`/DW 1000 /W [${w}] >>`);
|
|
515
843
|
}
|
|
516
844
|
// Maps glyph id -> Unicode so the text stays copy-/searchable (rendering doesn't need it). Only
|
|
517
845
|
// the used glyphs are listed, to match the subset.
|
|
518
|
-
buildToUnicode(ttf, used) {
|
|
846
|
+
buildToUnicode(ttf, used, shapedOrigins = new Map()) {
|
|
519
847
|
const hex4 = (n) => n.toString(16).padStart(4, "0").toUpperCase();
|
|
848
|
+
// A code point above the BMP needs its surrogate PAIR here - a `bfchar` destination is UTF-16BE.
|
|
849
|
+
const utf16 = (code) => code > 0xffff
|
|
850
|
+
? hex4(0xd800 + ((code - 0x10000) >> 10)) + hex4(0xdc00 + ((code - 0x10000) & 0x3ff))
|
|
851
|
+
: hex4(code);
|
|
520
852
|
const rev = ttf.reverseCmap();
|
|
853
|
+
// A SHAPED glyph wins: it is often absent from the cmap (a ligature) or maps back to a
|
|
854
|
+
// presentation form rather than the letter that was written (a joining form), so the reverse
|
|
855
|
+
// cmap alone would make copied text wrong.
|
|
521
856
|
const entries = [...used]
|
|
522
857
|
.sort((a, b) => a - b)
|
|
523
|
-
.filter((g) => rev.has(g))
|
|
524
|
-
.map((g) => [g, rev.get(g)]);
|
|
858
|
+
.filter((g) => shapedOrigins.has(g) || rev.has(g))
|
|
859
|
+
.map((g) => [g, shapedOrigins.get(g) ?? [rev.get(g)]]);
|
|
525
860
|
const blocks = [];
|
|
526
861
|
for (let i = 0; i < entries.length; i += 100) {
|
|
527
862
|
const block = entries.slice(i, i + 100);
|
|
528
|
-
const lines = block.map(([gid,
|
|
863
|
+
const lines = block.map(([gid, codes]) => `<${hex4(gid)}> <${codes.map(utf16).join("")}>`);
|
|
529
864
|
blocks.push(`${block.length} beginbfchar\n${lines.join("\n")}\nendbfchar`);
|
|
530
865
|
}
|
|
531
866
|
const body = `/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n` +
|
|
@@ -533,34 +868,159 @@ endstream`;
|
|
|
533
868
|
`/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n` +
|
|
534
869
|
`1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n${blocks.join("\n")}\n` +
|
|
535
870
|
`endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend`;
|
|
536
|
-
return this.stream("",
|
|
871
|
+
return this.stream("", bytesFromLatin1(body));
|
|
537
872
|
}
|
|
538
873
|
buildType0(name, cidFont, toUnicode) {
|
|
539
|
-
return (`<< /Type /Font /Subtype /Type0 /BaseFont /${name} /Encoding /Identity-H ` +
|
|
874
|
+
return (`<< /Type /Font /Subtype /Type0 /BaseFont /${pdfName(name)} /Encoding /Identity-H ` +
|
|
540
875
|
`/DescendantFonts [${cidFont} 0 R] /ToUnicode ${toUnicode} 0 R >>`);
|
|
541
876
|
}
|
|
877
|
+
// The vertical metrics of a face, in em fractions - what seats the baseline and sizes the line
|
|
878
|
+
// box (see text/line-metrics.ts). An embedded font answers from its own `hhea`; a standard-14
|
|
879
|
+
// font from its AFM header. Nothing here is guessed: both numbers come out of the font data.
|
|
880
|
+
//
|
|
881
|
+
// Asked once per LINE (not per character), but a long document has a lot of lines and the
|
|
882
|
+
// standard-14 lookup below is a linear scan, so the answer is memoised per face. Registering a
|
|
883
|
+
// font clears the cache - a name can go from standard-14 to embedded.
|
|
884
|
+
getFontVerticals(fontFamily, fontStyle) {
|
|
885
|
+
let byStyle = this.verticalsCache.get(fontFamily);
|
|
886
|
+
const hit = byStyle?.get(fontStyle);
|
|
887
|
+
if (hit)
|
|
888
|
+
return hit;
|
|
889
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
890
|
+
const verticals = ttf
|
|
891
|
+
? {
|
|
892
|
+
ascent: ttf.ascent / 1000,
|
|
893
|
+
descent: -ttf.descent / 1000, // hhea writes the descent negative
|
|
894
|
+
lineGap: ttf.lineGap / 1000,
|
|
895
|
+
}
|
|
896
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.verticals();
|
|
897
|
+
if (!byStyle) {
|
|
898
|
+
byStyle = new Map();
|
|
899
|
+
this.verticalsCache.set(fontFamily, byStyle);
|
|
900
|
+
}
|
|
901
|
+
byStyle.set(fontStyle, verticals);
|
|
902
|
+
return verticals;
|
|
903
|
+
}
|
|
904
|
+
// Underline / strikethrough geometry of a face, in em fractions. Same shape as getFontVerticals:
|
|
905
|
+
// an embedded font answers from its `post`/`OS/2` tables, a standard-14 one from its AFM header,
|
|
906
|
+
// and the answer is memoised per face because it is asked once per decorated run.
|
|
907
|
+
getFontDecoration(fontFamily, fontStyle) {
|
|
908
|
+
let byStyle = this.decorationCache.get(fontFamily);
|
|
909
|
+
const hit = byStyle?.get(fontStyle);
|
|
910
|
+
if (hit)
|
|
911
|
+
return hit;
|
|
912
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
913
|
+
const decoration = ttf
|
|
914
|
+
? ttf.decoration()
|
|
915
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser.decoration();
|
|
916
|
+
if (!byStyle) {
|
|
917
|
+
byStyle = new Map();
|
|
918
|
+
this.decorationCache.set(fontFamily, byStyle);
|
|
919
|
+
}
|
|
920
|
+
byStyle.set(fontStyle, decoration);
|
|
921
|
+
return decoration;
|
|
922
|
+
}
|
|
923
|
+
setKerning(enabled) {
|
|
924
|
+
this.kerningEnabled = enabled;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* The kerning adjustment for each adjacent code-point pair of `text`, in em/1000 (the sign the
|
|
928
|
+
* font declares: negative tightens). Length is `codePointCount - 1`. Zero at any pair next to a
|
|
929
|
+
* space - letters are never kerned against a space, which also keeps a per-word measure equal to a
|
|
930
|
+
* whole-line one (the space pairs contribute nothing either way).
|
|
931
|
+
*
|
|
932
|
+
* Standard-14 answers from the AFM `KPX` pairs. An embedded font has no kerning yet (its `kern` /
|
|
933
|
+
* `GPOS` tables are unparsed), so it returns zeros - never a wrong value, just no adjustment.
|
|
934
|
+
*/
|
|
935
|
+
getKernPairs(text, fontFamily, fontStyle) {
|
|
936
|
+
const chars = [...text];
|
|
937
|
+
if (chars.length < 2)
|
|
938
|
+
return [];
|
|
939
|
+
// A shaped run does not kern here: these pairs are keyed by the UNSHAPED glyphs, and the `TJ`
|
|
940
|
+
// path would re-shape each chunk on its own, turning every letter isolated. Arabic kerning is in
|
|
941
|
+
// GPOS, applied during shaping - not built (`todo.md`).
|
|
942
|
+
if (this.shapeText(text, fontFamily, fontStyle))
|
|
943
|
+
return [];
|
|
944
|
+
const out = [];
|
|
945
|
+
// An embedded font kerns by GLYPH id (kern table / GPOS); a standard-14 one by character (AFM).
|
|
946
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
947
|
+
const parser = ttf
|
|
948
|
+
? undefined
|
|
949
|
+
: this.getAVMParserByFont(undefined, fontFamily, fontStyle).parser;
|
|
950
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
951
|
+
const a = chars[i];
|
|
952
|
+
const b = chars[i + 1];
|
|
953
|
+
if (a === " " || b === " ") {
|
|
954
|
+
out.push(0);
|
|
955
|
+
}
|
|
956
|
+
else if (ttf) {
|
|
957
|
+
out.push(ttf.getKerning(ttf.getGlyphIndex(a.codePointAt(0)), ttf.getGlyphIndex(b.codePointAt(0))));
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
out.push(parser.getKerning(a, b));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return out;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Where the glyphs of `text` put ink inside a horizontal band, as x-intervals in POINTS measured
|
|
967
|
+
* from the start of the run. This is what an underline steps around ("skip-ink").
|
|
968
|
+
*
|
|
969
|
+
* `bandTop` / `bandBottom` are in points relative to the baseline, y UP (so both negative under
|
|
970
|
+
* it). Only an EMBEDDED font can answer: the standard-14 outlines live in the viewer, not here.
|
|
971
|
+
* Callers must check `isCustomFont` first rather than treat an empty result as "no descenders".
|
|
972
|
+
*/
|
|
973
|
+
getInkSpansInBand(text, fontSize, fontFamily, fontStyle, bandTop, bandBottom, letterSpacing = 0) {
|
|
974
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
975
|
+
if (!ttf)
|
|
976
|
+
return [];
|
|
977
|
+
const scale = fontSize / ttf.unitsPerEm;
|
|
978
|
+
const spans = [];
|
|
979
|
+
let pen = 0;
|
|
980
|
+
for (const ch of text) {
|
|
981
|
+
const codePoint = ch.codePointAt(0);
|
|
982
|
+
if (codePoint !== undefined) {
|
|
983
|
+
for (const [x0, x1] of ttf.inkSpansInBand(codePoint, bandTop / scale, bandBottom / scale)) {
|
|
984
|
+
spans.push([pen + x0 * scale, pen + x1 * scale]);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
// The pen advances by the glyph AND its letter-spacing, so the ink of the next glyph lands
|
|
988
|
+
// where `Tc` actually draws it - otherwise the skipped gaps drift under the wrong letters.
|
|
989
|
+
pen += this.getCharWidth(ch, fontSize, undefined, fontFamily, fontStyle) + letterSpacing;
|
|
990
|
+
}
|
|
991
|
+
return mergeSpans(spans);
|
|
992
|
+
}
|
|
542
993
|
// Returns the current width of a text, included kernings
|
|
543
994
|
getStringWidth(text, fontFamily, fontSize, fontStyle) {
|
|
995
|
+
// Shaping first: a joined run is narrower than the same letters apart, so measuring the unshaped
|
|
996
|
+
// form while drawing the shaped one would break every line. Both sides ask `shapeText`.
|
|
997
|
+
const shaped = this.shapeText(text, fontFamily, fontStyle);
|
|
998
|
+
if (shaped) {
|
|
999
|
+
const ttf = this.getCustomFont(fontFamily, fontStyle);
|
|
1000
|
+
return shaped.reduce((w, g) => w + ttf.unitsToPoints(g.advance, fontSize), 0);
|
|
1001
|
+
}
|
|
544
1002
|
let width = 0;
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
1003
|
+
// Iterate code points, not UTF-16 units: an astral char (emoji, CJK-ext) is a surrogate pair,
|
|
1004
|
+
// and indexing by unit would measure each half as its own (zero-width) "char". The spread splits
|
|
1005
|
+
// on code points; for BMP text this is one unit each, so the result is unchanged.
|
|
1006
|
+
//
|
|
1007
|
+
// NO KERNING. We write a run as a single `Tj`, and a viewer advances that by the font's plain
|
|
1008
|
+
// widths - PDF never kerns on its own; a producer has to say so with a `TJ` array. Folding the
|
|
1009
|
+
// AFM's kerning into the MEASUREMENT while the OUTPUT ignores it made every kerned string draw
|
|
1010
|
+
// wider than its box: "AVATAR Wave" at 40pt by 19pt, "Total" at 11pt by 5.7%. Measured must equal
|
|
1011
|
+
// drawn. The AFM kerning pairs stay parsed (`getKerning`) for the day we emit `TJ` - see the
|
|
1012
|
+
// "real kerning" item in todo.md, which must cover embedded fonts too (`kern`/`GPOS`).
|
|
1013
|
+
for (const char of text) {
|
|
1014
|
+
width += this.getCharWidth(char, fontSize, undefined, fontFamily, fontStyle);
|
|
557
1015
|
}
|
|
558
1016
|
return width;
|
|
559
1017
|
}
|
|
560
|
-
getCharCode(char) {
|
|
561
|
-
return char.charCodeAt(0).toString();
|
|
562
|
-
}
|
|
563
1018
|
getAVMParserByFont(fullFontName, fontName, fontStyle) {
|
|
1019
|
+
// A non-string family means font bytes were passed where a name belongs - hint at the fix instead of
|
|
1020
|
+
// stringifying the byte blob into an unreadable "parser not found".
|
|
1021
|
+
for (const name of [fullFontName, fontName])
|
|
1022
|
+
if (name != null && typeof name !== "string")
|
|
1023
|
+
throw new Error(`Font family must be a string name, got ${typeof name}. Register font bytes via the document \`fonts\` map (or addFont) and reference them by name.`);
|
|
564
1024
|
if (!fullFontName && (!fontName || !fontStyle)) {
|
|
565
1025
|
throw new Error("No font family is given. Please set a full font name or a font with font style");
|
|
566
1026
|
}
|
|
@@ -577,26 +1037,40 @@ endstream`;
|
|
|
577
1037
|
}
|
|
578
1038
|
// Methode zur Berechnung der Zeichenbreite anhand der Schriftgröße
|
|
579
1039
|
getCharWidth(char, fontSize, fullFontName, fontName, fontStyle) {
|
|
580
|
-
const
|
|
1040
|
+
const name = fullFontName ?? fontName;
|
|
1041
|
+
const ttf = this.getCustomFont(name, fontStyle);
|
|
1042
|
+
// Color-emoji fallback: a code point the current font can't color-render is measured with the
|
|
1043
|
+
// emoji font's advance (matching the renderer, which draws it from that font). Guarded on
|
|
1044
|
+
// emojiFontName so a normal document's measuring stays byte-identical.
|
|
1045
|
+
if (this.emojiFontName && this.emojiFontName !== name) {
|
|
1046
|
+
const cp = char.codePointAt(0);
|
|
1047
|
+
if (cp !== undefined && !(ttf && this.colorRenders(ttf, cp))) {
|
|
1048
|
+
const emoji = this.getColorFont(this.emojiFontName, fontStyle);
|
|
1049
|
+
if (emoji && this.colorRenders(emoji, cp))
|
|
1050
|
+
return emoji.getCharWidth(char, fontSize);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
// Image emoji source: an emoji code point is drawn as a 1em-square image, so it advances one em.
|
|
1054
|
+
if (this.emojiImage) {
|
|
1055
|
+
const cp = char.codePointAt(0);
|
|
1056
|
+
if (cp !== undefined && isEmojiCodePoint(cp) && !(ttf && this.colorRenders(ttf, cp))) {
|
|
1057
|
+
return fontSize;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
581
1060
|
if (ttf)
|
|
582
1061
|
return ttf.getCharWidth(char, fontSize);
|
|
583
1062
|
const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
|
|
584
|
-
|
|
585
|
-
//
|
|
1063
|
+
let advanceWidth = currentParser.parser.getAdvanceWidth(char);
|
|
1064
|
+
// A character a standard font cannot represent (not in Windows-1252) is drawn as "?" by the
|
|
1065
|
+
// encoder; measure it as "?" too so the width matches the glyph - graceful instead of a crash.
|
|
586
1066
|
if (!advanceWidth) {
|
|
587
|
-
|
|
1067
|
+
advanceWidth =
|
|
1068
|
+
currentParser.parser.getAdvanceWidth("?") || currentParser.parser.getAdvanceWidth(" ");
|
|
588
1069
|
}
|
|
589
1070
|
// Width of the character multiplied by the font size (scaled proportionally)
|
|
590
1071
|
return (advanceWidth / 1000) * fontSize;
|
|
591
1072
|
}
|
|
592
1073
|
// Method to get the kerning, if available, between two signs
|
|
593
|
-
getKerning(char, nextChar, fullFontName, fontName, fontStyle) {
|
|
594
|
-
// TrueType kerning (the kern/GPOS tables) is not wired up yet - no kerning for custom fonts.
|
|
595
|
-
if (this.getCustomFont(fullFontName !== null && fullFontName !== void 0 ? fullFontName : fontName, fontStyle))
|
|
596
|
-
return 0;
|
|
597
|
-
const currentParser = this.getAVMParserByFont(fullFontName, fontName, fontStyle);
|
|
598
|
-
return currentParser.parser.getKerning(char, nextChar) / 1000;
|
|
599
|
-
}
|
|
600
1074
|
// Returns all fonts
|
|
601
1075
|
getAllFontsRaw() {
|
|
602
1076
|
return this.fonts.getAllFonts();
|
|
@@ -632,14 +1106,14 @@ endstream`;
|
|
|
632
1106
|
const root = this.objects.findIndex((f) => f.toLowerCase().includes("catalog")) + 1;
|
|
633
1107
|
// A fresh document uses the same hash for both /ID strings; they only diverge on an update.
|
|
634
1108
|
const id = this.documentId ? ` /ID [${this.contentId()} ${this.contentId()}]` : "";
|
|
635
|
-
|
|
1109
|
+
const enc = this.encryptObjNum ? ` /Encrypt ${this.encryptObjNum} 0 R` : "";
|
|
1110
|
+
return `trailer\n<< /Size ${objectCount + 1} /Root ${root} 0 R${id}${enc} >>\nstartxref\n${startxref}\n%%EOF`;
|
|
636
1111
|
}
|
|
637
1112
|
contentId() {
|
|
638
|
-
return `<${(
|
|
1113
|
+
return `<${md5Hex(new TextEncoder().encode(this.objects.join(""))).toUpperCase()}>`;
|
|
639
1114
|
}
|
|
640
1115
|
// Returns the number of objects
|
|
641
1116
|
getObjectCount() {
|
|
642
1117
|
return this.objects.length;
|
|
643
1118
|
}
|
|
644
1119
|
}
|
|
645
|
-
exports.PDFObjectManager = PDFObjectManager;
|