doc-codec 1.0.1 → 1.1.0

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 (112) hide show
  1. package/README.md +192 -50
  2. package/dist/color.cjs +166 -0
  3. package/dist/color.d.cts +20 -0
  4. package/dist/color.d.ts +20 -0
  5. package/dist/color.js +160 -0
  6. package/dist/detect.cjs +3 -0
  7. package/dist/detect.d.cts +3 -1
  8. package/dist/detect.d.ts +3 -1
  9. package/dist/detect.js +3 -1
  10. package/dist/fib/fib.cjs +9 -1
  11. package/dist/fib/fib.d.cts +1 -28
  12. package/dist/fib/fib.d.ts +1 -28
  13. package/dist/fib/fib.js +9 -1
  14. package/dist/fib/offsets.cjs +9 -1
  15. package/dist/fib/offsets.d.cts +8 -0
  16. package/dist/fib/offsets.d.ts +8 -0
  17. package/dist/fib/offsets.js +9 -1
  18. package/dist/fib/write.cjs +43 -0
  19. package/dist/fib/write.d.cts +21 -0
  20. package/dist/fib/write.d.ts +21 -0
  21. package/dist/fib/write.js +42 -0
  22. package/dist/fib-CtsLtF96.d.cts +37 -0
  23. package/dist/fib-CtsLtF96.d.ts +37 -0
  24. package/dist/index.cjs +31 -0
  25. package/dist/index.d.cts +18 -7
  26. package/dist/index.d.ts +18 -7
  27. package/dist/index.js +14 -3
  28. package/dist/list/numbering.cjs +206 -0
  29. package/dist/list/numbering.d.cts +2 -0
  30. package/dist/list/numbering.d.ts +2 -0
  31. package/dist/list/numbering.js +205 -0
  32. package/dist/metadata.cjs +14 -0
  33. package/dist/metadata.d.cts +6 -0
  34. package/dist/metadata.d.ts +6 -0
  35. package/dist/metadata.js +13 -0
  36. package/dist/numbering-Cir-uGdc.d.cts +22 -0
  37. package/dist/numbering-DF1z1PyG.d.ts +22 -0
  38. package/dist/pap-BkFo8dZO.d.ts +29 -0
  39. package/dist/pap-BmelEYEP.d.cts +29 -0
  40. package/dist/prop/chp-write.cjs +47 -0
  41. package/dist/prop/chp-write.d.cts +5 -0
  42. package/dist/prop/chp-write.d.ts +5 -0
  43. package/dist/prop/chp-write.js +46 -0
  44. package/dist/prop/chp.cjs +15 -107
  45. package/dist/prop/chp.d.cts +3 -2
  46. package/dist/prop/chp.d.ts +3 -2
  47. package/dist/prop/chp.js +15 -107
  48. package/dist/prop/fkp-write.cjs +139 -0
  49. package/dist/prop/fkp-write.d.cts +21 -0
  50. package/dist/prop/fkp-write.d.ts +21 -0
  51. package/dist/prop/fkp-write.js +135 -0
  52. package/dist/prop/pap-write.cjs +67 -0
  53. package/dist/prop/pap-write.d.cts +5 -0
  54. package/dist/prop/pap-write.d.ts +5 -0
  55. package/dist/prop/pap-write.js +66 -0
  56. package/dist/prop/pap.cjs +10 -0
  57. package/dist/prop/pap.d.cts +1 -24
  58. package/dist/prop/pap.d.ts +1 -24
  59. package/dist/prop/pap.js +11 -1
  60. package/dist/prop/sep-write.cjs +63 -0
  61. package/dist/prop/sep-write.d.cts +9 -0
  62. package/dist/prop/sep-write.d.ts +9 -0
  63. package/dist/prop/sep-write.js +60 -0
  64. package/dist/prop/sep.cjs +63 -0
  65. package/dist/prop/sep.d.cts +16 -0
  66. package/dist/prop/sep.d.ts +16 -0
  67. package/dist/prop/sep.js +61 -0
  68. package/dist/prop/sprm.d.cts +1 -25
  69. package/dist/prop/sprm.d.ts +1 -25
  70. package/dist/read.cjs +44 -18
  71. package/dist/read.d.cts +21 -4
  72. package/dist/read.d.ts +21 -4
  73. package/dist/read.js +46 -20
  74. package/dist/sprm-Djg5RNiq.d.cts +26 -0
  75. package/dist/sprm-Djg5RNiq.d.ts +26 -0
  76. package/dist/style/fonts.cjs +68 -0
  77. package/dist/style/fonts.d.cts +5 -0
  78. package/dist/style/fonts.d.ts +5 -0
  79. package/dist/style/fonts.js +66 -0
  80. package/dist/style/stsh.cjs +23 -0
  81. package/dist/style/stsh.d.cts +2 -1
  82. package/dist/style/stsh.d.ts +2 -1
  83. package/dist/style/stsh.js +23 -1
  84. package/dist/table/decoration.cjs +220 -0
  85. package/dist/table/decoration.d.cts +41 -0
  86. package/dist/table/decoration.d.ts +41 -0
  87. package/dist/table/decoration.js +205 -0
  88. package/dist/table/read.cjs +192 -0
  89. package/dist/table/read.d.cts +6 -0
  90. package/dist/table/read.d.ts +6 -0
  91. package/dist/table/read.js +191 -0
  92. package/dist/table/tap-write.cjs +125 -0
  93. package/dist/table/tap-write.d.cts +13 -0
  94. package/dist/table/tap-write.d.ts +13 -0
  95. package/dist/table/tap-write.js +124 -0
  96. package/dist/table/tap.cjs +217 -0
  97. package/dist/table/tap.d.cts +29 -0
  98. package/dist/table/tap.d.ts +29 -0
  99. package/dist/table/tap.js +213 -0
  100. package/dist/table/write.cjs +136 -0
  101. package/dist/table/write.d.cts +13 -0
  102. package/dist/table/write.d.ts +13 -0
  103. package/dist/table/write.js +135 -0
  104. package/dist/text/piece-table-write.cjs +24 -0
  105. package/dist/text/piece-table-write.d.cts +4 -0
  106. package/dist/text/piece-table-write.d.ts +4 -0
  107. package/dist/text/piece-table-write.js +23 -0
  108. package/dist/write.cjs +175 -0
  109. package/dist/write.d.cts +5 -0
  110. package/dist/write.d.ts +5 -0
  111. package/dist/write.js +174 -0
  112. package/package.json +3 -3
package/dist/read.d.ts CHANGED
@@ -1,12 +1,29 @@
1
- import { Fib } from "./fib/fib.js";
2
- import { ContentDocument } from "document-schema.js";
1
+ import { t as Fib } from "./fib-CtsLtF96.js";
2
+ import { t as Prl } from "./sprm-Djg5RNiq.js";
3
+ import { t as ParagraphProperties } from "./pap-BkFo8dZO.js";
4
+ import { n as NumberingDefinitions } from "./numbering-DF1z1PyG.js";
5
+ import { ContentDocument, ContentParagraph } from "document-schema.js";
3
6
  //#region src/read.d.ts
4
7
  interface DocStreams {
5
8
  readonly wordDocument: Uint8Array;
6
9
  readonly table: Uint8Array;
7
10
  readonly fib: Fib;
11
+ /** The raw "\x05SummaryInformation" stream bytes, or undefined when the container carries none -- a valid, spec-conformant Word Binary File need not carry document properties at all. */
12
+ readonly metadata: Uint8Array<ArrayBuffer> | undefined;
8
13
  }
9
14
  declare function readDocStreams(bytes: Uint8Array<ArrayBuffer>): DocStreams;
10
- declare function readDocContent(bytes: Uint8Array<ArrayBuffer>): ContentDocument;
15
+ /** readDocContent's own return type: a ContentDocument (kind 'wordprocessing') plus numbering -- the list-level formatting (glyph/format, level-text template, start-at value) PlfLst/PlfLfo carry, which ContentListMembership has nowhere to hold. Mirrors ooxml.js's own DocxDocument.numbering exactly in field name and NumberingDefinitions' own shape (see list/numbering.ts's top comment for why it sits outside the shared schema rather than inside ContentListMembership); unlike DocxDocument, DocContent stays a genuine ContentDocument subtype (an intersection, not a fresh shape) since readDocContent already had one return type to widen rather than two to reconcile. */
16
+ type DocContent = ContentDocument & {
17
+ readonly numbering: NumberingDefinitions;
18
+ };
19
+ declare function readDocContent(bytes: Uint8Array<ArrayBuffer>): DocContent;
20
+ /** One paragraph/cell/row-ending mark, still flat -- table/read.ts's assembleBlocks is what folds a run of these into a real ContentTable. `properties` and `grpprl` are carried alongside the already-built `paragraph` because table grouping needs sprmPFInTable/sprmPFTtp/sprmPItap (properties) and, on a row's own mark, its table-defining sgc-5 sprms (grpprl) -- neither of which survives onto a plain ContentParagraph. */
21
+ interface ParagraphEntry {
22
+ readonly paragraph: ContentParagraph;
23
+ readonly properties: ParagraphProperties;
24
+ readonly grpprl: readonly Prl[];
25
+ /** The character that terminated this paragraph in the text stream: PARAGRAPH_MARK, CELL_MARK, or SECTION_MARK. */
26
+ readonly terminator: number;
27
+ }
11
28
  //#endregion
12
- export { DocStreams, readDocContent, readDocStreams };
29
+ export { DocContent, DocStreams, ParagraphEntry, readDocContent, readDocStreams };
package/dist/read.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { DocFormatError } from "./errors.js";
2
2
  import { slice } from "./bytes.js";
3
- import { WORD_DOCUMENT_STREAM } from "./detect.js";
3
+ import { SUMMARY_INFORMATION_STREAM, WORD_DOCUMENT_STREAM } from "./detect.js";
4
4
  import { parseFib, tableStreamName } from "./fib/fib.js";
5
5
  import { parseClx } from "./text/piece-table.js";
6
6
  import { readTextRange } from "./text/characters.js";
@@ -9,10 +9,14 @@ import { readGrpprl } from "./prop/sprm.js";
9
9
  import { PropertyBinTable } from "./prop/fkp.js";
10
10
  import { applyCharacterSprms } from "./prop/chp.js";
11
11
  import { applyParagraphSprms } from "./prop/pap.js";
12
+ import { readSectionProperties } from "./prop/sep.js";
12
13
  import { headingLevelFromIstd, parseStsh } from "./style/stsh.js";
13
- import { readCompoundFile } from "archive-codec";
14
+ import { parseFontTable } from "./style/fonts.js";
15
+ import { readNumberingDefinitions } from "./list/numbering.js";
16
+ import { assembleBlocks } from "./table/read.js";
17
+ import { readCompoundFile, readSummaryInformation, summaryInformationToLayoutMetadata } from "archive-codec";
14
18
  //#region src/read.ts
15
- /** The page geometry every section is given, because this reader does not yet read a document's own. US Letter with one-inch margins is Word's own default for a new document; a document that states otherwise is not yet consulted, so this is a placeholder the schema requires rather than a fact read from the file. */
19
+ /** Word's own default for a new document (US Letter, one-inch margins) -- what a field this reader resolves from PlcfSed/Sepx (prop/sep.ts's readSectionProperties) falls back to when the file states nothing for it, exactly as it would fall back to Word's own implementation-dependent default for that one unstated sprm. */
16
20
  const DEFAULT_PAGE_SIZE = {
17
21
  widthPt: 612,
18
22
  heightPt: 792
@@ -31,64 +35,86 @@ function readDocStreams(bytes) {
31
35
  const wanted = tableStreamName(fib);
32
36
  const table = streams.find((stream) => stream.path === wanted);
33
37
  if (table === void 0) throw new DocFormatError(`FibBase.fWhichTblStm selects the "${wanted}" stream, which this compound file does not contain`);
38
+ const metadata = streams.find((stream) => stream.path === SUMMARY_INFORMATION_STREAM);
34
39
  return {
35
40
  wordDocument: wordDocument.bytes,
36
41
  table: table.bytes,
37
- fib
42
+ fib,
43
+ metadata: metadata?.bytes
38
44
  };
39
45
  }
40
46
  function readDocContent(bytes) {
41
- const { wordDocument, table, fib } = readDocStreams(bytes);
47
+ const { wordDocument, table, fib, metadata } = readDocStreams(bytes);
42
48
  const pieceTable = parseClx(slice(table, fib.fcClx, fib.lcbClx, "Clx in the Table stream"));
43
49
  const styles = fib.lcbStshf > 0 ? parseStsh(slice(table, fib.fcStshf, fib.lcbStshf, "STSH in the Table stream")) : void 0;
44
50
  const chpxTable = new PropertyBinTable(wordDocument, slice(table, fib.fcPlcfBteChpx, fib.lcbPlcfBteChpx, "PlcBteChpx in the Table stream"), "PlcBteChpx");
45
51
  const papxTable = new PropertyBinTable(wordDocument, slice(table, fib.fcPlcfBtePapx, fib.lcbPlcfBtePapx, "PlcBtePapx in the Table stream"), "PlcBtePapx");
52
+ const fonts = fib.lcbSttbfFfn > 0 ? parseFontTable(slice(table, fib.fcSttbfFfn, fib.lcbSttbfFfn, "SttbfFfn in the Table stream")) : void 0;
46
53
  const range = readTextRange(wordDocument, pieceTable, 0, fib.ccpText);
47
- const blocks = readParagraphs(range.text, range.fcs, {
54
+ const entries = readParagraphs(range.text, range.fcs, {
48
55
  chpxTable,
49
56
  papxTable,
50
57
  styles,
58
+ fonts,
51
59
  characterProperties: /* @__PURE__ */ new Map()
52
60
  });
61
+ const blocks = assembleBlocks(entries);
62
+ const numbering = readNumberingDefinitions(table, fib);
63
+ const sectionProperties = readSectionProperties(wordDocument, table, fib);
53
64
  return {
54
65
  kind: "wordprocessing",
55
- metadata: {},
66
+ metadata: metadata === void 0 ? {} : summaryInformationToLayoutMetadata(readSummaryInformation(metadata)),
56
67
  sections: [{
57
- pageSize: DEFAULT_PAGE_SIZE,
58
- margins: DEFAULT_MARGINS,
68
+ pageSize: {
69
+ widthPt: sectionProperties.pageWidthPt ?? DEFAULT_PAGE_SIZE.widthPt,
70
+ heightPt: sectionProperties.pageHeightPt ?? DEFAULT_PAGE_SIZE.heightPt
71
+ },
72
+ margins: {
73
+ leftPt: sectionProperties.marginLeftPt ?? DEFAULT_MARGINS.leftPt,
74
+ rightPt: sectionProperties.marginRightPt ?? DEFAULT_MARGINS.rightPt,
75
+ topPt: sectionProperties.marginTopPt ?? DEFAULT_MARGINS.topPt,
76
+ bottomPt: sectionProperties.marginBottomPt ?? DEFAULT_MARGINS.bottomPt
77
+ },
59
78
  blocks
60
- }]
79
+ }],
80
+ numbering
61
81
  };
62
82
  }
63
83
  function readParagraphs(text, fcs, context) {
64
- const blocks = [];
84
+ const entries = [];
65
85
  let start = 0;
66
86
  for (let index = 0; index < text.length; index += 1) {
67
87
  const code = text.charCodeAt(index);
68
88
  if (!endsParagraph(code)) continue;
69
89
  const markFc = fcs[index];
70
90
  if (markFc === void 0) throw new DocFormatError(`character ${index} has no byte offset, so its paragraph's properties cannot be located`);
71
- blocks.push(buildParagraph(text.slice(start, index), fcs.slice(start, index), markFc, context));
91
+ entries.push(buildParagraph(text.slice(start, index), fcs.slice(start, index), markFc, code, context));
72
92
  start = index + 1;
73
93
  }
74
94
  if (start < text.length) {
75
95
  const firstFc = fcs[start];
76
96
  if (firstFc === void 0) throw new DocFormatError(`character ${start} has no byte offset, so the trailing paragraph's properties cannot be located`);
77
- blocks.push(buildParagraph(text.slice(start), fcs.slice(start), firstFc, context));
97
+ entries.push(buildParagraph(text.slice(start), fcs.slice(start), firstFc, 13, context));
78
98
  }
79
- return blocks;
99
+ return entries;
80
100
  }
81
- function buildParagraph(text, fcs, propertyFc, context) {
101
+ function buildParagraph(text, fcs, propertyFc, terminator, context) {
82
102
  const papx = context.papxTable.papx(propertyFc);
83
103
  const properties = {};
104
+ const grpprl = papx !== void 0 ? readGrpprl(papx.grpprl) : [];
84
105
  if (papx !== void 0) {
85
106
  properties.istd = papx.istd;
86
- applyParagraphSprms(readGrpprl(papx.grpprl), properties);
107
+ applyParagraphSprms(grpprl, properties);
87
108
  }
88
109
  return {
89
- kind: "paragraph",
90
- runs: buildRuns(text, fcs, context),
91
- ...paragraphAttributes(properties, context)
110
+ paragraph: {
111
+ kind: "paragraph",
112
+ runs: buildRuns(text, fcs, context),
113
+ ...paragraphAttributes(properties, context)
114
+ },
115
+ properties,
116
+ grpprl,
117
+ terminator
92
118
  };
93
119
  }
94
120
  function paragraphAttributes(properties, context) {
@@ -155,7 +181,7 @@ function buildRuns(text, fcs, context) {
155
181
  let properties = context.characterProperties.get(key);
156
182
  if (properties === void 0) {
157
183
  properties = {};
158
- if (grpprl !== void 0) applyCharacterSprms(readGrpprl(grpprl), properties);
184
+ if (grpprl !== void 0) applyCharacterSprms(readGrpprl(grpprl), properties, context.fonts);
159
185
  context.characterProperties.set(key, properties);
160
186
  }
161
187
  currentProperties = properties;
@@ -0,0 +1,26 @@
1
+ //#region src/prop/sprm.d.ts
2
+ interface Sprm {
3
+ /** The raw 16-bit opcode, which is what the property tables in [MS-DOC] 2.6.1-2.6.5 are keyed on. */
4
+ readonly value: number;
5
+ readonly ispmd: number;
6
+ readonly fSpec: 0 | 1;
7
+ readonly sgc: number;
8
+ readonly spra: number;
9
+ }
10
+ interface Prl {
11
+ readonly sprm: Sprm;
12
+ readonly operand: Uint8Array;
13
+ }
14
+ /** Sprm.sgc, [MS-DOC] 2.6.1 -- which property family the opcode belongs to. */
15
+ declare const SGC: {
16
+ readonly paragraph: 1;
17
+ readonly character: 2;
18
+ readonly picture: 3;
19
+ readonly section: 4;
20
+ readonly table: 5;
21
+ };
22
+ declare function decodeSprm(value: number): Sprm;
23
+ declare function operandSize(sprm: Sprm, bytes: Uint8Array, offset: number): number;
24
+ declare function readGrpprl(bytes: Uint8Array): Prl[];
25
+ //#endregion
26
+ export { operandSize as a, decodeSprm as i, SGC as n, readGrpprl as o, Sprm as r, Prl as t };
@@ -0,0 +1,26 @@
1
+ //#region src/prop/sprm.d.ts
2
+ interface Sprm {
3
+ /** The raw 16-bit opcode, which is what the property tables in [MS-DOC] 2.6.1-2.6.5 are keyed on. */
4
+ readonly value: number;
5
+ readonly ispmd: number;
6
+ readonly fSpec: 0 | 1;
7
+ readonly sgc: number;
8
+ readonly spra: number;
9
+ }
10
+ interface Prl {
11
+ readonly sprm: Sprm;
12
+ readonly operand: Uint8Array;
13
+ }
14
+ /** Sprm.sgc, [MS-DOC] 2.6.1 -- which property family the opcode belongs to. */
15
+ declare const SGC: {
16
+ readonly paragraph: 1;
17
+ readonly character: 2;
18
+ readonly picture: 3;
19
+ readonly section: 4;
20
+ readonly table: 5;
21
+ };
22
+ declare function decodeSprm(value: number): Sprm;
23
+ declare function operandSize(sprm: Sprm, bytes: Uint8Array, offset: number): number;
24
+ declare function readGrpprl(bytes: Uint8Array): Prl[];
25
+ //#endregion
26
+ export { operandSize as a, decodeSprm as i, SGC as n, readGrpprl as o, Sprm as r, Prl as t };
@@ -0,0 +1,68 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_errors = require("../errors.cjs");
3
+ const require_bytes = require("../bytes.cjs");
4
+ //#region src/style/fonts.ts
5
+ /** FFN's fixed head before xszFfn: ffid(1) + wWeight(2) + chs(1) + ixchSzAlt(1) + panose(10) + fs(24). */
6
+ const FFN_FIXED_SIZE = 39;
7
+ /** chs's ANSI_CHARSET, [MS-DOC] 2.9.87's own value 0 -- the character set every font this package writes is declared to use, since the document's own text is written as Unicode regardless (see text/piece-table-write.ts). */
8
+ const FFN_CHARSET_ANSI = 0;
9
+ /** A non-extended STTB's own per-entry length prefix is one byte, so one FFN record (fixed head plus a null-terminated name, each name character and terminator being 2 bytes) cannot exceed this. */
10
+ const MAX_ENTRY_BYTES = 255;
11
+ function parseFontTable(sttbfFfn) {
12
+ const cData = require_bytes.readUint16LE(sttbfFfn, 0);
13
+ const cbExtra = require_bytes.readUint16LE(sttbfFfn, 2);
14
+ if (cbExtra !== 0) throw new require_errors.DocFormatError(`SttbfFfn.cbExtra is ${cbExtra}, but [MS-DOC] 2.9.253 requires it to be 0`);
15
+ const names = [];
16
+ let cursor = 4;
17
+ for (let index = 0; index < cData; index += 1) {
18
+ const cch = require_bytes.readUint8(sttbfFfn, cursor);
19
+ const record = require_bytes.slice(sttbfFfn, cursor + 1, cch, `SttbfFfn entry ${index}`);
20
+ names.push(readFfnName(record, index));
21
+ cursor += 1 + cch;
22
+ }
23
+ return names;
24
+ }
25
+ function readFfnName(record, index) {
26
+ if (record.length < 41) throw new require_errors.DocFormatError(`FFN record ${index} is ${record.length} bytes, shorter than the fixed ${FFN_FIXED_SIZE}-byte head plus xszFfn's own null terminator`);
27
+ let name = "";
28
+ for (let offset = FFN_FIXED_SIZE; offset + 2 <= record.length; offset += 2) {
29
+ const unit = require_bytes.readUint16LE(record, offset);
30
+ if (unit === 0) return name;
31
+ name += String.fromCharCode(unit);
32
+ }
33
+ throw new require_errors.DocFormatError(`FFN record ${index}'s xszFfn runs to the end of the record with no null terminator`);
34
+ }
35
+ function buildFontTable(names) {
36
+ const bytes = [];
37
+ const push16 = (value) => {
38
+ bytes.push(value & 255, value >> 8 & 255);
39
+ };
40
+ push16(names.length);
41
+ push16(0);
42
+ for (const name of names) {
43
+ const record = buildFfnRecord(name);
44
+ if (record.length > MAX_ENTRY_BYTES) throw new require_errors.DocFormatError(`font name ${JSON.stringify(name)} produces a ${record.length}-byte FFN record, past the ${MAX_ENTRY_BYTES}-byte limit a non-extended STTB's one-byte cch can address`);
45
+ bytes.push(record.length, ...record);
46
+ }
47
+ return new Uint8Array(bytes);
48
+ }
49
+ function buildFfnRecord(name) {
50
+ const record = [
51
+ 0,
52
+ 144,
53
+ 1,
54
+ FFN_CHARSET_ANSI,
55
+ 0,
56
+ ...new Array(10).fill(0),
57
+ ...new Array(24).fill(0)
58
+ ];
59
+ for (const character of name) {
60
+ const code = character.charCodeAt(0);
61
+ record.push(code & 255, code >> 8 & 255);
62
+ }
63
+ record.push(0, 0);
64
+ return record;
65
+ }
66
+ //#endregion
67
+ exports.buildFontTable = buildFontTable;
68
+ exports.parseFontTable = parseFontTable;
@@ -0,0 +1,5 @@
1
+ //#region src/style/fonts.d.ts
2
+ declare function parseFontTable(sttbfFfn: Uint8Array): string[];
3
+ declare function buildFontTable(names: readonly string[]): Uint8Array;
4
+ //#endregion
5
+ export { buildFontTable, parseFontTable };
@@ -0,0 +1,5 @@
1
+ //#region src/style/fonts.d.ts
2
+ declare function parseFontTable(sttbfFfn: Uint8Array): string[];
3
+ declare function buildFontTable(names: readonly string[]): Uint8Array;
4
+ //#endregion
5
+ export { buildFontTable, parseFontTable };
@@ -0,0 +1,66 @@
1
+ import { DocFormatError } from "../errors.js";
2
+ import { readUint16LE, readUint8, slice } from "../bytes.js";
3
+ //#region src/style/fonts.ts
4
+ /** FFN's fixed head before xszFfn: ffid(1) + wWeight(2) + chs(1) + ixchSzAlt(1) + panose(10) + fs(24). */
5
+ const FFN_FIXED_SIZE = 39;
6
+ /** chs's ANSI_CHARSET, [MS-DOC] 2.9.87's own value 0 -- the character set every font this package writes is declared to use, since the document's own text is written as Unicode regardless (see text/piece-table-write.ts). */
7
+ const FFN_CHARSET_ANSI = 0;
8
+ /** A non-extended STTB's own per-entry length prefix is one byte, so one FFN record (fixed head plus a null-terminated name, each name character and terminator being 2 bytes) cannot exceed this. */
9
+ const MAX_ENTRY_BYTES = 255;
10
+ function parseFontTable(sttbfFfn) {
11
+ const cData = readUint16LE(sttbfFfn, 0);
12
+ const cbExtra = readUint16LE(sttbfFfn, 2);
13
+ if (cbExtra !== 0) throw new DocFormatError(`SttbfFfn.cbExtra is ${cbExtra}, but [MS-DOC] 2.9.253 requires it to be 0`);
14
+ const names = [];
15
+ let cursor = 4;
16
+ for (let index = 0; index < cData; index += 1) {
17
+ const cch = readUint8(sttbfFfn, cursor);
18
+ const record = slice(sttbfFfn, cursor + 1, cch, `SttbfFfn entry ${index}`);
19
+ names.push(readFfnName(record, index));
20
+ cursor += 1 + cch;
21
+ }
22
+ return names;
23
+ }
24
+ function readFfnName(record, index) {
25
+ if (record.length < 41) throw new DocFormatError(`FFN record ${index} is ${record.length} bytes, shorter than the fixed ${FFN_FIXED_SIZE}-byte head plus xszFfn's own null terminator`);
26
+ let name = "";
27
+ for (let offset = FFN_FIXED_SIZE; offset + 2 <= record.length; offset += 2) {
28
+ const unit = readUint16LE(record, offset);
29
+ if (unit === 0) return name;
30
+ name += String.fromCharCode(unit);
31
+ }
32
+ throw new DocFormatError(`FFN record ${index}'s xszFfn runs to the end of the record with no null terminator`);
33
+ }
34
+ function buildFontTable(names) {
35
+ const bytes = [];
36
+ const push16 = (value) => {
37
+ bytes.push(value & 255, value >> 8 & 255);
38
+ };
39
+ push16(names.length);
40
+ push16(0);
41
+ for (const name of names) {
42
+ const record = buildFfnRecord(name);
43
+ if (record.length > MAX_ENTRY_BYTES) throw new DocFormatError(`font name ${JSON.stringify(name)} produces a ${record.length}-byte FFN record, past the ${MAX_ENTRY_BYTES}-byte limit a non-extended STTB's one-byte cch can address`);
44
+ bytes.push(record.length, ...record);
45
+ }
46
+ return new Uint8Array(bytes);
47
+ }
48
+ function buildFfnRecord(name) {
49
+ const record = [
50
+ 0,
51
+ 144,
52
+ 1,
53
+ FFN_CHARSET_ANSI,
54
+ 0,
55
+ ...new Array(10).fill(0),
56
+ ...new Array(24).fill(0)
57
+ ];
58
+ for (const character of name) {
59
+ const code = character.charCodeAt(0);
60
+ record.push(code & 255, code >> 8 & 255);
61
+ }
62
+ record.push(0, 0);
63
+ return record;
64
+ }
65
+ //#endregion
66
+ export { buildFontTable, parseFontTable };
@@ -58,8 +58,31 @@ function readXstz(bytes, offset, what) {
58
58
  function headingLevelFromIstd(istd) {
59
59
  return istd >= 1 && istd <= 9 ? istd : void 0;
60
60
  }
61
+ function buildEmptyStsh() {
62
+ const stshi = [];
63
+ const push16 = (value) => {
64
+ stshi.push(value & 255, value >> 8 & 255);
65
+ };
66
+ push16(0);
67
+ push16(STDF_SIZE_WITHOUT_POST_2000);
68
+ push16(1);
69
+ push16(0);
70
+ push16(15);
71
+ push16(0);
72
+ push16(0);
73
+ push16(0);
74
+ push16(0);
75
+ push16(0);
76
+ push16(4);
77
+ return new Uint8Array([
78
+ stshi.length & 255,
79
+ stshi.length >> 8 & 255,
80
+ ...stshi
81
+ ]);
82
+ }
61
83
  //#endregion
62
84
  exports.STI_USER_DEFINED = STI_USER_DEFINED;
63
85
  exports.STK = STK;
86
+ exports.buildEmptyStsh = buildEmptyStsh;
64
87
  exports.headingLevelFromIstd = headingLevelFromIstd;
65
88
  exports.parseStsh = parseStsh;
@@ -25,5 +25,6 @@ interface StyleSheet {
25
25
  }
26
26
  declare function parseStsh(stsh: Uint8Array): StyleSheet;
27
27
  declare function headingLevelFromIstd(istd: number): number | undefined;
28
+ declare function buildEmptyStsh(): Uint8Array;
28
29
  //#endregion
29
- export { STI_USER_DEFINED, STK, Style, StyleSheet, headingLevelFromIstd, parseStsh };
30
+ export { STI_USER_DEFINED, STK, Style, StyleSheet, buildEmptyStsh, headingLevelFromIstd, parseStsh };
@@ -25,5 +25,6 @@ interface StyleSheet {
25
25
  }
26
26
  declare function parseStsh(stsh: Uint8Array): StyleSheet;
27
27
  declare function headingLevelFromIstd(istd: number): number | undefined;
28
+ declare function buildEmptyStsh(): Uint8Array;
28
29
  //#endregion
29
- export { STI_USER_DEFINED, STK, Style, StyleSheet, headingLevelFromIstd, parseStsh };
30
+ export { STI_USER_DEFINED, STK, Style, StyleSheet, buildEmptyStsh, headingLevelFromIstd, parseStsh };
@@ -57,5 +57,27 @@ function readXstz(bytes, offset, what) {
57
57
  function headingLevelFromIstd(istd) {
58
58
  return istd >= 1 && istd <= 9 ? istd : void 0;
59
59
  }
60
+ function buildEmptyStsh() {
61
+ const stshi = [];
62
+ const push16 = (value) => {
63
+ stshi.push(value & 255, value >> 8 & 255);
64
+ };
65
+ push16(0);
66
+ push16(STDF_SIZE_WITHOUT_POST_2000);
67
+ push16(1);
68
+ push16(0);
69
+ push16(15);
70
+ push16(0);
71
+ push16(0);
72
+ push16(0);
73
+ push16(0);
74
+ push16(0);
75
+ push16(4);
76
+ return new Uint8Array([
77
+ stshi.length & 255,
78
+ stshi.length >> 8 & 255,
79
+ ...stshi
80
+ ]);
81
+ }
60
82
  //#endregion
61
- export { STI_USER_DEFINED, STK, headingLevelFromIstd, parseStsh };
83
+ export { STI_USER_DEFINED, STK, buildEmptyStsh, headingLevelFromIstd, parseStsh };