@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -21
  3. package/dist/api/args.d.ts +1 -1
  4. package/dist/api/args.js +2 -5
  5. package/dist/api/color.d.ts +4 -4
  6. package/dist/api/color.js +11 -17
  7. package/dist/api/content.d.ts +20 -13
  8. package/dist/api/content.js +44 -29
  9. package/dist/api/descriptor.d.ts +2 -2
  10. package/dist/api/descriptor.js +121 -31
  11. package/dist/api/dimension.d.ts +76 -0
  12. package/dist/api/dimension.js +43 -0
  13. package/dist/api/font-url.d.ts +28 -0
  14. package/dist/api/font-url.js +134 -0
  15. package/dist/api/forms.d.ts +347 -0
  16. package/dist/api/forms.js +189 -0
  17. package/dist/api/gradient.d.ts +53 -0
  18. package/dist/api/gradient.js +97 -0
  19. package/dist/api/index.d.ts +13 -8
  20. package/dist/api/index.js +13 -24
  21. package/dist/api/insets.d.ts +19 -12
  22. package/dist/api/insets.js +19 -12
  23. package/dist/api/layout.d.ts +149 -25
  24. package/dist/api/layout.js +182 -51
  25. package/dist/api/page-builder.d.ts +40 -0
  26. package/dist/api/page-builder.js +44 -0
  27. package/dist/api/structure.d.ts +100 -13
  28. package/dist/api/structure.js +179 -88
  29. package/dist/api/table.d.ts +5 -5
  30. package/dist/api/table.js +38 -26
  31. package/dist/api/text.d.ts +90 -4
  32. package/dist/api/text.js +116 -31
  33. package/dist/assets/font-data.d.ts +2 -0
  34. package/dist/assets/font-data.js +21 -0
  35. package/dist/assets/font-data.ts +37 -0
  36. package/dist/common/color.js +1 -5
  37. package/dist/constants/page-sizes.js +3 -6
  38. package/dist/constants/pdf-parts.js +1 -4
  39. package/dist/crypto/legacy-handler.d.ts +43 -0
  40. package/dist/crypto/legacy-handler.js +145 -0
  41. package/dist/crypto/security-handler.d.ts +62 -0
  42. package/dist/crypto/security-handler.js +155 -0
  43. package/dist/crypto/webcrypto.d.ts +11 -0
  44. package/dist/crypto/webcrypto.js +62 -0
  45. package/dist/edit/acroform-reader.d.ts +70 -0
  46. package/dist/edit/acroform-reader.js +126 -0
  47. package/dist/edit/appearance.d.ts +47 -0
  48. package/dist/edit/appearance.js +171 -0
  49. package/dist/edit/document.d.ts +145 -0
  50. package/dist/edit/document.js +642 -0
  51. package/dist/edit/fill.d.ts +65 -0
  52. package/dist/edit/fill.js +424 -0
  53. package/dist/edit/flatten.d.ts +54 -0
  54. package/dist/edit/flatten.js +462 -0
  55. package/dist/edit/index.d.ts +20 -0
  56. package/dist/edit/index.js +18 -0
  57. package/dist/edit/lexer.d.ts +30 -0
  58. package/dist/edit/lexer.js +285 -0
  59. package/dist/edit/objects.d.ts +68 -0
  60. package/dist/edit/objects.js +83 -0
  61. package/dist/edit/writer.d.ts +60 -0
  62. package/dist/edit/writer.js +283 -0
  63. package/dist/elements/container-element.d.ts +50 -5
  64. package/dist/elements/container-element.js +100 -39
  65. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  66. package/dist/elements/forms/checkbox-element.js +48 -0
  67. package/dist/elements/forms/choice-element.d.ts +53 -0
  68. package/dist/elements/forms/choice-element.js +59 -0
  69. package/dist/elements/forms/push-button-element.d.ts +56 -0
  70. package/dist/elements/forms/push-button-element.js +49 -0
  71. package/dist/elements/forms/radio-element.d.ts +52 -0
  72. package/dist/elements/forms/radio-element.js +49 -0
  73. package/dist/elements/forms/signature-element.d.ts +54 -0
  74. package/dist/elements/forms/signature-element.js +47 -0
  75. package/dist/elements/forms/text-field-element.d.ts +61 -0
  76. package/dist/elements/forms/text-field-element.js +54 -0
  77. package/dist/elements/image-element.d.ts +58 -6
  78. package/dist/elements/image-element.js +145 -111
  79. package/dist/elements/index.d.ts +26 -10
  80. package/dist/elements/index.js +26 -28
  81. package/dist/elements/layout/anchor-element.d.ts +23 -0
  82. package/dist/elements/layout/anchor-element.js +27 -0
  83. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  84. package/dist/elements/layout/bookmark-element.js +30 -0
  85. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  86. package/dist/elements/layout/default-text-style-element.js +47 -0
  87. package/dist/elements/layout/deferred-element.d.ts +3 -3
  88. package/dist/elements/layout/deferred-element.js +4 -8
  89. package/dist/elements/layout/expanded-element.d.ts +6 -4
  90. package/dist/elements/layout/expanded-element.js +22 -17
  91. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  92. package/dist/elements/layout/keep-together-element.js +85 -0
  93. package/dist/elements/layout/link-element.d.ts +33 -0
  94. package/dist/elements/layout/link-element.js +39 -0
  95. package/dist/elements/layout/padding-element.d.ts +10 -5
  96. package/dist/elements/layout/padding-element.js +25 -16
  97. package/dist/elements/layout/page-break-element.d.ts +17 -0
  98. package/dist/elements/layout/page-break-element.js +24 -0
  99. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  100. package/dist/elements/layout/page-builder-element.js +45 -0
  101. package/dist/elements/layout/positioned-element.d.ts +47 -0
  102. package/dist/elements/layout/positioned-element.js +73 -0
  103. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  104. package/dist/elements/layout/repeating-header-element.js +8 -12
  105. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  106. package/dist/elements/layout/rotated-box-element.js +54 -0
  107. package/dist/elements/layout/rotated-element.d.ts +32 -0
  108. package/dist/elements/layout/rotated-element.js +41 -0
  109. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  110. package/dist/elements/layout/sized-container-element.js +6 -11
  111. package/dist/elements/layout/struct-group.d.ts +28 -0
  112. package/dist/elements/layout/struct-group.js +42 -0
  113. package/dist/elements/line-element.d.ts +3 -3
  114. package/dist/elements/line-element.js +5 -10
  115. package/dist/elements/page-element.d.ts +26 -6
  116. package/dist/elements/page-element.js +72 -27
  117. package/dist/elements/pdf-document-element.d.ts +10 -4
  118. package/dist/elements/pdf-document-element.js +11 -10
  119. package/dist/elements/pdf-element.d.ts +150 -4
  120. package/dist/elements/pdf-element.js +138 -20
  121. package/dist/elements/rectangle-element.d.ts +52 -11
  122. package/dist/elements/rectangle-element.js +129 -36
  123. package/dist/elements/row-element.d.ts +45 -4
  124. package/dist/elements/row-element.js +66 -18
  125. package/dist/elements/text-element.d.ts +132 -11
  126. package/dist/elements/text-element.js +206 -56
  127. package/dist/forms/acroform.d.ts +63 -0
  128. package/dist/forms/acroform.js +427 -0
  129. package/dist/forms/appearance.d.ts +58 -0
  130. package/dist/forms/appearance.js +204 -0
  131. package/dist/forms/field.d.ts +164 -0
  132. package/dist/forms/field.js +1 -0
  133. package/dist/forms/pdf.d.ts +19 -0
  134. package/dist/forms/pdf.js +28 -0
  135. package/dist/index.d.ts +3 -3
  136. package/dist/index.js +3 -19
  137. package/dist/ir/display-list.d.ts +197 -8
  138. package/dist/ir/display-list.js +8 -2
  139. package/dist/layout/alignment.d.ts +10 -0
  140. package/dist/layout/alignment.js +6 -0
  141. package/dist/layout/box-constraints.d.ts +61 -0
  142. package/dist/layout/box-constraints.js +88 -6
  143. package/dist/layout/collect-images.d.ts +9 -0
  144. package/dist/layout/collect-images.js +34 -0
  145. package/dist/layout/fragmentation.d.ts +20 -1
  146. package/dist/layout/fragmentation.js +74 -18
  147. package/dist/layout/insets.d.ts +28 -0
  148. package/dist/layout/insets.js +20 -0
  149. package/dist/platform/browser-fs.d.ts +2 -0
  150. package/dist/platform/browser-fs.js +9 -0
  151. package/dist/platform/browser-image.d.ts +5 -0
  152. package/dist/platform/browser-image.js +13 -0
  153. package/dist/platform/node-fs.d.ts +2 -0
  154. package/dist/platform/node-fs.js +10 -0
  155. package/dist/platform/node-image.d.ts +5 -0
  156. package/dist/platform/node-image.js +9 -0
  157. package/dist/renderer/anchor-renderer.d.ts +6 -0
  158. package/dist/renderer/anchor-renderer.js +11 -0
  159. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  160. package/dist/renderer/bookmark-renderer.js +11 -0
  161. package/dist/renderer/container-renderer.d.ts +3 -3
  162. package/dist/renderer/container-renderer.js +11 -28
  163. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  164. package/dist/renderer/default-text-style-renderer.js +10 -0
  165. package/dist/renderer/deferred-renderer.d.ts +3 -3
  166. package/dist/renderer/deferred-renderer.js +8 -23
  167. package/dist/renderer/emoji-image.d.ts +9 -0
  168. package/dist/renderer/emoji-image.js +68 -0
  169. package/dist/renderer/expanded-renderer.d.ts +3 -3
  170. package/dist/renderer/expanded-renderer.js +6 -21
  171. package/dist/renderer/form-field-renderer.d.ts +20 -0
  172. package/dist/renderer/form-field-renderer.js +12 -0
  173. package/dist/renderer/image-renderer.d.ts +4 -4
  174. package/dist/renderer/image-renderer.js +86 -75
  175. package/dist/renderer/index.d.ts +10 -10
  176. package/dist/renderer/index.js +10 -26
  177. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  178. package/dist/renderer/keep-together-renderer.js +12 -0
  179. package/dist/renderer/line-renderer.d.ts +3 -3
  180. package/dist/renderer/line-renderer.js +13 -28
  181. package/dist/renderer/link-renderer.d.ts +6 -0
  182. package/dist/renderer/link-renderer.js +11 -0
  183. package/dist/renderer/padding-renderer.d.ts +3 -3
  184. package/dist/renderer/padding-renderer.js +6 -21
  185. package/dist/renderer/page-break-renderer.d.ts +14 -0
  186. package/dist/renderer/page-break-renderer.js +15 -0
  187. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  188. package/dist/renderer/page-builder-renderer.js +11 -0
  189. package/dist/renderer/page-renderer.d.ts +2 -2
  190. package/dist/renderer/page-renderer.js +115 -77
  191. package/dist/renderer/pdf-backend.d.ts +31 -4
  192. package/dist/renderer/pdf-backend.js +287 -49
  193. package/dist/renderer/pdf-config.js +4 -7
  194. package/dist/renderer/pdf-document-class.d.ts +5 -5
  195. package/dist/renderer/pdf-document-class.js +27 -41
  196. package/dist/renderer/pdf-document-renderer.d.ts +19 -10
  197. package/dist/renderer/pdf-document-renderer.js +135 -91
  198. package/dist/renderer/pdf-renderer.d.ts +2 -2
  199. package/dist/renderer/pdf-renderer.js +165 -90
  200. package/dist/renderer/positioned-renderer.d.ts +6 -0
  201. package/dist/renderer/positioned-renderer.js +10 -0
  202. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  203. package/dist/renderer/rectangle-renderer.js +49 -44
  204. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  205. package/dist/renderer/repeating-header-renderer.js +10 -26
  206. package/dist/renderer/rotated-renderer.d.ts +7 -0
  207. package/dist/renderer/rotated-renderer.js +29 -0
  208. package/dist/renderer/row-renderer.d.ts +3 -3
  209. package/dist/renderer/row-renderer.js +11 -28
  210. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  211. package/dist/renderer/struct-group-renderer.js +24 -0
  212. package/dist/renderer/text-renderer.d.ts +9 -5
  213. package/dist/renderer/text-renderer.js +437 -89
  214. package/dist/text/advance.d.ts +34 -0
  215. package/dist/text/advance.js +45 -0
  216. package/dist/text/arabic.d.ts +20 -0
  217. package/dist/text/arabic.js +101 -0
  218. package/dist/text/bidi.d.ts +33 -0
  219. package/dist/text/bidi.js +94 -0
  220. package/dist/text/emoji-codepoints.d.ts +1 -0
  221. package/dist/text/emoji-codepoints.js +29 -0
  222. package/dist/text/font-fallback.d.ts +25 -0
  223. package/dist/text/font-fallback.js +38 -0
  224. package/dist/text/line-breaker.d.ts +50 -12
  225. package/dist/text/line-breaker.js +158 -37
  226. package/dist/text/line-metrics.d.ts +49 -0
  227. package/dist/text/line-metrics.js +57 -0
  228. package/dist/text/orphans-widows.d.ts +27 -0
  229. package/dist/text/orphans-widows.js +53 -0
  230. package/dist/text/shape.d.ts +29 -0
  231. package/dist/text/shape.js +67 -0
  232. package/dist/text/text-decoration.d.ts +50 -0
  233. package/dist/text/text-decoration.js +62 -0
  234. package/dist/text/text-style.d.ts +60 -0
  235. package/dist/text/text-style.js +79 -0
  236. package/dist/utils/afm-parser.d.ts +34 -0
  237. package/dist/utils/afm-parser.js +76 -17
  238. package/dist/utils/bytes.d.ts +24 -0
  239. package/dist/utils/bytes.js +76 -0
  240. package/dist/utils/dest-registry.d.ts +22 -0
  241. package/dist/utils/dest-registry.js +32 -0
  242. package/dist/utils/flex-layout.d.ts +36 -9
  243. package/dist/utils/flex-layout.js +207 -30
  244. package/dist/utils/font-metrics.d.ts +20 -1
  245. package/dist/utils/font-metrics.js +1 -2
  246. package/dist/utils/font-path.js +3 -6
  247. package/dist/utils/gsub.d.ts +36 -0
  248. package/dist/utils/gsub.js +178 -0
  249. package/dist/utils/image-helper.d.ts +6 -5
  250. package/dist/utils/image-helper.js +101 -111
  251. package/dist/utils/inflate.d.ts +16 -0
  252. package/dist/utils/inflate.js +52 -0
  253. package/dist/utils/md5.d.ts +4 -0
  254. package/dist/utils/md5.js +79 -0
  255. package/dist/utils/outline.d.ts +21 -0
  256. package/dist/utils/outline.js +79 -0
  257. package/dist/utils/pdf-object-manager.d.ts +110 -8
  258. package/dist/utils/pdf-object-manager.js +583 -141
  259. package/dist/utils/renderer-registry.d.ts +13 -1
  260. package/dist/utils/renderer-registry.js +23 -6
  261. package/dist/utils/struct-tree.d.ts +44 -0
  262. package/dist/utils/struct-tree.js +129 -0
  263. package/dist/utils/ttf-parser.d.ts +134 -2
  264. package/dist/utils/ttf-parser.js +873 -50
  265. package/dist/utils/ttf-subsetter.d.ts +1 -1
  266. package/dist/utils/ttf-subsetter.js +40 -42
  267. package/dist/utils/ua-xmp.d.ts +9 -0
  268. package/dist/utils/ua-xmp.js +32 -0
  269. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  270. package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
  271. package/dist/utils/woff.d.ts +10 -0
  272. package/dist/utils/woff.js +116 -0
  273. package/dist/validators/element-validator.d.ts +2 -2
  274. package/dist/validators/element-validator.js +31 -26
  275. package/package.json +22 -2
@@ -0,0 +1,283 @@
1
+ import { isDict, isRef, isStream, isString, stringsIn, } from "./objects.js";
2
+ /**
3
+ * Writing changes back into an existing PDF, as an **incremental update**.
4
+ *
5
+ * The original bytes are never rewritten. Changed objects are APPENDED, followed by a fresh
6
+ * cross-reference section whose `/Prev` points back at the old one. That is what the format was designed
7
+ * for, and it buys three things: we never have to understand (let alone re-emit) the parts of a foreign
8
+ * file we did not come for; an existing signature over the original bytes stays intact; and the result
9
+ * carries a property worth testing - **the original file is a literal PREFIX of the output**.
10
+ */
11
+ const enc = (s) => new TextEncoder().encode(s);
12
+ /**
13
+ * A PDF string, written back byte-faithfully.
14
+ *
15
+ * Only printable ASCII goes into a `(literal)`. Anything else becomes a `<hex>` string, which carries
16
+ * the SAME bytes but survives the UTF-8 encoder this file is finally written through - a literal holding
17
+ * byte 0xE4 would otherwise leave as the two bytes 0xC3 0xA4 and read back as mojibake.
18
+ */
19
+ const stringToken = (b) => {
20
+ const printable = b.every((byte) => byte >= 0x20 && byte <= 0x7e);
21
+ if (!printable) {
22
+ return `<${Array.from(b, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
23
+ }
24
+ let out = "";
25
+ for (const byte of b) {
26
+ if (byte === 0x28 || byte === 0x29 || byte === 0x5c)
27
+ out += "\\" + String.fromCharCode(byte);
28
+ else
29
+ out += String.fromCharCode(byte);
30
+ }
31
+ return `(${out})`;
32
+ };
33
+ /** Escape a PDF name: everything outside the regular characters becomes `#XX`. */
34
+ const escNameToken = (s) => Array.from(enc(s))
35
+ .map((b) => b >= 0x21 && b <= 0x7e && !"()<>[]{}/%#".includes(String.fromCharCode(b))
36
+ ? String.fromCharCode(b)
37
+ : "#" + b.toString(16).padStart(2, "0").toUpperCase())
38
+ .join("");
39
+ const num = (n) => (Number.isInteger(n) ? String(n) : String(Number(n.toFixed(6))));
40
+ /**
41
+ * Serialise an object back to PDF syntax. Used only for objects we deliberately change - everything we
42
+ * do not touch stays in the original bytes, untouched and unre-encoded.
43
+ *
44
+ * A string keeps the spelling it was read with: a name written as `<FEFF…>` stays hex, because that is
45
+ * how the producer encodes unicode and rewriting it as a literal would corrupt it.
46
+ */
47
+ export function serialize(o, cipher) {
48
+ if (o === undefined || o === null)
49
+ return "null";
50
+ if (typeof o === "number")
51
+ return num(o);
52
+ if (typeof o === "boolean")
53
+ return String(o);
54
+ if (Array.isArray(o))
55
+ return `[${o.map((e) => serialize(e, cipher)).join(" ")}]`;
56
+ if (isRef(o))
57
+ return `${o.num} ${o.gen} R`;
58
+ if (isString(o)) {
59
+ // In an encrypted document every string was deciphered on open, so it is PLAINTEXT here. Writing it
60
+ // back as-is would leak it; the caller pre-enciphers each one and passes the result in.
61
+ const enciphered = cipher?.get(o);
62
+ if (enciphered !== undefined)
63
+ return enciphered;
64
+ // A string the producer wrote as hex stays hex; the rest is decided by its bytes.
65
+ return o.hex
66
+ ? `<${Array.from(o.bytes, (b) => b.toString(16).padStart(2, "0").toUpperCase()).join("")}>`
67
+ : stringToken(o.bytes);
68
+ }
69
+ if (isStream(o))
70
+ return `${serialize(o.dict, cipher)}\nstream\n<<stream data>>\nendstream`;
71
+ if (isDict(o)) {
72
+ const parts = [];
73
+ for (const [k, v] of o.map)
74
+ parts.push(`/${escNameToken(k)} ${serialize(v, cipher)}`);
75
+ return `<< ${parts.join(" ")} >>`;
76
+ }
77
+ return `/${escNameToken(o.name)}`;
78
+ }
79
+ export class IncrementalWriter {
80
+ constructor(doc) {
81
+ this.doc = doc;
82
+ /** Object number -> its new body, in PDF syntax. */
83
+ this.changed = new Map();
84
+ /** Numbers this writer invented, as opposed to ones it replaces - they have no recorded generation. */
85
+ this.added = new Set();
86
+ // New objects continue after the highest number the file already uses.
87
+ this.nextNum = Math.max(0, ...doc.objectNumbers()) + 1;
88
+ }
89
+ /** Replace an existing object. */
90
+ update(objNum, body) {
91
+ this.changed.set(objNum, body);
92
+ }
93
+ /** Append a new object and return its number. */
94
+ add(body) {
95
+ const n = this.nextNum++;
96
+ this.added.add(n);
97
+ this.changed.set(n, body);
98
+ return n;
99
+ }
100
+ /** A shared Helvetica for regenerated appearances, added once and only when one is drawn. */
101
+ helv() {
102
+ return (this.helvNum ?? (this.helvNum = this.add("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")));
103
+ }
104
+ /** The generation to write for an object: the file's own for one we replace, 0 for one we add. */
105
+ generationOf(objNum) {
106
+ return this.added.has(objNum) ? 0 : this.doc.generationOf(objNum);
107
+ }
108
+ get hasChanges() {
109
+ return this.changed.size > 0;
110
+ }
111
+ /**
112
+ * Produce the updated file: the original bytes, then every changed object, then a new cross-reference
113
+ * section chained to the old one.
114
+ */
115
+ save() {
116
+ if (this.changed.size === 0)
117
+ return this.doc.bytes;
118
+ const original = this.doc.bytes;
119
+ const chunks = [original];
120
+ let offset = original.length;
121
+ // A file that does not end in a newline would run its last line into our first object.
122
+ if (original[original.length - 1] !== 10 && original[original.length - 1] !== 13) {
123
+ chunks.push(enc("\n"));
124
+ offset += 1;
125
+ }
126
+ const offsets = new Map();
127
+ for (const [objNum, body] of [...this.changed].sort((a, b) => a[0] - b[0])) {
128
+ offsets.set(objNum, offset);
129
+ // The generation has to match what the file records, or the new entry describes a different
130
+ // object than the one being replaced. Objects we ADD are new numbers, and those start at 0.
131
+ const head = enc(`${objNum} ${this.generationOf(objNum)} obj\n`);
132
+ chunks.push(head);
133
+ offset += head.length;
134
+ if (typeof body === "string") {
135
+ const b = enc(`${body}\nendobj\n`);
136
+ chunks.push(b);
137
+ offset += b.length;
138
+ }
139
+ else {
140
+ const open = enc(`<< ${body.dict} /Length ${body.data.length} >>\nstream\n`);
141
+ const close = enc(`\nendstream\nendobj\n`);
142
+ chunks.push(open, body.data, close);
143
+ offset += open.length + body.data.length + close.length;
144
+ }
145
+ }
146
+ const xrefOffset = offset;
147
+ chunks.push(this.xrefSection(offsets, xrefOffset));
148
+ let total = 0;
149
+ for (const c of chunks)
150
+ total += c.length;
151
+ const out = new Uint8Array(total);
152
+ let at = 0;
153
+ for (const c of chunks) {
154
+ out.set(c, at);
155
+ at += c.length;
156
+ }
157
+ return out;
158
+ }
159
+ /**
160
+ * The new cross-reference section, as BYTES.
161
+ *
162
+ * Bytes, not a string: an xref stream is binary, and running it through a UTF-8 encoder would turn
163
+ * every value above 127 into two bytes - which is most offsets, so the whole index becomes garbage.
164
+ *
165
+ * A classic table is written when the file already had one; an xref-STREAM file gets a stream,
166
+ * because mixing the two leaves readers that follow only one kind unable to walk the chain back.
167
+ */
168
+ xrefSection(offsets, xrefOffset) {
169
+ const prev = this.previousStartXref();
170
+ const rootRef = this.doc.trailer.map.get("Root");
171
+ const idRef = this.doc.trailer.map.get("ID");
172
+ const infoRef = this.doc.trailer.map.get("Info");
173
+ // /Encrypt MUST be carried over. Dropping it declares the file plaintext while its contents are
174
+ // still ciphertext, so every reader - including ours - hands back raw bytes as field names.
175
+ const encryptRef = this.doc.trailer.map.get("Encrypt");
176
+ // An xref stream is itself an object and has to appear in its own index, so its number is taken
177
+ // first and its offset is where this whole section begins.
178
+ const all = new Map(offsets);
179
+ let selfNum;
180
+ if (this.doc.usesXrefStream) {
181
+ selfNum = this.nextNum++;
182
+ all.set(selfNum, xrefOffset);
183
+ }
184
+ const size = Math.max(this.nextNum, ...all.keys()) + 1;
185
+ // Group consecutive object numbers into subsections, which is what both formats want.
186
+ const nums = [...all.keys()].sort((a, b) => a - b);
187
+ const runs = [];
188
+ for (const n of nums) {
189
+ const last = runs[runs.length - 1];
190
+ if (last && n === last[0] + last[1].length)
191
+ last[1].push(n);
192
+ else
193
+ runs.push([n, [n]]);
194
+ }
195
+ const entry = (key, ref) => ref === undefined ? "" : `${key} ${serialize(ref)}`;
196
+ if (selfNum !== undefined) {
197
+ // Rows of `/W [1 4 2]`: in-use marker, 4-byte offset, 2-byte generation. Left uncompressed - a
198
+ // filter is optional, and a readable appended section is worth more here than a few hundred bytes.
199
+ const rows = new Uint8Array(nums.length * 7);
200
+ nums.forEach((n, i) => {
201
+ const off = all.get(n);
202
+ const g = n === selfNum ? 0 : this.generationOf(n);
203
+ rows.set([
204
+ 1,
205
+ (off >>> 24) & 0xff,
206
+ (off >>> 16) & 0xff,
207
+ (off >>> 8) & 0xff,
208
+ off & 0xff,
209
+ (g >>> 8) & 0xff,
210
+ g & 0xff,
211
+ ], i * 7);
212
+ });
213
+ const index = runs.map(([start, group]) => `${start} ${group.length}`).join(" ");
214
+ const dict = [
215
+ "/Type /XRef",
216
+ `/Size ${size}`,
217
+ `/Index [${index}]`,
218
+ "/W [1 4 2]",
219
+ entry("/Root", rootRef),
220
+ entry("/Info", infoRef),
221
+ entry("/ID", idRef),
222
+ entry("/Encrypt", encryptRef),
223
+ prev !== undefined ? `/Prev ${prev}` : "",
224
+ `/Length ${rows.length}`,
225
+ ].filter(Boolean);
226
+ const head = enc(`${selfNum} 0 obj\n<< ${dict.join(" ")} >>\nstream\n`);
227
+ const tail = enc(`\nendstream\nendobj\nstartxref\n${xrefOffset}\n%%EOF\n`);
228
+ const out = new Uint8Array(head.length + rows.length + tail.length);
229
+ out.set(head, 0);
230
+ out.set(rows, head.length);
231
+ out.set(tail, head.length + rows.length);
232
+ return out;
233
+ }
234
+ let table = "xref\n";
235
+ for (const [start, group] of runs) {
236
+ table += `${start} ${group.length}\n`;
237
+ for (const n of group) {
238
+ const g = String(this.generationOf(n)).padStart(5, "0");
239
+ table += `${String(all.get(n)).padStart(10, "0")} ${g} n \n`;
240
+ }
241
+ }
242
+ const trailer = [
243
+ `/Size ${size}`,
244
+ entry("/Root", rootRef),
245
+ entry("/Info", infoRef),
246
+ entry("/ID", idRef),
247
+ entry("/Encrypt", encryptRef),
248
+ prev !== undefined ? `/Prev ${prev}` : "",
249
+ ].filter(Boolean);
250
+ return enc(`${table}trailer\n<< ${trailer.join(" ")} >>\nstartxref\n${xrefOffset}\n%%EOF\n`);
251
+ }
252
+ /** The offset the ORIGINAL file's `startxref` pointed at - our `/Prev`. */
253
+ previousStartXref() {
254
+ const tail = new TextDecoder("latin1").decode(this.doc.bytes.subarray(Math.max(0, this.doc.bytes.length - 2048)));
255
+ const at = tail.lastIndexOf("startxref");
256
+ if (at < 0)
257
+ return undefined;
258
+ const m = /startxref\s+(\d+)/.exec(tail.slice(at));
259
+ return m ? Number(m[1]) : undefined;
260
+ }
261
+ }
262
+ /** Bytes as an enciphered PDF hex string, the form every rewritten string takes in an encrypted file. */
263
+ const hexString = (b) => `<${Array.from(b, (x) => x.toString(16).padStart(2, "0").toUpperCase()).join("")}>`;
264
+ /**
265
+ * Encipher every string the given objects carry, into the cipher map `serialize` consults.
266
+ *
267
+ * Not optional on an encrypted document: `serialize` writes any string missing from the map as
268
+ * PLAINTEXT, so an object rewritten without this pass leaks whatever it holds.
269
+ */
270
+ export async function carryStrings(doc, objects, into = new Map()) {
271
+ if (!doc.isEncrypted)
272
+ return into;
273
+ for (const o of objects) {
274
+ for (const str of stringsIn(o)) {
275
+ if (into.has(str))
276
+ continue;
277
+ const cipher = await doc.encryptForWrite(str.bytes);
278
+ if (cipher)
279
+ into.set(str, hexString(cipher));
280
+ }
281
+ }
282
+ return into;
283
+ }
@@ -1,7 +1,7 @@
1
- import { MainAlign, CrossAlign } from "../utils/flex-layout";
2
- import { BoxConstraints, Offset, Size } from "../layout/box-constraints";
3
- import { Fragmentable, FragmentResult } from "../layout/fragmentation";
4
- import { LayoutContext, SizedElement, SizedPDFElement, WithChildren } from "./pdf-element";
1
+ import { MainAlign, CrossAlign } from "../utils/flex-layout.js";
2
+ import { BoxConstraints, Offset, Size } from "../layout/box-constraints.js";
3
+ import { Fragmentable, FragmentResult } from "../layout/fragmentation.js";
4
+ import { LayoutContext, SizedElement, SizedPDFElement, WithChildren } from "./pdf-element.js";
5
5
  interface ContainerElementParams extends SizedElement, WithChildren {
6
6
  /** Space between children. */
7
7
  gap?: number;
@@ -9,13 +9,45 @@ interface ContainerElementParams extends SizedElement, WithChildren {
9
9
  main?: MainAlign;
10
10
  /** Horizontal alignment of each child (cross axis); defaults to `stretch`. */
11
11
  cross?: CrossAlign;
12
+ /** Lay the children out backwards along the main axis (CSS `row-reverse`). */
13
+ reverse?: boolean;
14
+ /** Let the children flow onto further lines when they do not fit (CSS `flex-wrap`). */
15
+ wrap?: boolean;
16
+ /** How the block of wrapped lines sits across the axis (CSS `align-content`). */
17
+ alignContent?: MainAlign;
18
+ /** Width/height as a fraction (0..1) of the offered box instead of `width`/`height` (relative sizing). */
19
+ widthFactor?: number;
20
+ heightFactor?: number;
21
+ /** Lower / upper bounds per axis, in points or as a fraction; see `SizingParams`. */
22
+ minWidth?: number;
23
+ maxWidth?: number;
24
+ minHeight?: number;
25
+ maxHeight?: number;
26
+ minWidthFactor?: number;
27
+ maxWidthFactor?: number;
28
+ minHeightFactor?: number;
29
+ maxHeightFactor?: number;
30
+ /** width / height; derives whichever axis is left open (CSS `aspect-ratio`). */
31
+ aspectRatio?: number;
32
+ /** Start this stack on a fresh page (CSS `break-before: page`). */
33
+ breakBefore?: boolean;
34
+ /** Start everything after this stack on a fresh page (CSS `break-after: page`). */
35
+ breakAfter?: boolean;
12
36
  }
13
37
  export declare class ContainerElement extends SizedPDFElement implements Fragmentable {
14
38
  private children;
15
39
  private gap;
16
40
  private main;
17
41
  private cross;
18
- constructor({ x, y, width, height, children, gap, main, cross }: ContainerElementParams);
42
+ private reverse;
43
+ private wrap;
44
+ private alignContent?;
45
+ private breakBefore;
46
+ private breakAfter;
47
+ private requested;
48
+ constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }: ContainerElementParams);
49
+ breaksBefore(): boolean;
50
+ breaksAfter(): boolean;
19
51
  /**
20
52
  * Splits the vertical stack across pages. Children are measured against the content
21
53
  * width and packed until one would exceed `maxHeight`. That straddling child is itself
@@ -27,8 +59,21 @@ export declare class ContainerElement extends SizedPDFElement implements Fragmen
27
59
  * Flex children make the container absorb the leftover space, so it never overflows -
28
60
  * in that case we don't fragment and hand the whole container back as `fitted`.
29
61
  */
62
+ hasForcedBreak(): boolean;
30
63
  fragment(maxHeight: number, width: number, ctx: LayoutContext): FragmentResult;
31
64
  private cloneWithChildren;
65
+ relativeSizeFactor(horizontal: boolean): number | undefined;
66
+ /**
67
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
68
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
69
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
70
+ * Column nested in a Row is exactly where that width goes missing.
71
+ *
72
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
73
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
74
+ * every existing layout untouched.
75
+ */
76
+ needsBoundedMain(horizontal: boolean): boolean;
32
77
  calculateLayout(constraints: BoxConstraints, offset: Offset, ctx: LayoutContext): Size;
33
78
  getProps(): ContainerElementParams;
34
79
  }
@@ -1,16 +1,26 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ContainerElement = void 0;
4
- const flex_layout_1 = require("../utils/flex-layout");
5
- const fragmentation_1 = require("../layout/fragmentation");
6
- const pdf_element_1 = require("./pdf-element");
7
- class ContainerElement extends pdf_element_1.SizedPDFElement {
8
- constructor({ x, y, width, height, children, gap, main, cross }) {
1
+ import { FlexLayoutHelper, VERTICAL_AXIS } from "../utils/flex-layout.js";
2
+ import { extentSpecs, resolveSize, } from "../layout/box-constraints.js";
3
+ import { childrenForceBreak, packChildren, } from "../layout/fragmentation.js";
4
+ import { FlexiblePDFElement, SizedPDFElement, } from "./pdf-element.js";
5
+ export class ContainerElement extends SizedPDFElement {
6
+ constructor({ x, y, width, height, children, gap, main, cross, reverse, wrap, alignContent, breakBefore, breakAfter, ...sizing }) {
9
7
  super({ x, y, width, height });
10
8
  this.children = children;
11
- this.gap = gap !== null && gap !== void 0 ? gap : 0;
12
- this.main = main !== null && main !== void 0 ? main : "start";
13
- this.cross = cross !== null && cross !== void 0 ? cross : "stretch";
9
+ this.gap = gap ?? 0;
10
+ this.main = main ?? "start";
11
+ this.cross = cross ?? "stretch";
12
+ this.reverse = reverse ?? false;
13
+ this.wrap = wrap ?? false;
14
+ this.alignContent = alignContent;
15
+ this.requested = { ...sizing, width, height };
16
+ this.breakBefore = breakBefore ?? false;
17
+ this.breakAfter = breakAfter ?? false;
18
+ }
19
+ breaksBefore() {
20
+ return this.breakBefore;
21
+ }
22
+ breaksAfter() {
23
+ return this.breakAfter;
14
24
  }
15
25
  /**
16
26
  * Splits the vertical stack across pages. Children are measured against the content
@@ -23,57 +33,109 @@ class ContainerElement extends pdf_element_1.SizedPDFElement {
23
33
  * Flex children make the container absorb the leftover space, so it never overflows -
24
34
  * in that case we don't fragment and hand the whole container back as `fitted`.
25
35
  */
36
+ hasForcedBreak() {
37
+ return childrenForceBreak(this.children);
38
+ }
26
39
  fragment(maxHeight, width, ctx) {
27
- const { fitted, remainder } = (0, fragmentation_1.packChildren)(this.children, maxHeight, width, ctx, this.gap);
28
- // Fits as one region: hand the whole container back so the page renders unchanged
29
- // (its normal layout distributes flex / fills the page).
30
- if (remainder.length === 0)
40
+ const { fitted, remainder, forceBreak } = packChildren(this.children, maxHeight, width, ctx, this.gap);
41
+ // Fits as one region with no forced cut: hand the whole container back so the page renders
42
+ // unchanged (its normal layout distributes flex / fills the page). A forced break is excluded
43
+ // here even when nothing follows it (a trailing `PageBreak`): return the packed children, which
44
+ // no longer hold the consumed break marker, so it does not leak into the render pass.
45
+ if (remainder.length === 0 && !forceBreak)
31
46
  return { fitted: this, remainder: null };
32
47
  return {
33
48
  fitted: this.cloneWithChildren(fitted),
34
- remainder: this.cloneWithChildren(remainder),
49
+ // Only the CONTINUATION carries `breakAfter`, so a stack that splits across pages still breaks
50
+ // after its LAST fragment (not after the first one). `breakBefore` is dropped on both: it was
51
+ // already honoured by the parent before this stack was ever fragmented.
52
+ remainder: remainder.length === 0 ? null : this.cloneWithChildren(remainder, this.breakAfter),
53
+ forceBreak,
35
54
  };
36
55
  }
37
- cloneWithChildren(children) {
56
+ cloneWithChildren(children, breakAfter = false) {
38
57
  return new ContainerElement({
39
58
  x: this.x,
40
59
  y: this.y,
41
- width: this.width,
42
- height: this.height,
60
+ // Carry the requested WIDTH (a %-width column keeps its width on every page); the height is
61
+ // intentionally dropped so each fragment shrink-wraps to the content it actually holds.
62
+ width: this.requested.width,
63
+ widthFactor: this.requested.widthFactor,
43
64
  children,
44
65
  gap: this.gap,
45
66
  main: this.main,
46
67
  cross: this.cross,
68
+ reverse: this.reverse,
69
+ wrap: this.wrap,
70
+ alignContent: this.alignContent,
71
+ breakAfter,
47
72
  });
48
73
  }
74
+ relativeSizeFactor(horizontal) {
75
+ return horizontal ? this.requested.widthFactor : this.requested.heightFactor;
76
+ }
77
+ /**
78
+ * A Column stacks vertically, so a flex child of its OWN only ever needs a bounded height. But the need
79
+ * also propagates: a `Spacer` two levels down is just as unable to resolve against an infinite axis, so
80
+ * we ask the children too - on both axes, since a nested Row's `Spacer` needs a bounded WIDTH and a
81
+ * Column nested in a Row is exactly where that width goes missing.
82
+ *
83
+ * An explicit extent on an axis short-circuits it: the element already knows how big it is there, and
84
+ * with no flex descendant nothing needs distributing, so it keeps shrink-wrapping. That is what leaves
85
+ * every existing layout untouched.
86
+ */
87
+ needsBoundedMain(horizontal) {
88
+ const requested = horizontal
89
+ ? [this.requested.width, this.requested.widthFactor]
90
+ : [this.requested.height, this.requested.heightFactor];
91
+ if (requested.some((v) => v !== undefined))
92
+ return false;
93
+ const ownFlexChild = !horizontal && this.children.some((c) => c instanceof FlexiblePDFElement);
94
+ return ownFlexChild || this.children.some((c) => c.needsBoundedMain(horizontal));
95
+ }
49
96
  calculateLayout(constraints, offset, ctx) {
50
- // The container fills the width/height it is offered; when an axis is unbounded it
51
- // shrink-wraps to its children instead (mirrors how Row shrink-wraps). Width unbounded
52
- // happens for a Column nested in a Row (the Row offers its fixed children unbounded
53
- // width); passing 0 there would collapse every child to width 0.
54
- if (constraints.hasBoundedWidth)
55
- this.width = constraints.maxWidth;
56
- if (constraints.hasBoundedHeight)
57
- this.height = constraints.maxHeight;
58
97
  this.x = offset.x;
59
98
  this.y = offset.y;
60
- const crossAvail = constraints.hasBoundedWidth ? this.width : Infinity;
61
- const mainAvail = constraints.hasBoundedHeight ? this.height : Infinity;
99
+ // Relative sizing: a pinned extent (fixed points or a fraction of the offered box, clamped into
100
+ // the constraints) wins; else fill a bounded axis; else stay `undefined` and shrink-wrap below.
101
+ // A Column nested in a Row gets unbounded width - passing 0 there would collapse children to 0.
102
+ const specs = extentSpecs(this.requested);
103
+ const resolved = resolveSize(specs.width, specs.height, this.requested.aspectRatio, constraints);
104
+ const { width: explicitWidth, height: explicitHeight, constraints: bounds } = resolved;
105
+ // Fill-or-shrink-wrap is decided by the constraints we were GIVEN; a min/max only caps the result.
106
+ const boundedWidth = explicitWidth !== undefined
107
+ ? bounds.constrainWidth(explicitWidth)
108
+ : constraints.hasBoundedWidth
109
+ ? bounds.maxWidth
110
+ : undefined;
111
+ const boundedHeight = explicitHeight !== undefined
112
+ ? bounds.constrainHeight(explicitHeight)
113
+ : constraints.hasBoundedHeight
114
+ ? bounds.maxHeight
115
+ : undefined;
116
+ // Vertical stack: cross = width (children fill it), main = height (the stacking extent).
117
+ const crossAvail = boundedWidth ?? Infinity;
118
+ const mainAvail = boundedHeight ?? Infinity;
62
119
  let result = { mainUsed: 0, crossUsed: 0 };
63
120
  if (this.children) {
64
121
  // Vertical flex stack (main = height, cross = width). The shared helper measures
65
122
  // fixed children, distributes the leftover to flex children, and places everything
66
123
  // in source order.
67
- result = flex_layout_1.FlexLayoutHelper.layout(this.children, flex_layout_1.VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, { gap: this.gap, main: this.main, cross: this.cross }, ctx);
124
+ result = FlexLayoutHelper.layout(this.children, VERTICAL_AXIS, mainAvail, crossAvail, this.y, this.x, {
125
+ gap: this.gap,
126
+ main: this.main,
127
+ cross: this.cross,
128
+ reverse: this.reverse,
129
+ wrap: this.wrap,
130
+ alignContent: this.alignContent,
131
+ }, ctx);
68
132
  }
69
- // Bounded: fill the offered extent. Unbounded: shrink to the children (height = the
70
- // stack, width = the widest child). Top-left coordinates; the container draws nothing,
71
- // and the Y-flip happens once at the IR -> backend seam.
72
- const width = constraints.hasBoundedWidth ? this.width : result.crossUsed;
73
- const height = constraints.hasBoundedHeight ? this.height : result.mainUsed;
74
- this.width = width;
75
- this.height = height;
76
- return { width, height };
133
+ // Pinned/bounded axis takes that extent; an unbounded one shrink-wraps to the children (height =
134
+ // the stack, width = the widest child). Top-left coordinates; the container draws nothing, and
135
+ // the Y-flip happens once at the IR -> backend seam.
136
+ this.width = boundedWidth ?? result.crossUsed;
137
+ this.height = boundedHeight ?? result.mainUsed;
138
+ return { width: this.width, height: this.height };
77
139
  }
78
140
  getProps() {
79
141
  return {
@@ -88,4 +150,3 @@ class ContainerElement extends pdf_element_1.SizedPDFElement {
88
150
  };
89
151
  }
90
152
  }
91
- exports.ContainerElement = ContainerElement;
@@ -0,0 +1,51 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { BoxConstraints, Offset, Size } from "../../layout/box-constraints.js";
3
+ import { LayoutContext, SizedPDFElement } from "../pdf-element.js";
4
+ import type { CheckboxSpec, FieldStyle } from "../../forms/field.js";
5
+ export interface CheckboxParams {
6
+ /** The field name (/T) - unique in the document. */
7
+ name: string;
8
+ /** Whether it starts checked. */
9
+ checked?: boolean;
10
+ /** The "on" export value (default `"Yes"`). */
11
+ onValue?: string;
12
+ /** Tooltip / accessible name. */
13
+ tooltip?: string;
14
+ /** Show but do not allow toggling. */
15
+ readOnly?: boolean;
16
+ /** Mark it as a field that must be filled in before submitting. */
17
+ required?: boolean;
18
+ /** Hide the widget entirely - neither on screen nor in print. */
19
+ hidden?: boolean;
20
+ /** Include the widget when printing (default true). */
21
+ print?: boolean;
22
+ /** Box side length in points (default 14). */
23
+ size?: number;
24
+ /** Checkmark colour (default black). */
25
+ color?: Color;
26
+ /** Box border colour (default a mid grey, so the box is visible). */
27
+ border?: Color;
28
+ /** Box background fill. */
29
+ background?: Color;
30
+ /** Border thickness (default 1). */
31
+ borderWidth?: number;
32
+ }
33
+ /**
34
+ * A checkbox (AcroForm /Btn). A small square leaf that draws nothing itself: it reserves its box and
35
+ * emits a `formfield` IR node whose checkbox spec bakes an /AP checkmark at the seam.
36
+ */
37
+ export declare class CheckboxElement extends SizedPDFElement {
38
+ private spec;
39
+ private style;
40
+ private size;
41
+ constructor(p: CheckboxParams);
42
+ calculateLayout(_constraints: BoxConstraints, offset: Offset, _ctx: LayoutContext): Size;
43
+ getProps(): {
44
+ x: number;
45
+ y: number;
46
+ width: number;
47
+ height: number;
48
+ spec: CheckboxSpec;
49
+ style: FieldStyle;
50
+ };
51
+ }
@@ -0,0 +1,48 @@
1
+ import { Color } from "../../common/color.js";
2
+ import { SizedPDFElement } from "../pdf-element.js";
3
+ /**
4
+ * A checkbox (AcroForm /Btn). A small square leaf that draws nothing itself: it reserves its box and
5
+ * emits a `formfield` IR node whose checkbox spec bakes an /AP checkmark at the seam.
6
+ */
7
+ export class CheckboxElement extends SizedPDFElement {
8
+ constructor(p) {
9
+ const size = p.size ?? 14;
10
+ super({ x: 0, y: 0, width: size, height: size });
11
+ this.size = size;
12
+ this.spec = {
13
+ kind: "checkbox",
14
+ name: p.name,
15
+ checked: p.checked,
16
+ onValue: p.onValue,
17
+ tooltip: p.tooltip,
18
+ readOnly: p.readOnly,
19
+ required: p.required,
20
+ hidden: p.hidden,
21
+ print: p.print,
22
+ };
23
+ this.style = {
24
+ border: p.border ?? new Color(102, 102, 102),
25
+ background: p.background,
26
+ color: p.color ?? new Color(0, 0, 0),
27
+ fontSize: 0,
28
+ borderWidth: p.borderWidth ?? 1,
29
+ };
30
+ }
31
+ calculateLayout(_constraints, offset, _ctx) {
32
+ this.x = offset.x;
33
+ this.y = offset.y;
34
+ this.width = this.size;
35
+ this.height = this.size;
36
+ return { width: this.size, height: this.size };
37
+ }
38
+ getProps() {
39
+ return {
40
+ x: this.x,
41
+ y: this.y,
42
+ width: this.width ?? 0,
43
+ height: this.height ?? 0,
44
+ spec: this.spec,
45
+ style: this.style,
46
+ };
47
+ }
48
+ }