Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| export { } |
| export { } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| Copyright 2015-2021 Google LLC. All Rights Reserved. | ||
| This Font Software is licensed under the SIL Open Font License, Version 1.1. | ||
| This license is copied below, and is also available with a FAQ at: | ||
| https://openfontlicense.org | ||
| ----------------------------------------------------------- | ||
| SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 | ||
| ----------------------------------------------------------- | ||
| PREAMBLE | ||
| The goals of the Open Font License (OFL) are to stimulate worldwide | ||
| development of collaborative font projects, to support the font creation | ||
| efforts of academic and linguistic communities, and to provide a free and | ||
| open framework in which fonts may be shared and improved in partnership | ||
| with others. | ||
| The OFL allows the licensed fonts to be used, studied, modified and | ||
| redistributed freely as long as they are not sold by themselves. The | ||
| fonts, including any derivative works, can be bundled, embedded, | ||
| redistributed and/or sold with any software provided that any reserved | ||
| names are not used by derivative works. The fonts and derivatives, | ||
| however, cannot be released under any other type of license. The | ||
| requirement for fonts to remain under this license does not apply | ||
| to any document created using the fonts or their derivatives. | ||
| DEFINITIONS | ||
| "Font Software" refers to the set of files released by the Copyright | ||
| Holder(s) under this license and clearly marked as such. This may | ||
| include source files, build scripts and documentation. | ||
| "Reserved Font Name" refers to any names specified as such after the | ||
| copyright statement(s). | ||
| "Original Version" refers to the collection of Font Software components as | ||
| distributed by the Copyright Holder(s). | ||
| "Modified Version" refers to any derivative made by adding to, deleting, | ||
| or substituting -- in part or in whole -- any of the components of the | ||
| Original Version, by changing formats or by porting the Font Software to a | ||
| new environment. | ||
| "Author" refers to any designer, engineer, programmer, technical | ||
| writer or other person who contributed to the Font Software. | ||
| PERMISSION & CONDITIONS | ||
| Permission is hereby granted, free of charge, to any person obtaining | ||
| a copy of the Font Software, to use, study, copy, merge, embed, modify, | ||
| redistribute, and sell modified and unmodified copies of the Font | ||
| Software, subject to the following conditions: | ||
| 1) Neither the Font Software nor any of its individual components, | ||
| in Original or Modified Versions, may be sold by itself. | ||
| 2) Original or Modified Versions of the Font Software may be bundled, | ||
| redistributed and/or sold with any software, provided that each copy | ||
| contains the above copyright notice and this license. These can be | ||
| included either as stand-alone text files, human-readable headers or | ||
| in the appropriate machine-readable metadata fields within text or | ||
| binary files as long as those fields can be easily viewed by the user. | ||
| 3) No Modified Version of the Font Software may use the Reserved Font | ||
| Name(s) unless explicit written permission is granted by the corresponding | ||
| Copyright Holder. This restriction only applies to the primary font name as | ||
| presented to the users. | ||
| 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font | ||
| Software shall not be used to promote, endorse or advertise any | ||
| Modified Version, except to acknowledge the contribution(s) of the | ||
| Copyright Holder(s) and the Author(s) or with their explicit written | ||
| permission. | ||
| 5) The Font Software, modified or unmodified, in part or in whole, | ||
| must be distributed entirely under this license, and must not be | ||
| distributed under any other license. The requirement for fonts to | ||
| remain under this license does not apply to any document created | ||
| using the Font Software. | ||
| TERMINATION | ||
| This license becomes null and void if any of the above conditions are | ||
| not met. | ||
| DISCLAIMER | ||
| THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
| EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF | ||
| MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT | ||
| OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE | ||
| COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | ||
| INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL | ||
| DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING | ||
| FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM | ||
| OTHER DEALINGS IN THE FONT SOFTWARE. |
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
| /** | ||
| * PRE-BUILT FONT DATA — NotoSansArmenian-Regular | ||
| * =================================== | ||
| * Generated by: scripts/build-font-data.cjs | ||
| * Source: NotoSansArmenian-Regular.ttf | ||
| * License: SIL Open Font License 1.1 | ||
| * | ||
| * DO NOT EDIT — Regenerate with: | ||
| * node scripts/build-font-data.cjs assets/fonts/NotoSansArmenian-Regular.ttf assets/fonts/notosansarmenian-regular-data.js | ||
| */ | ||
| // Font metrics | ||
| export const metrics = {"unitsPerEm":1000,"ascent":1068,"descent":-292,"capHeight":714,"stemV":48,"bbox":[-141,-245,1440,829],"defaultWidth":260,"numGlyphs":108}; | ||
| // Font name for PDF /BaseFont | ||
| export const fontName = 'NotoSansArmenian-Regular'; | ||
| // Unicode codepoint → Glyph ID mapping (sparse object, ~O(1) lookup) | ||
| export const cmap = {0:2,13:1,32:6,45:4,160:7,776:3,1329:8,1330:9,1331:10,1332:11,1333:12,1334:13,1335:14,1336:15,1337:16,1338:17,1339:18,1340:19,1341:20,1342:21,1343:22,1344:23,1345:24,1346:25,1347:26,1348:27,1349:28,1350:29,1351:30,1352:31,1353:33,1354:34,1355:35,1356:36,1357:37,1358:38,1359:39,1360:40,1361:41,1362:42,1363:43,1364:44,1365:45,1366:46,1369:47,1370:48,1371:49,1372:50,1373:51,1374:52,1375:53,1376:54,1377:55,1378:56,1379:57,1380:58,1381:59,1382:60,1383:61,1384:62,1385:63,1386:64,1387:65,1388:66,1389:67,1390:68,1391:69,1392:70,1393:71,1394:72,1395:73,1396:74,1397:75,1398:76,1399:77,1400:78,1401:80,1402:81,1403:82,1404:83,1405:84,1406:85,1407:86,1408:87,1409:88,1410:89,1411:90,1412:91,1413:92,1414:93,1415:94,1416:95,1417:96,1418:97,1421:98,1422:99,1423:100,8208:5,8217:107,9676:101,64275:102,64276:103,64277:104,64278:105,64279:106}; | ||
| // Glyph ID → Advance Width (only non-default widths; default = 260) | ||
| export const defaultWidth = 260; | ||
| export const widths = {0:600,2:0,3:0,4:322,5:322,8:831,9:696,10:758,11:764,12:696,13:670,14:629,15:696,16:811,17:765,18:702,19:412,20:819,21:711,22:709,23:575,24:649,25:764,26:688,27:804,28:627,29:628,30:654,31:725,32:1316,33:634,34:793,35:642,36:805,37:725,38:787,39:571,40:673,41:668,42:598,43:800,44:617,45:776,46:699,47:211,48:211,49:211,50:291,51:211,52:442,53:415,54:920,55:920,56:597,57:639,58:641,59:597,60:600,61:474,62:597,63:668,64:609,65:597,66:265,67:920,68:589,69:597,70:562,71:599,72:599,73:576,74:602,75:265,76:599,77:414,78:597,79:905,80:344,81:920,82:517,83:619,84:597,85:598,86:931,87:597,88:599,89:321,90:931,91:597,92:584,93:640,94:630,95:280,96:254,97:441,98:817,99:817,100:732,101:594,102:1195,103:1218,104:1215,105:1187,106:1527,107:211}; | ||
| // GSUB SingleSubst: fromGid → substituteGid | ||
| // Used by the Thai mini-shaper to select below-clash variants of consonants. | ||
| export const gsub = {}; | ||
| // GPOS MarkToBase anchors — used by the Thai mini-shaper for mark positioning. | ||
| // marks[gid] = [classIdx, anchorX, anchorY] (design units) | ||
| // bases[gid] = { classIdx: [anchorX, anchorY] } | ||
| export const markAnchors = { | ||
| marks: {3:[0,-1,536]}, | ||
| bases: {8:{0:[363,714]},31:{0:[362,714]},45:{0:[388,714]},54:{0:[460,492]},55:{0:[460,492]},78:{0:[298,492]},92:{0:[292,492]},101:{0:[297,540]}} | ||
| }; | ||
| // GPOS MarkToMark anchors — used for Thai vowel+tone stacking. | ||
| // mark1Anchors[mark1Gid] = { classIdx: [anchorX, anchorY] } (base mark, e.g. above vowel) | ||
| // mark2Classes[mark2Gid] = [classIdx, anchorX, anchorY] (combining mark, e.g. tone) | ||
| export const mark2mark = { | ||
| mark1Anchors: {}, | ||
| mark2Classes: {} | ||
| }; | ||
| // PDF /W array string (pre-formatted for CIDFont object) | ||
| export const pdfWidthArray = '0 [600] 2 [0 0 322 322] 8 [831 696 758 764 696 670 629 696 811 765 702 412 819 711 709 575 649 764 688 804 627 628 654 725 1316 634 793 642 805 725 787 571 673 668 598 800 617 776 699 211 211 211 291 211 442 415 920 920 597 639 641 597 600 474 597 668 609 597 265 920 589 597 562 599 599 576 602 265 599 414 597 905 344 920 517 619 597 598 931 597 599 321 931 597 584 640 630 280 254 441 817 817 732 594 1195 1218 1215 1187 1527 211]'; | ||
| // Raw TTF binary as base64 (for PDF FontFile2 embedding) | ||
| export const ttfBase64 = 'AAEAAAARAQAABAAQR0RFRgIiAhwAAAEcAAAATEdQT1MfuGG/AAABaAAAEVZHU1VCIgcZVAAAEsAAAACYT1MvMoT3EBIAABNYAAAAYGNtYXBVpi4EAAATuAAAAKRjdnQgBIYMMAAAaTQAAAA2ZnBnbWIu+3sAAGlsAAAODGdhc3AAAAAQAABpLAAAAAhnbHlmtqfrVQAAFFwAAEhQaGVhZB9hukYAAFysAAAANmhoZWEJQgS0AABc5AAAACRobXR4CLYaKgAAXQgAAAGwbG9jYR09CuIAAF64AAAA2m1heHACMA+FAABflAAAACBuYW1lggGnXgAAX7QAAAUycG9zdEl80NAAAGToAAAEQnByZXCqR8mcAAB3eAAAAKcAAQAAAAwAAAAAAAAAAgAKAAMAAwADAAgACAABAB8AHwABACAAIAACAC0ALQABADYANwABAE4ATgABAE8ATwACAFwAXAABAGUAZQABAAEAAAAKACYAGAACREZMVAAwYXJtbgA0AAYAKgAyAEoAVAA6AEIAA2tlcm4AUmtlcm4AWG1hcmsAYABUAAAAWgAAAAQAAAABAGoABQAAAAEAbgABAAAAAQByAAEAAAABAEgAAgAIAAIArgDkAAgAAAADAGgAfACQAAAAAQADAAAAAgACAAMAAAACAAAAAQAA//8AAgAAAAIAAP//AAIAAQACAAEMJgAFAKoAqgABDA4MUgABDAIMLgABDAIMCAABC/YL/AACDA4ABQACAMgAyAEsASwAAwACC+YMGAABDB4AAAABAAAABAADAAIL0gwqAAEL4AAAAAEAAAAFAAMAAgu+DBYAAQv8AAAAAQAAAAQAAQ0aAAQAAAAWDUoMZAwQDAoMiAwsDCwMEAzSDBYMIgx2DPQMOgxIDZQMtAxWDFYMngwcDFYAAg4kAAQAAA5GDwwAIQAsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkAAAAAAAAAAAAA/5z/xP+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ywAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+Rv7d/y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/x/+f/5wAAAAAAAAAAAB0AGQAAAAAAGAAAAEsAIwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAP7b/t3/LQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+Sf7d/y0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/ygAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+c/5z/nAAAAAAAAAAAAAAAAP/sAAAAAAAAAAAAAAAAAAD/8f/jAAAAAP/2/+MAAAAAAAAAAAAAAAAAAAAA/9MAAAAAAAAAAAAAAAAAAAAAAAAAAP/nAAAAAAAAAAAAAAAAAAAAAAAA//YAAAAAAAAAAP/sAAD/9gAAAAAAAAAA/90AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/9QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAA/+j/4wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MAAAAAAAAAAAAAAAD/8QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//EAAP7b/t3/LQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/88AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7AAAAAAAAP/oAAAAAAAAAAAAAAAAAAAAAP/sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/94AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/9gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/3QAAAAAAAAAAAAAAAP/qAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/8QAAAAAAAAAAAAAAAAAA//YAAAAAAAAAAAAA/+L/sv/iAAD/2P/sAAD/8f+yAAD/zgAA/7L/sv+y/9n/7P/2AAAAAAAAAAAAAAAdAAAAAP/j/+P/8QAA//H/6P/xAAAAAP/q/+z/6gAAAAD/w//v/+IAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//EAAAAAAAAAAAAA/8n/zv/Y/9MAAP/1/9P/3f/Y/+IAAAAAAAD/2wAA/+z/2P/d/97/yQAAAAAAAAAAAAD/5//x/84AAAAA/9kAAP/tAAAAAAAA/+wAAP/2//H/0//s/94AAAAAAAAAAAAAAAAAAAAA//EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//YAAAAAAAD/6gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/xAAAAAP/2//QAAAAAAAAAAAAAAAAAAAAAAAD/yAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAP/sAAD/8QAAAAD/8QAA/+z/9P/YAAAAAAAA/9kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//YAAAAAAAAAAP/mAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/2AAAAAAAAAAD/5wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/4wAAAAAAAAAAAAD/8QAAAAAAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/7//x//YAAAAAAAAAAAAAAAAAAAAA/+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/n/+8AAP/2AAAAAAAA//oAAAAAAAD/6gAAAAAAAP/tAAEAAAR4AAIEagRuAAEAAQADAAEAAgAgAE8AAQADADEAMgAzAAEAAgAyADQACARSBHAEWAReBF4EdgRkBGoAAQABAFwAAQABADIAAQABADQAAQAIAAgAHwAtADYANwBOAFwAZQABAAIATgBcAAEAIv/xAAEAIv/iAAEAF//jAAEAXf/sAAIAF//TACL/4gADAET/7ABY/+wAXP/sAAMAFv/sAEf/7ABM/+wAAwAK/8UAFv/BACb/wQADADH/LQAy/t0ANP5GAAQAC//vABn/7wBQ/+wAZP/vAAQACv/DABb/wQAm/8EAUv/sAAUAFf/1AB3/+QAi/+IATP/sAFL/9gAFAEsAFwBNADwAUABhAFIAHgBbABMABwAK/+oAC//xABb/8QAZ//EAHf/0AEf/8QBk//EACAAK/8oAFf/0ABb/vgAh/+cAIv/OACb/vgAr/9gALf/iAAkAEv/sABT/7AAX/+MAKv/iADj/sgA6/7IAPv+yAEL/sgBI/7IAAQAWAAgADAANAA4ADwAQABEAEgATABQAFwAZABsAJAAmACgALAA+AEEASABSAFwAEgAI//EACv++AAv/qwAM//EAFf/eABb/uQAZ/6sAG//xAB3/5wAe/+EAIf/lACL/uAAl//EAJv/YACv/4wAt/+MATf/xAGT/qwAxABH/wQAW/+wAHv/dACn/8QA2/84AN//OADj/zgA5/7oAOv/OADv/2AA8/7oAPf/YAD7/zgA//84AQP+6AEH/4gBC/84AQ//iAET/ugBF/84ARv/YAEf/ugBI/84ASv/OAEv/zgBM/7oATf/iAE7/zgBP/84AUP/EAFH/zgBS/7oAU//OAFT/zgBV/84AVv/OAFf/zgBY/7oAWf/OAFr/zgBb/84AXP+6AF7/2ABf/84AZv/OAGf/zgBo/84Aaf/OAGr/zgACAAUACQAnAAAAKQAxAB8AMwBhACgAZABkAFcAZgBqAFgAAQAJAGAACAABAAEAAAAgAB8ACAAeAB0AHAABABsAGgAAABkAGAABABcAFgAVABQAEwAAAAcAEgAAABEAAQAAAAEAEAAAAA8ABwAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAABgAGAAUAAwANAAAADAAAAAIAAwAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAgAEAAsAAAAAAAoAAAADAAAAAgAAAAQAAgAAAAAACQAEAAAAAAAAAAAAAAABAAAAAAAFAAIAAQAIAGMABgAFACsACAAGACoAAAAFAAUAKQAAAAAAAAAoACcAAAAmAAgAJQAGACQAIwAiAAUAAAAhAAsAIAAFAAYAHwAeAAsAHQAAABwAGwAaABkAGAAAABcAFgAAABUAAAABAAEAAwACAAQABwACAAcAAwADAAIACgADAAoAAgABABQAEwAEABIAAQAJABEAEAAEAAMADwABAA4ABAABAAEAAQADAAIABAABAA0AAgAMAAcACQAAAAAAAAAAAAgAAAABAAEAAQABAAEAAQAsAAEALgAB//8CGAABAWsCygABAYQCygABAcwB7AABASQB7AABASkCHAABAWoCygABASoB7AAAAAEAAAAWAA4ACgABABoAAWxpZ2EAHgACREZMVAAcYXJtbgAcAAQAAAABABIAAAABAAAAEgAAAAEALAAEABYAIgAaAB4AAP//AAEAAAABACIAAQAkAAEAJgAEACgALgA0ADoAAQAEAB8ASgBOAFUAIAACACoATwACAFkAaQACAEwAZwACADsAaAACAEEAagACAEMAZgACAEwABAJ/AZAABQAAAooCWAAAAEsCigJYAAABXgAyAUIAAAAAAAAAAAAAAACAAARDQAAgAAAAAAAAAAAAR09PRwDAAAD7FwQs/twAAAQsASQAAAABAAAAAAIYAsoAAAAgAAIAAAACAAAAAwAAABQAAwABAAAAFAAEAJAAAAAgACAABAAAAAAADQAgAC0AoAMIBUgFVgV4BYoFjyAQIBklzPsX//8AAAAAAA0AIAAtAKADCAUxBUkFWQV5BY0gECAZJcz7E///AAL/9P/m/9f/Z/z7+tf62PrW+tf61d/14FLamQVTAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAF4AAAH5AsoAAwAHACpAJwAAAAMCAANnAAIBAQJXAAICAV8EAQECAU8AAAcGBQQAAwADEQUGFyszESERJSERIV4Bm/6YATX+ywLK/TYzAmQAAAAC/3MCdwCMAtoACwAXACWxBmREQBoCAQABAQBZAgEAAAFhAwEBAAFRJCQkIgQGGiuxBgBEAzQ2MzIWFRQGIyImNzQ2MzIWFRQGIyImjRwTExwcExMcvBsTExwcExMbAqkaFxcaGRkZ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AGcAbABlACAASQBuAGMALgBNAG8AbgBvAHQAeQBwAGUAIABJAG0AYQBnAGkAbgBnACAASQBuAGMALgBNAG8AbgBvAHQAeQBwAGUAIABEAGUAcwBpAGcAbgAgAFQAZQBhAG0ARABlAHMAaQBnAG4AZQBkACAAYgB5ACAATQBvAG4AbwB0AHkAcABlACAAZABlAHMAaQBnAG4AIAB0AGUAYQBtAC4AaAB0AHQAcAA6AC8ALwB3AHcAdwAuAGcAbwBvAGcAbABlAC4AYwBvAG0ALwBnAGUAdAAvAG4AbwB0AG8ALwBoAHQAdABwADoALwAvAHcAdwB3AC4AbQBvAG4AbwB0AHkAcABlAC4AYwBvAG0ALwBzAHQAdQBkAGkAbwBUAGgAaQBzACAARgBvAG4AdAAgAFMAbwBmAHQAdwBhAHIAZQAgAGkAcwAgAGwAaQBjAGUAbgBzAGUAZAAgAHUAbgBkAGUAcgAgAHQAaABlACAAUwBJAEwAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUALAAgAFYAZQByAHMAaQBvAG4AIAAxAC4AMQAuACAAVABoAGkAcwAgAGwAaQBjAGUAbgBzAGUAIABpAHMAIABhAHYAYQBpAGwAYQBiAGwAZQAgAHcAaQB0AGgAIABhACAARgBBAFEAIABhAHQAOgAgAGgAdAB0AHAAcwA6AC8ALwBzAGMAcgBpAHAAdABzAC4AcwBpAGwALgBvAHIAZwAvAE8ARgBMAGgAdAB0AHAAcwA6AC8ALwBzAGMAcgBpAHAAdABzAC4AcwBpAGwALgBvAHIAZwAvAE8ARgBMAAAAAgAAAAAAAP+cADIAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAABAgEDAQQAEAEFAAMBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQEiASMBJAElASYBJwEoASkBKgErASwBLQEuAS8BMAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQFqAkNSBE5VTEwHdW5pMDMwOAd1bmkyMDEwB3VuaTAwQTAHdW5pMDUzMQd1bmkwNTMyB3VuaTA1MzMHdW5pMDUzNAd1bmkwNTM1B3VuaTA1MzYHdW5pMDUzNwd1bmkwNTM4B3VuaTA1MzkHdW5pMDUzQQd1bmkwNTNCB3VuaTA1M0MHdW5pMDUzRAd1bmkwNTNFB3VuaTA1M0YHdW5pMDU0MAd1bmkwNTQxB3VuaTA1NDIHdW5pMDU0Mwd1bmkwNTQ0B3VuaTA1NDUHdW5pMDU0Ngd1bmkwNTQ3B3VuaTA1NDgLdW5pMDU0ODA1NTIHdW5pMDU0OQd1bmkwNTRBB3VuaTA1NEIHdW5pMDU0Qwd1bmkwNTREB3VuaTA1NEUHdW5pMDU0Rgd1bmkwNTUwB3VuaTA1NTEHdW5pMDU1Mgd1bmkwNTUzB3VuaTA1NTQHdW5pMDU1NQd1bmkwNTU2B3VuaTA1NTkHdW5pMDU1QQd1bmkwNTVCB3VuaTA1NUMHdW5pMDU1RAd1bmkwNTVFB3VuaTA1NUYHdW5pMDU2MAd1bmkwNTYxB3VuaTA1NjIHdW5pMDU2Mwd1bmkwNTY0B3VuaTA1NjUHdW5pMDU2Ngd1bmkwNTY3B3VuaTA1NjgHdW5pMDU2OQd1bmkwNTZBB3VuaTA1NkIHdW5pMDU2Qwd1bmkwNTZEB3VuaTA1NkUHdW5pMDU2Rgd1bmkwNTcwB3VuaTA1NzEHdW5pMDU3Mgd1bmkwNTczB3VuaTA1NzQHdW5pMDU3NQd1bmkwNTc2B3VuaTA1NzcHdW5pMDU3OAt1bmkwNTc4MDU4Mgd1bmkwNTc5B3VuaTA1N0EHdW5pMDU3Qgd1bmkwNTdDB3VuaTA1N0QHdW5pMDU3RQd1bmkwNTdGB3VuaTA1ODAHdW5pMDU4MQd1bmkwNTgyB3VuaTA1ODMHdW5pMDU4NAd1bmkwNTg1B3VuaTA1ODYHdW5pMDU4Nwd1bmkwNTg4B3VuaTA1ODkHdW5pMDU4QQd1bmkwNThEB3VuaTA1OEUHdW5pMDU4Rgd1bmkyNUNDB3VuaUZCMTMHdW5pRkIxNAd1bmlGQjE1B3VuaUZCMTYHdW5pRkIxNwd1bmkyMDE5AAAAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAAAAAAFkAWQBJAEkB7ALKAAAC+AHsAAD/FALU//YDAgH2//b/CwAAsAAsILAAVVhFWSAg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'; | ||
| // Utility: get glyph width | ||
| export function getGlyphWidth(glyphId) { | ||
| return widths[glyphId] !== undefined ? widths[glyphId] : 260; | ||
| } | ||
| // Utility: get glyph ID for unicode code point | ||
| export function getGlyphId(codePoint) { | ||
| return cmap[codePoint] || 0; | ||
| } |
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; |
| /** | ||
| * PRE-BUILT FONT DATA — NotoSansHebrew-Regular | ||
| * =================================== | ||
| * Generated by: scripts/build-font-data.cjs | ||
| * Source: NotoSansHebrew-Regular.ttf | ||
| * License: SIL Open Font License 1.1 | ||
| * | ||
| * DO NOT EDIT — Regenerate with: | ||
| * node scripts/build-font-data.cjs assets/fonts/NotoSansHebrew-Regular.ttf assets/fonts/notosanshebrew-regular-data.js | ||
| */ | ||
| // Font metrics | ||
| export const metrics = {"unitsPerEm":1000,"ascent":1068,"descent":-292,"capHeight":714,"stemV":48,"bbox":[-210,-292,814,1068],"defaultWidth":270,"numGlyphs":151}; | ||
| // Font name for PDF /BaseFont | ||
| export const fontName = 'NotoSansHebrew-Regular'; | ||
| // Unicode codepoint → Glyph ID mapping (sparse object, ~O(1) lookup) | ||
| export const cmap = {0:2,13:1,32:106,45:48,160:66,847:149,1425:20,1426:92,1427:93,1428:144,1429:143,1430:114,1431:88,1432:145,1433:72,1434:137,1435:113,1436:30,1437:31,1438:33,1439:81,1440:109,1441:74,1442:9,1443:65,1444:58,1445:64,1446:63,1447:18,1448:78,1449:110,1450:136,1451:70,1452:50,1453:19,1454:148,1455:60,1456:95,1457:40,1458:36,1459:38,1460:45,1461:117,1462:91,1463:73,1464:79,1465:46,1466:47,1467:84,1468:15,1469:103,1470:59,1471:85,1472:71,1473:100,1474:104,1475:105,1476:123,1477:57,1478:69,1479:80,1488:3,1489:12,1490:34,1491:16,1492:42,1493:124,1494:146,1495:44,1496:111,1497:138,1498:21,1499:52,1500:55,1501:23,1502:61,1503:24,1504:67,1505:89,1506:10,1507:25,1508:75,1509:27,1510:115,1511:82,1512:86,1513:96,1514:107,1519:150,1520:127,1521:128,1522:141,1523:29,1524:32,8204:118,8205:119,8206:120,8207:121,8208:49,8362:94,9676:122,64285:140,64286:51,64287:142,64288:11,64289:129,64290:130,64291:131,64292:132,64293:133,64294:28,64295:134,64296:135,64297:8,64298:101,64299:102,64300:98,64301:99,64302:6,64303:7,64304:4,64305:13,64306:35,64307:17,64308:43,64309:125,64310:147,64312:112,64313:139,64314:22,64315:53,64316:56,64318:62,64320:68,64321:90,64323:26,64324:76,64326:116,64327:83,64328:87,64329:97,64330:108,64331:126,64332:14,64333:54,64334:77,64335:5}; | ||
| // Glyph ID → Advance Width (only non-default widths; default = 270) | ||
| export const defaultWidth = 270; | ||
| export const widths = {0:500,1:260,2:0,3:632,4:632,5:643,6:632,7:632,8:569,9:0,10:593,11:582,12:572,13:572,14:565,15:0,16:542,17:542,18:0,19:0,20:0,21:545,22:545,23:684,24:329,25:620,26:620,27:529,28:794,29:186,30:0,31:0,32:365,33:0,34:374,35:374,36:0,37:0,38:0,39:0,40:0,41:0,42:627,43:627,44:662,45:0,46:0,47:0,48:321,49:321,50:0,51:0,52:515,53:515,54:515,55:522,56:522,57:0,58:0,59:324,60:0,61:660,62:660,63:0,64:0,65:0,66:266,67:379,68:379,69:397,70:0,71:525,72:0,73:0,74:0,75:601,76:601,77:601,78:0,79:0,80:0,81:0,82:627,83:627,84:0,85:0,86:523,87:523,88:0,89:643,90:643,91:0,92:0,93:0,94:792,95:0,96:730,97:730,98:730,99:730,100:0,101:730,102:730,103:0,104:0,105:246,107:685,108:685,109:0,110:0,111:634,112:634,113:0,114:0,115:556,116:556,117:0,118:0,119:0,120:0,121:0,122:594,123:0,124:301,125:301,126:301,127:589,128:602,129:761,130:674,131:799,132:679,133:644,134:678,135:844,136:0,137:0,138:295,139:295,140:306,141:599,142:612,143:0,144:0,145:0,146:330,147:330,148:0,149:0,150:633}; | ||
| // GSUB SingleSubst: fromGid → substituteGid | ||
| // Used by the Thai mini-shaper to select below-clash variants of consonants. | ||
| export const gsub = {}; | ||
| // GPOS MarkToBase anchors — used by the Thai mini-shaper for mark positioning. | ||
| // marks[gid] = [classIdx, anchorX, anchorY] (design units) | ||
| // bases[gid] = { classIdx: [anchorX, anchorY] } | ||
| export const markAnchors = { | ||
| marks: {9:[0,127,0],15:[0,82,300],18:[0,176,0],19:[0,70,0],20:[0,126,0],30:[0,70,537],31:[0,134,537],33:[0,137,537],36:[0,189,0],37:[0,227,0],38:[0,189,0],39:[0,229,0],40:[0,187,0],41:[0,228,0],45:[0,83,0],46:[0,83,537],47:[0,83,537],50:[0,128,537],51:[0,160,537],57:[0,83,0],58:[0,113,0],60:[0,124,537],63:[0,224,0],64:[0,180,0],65:[0,196,0],70:[0,113,537],72:[0,180,537],73:[0,140,0],74:[0,122,537],78:[0,161,537],79:[0,140,0],80:[0,115,0],81:[0,235,537],84:[0,159,0],85:[0,150,537],88:[0,146,537],91:[0,137,0],92:[0,161,537],93:[0,107,537],95:[0,82,0],100:[0,83,537],103:[0,72,0],104:[0,83,537],109:[0,136,537],110:[0,131,537],113:[0,168,0],114:[0,70,0],117:[0,134,0],123:[0,83,537],136:[0,127,0],137:[0,113,0],143:[0,118,537],144:[0,83,537],145:[0,160,537],148:[0,160,537]}, | ||
| bases: {3:{0:[65,537]},4:{0:[65,537]},6:{0:[65,537]},7:{0:[65,537]},10:{0:[38,537]},11:{0:[53,537]},12:{0:[33,537]},13:{0:[33,537]},14:{0:[33,537]},16:{0:[-1,537]},17:{0:[-1,537]},21:{0:[4,537]},22:{0:[4,537]},23:{0:[89,537]},24:{0:[52,537]},25:{0:[15,537]},26:{0:[15,537]},27:{0:[4,537]},34:{0:[56,537]},35:{0:[56,537]},42:{0:[41,537]},43:{0:[41,537]},44:{0:[78,537]},52:{0:[3,537]},53:{0:[3,537]},54:{0:[3,537]},55:{0:[-48,537]},56:{0:[-48,537]},61:{0:[79,537]},62:{0:[79,537]},67:{0:[53,537]},68:{0:[53,537]},75:{0:[52,537]},76:{0:[52,537]},77:{0:[52,537]},82:{0:[53,537]},83:{0:[53,537]},86:{0:[-17,537]},87:{0:[-17,537]},89:{0:[11,537]},90:{0:[11,537]},96:{0:[622,537]},97:{0:[622,537]},98:{0:[622,537]},99:{0:[622,537]},101:{0:[622,537]},102:{0:[622,537]},107:{0:[77,537]},108:{0:[77,537]},111:{0:[53,537]},112:{0:[53,537]},115:{0:[15,537]},116:{0:[15,537]},122:{0:[546,714]},124:{0:[175,537]},125:{0:[175,537]},126:{0:[175,537]},127:{0:[106,537]},138:{0:[131,537]},139:{0:[131,537]},140:{0:[131,537]},141:{0:[152,537]},146:{0:[35,537]},147:{0:[35,537]}} | ||
| }; | ||
| // GPOS MarkToMark anchors — used for Thai vowel+tone stacking. | ||
| // mark1Anchors[mark1Gid] = { classIdx: [anchorX, anchorY] } (base mark, e.g. above vowel) | ||
| // mark2Classes[mark2Gid] = [classIdx, anchorX, anchorY] (combining mark, e.g. tone) | ||
| export const mark2mark = { | ||
| mark1Anchors: {}, | ||
| mark2Classes: {} | ||
| }; | ||
| // PDF /W array string (pre-formatted for CIDFont object) | ||
| export const pdfWidthArray = '0 [500 260 0 632 632 643 632 632 569 0 593 582 572 572 565 0 542 542 0 0 0 545 545 684 329 620 620 529 794 186 0 0 365 0 374 374 0 0 0 0 0 0 627 627 662 0 0 0 321 321 0 0 515 515 515 522 522 0 0 324 0 660 660 0 0 0 266 379 379 397 0 525 0 0 0 601 601 601 0 0 0 0 627 627 0 0 523 523 0 643 643 0 0 0 792 0 730 730 730 730 0 730 730 0 0 246] 107 [685 685 0 0 634 634 0 0 556 556 0 0 0 0 0 594 0 301 301 301 589 602 761 674 799 679 644 678 844 0 0 295 295 306 599 612 0 0 0 330 330 0 0 633]'; | ||
| // Raw TTF binary as base64 (for PDF FontFile2 embedding) | ||
| export const ttfBase64 = 'AAEAAAARAQAABAAQR0RFRgaVAUgAAAEcAAABZkdQT1NaQhnvAAAChAAADhpHU1VCh6R+9QAAEKAAAADMT1MvMojYD+4AABFsAAAAYGNtYXB+uS+yAAARzAAAAdBjdnQgAjECOgAAWhAAAAAoZnBnbWIu+3sAAFo4AAAODGdhc3AAAAAQAABaCAAAAAhnbHlmlrHXsQAAE5wAADYeaGVhZCHk5P8AAEm8AAAANmhoZWEGiwUDAABJ9AAAACRobXR4wBEY6QAAShgAAAJcbG9jYQV4ElsAAEx0AAABMG1heHACJw89AABNpAAAACBuYW1lleu9UQAATcQAAAY+cG9zdHEiQSsAAFQEAAAGBHByZXBlQK6cAABoRAAAAKcAAQAAAAwAAAE6AAAAAQADAJQAAQABAAIAAQABAAAAAwABAAEAAQABAAEAAwABAAEAAwADAAMAAQABAAEAAQABAAEAAQAAAAAAAwADAAAAAwABAAEAAwADAAMAAwADAAMAAQABAAEAAwADAAMAAAAAAAMAAwABAAEAAQABAAEAAwADAAAAAwABAAEAAwADAAMAAAABAAEAAAADAAAAAwADAAMAAQABAAEAAwADAAMAAwABAAEAAwADAAEAAQADAAEAAQADAAMAAwAAAAMAAQABAAEAAQADAAEAAQADAAMAAAAAAAEAAQADAAMAAQABAAMAAwABAAEAAwAAAAAAAAAAAAEAAwABAAEAAQABAAEAAAAAAAAAAAAAAAAAAAADAAMAAQABAAEAAQABAAMAAwADAAEAAQADAAMAAQAKAAMAFAAcACQAAQADAAUAfwCAAAEABAABAUIAAQAEAAEBJwABAAQAAQEtAAAAAQAAAAoAGAAmAAJERkxUACxoZWJyACwAAmtlcm4AIm1hcmsAYAAHABoAIgAqADIAOgBCAEoAUgAAAAAAAQAAAAIACAABAJoABAAAAAEASgAEAAAAAQBOAAQAAAABAFIABAAAAAEAVgAEAAAAAQBaAAQAAAABAF4AAAAGAAEAAgADAAQABQAGAAD//wACAAAAAQABAOYF4gABAbIFZAABALwI3gABAVwG1gABAGAHTAABAGYGSgABAOQIxgABAcwHwgABAIIIugABAPwIOAABAEgAZAABAE4AVAABApwABQAAABMCKgESAeoElALGAlAD2gIKAVYC8gQYAx4DSgRW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FK4U0hUKFRwVWhV0FaAVzBX6FuwXFhc0F0AXTBdYF2QXshfeGBQYRBhuGJYYzBkGGSwZTBlcGW4ZehmQGdwaHhpsGqAarBr6GvobDwABAAAAlwCAABAANgADAAIAQAB4AI0AAACsDgwAAwABAAAADwC6AAMAAQQJAAAAmgAAAAMAAQQJAAEAIACaAAMAAQQJAAIADgC6AAMAAQQJAAMAQgDIAAMAAQQJAAQAMAEKAAMAAQQJAAUAVAE6AAMAAQQJAAYALAGOAAMAAQQJAAcARAG6AAMAAQQJAAgAFAH+AAMAAQQJAAkAFAISAAMAAQQJAAoALAImAAMAAQQJAAsAPgJSAAMAAQQJAAwAKgKQAAMAAQQJAA0ClgK6AAMAAQQJAA4ANAVQAEMAbwBwAHkAcgBpAGcAaAB0ACAAMgAwADIANAAgAFQAaABlACAATgBvAHQAbwAgAFAAcgBvAGoAZQBjAHQAIABBAHUAdABoAG8AcgBzACAAKABoAHQAdABwAHMAOgAvAC8AZwBpAHQAaAB1AGIALgBjAG8AbQAvAG4AbwB0AG8AZgBvAG4AdABzAC8AaABlAGIAcgBlAHcAKQBOAG8AdABvACAAUwBhAG4AcwAgAEgAZQBiAHIAZQB3AFIAZQBnAHUAbABhAHIAMwAuADAAMAAxADsARwBPAE8ARwA7AE4AbwB0AG8AUwBhAG4AcwBIAGUAYgByAGUAdwAtAFIAZQBnAHUAbABhAHIATgBvAHQAbwAgAFMAYQBuAHMAIABIAGUAYgByAGUAdwAgAFIAZQBnAHUAbABhAHIAVgBlAHIAcwBpAG8AbgAgADMALgAwADAAMQA7ACAAdAB0AGYAYQB1AHQAbwBoAGkAbgB0ACAAKAB2ADEALgA4AC4ANAAuADcALQA1AGQANQBiACkATgBvAHQAbwBTAGEAbgBzAEgAZQBiAHIAZQB3AC0AUgBlAGcAdQBsAGEAcgBOAG8AdABvACAAaQBzACAAYQAgAHQAcgBhAGQAZQBtAGEAcgBrACAAbwBmACAARwBvAG8AZwBsAGUAIABJAG4AYwAuAEcAbwBvAGcAbABlACAATABMAEMAQgBlAG4AIABOAGEAdABoAGEAbgBEAGUAcwBpAGcAbgBlAGQAIABiAHkAIABCAGUAbgAgAE4AYQB0AGgAYQBuAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBnAG8AbwBnAGwAZQAuAGMAbwBtAC8AZwBlAHQALwBuAG8AdABvAC8AaAB0AHQAcABzADoALwAvAGgAYQBmAG8AbgB0AGkAYQAuAGMAbwBtAC8AVABoAGkAcwAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUAIABpAHMAIABsAGkAYwBlAG4AcwBlAGQAIAB1AG4AZABlAHIAIAB0AGgAZQAgAFMASQBMACAATwBwAGUAbgAgAEYAbwBuAHQAIABMAGkAYwBlAG4AcwBlACwAIABWAGUAcgBzAGkAbwBuACAAMQAuADEALgAgAFQAaABpAHMAIABGAG8AbgB0ACAAUwBvAGYAdAB3AGEAcgBlACAAaQBzACAAZABpAHMAdAByAGkAYgB1AHQAZQBkACAAbwBuACAAYQBuACAAIgBBAFMAIABJAFMAIgAgAEIAQQBTAEkAUwAsACAAVwBJAFQASABPAFUAVAAgAFcAQQBSAFIAQQBOAFQASQBFAFMAIABPAFIAIABDAE8ATgBEAEkAVABJAE8ATgBTACAATwBGACAAQQBOAFkAIABLAEkATgBEACwAIABlAGkAdABoAGUAcgAgAGUAeABwAHIAZQBzAHMAIABvAHIAIABpAG0AcABsAGkAZQBkAC4AIABTAGUAZQAgAHQAaABlACAAUwBJAEwAIABPAHAAZQBuACAARgBvAG4AdAAgAEwAaQBjAGUAbgBzAGUAIABmAG8AcgAgAHQAaABlACAAcwBwAGUAYwBpAGYAaQBjACAAbABhAG4AZwB1AGEAZwBlACwAIABwAGUAcgBtAGkAcwBzAGkAbwBuAHMAIABhAG4AZAAgAGwAaQBtAGkAdABhAHQAaQBvAG4AcwAgAGcAbwB2AGUAcgBuAGkAbgBnACAAeQBvAHUAcgAgAHUAcwBlACAAbwBmACAAdABoAGkAcwAgAEYAbwBuAHQAIABTAG8AZgB0AHcAYQByAGUALgBoAHQAdABwADoALwAvAHMAYwByAGkAcAB0AHMALgBzAGkAbAAuAG8AcgBnAC8ATwBGAEwAAAACAAAAAAAA/5wAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAJcAAAECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgEjASQBJQEmAScBKAEpASoBKwEsAS0BLgEvATAAEAExATIBMwE0ATUBNgE3ATgBOQE6ATsBPAE9AT4BPwFAAUEBQgFDAUQBRQFGAUcBSAFJAUoBSwFMAU0BTgFPAVABUQFSAVMBVAFVAVYBVwFYAVkBWgFbAVwBXQFeAV8BYAFhAWIBYwFkAWUBZgFnAWgBaQADAWoBawFsAW0BbgFvAXABcQFyAXMBdAF1AXYBdwF4AXkBegF7AXwBfQF+AX8BgAGBAYIBgwGEAYUBhgGHAYgBiQGKAYsBjAGNAY4BjwGQAZEBkgGTAZQBlQJDUgROVUxMB3VuaTA1RDAHdW5pRkIzMAd1bmlGQjRGB3VuaUZCMkUHdW5pRkIyRgd1bmlGQjI5B3VuaTA1QTIHdW5pMDVFMgd1bmlGQjIwB3VuaTA1RDEHdW5pRkIzMQd1bmlGQjRDB3VuaTA1QkMHdW5pMDVEMwd1bmlGQjMzB3VuaTA1QTcHdW5pMDVBRAd1bmkwNTkxB3VuaTA1REEHdW5pRkIzQQd1bmkwNUREB3VuaTA1REYHdW5pMDVFMwd1bmlGQjQzB3VuaTA1RTUHdW5pRkIyNgd1bmkwNUYzB3VuaTA1OUMHdW5pMDU5RAd1bmkwNUY0B3VuaTA1OUUHdW5pMDVEMgd1bmlGQjMyB3VuaTA1QjIPdW5pMDVCMjIwMEQwNUJEB3VuaTA1QjMPdW5pMDVCMzIwMEQwNUJEB3VuaTA1QjEPdW5pMDVCMTIwMEQwNUJEB3VuaTA1RDQHdW5pRkIzNAd1bmkwNUQ3B3VuaTA1QjQHdW5pMDVCOQd1bmkwNUJBB3VuaTIwMTAHdW5pMDVBQwd1bmlGQjFFB3VuaTA1REIHdW5pRkIzQgd1bmlGQjREB3VuaTA1REMHdW5pRkIzQwd1bmkwNUM1B3VuaTA1QTQHdW5pMDVCRQd1bmkwNUFGB3VuaTA1REUHdW5pRkIzRQd1bmkwNUE2B3VuaTA1QTUHdW5pMDVBMwd1bmkwMEEwB3VuaTA1RTAHdW5pRkI0MAd1bmkwNUM2B3VuaTA1QUIHdW5pMDVDMAd1bmkwNTk5B3VuaTA1QjcHdW5pMDVBMQd1bmkwNUU0B3VuaUZCNDQHdW5pRkI0RQd1bmkwNUE4B3VuaTA1QjgHdW5pMDVDNwd1bmkwNTlGB3VuaTA1RTcHdW5pRkI0Nwd1bmkwNUJCB3VuaTA1QkYHdW5pMDVFOAd1bmlGQjQ4B3VuaTA1OTcHdW5pMDVFMQd1bmlGQjQxB3VuaTA1QjYHdW5pMDU5Mgd1bmkwNTkzB3VuaTIwQUEHdW5pMDVCMAd1bmkwNUU5B3VuaUZCNDkHdW5pRkIyQwd1bmlGQjJEB3VuaTA1QzEHdW5pRkIyQQd1bmlGQjJCB3VuaTA1QkQHdW5pMDVDMgd1bmkwNUMzB3VuaTA1RUEHdW5pRkI0QQd1bmkwNUEwB3VuaTA1QTkHdW5pMDVEOAd1bmlGQjM4B3VuaTA1OUIHdW5pMDU5Ngd1bmkwNUU2B3VuaUZCNDYHdW5pMDVCNQd1bmkyMDBDB3VuaTIwMEQHdW5pMjAwRQd1bmkyMDBGB3VuaTI1Q0MHdW5pMDVDNAd1bmkwNUQ1B3VuaUZCMzUHdW5pRkI0Qgd1bmkwNUYwB3VuaTA1RjEHdW5pRkIyMQd1bmlGQjIyB3VuaUZCMjMHdW5pRkIyNAd1bmlGQjI1B3VuaUZCMjcHdW5pRkIyOAd1bmkwNUFBB3VuaTA1OUEHdW5pMDVEOQd1bmlGQjM5B3VuaUZCMUQHdW5pMDVGMgd1bmlGQjFGB3VuaTA1OTUHdW5pMDU5NAd1bmkwNTk4B3VuaTA1RDYHdW5pRkIzNgd1bmkwNUFFB3VuaTAzNEYLeW9keW9keW9kaGIAAQAB//8ADwAAAAAAAAAAAAAAAAAAAAAAAAAAAFwAXABMAEwCUQAA/0ECUf/2/0GwACwgsABVWEVZICBLuAAOUUuwBlNaWLA0G7AoWWBmIIpVWLACJWG5CAAIAGNjI2IbISGwAFmwAEMjRLIAAQBDYEItsAEssCBgZi2wAiwjISMhLbADLCBkswMUFQBCQ7ATQyBgYEKxAhRDQrElA0OwAkNUeCCwDCOwAkNDYWSwBFB4sgICAkNgQrAhZRwhsAJDQ7IOFQFCHCCwAkMjQrITARNDYEIjsABQWGVZshYBAkNgQi2wBCywAyuwFUNYIyEjIbAWQ0MjsABQWGVZGyBkILDAULAEJlqyKAENQ0VjRbAGRVghsAMlWVJbWCEjIRuKWCCwUFBYIbBAWRsgsDhQWCGwOFlZILEBDUNFY0VhZLAoUFghsQENQ0VjRSCwMFBYIbAwWRsgsMBQWCBmIIqK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'; | ||
| // Utility: get glyph width | ||
| export function getGlyphWidth(glyphId) { | ||
| return widths[glyphId] !== undefined ? widths[glyphId] : 270; | ||
| } | ||
| // Utility: get glyph ID for unicode code point | ||
| export function getGlyphId(codePoint) { | ||
| return cmap[codePoint] || 0; | ||
| } |
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
| // Auto-generated font data module — type declarations only (DO NOT EDIT) | ||
| // Source: scripts/build-font-data.cjs — regenerate with build script | ||
| export declare const metrics: { unitsPerEm: number; ascent: number; descent: number; capHeight: number; stemV: number; bbox: number[]; defaultWidth: number; numGlyphs: number; }; | ||
| export declare const fontName: string; | ||
| export declare const cmap: Record<number, number>; | ||
| export declare const defaultWidth: number; | ||
| export declare const widths: Record<number, number>; | ||
| export declare const gsub: Record<number, number>; | ||
| export declare const ligatures: Record<number, number[][]>; | ||
| export declare const markAnchors: Record<number, unknown>; | ||
| export declare const mark2mark: Record<string, unknown>; | ||
| export declare const pdfWidthArray: string; | ||
| export declare const ttfBase64: string; |
Sorry, the diff of this file is too big to display
+21
| MIT License | ||
| Copyright (c) 2026 Nizoka — Plika (https://plika.app) | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+1082
| # pdfnative | ||
| [](https://github.com/Nizoka/pdfnative/actions/workflows/ci.yml) | ||
| [](https://github.com/Nizoka/pdfnative/actions/workflows/codeql.yml) | ||
| [](https://www.npmjs.com/package/pdfnative) | ||
| [](https://www.npmjs.com/package/pdfnative) | ||
| [](https://bundlephobia.com/package/pdfnative) | ||
| [](https://www.npmjs.com/package/pdfnative) | ||
| [](https://www.typescriptlang.org/) | ||
| [](LICENSE) | ||
| [](https://docs.npmjs.com/generating-provenance-statements) | ||
| [](https://pdfnative.dev) | ||
| Pure native PDF generation library — zero vendor dependencies. ISO 32000-1 (PDF 1.7) compliant. | ||
| ## Highlights | ||
| - **Zero dependencies** — built from scratch in pure TypeScript. Zero runtime dependencies, tree-shakeable, auditable | ||
| - **ISO 32000-1 compliant** — valid xref tables, /Info metadata, proper font embedding | ||
| - **16 Unicode scripts** — Thai, Japanese, Chinese (SC), Korean, Greek, Devanagari, Turkish, Vietnamese, Polish, Arabic, Hebrew, Cyrillic, Georgian, Armenian, Bengali, Tamil | ||
| - **Thai OpenType shaping** — GSUB substitution + GPOS mark-to-base + mark-to-mark positioning | ||
| - **Arabic positional shaping** — GSUB isolated/initial/medial/final forms + lam-alef ligatures | ||
| - **BiDi text layout** — simplified Unicode Bidirectional Algorithm (UAX #9) with glyph mirroring | ||
| - **Multi-font fallback** — automatic cross-script font switching with continuation bias | ||
| - **TTF subsetting** — only used glyphs embedded (dramatic file size reduction) | ||
| - **Tagged PDF / PDF/A** — structure tree, /ActualText, XMP metadata, sRGB OutputIntent (PDF/A-1b, 2b, 2u, 3b with embedded file attachments) | ||
| - **PDF Encryption** — AES-128 (V4/R4) and AES-256 (V5/R6), owner + user passwords, granular permissions | ||
| - **Free-form document builder** — headings, paragraphs, lists, tables, images, barcodes, SVG paths, form fields, spacers, page breaks, table of contents | ||
| - **Barcode & QR code generation** — Code 128, EAN-13, QR Code, Data Matrix, PDF417 — pure PDF path operators (no images) | ||
| - **SVG path rendering** — path, rect, circle, ellipse, line, polyline, polygon as native PDF operators | ||
| - **AcroForm fields** — text, multiline, checkbox, radio, dropdown, listbox with appearance streams (ISO 32000-1 §12.7) | ||
| - **Digital signatures** — CMS/PKCS#7 detached signatures with RSA + ECDSA, SHA-256/384/512, X.509 parsing (ISO 32000-1 §12.8) | ||
| - **Streaming output** — AsyncGenerator-based progressive PDF emission with configurable chunk size | ||
| - **PDF parser & modifier** — read existing PDFs (tokenizer, xref, object parser, FlateDecode inflate) + incremental modification | ||
| - **Image embedding** — JPEG (DCTDecode) and PNG (FlateDecode) with auto-scaling and alignment | ||
| - **Hyperlinks** — PDF link annotations (/URI) with URL validation, blue underlined text, tagged /Link | ||
| - **Header/footer templates** — configurable `PageTemplate` with left/center/right zones and `{page}`/`{pages}`/`{date}`/`{title}` placeholders | ||
| - **Watermarks** — text and image overlays with configurable opacity, rotation, and position (background/foreground) | ||
| - **Table of contents** — auto-generated TOC with internal /GoTo links, dot leaders, and page numbers | ||
| - **FlateDecode compression** — zlib stream compression (50–90% size reduction), zero-dependency, platform-native | ||
| - **Web Worker support** — off-main-thread generation for large datasets | ||
| - **Tree-shakeable** — ESM + CJS dual build with TypeScript declarations | ||
| - **95%+ test coverage** — 1588+ tests across 40 files, fuzz suite, performance benchmarks | ||
| - **NPM provenance** — signed builds via GitHub Actions OIDC | ||
| ## Installation | ||
| ```bash | ||
| npm install pdfnative | ||
| ``` | ||
| **Requirements:** Node.js >= 22 | Modern browsers | Deno | Bun | ||
| ## Why pdfnative? | ||
| pdfnative was designed for teams that need **ISO-compliant, production-grade PDF generation** with zero supply-chain risk. Here is how it compares to other popular JavaScript PDF libraries: | ||
| | Feature | pdfnative | jsPDF | pdfkit | pdf-lib | pdfmake | | ||
| |---------|:---------:|:-----:|:------:|:-------:|:------:| | ||
| | Runtime dependencies | **0** | 3 | 6 | 4 | 3 | | ||
| | TypeScript declarations | Built-in | Built-in | @types/* | Built-in | @types/* | | ||
| | PDF/A (ISO 19005) | 1b, 2b, 2u, 3b | — | — | — | — | | ||
| | Tagged PDF / PDF/UA | ✅ | — | ✅ | — | — | | ||
| | Encryption | AES-128/256 | ✅ | ✅ | — | ✅ | | ||
| | Complex text shaping (GSUB/GPOS) | ✅ Thai, Arabic, Devanagari, Bengali, Tamil | — | Via fontkit | Via @pdf-lib/fontkit | Via pdfkit | | ||
| | BiDi (RTL) layout | ✅ | — | — | — | — | | ||
| | Modify existing PDFs | ✅ (incremental) | — | — | ✅ | — | | ||
| | Forms (AcroForms) | ✅ | ✅ | ✅ | ✅ | — | | ||
| | Digital signatures | ✅ (RSA + ECDSA) | — | — | — | — | | ||
| | Barcode / QR code (native) | ✅ 5 formats | — | — | — | QR | | ||
| | SVG path rendering | ✅ | ✅ | ✅ | ✅ | ✅ | | ||
| | Streaming output | ✅ | — | ✅ | — | ✅ | | ||
| | PDF parser | ✅ | — | — | ✅ | — | | ||
| | Tree-shakeable (ESM) | ✅ | — | — | ✅ | — | | ||
| | NPM provenance (SLSA) | ✅ | — | — | — | ✅ | | ||
| > **Data sources:** npm registry metadata and official README/documentation for each library as of April 2026. Dependency counts reflect direct `dependencies` listed in each package's `package.json`. "—" means the feature is not supported or not documented. Feature claims about third-party libraries are based on their public documentation and may not reflect the latest version — please verify against current releases. Sample PDFs validate with veraPDF (PDF/A) and Adobe Acrobat. | ||
| **When to choose another library:** You need advanced vector graphics (complex gradients, arbitrary transforms), rich interactive form scripting (JavaScript actions), or mature ecosystem integrations with existing toolchains. | ||
| **When to choose pdfnative:** You need zero-dependency PDF generation with ISO archival compliance (PDF/A), accessibility (tagged PDF), AES encryption, digital signatures, multi-script Unicode support — particularly Arabic/Hebrew BiDi and Thai GSUB/GPOS shaping — form fields, barcode generation, SVG rendering, or the ability to parse and incrementally modify existing PDFs. | ||
| ## Quick Start | ||
| ```typescript | ||
| import { buildPDFBytes, downloadBlob } from 'pdfnative'; | ||
| const pdf = buildPDFBytes({ | ||
| title: 'Monthly Report', | ||
| infoItems: [ | ||
| { label: 'Period', value: 'January 2026' }, | ||
| { label: 'Account', value: 'Main Account' }, | ||
| ], | ||
| balanceText: 'Balance: $1,234.56', | ||
| countText: '42 transactions', | ||
| headers: ['Date', 'Description', 'Category', 'Amount', 'Status'], | ||
| rows: [ | ||
| { cells: ['01/15', 'Grocery Store', 'Food', '-$45.00', ''], type: 'debit', pointed: false }, | ||
| { cells: ['01/16', 'Salary', 'Income', '+$3,000.00', 'X'], type: 'credit', pointed: true }, | ||
| ], | ||
| footerText: 'Generated by MyApp', | ||
| }); | ||
| // Browser: trigger download | ||
| downloadBlob(pdf, 'report.pdf'); | ||
| // Node.js: write to file | ||
| import { writeFileSync } from 'fs'; | ||
| writeFileSync('report.pdf', pdf); | ||
| ``` | ||
| ### Document Builder | ||
| Build free-form documents with headings, paragraphs, lists, tables, images, barcodes, and more: | ||
| ```typescript | ||
| import { buildDocumentPDFBytes } from 'pdfnative'; | ||
| const pdf = buildDocumentPDFBytes({ | ||
| title: 'Project Report', | ||
| blocks: [ | ||
| { type: 'toc' }, | ||
| { type: 'heading', text: 'Executive Summary', level: 1 }, | ||
| { type: 'paragraph', text: 'This quarter saw strong growth across all divisions...' }, | ||
| { type: 'image', data: jpegBytes, width: 400, align: 'center', alt: 'Revenue chart' }, | ||
| { type: 'list', items: ['Revenue up 15%', 'Costs down 8%', 'Net profit +23%'], style: 'bullet' }, | ||
| { type: 'table', headers: ['Q1', 'Q2', 'Q3', 'Q4'], rows: [ | ||
| { cells: ['$1.2M', '$1.4M', '$1.6M', '$1.8M'], type: 'credit', pointed: false }, | ||
| ]}, | ||
| { type: 'spacer', height: 20 }, | ||
| { type: 'heading', text: 'Next Steps', level: 2 }, | ||
| { type: 'paragraph', text: 'Focus areas for next quarter include...', align: 'left' }, | ||
| { type: 'link', text: 'View full report online', url: 'https://example.com/report' }, | ||
| { type: 'barcode', format: 'qr', data: 'https://example.com/report', align: 'center' }, | ||
| ], | ||
| footerText: 'Confidential', | ||
| }, { | ||
| headerTemplate: { center: 'Project Report', right: '{date}' }, | ||
| footerTemplate: { left: 'Confidential', right: 'Page {page} of {pages}' }, | ||
| }); | ||
| ``` | ||
| ## Unicode Font Support | ||
| For non-Latin scripts, register font data loaders (lazy-loaded on demand): | ||
| ```typescript | ||
| import { registerFonts, loadFontData, buildPDFBytes } from 'pdfnative'; | ||
| registerFonts({ | ||
| th: () => import('pdfnative/fonts/noto-thai-data.js'), | ||
| ja: () => import('pdfnative/fonts/noto-jp-data.js'), | ||
| zh: () => import('pdfnative/fonts/noto-sc-data.js'), | ||
| ko: () => import('pdfnative/fonts/noto-kr-data.js'), | ||
| el: () => import('pdfnative/fonts/noto-greek-data.js'), | ||
| hi: () => import('pdfnative/fonts/noto-devanagari-data.js'), | ||
| tr: () => import('pdfnative/fonts/noto-turkish-data.js'), | ||
| vi: () => import('pdfnative/fonts/noto-vietnamese-data.js'), | ||
| pl: () => import('pdfnative/fonts/noto-polish-data.js'), | ||
| ar: () => import('pdfnative/fonts/noto-arabic-data.js'), | ||
| he: () => import('pdfnative/fonts/noto-hebrew-data.js'), | ||
| ru: () => import('pdfnative/fonts/noto-cyrillic-data.js'), | ||
| ka: () => import('pdfnative/fonts/noto-georgian-data.js'), | ||
| hy: () => import('pdfnative/fonts/noto-armenian-data.js'), | ||
| bn: () => import('pdfnative/fonts/noto-bengali-data.js'), | ||
| ta: () => import('pdfnative/fonts/noto-tamil-data.js'), | ||
| }); | ||
| const thaiFont = await loadFontData('th'); | ||
| const pdf = buildPDFBytes({ | ||
| title: 'รายงานประจำเดือน', | ||
| // ... other params | ||
| fontEntries: thaiFont ? [{ fontData: thaiFont, fontRef: '/F3', lang: 'th' }] : [], | ||
| }); | ||
| ``` | ||
| ### Supported Languages | ||
| | Language | Code | Font | Script | | ||
| |----------|------|------|--------| | ||
| | Thai | `th` | Noto Sans Thai | GSUB + GPOS shaping | | ||
| | Japanese | `ja` | Noto Sans JP | CJK ideographs + kana | | ||
| | Chinese (Simplified) | `zh` | Noto Sans SC | CJK ideographs | | ||
| | Korean | `ko` | Noto Sans KR | Hangul syllables | | ||
| | Greek | `el` | Noto Sans Greek | Greek alphabet | | ||
| | Hindi (Devanagari) | `hi` | Noto Sans Devanagari | GSUB conjuncts + GPOS marks | | ||
| | Turkish | `tr` | Noto Sans Turkish | Latin extended (İ/ı) | | ||
| | Vietnamese | `vi` | Noto Sans Vietnamese | Latin + combining marks | | ||
| | Polish | `pl` | Noto Sans Polish | Latin extended (Ł/ł) | | ||
| | Arabic | `ar` | Noto Sans Arabic | GSUB positional shaping | | ||
| | Hebrew | `he` | Noto Sans Hebrew | Right-to-left script | | ||
| | Russian (Cyrillic) | `ru` | Noto Sans | Cyrillic alphabet | | ||
| | Georgian | `ka` | Noto Sans Georgian | Mkhedruli script | | ||
| | Armenian | `hy` | Noto Sans Armenian | Armenian alphabet |\n| Bengali | `bn` | Noto Sans Bengali | GSUB conjuncts + GPOS marks |\n| Tamil | `ta` | Noto Sans Tamil | GSUB ligatures + split vowels | | ||
| ## Multi-Font (Mixed Scripts) | ||
| Generate PDFs with multiple scripts in the same document: | ||
| ```typescript | ||
| const fonts = await Promise.all([ | ||
| loadFontData('th'), | ||
| loadFontData('ja'), | ||
| loadFontData('zh'), | ||
| ]); | ||
| const fontEntries = fonts | ||
| .filter(Boolean) | ||
| .map((fd, i) => ({ fontData: fd!, fontRef: `/F${3 + i}`, lang: ['th', 'ja', 'zh'][i] })); | ||
| const pdf = buildPDFBytes({ | ||
| title: 'Multi-Language Report', | ||
| headers: ['Date', 'Description', 'Category', 'Amount', 'Status'], | ||
| rows: [ | ||
| { cells: ['01/01', 'English text', 'Test', '+100', 'OK'], type: 'credit', pointed: false }, | ||
| { cells: ['01/02', 'ข้อความไทย', 'ทดสอบ', '-50', ''], type: 'debit', pointed: false }, | ||
| { cells: ['01/03', '日本語テキスト', 'テスト', '+200', '済'], type: 'credit', pointed: true }, | ||
| ], | ||
| // ... other params | ||
| fontEntries, | ||
| }); | ||
| ``` | ||
| ## Web Worker (Large Datasets) | ||
| ```typescript | ||
| import { createPDF } from 'pdfnative'; | ||
| const pdf = await createPDF(params, { | ||
| workerUrl: new URL('pdfnative/worker', import.meta.url), | ||
| threshold: 500, // use Worker above 500 rows | ||
| timeout: 30000, // Worker timeout in ms (default: 60000) | ||
| onProgress: (percent) => console.log(`${percent}%`), | ||
| }); | ||
| ``` | ||
| For lower-level control, use `generatePDFInWorker` directly with `WorkerGenerationOptions`: | ||
| ```typescript | ||
| import { generatePDFInWorker } from 'pdfnative'; | ||
| import type { WorkerGenerationOptions } from 'pdfnative'; | ||
| const options: WorkerGenerationOptions = { | ||
| timeout: 15000, | ||
| onProgress: (percent) => console.log(`${percent}%`), | ||
| }; | ||
| const pdf = await generatePDFInWorker(workerUrl, params, options); | ||
| ``` | ||
| ## Layout Customization | ||
| ```typescript | ||
| const pdf = buildPDFBytes(params, { | ||
| pageWidth: 595.28, // A4 (default) | ||
| pageHeight: 841.89, // A4 (default) | ||
| margins: { t: 45, r: 36, b: 35, l: 36 }, | ||
| colors: { | ||
| title: '#2563EB', // hex — primary format | ||
| credit: [15, 145, 121], // RGB tuple [0–255] | ||
| debit: '0.863 0.149 0.149', // PDF operator string [0.0–1.0] | ||
| // ... see PdfColors type | ||
| }, | ||
| columns: [ | ||
| { f: 0.15, a: 'l', mx: 12, mxH: 12 }, | ||
| { f: 0.35, a: 'l', mx: 50, mxH: 50 }, | ||
| { f: 0.20, a: 'r', mx: 20, mxH: 20 }, | ||
| { f: 0.30, a: 'r', mx: 30, mxH: 30 }, | ||
| ], | ||
| }); | ||
| ``` | ||
| ### Color Formats | ||
| All color values accept three formats: | ||
| | Format | Example | Description | | ||
| |--------|---------|-------------| | ||
| | Hex string | `'#2563EB'` or `'#26E'` | Primary format — `#RRGGBB` or `#RGB` | | ||
| | RGB tuple | `[37, 99, 235]` | Array with values 0–255 | | ||
| | PDF operator | `'0.145 0.388 0.922'` | Raw PDF RGB string (0.0–1.0) | | ||
| ```typescript | ||
| import { parseColor } from 'pdfnative'; | ||
| parseColor('#2563EB'); // '0.145 0.388 0.922' | ||
| parseColor([37, 99, 235]); // '0.145 0.388 0.922' | ||
| parseColor('0.145 0.388 0.922'); // '0.145 0.388 0.922' | ||
| ``` | ||
| All inputs are validated and normalized before interpolation into PDF content streams, preventing operator injection. | ||
| ### Font Sizes | ||
| Customize font sizes for each zone (title, info bar, table header, table cells, footer): | ||
| ```typescript | ||
| const pdf = buildPDFBytes(params, { | ||
| fontSizes: { | ||
| title: 20, // Title text (default: 16) | ||
| info: 10, // Info bar items (default: 9) | ||
| th: 9, // Table header cells (default: 8) | ||
| td: 8, // Table body cells (default: 7.5) | ||
| ft: 8, // Footer text (default: 7) | ||
| }, | ||
| }); | ||
| ``` | ||
| | Zone | Key | Default | Description | | ||
| |------|-----|---------|-------------| | ||
| | Title | `title` | 16 | PDF title text | | ||
| | Info bar | `info` | 9 | Key-value pairs below title | | ||
| | Table header | `th` | 8 | Column header row | | ||
| | Table cells | `td` | 7.5 | Data row cells | | ||
| | Footer | `ft` | 7 | Page footer text | | ||
| All values are in PDF points (1pt = 1/72 inch). Partial overrides are supported — unspecified keys use defaults. | ||
| ## Building Custom Font Data | ||
| ### Obtaining TTF Files | ||
| For Noto Sans fonts, download the raw `.ttf` file directly from the [noto-fonts GitHub repository](https://github.com/notofonts): | ||
| 1. Navigate to the font's GitHub repository (e.g., `github.com/notofonts/bengali`) | ||
| 2. Find the TTF file under `fonts/NotoSansBengali/unhinted/ttf/` (or similar path) | ||
| 3. Click the file, then click **"Download raw file"** (or use the raw URL) | ||
| 4. Save it to `fonts/ttf/` | ||
| No zip download or extraction needed — each TTF is a standalone file you can download directly. | ||
| ### Building the Data Module | ||
| Convert any TTF font into an importable data module: | ||
| ```bash | ||
| npx pdfnative-build-font fonts/ttf/MyFont.ttf fonts/my-font-data.js | ||
| ``` | ||
| The tool extracts cmap, widths, metrics, GSUB, GPOS, and embeds the raw TTF as base64. | ||
| ## Visual PDF Inspection | ||
| Generate sample PDFs for all supported languages to visually verify output: | ||
| ```bash | ||
| npm run test:generate | ||
| ``` | ||
| This creates **140+ PDF files** in `test-output/` (git-ignored), organized in twenty-three categories. | ||
| See [scripts/README.md](scripts/README.md) for the modular generator architecture. | ||
| ### Financial Statements (per language) | ||
| | File | Content | | ||
| |------|---------| | ||
| | `sample-latin.pdf` | English / Helvetica | | ||
| | `sample-th.pdf` | Thai with GSUB + GPOS shaping | | ||
| | `sample-ja.pdf` | Japanese (CJK ideographs) | | ||
| | `sample-zh.pdf` | Chinese Simplified | | ||
| | `sample-ko.pdf` | Korean (Hangul) | | ||
| | `sample-el.pdf` | Greek | | ||
| | `sample-hi.pdf` | Hindi (Devanagari) | | ||
| | `sample-tr.pdf` | Turkish (İ/ı special casing) | | ||
| | `sample-vi.pdf` | Vietnamese (combining marks) | | ||
| | `sample-pl.pdf` | Polish (Ł/ł) | | ||
| | `sample-ar.pdf` | Arabic (RTL, positional shaping) | | ||
| | `sample-he.pdf` | Hebrew (RTL) | | ||
| | `sample-ru.pdf` | Russian (Cyrillic) | | ||
| | `sample-ka.pdf` | Georgian (Mkhedruli) | | ||
| | `sample-hy.pdf` | Armenian | | ||
| | `sample-bn.pdf` | Bengali (GSUB conjuncts + GPOS marks) | | ||
| | `sample-ta.pdf` | Tamil (GSUB + split vowel decomposition) | | ||
| | `sample-multi.pdf` | Mixed: all 16 scripts in one PDF | | ||
| | `sample-pagination.pdf` | 200 rows, multi-page layout | | ||
| ### Diverse Use Cases (non-financial) | ||
| | File | Content | | ||
| |------|---------| | ||
| | `diverse-student-transcript.pdf` | University academic transcript (Latin) | | ||
| | `diverse-recipe-th.pdf` | Thai recipe — Tom Yum Goong ingredients (Thai) | | ||
| | `diverse-server-ja.pdf` | Server monitoring dashboard (Japanese) | | ||
| | `diverse-inventory-zh.pdf` | Warehouse product inventory (Chinese) | | ||
| | `diverse-sports-ko.pdf` | K-League football standings (Korean) | | ||
| | `diverse-library-el.pdf` | Classical Greek library catalog (Greek) | | ||
| | `diverse-medical-hi.pdf` | Blood test lab results (Hindi) | | ||
| | `diverse-menu-tr.pdf` | Turkish restaurant dinner menu (Turkish) | | ||
| | `diverse-weather-vi.pdf` | Weekly weather forecast — Hanoi (Vietnamese) | | ||
| | `diverse-train-pl.pdf` | Train schedule — Warsaw (Polish) | | ||
| | `diverse-marketplace-ar.pdf` | Gold marketplace catalog — Dubai (Arabic) | | ||
| | `diverse-museum-he.pdf` | Museum exhibition catalog — Jerusalem (Hebrew) | | ||
| ### Alphabet / Character Coverage | ||
| | File | Content | | ||
| |------|---------| | ||
| | `alphabet-thai.pdf` | 44 consonants, vowels, tone marks, digits | | ||
| | `alphabet-japanese.pdf` | Hiragana, Katakana, Kanji numerals & common | | ||
| | `alphabet-chinese.pdf` | 121 characters by category (HSK frequency) | | ||
| | `alphabet-korean.pdf` | Hangul jamo, syllables, complex clusters | | ||
| | `alphabet-greek.pdf` | Full uppercase/lowercase, accented, archaic | | ||
| | `alphabet-devanagari.pdf` | Vowels, consonants, matras, conjuncts, digits | | ||
| | `alphabet-turkish.pdf` | 29 letters, İ/ı dotted-I distinction test | | ||
| | `alphabet-vietnamese.pdf` | 7 base vowels × 6 tones, all diacritics | | ||
| | `alphabet-polish.pdf` | 32 letters, digraphs, pangram | | ||
| | `alphabet-arabic.pdf` | 28 letters, harakat, numerals, ligatures | | ||
| | `alphabet-hebrew.pdf` | 22 letters, final forms, vowel points | | ||
| | `alphabet-cyrillic.pdf` | 33 Russian letters, Ukrainian/Serbian extended | | ||
| | `alphabet-georgian.pdf` | 33 Mkhedruli letters, Asomtavruli | | ||
| | `alphabet-armenian.pdf` | 38 letters, ligatures | | ||
| | `alphabet-bengali.pdf` | Vowels, consonants, conjuncts, digits | | ||
| | `alphabet-tamil.pdf` | Vowels, consonants, compound characters, digits | | ||
| ### PDF/A Conformance Variants | ||
| | File | Content | | ||
| |------|---------| | ||
| | `tagged-pdfa2b-default.pdf` | PDF/A-2b (tagged=true, default) | | ||
| | `tagged-pdfa2b-explicit.pdf` | PDF/A-2b (tagged='pdfa2b', explicit) | | ||
| | `tagged-pdfa1b.pdf` | PDF/A-1b (tagged='pdfa1b', legacy) | | ||
| | `tagged-pdfa2u.pdf` | PDF/A-2u (tagged='pdfa2u', Unicode) | | ||
| | `tagged-pdfa3b.pdf` | PDF/A-3b (tagged='pdfa3b', embedded file attachments) | | ||
| ### Encrypted PDFs | ||
| | File | Content | | ||
| |------|---------| | ||
| | `encrypted-aes128.pdf` | AES-128 (V4/R4) owner-only | | ||
| | `encrypted-aes256.pdf` | AES-256 (V5/R6) owner-only | | ||
| | `encrypted-aes128-user.pdf` | AES-128 with user+owner passwords | | ||
| | `encrypted-aes256-user.pdf` | AES-256 with user+owner passwords | | ||
| | `encrypted-readonly.pdf` | AES-128 read-only (no copy/modify) | | ||
| | `encrypted-noprint.pdf` | AES-128 fully restricted | | ||
| **Sample passwords** (for testing only — all documented in `scripts/generate-samples.ts`): | ||
| | File | Owner Password | User Password | | ||
| |------|---------------|---------------| | ||
| | `encrypted-aes128.pdf` | `owner123` | _(none — opens freely)_ | | ||
| | `encrypted-aes256.pdf` | `owner256` | _(none — opens freely)_ | | ||
| | `encrypted-aes128-user.pdf` | `owner123` | `user456` | | ||
| | `encrypted-aes256-user.pdf` | `owner256` | `user789` | | ||
| | `encrypted-readonly.pdf` | `owner-ro` | _(none — opens freely)_ | | ||
| | `encrypted-noprint.pdf` | `owner-np` | _(none — opens freely)_ | | ||
| | `doc-encrypted-aes128.pdf` | `docowner` | `docuser` | | ||
| | `doc-encrypted-aes256.pdf` | `strongowner256` | _(none — opens freely)_ | | ||
| ### Document Builder Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `doc-headings-paragraphs.pdf` | H1/H2/H3 + paragraphs with text wrapping | | ||
| | `doc-lists.pdf` | Bullet + numbered lists | | ||
| | `doc-links.pdf` | External hyperlink annotations | | ||
| | `doc-table.pdf` | Embedded table in document | | ||
| | `doc-spacer-pagebreak.pdf` | Spacers + forced page breaks (3 pages) | | ||
| | `doc-encrypted-aes128.pdf` | Document builder + AES-128 encryption | | ||
| | `doc-encrypted-aes256.pdf` | Document builder + AES-256 encryption | | ||
| | `doc-image.pdf` | Image embedding (JPEG, centered) | | ||
| | `doc-custom-colors.pdf` | Color formats (hex, tuple, PDF operator) | | ||
| | `doc-japanese.pdf` | Japanese Unicode document (headings, lists, table) | | ||
| | `doc-arabic.pdf` | Arabic RTL document (headings, lists, table, BiDi) | | ||
| | `doc-hebrew.pdf` | Hebrew RTL document (headings, lists, table, BiDi) | | ||
| | `doc-thai.pdf` | Thai user manual (GSUB+GPOS shaping, pricing table) | | ||
| | `doc-chinese-catalog.pdf` | Chinese product catalog (tables, ordering info) | | ||
| | `doc-multi-language.pdf` | Multi-language: EN + Arabic + Japanese in one PDF | | ||
| | `doc-invoice.pdf` | Invoice template (line items, totals, payment link) | | ||
| | `doc-report-multipage.pdf` | 3-page technical report (7 sections, 4 tables) | | ||
| | `doc-contract-bilingual.pdf` | Bilingual EN/AR contract (legal sections, signatures) | | ||
| | `doc-showcase-all-blocks.pdf` | All 12 block types in one PDF | | ||
| ### Compressed PDFs (FlateDecode) | ||
| | File | Content | | ||
| |------|---------| | ||
| | `compressed-latin-100rows.pdf` | 100-row Latin table (87% smaller) | | ||
| | `uncompressed-latin-100rows.pdf` | Same 100-row table without compression (baseline) | | ||
| | `compressed-japanese.pdf` | Japanese CIDFont + TTF subset (62% smaller) | | ||
| | `compressed-arabic.pdf` | Arabic RTL + GSUB shaping (compressed) | | ||
| | `compressed-thai.pdf` | Thai GSUB+GPOS shaping (compressed) | | ||
| | `compressed-tagged-pdfa2b.pdf` | FlateDecode + Tagged PDF/A-2b (XMP uncompressed) | | ||
| | `compressed-encrypted-aes128.pdf` | FlateDecode + AES-128 encryption | | ||
| | `doc-compressed.pdf` | Document builder with FlateDecode | | ||
| ### Stress Test PDFs | ||
| | File | Content | | ||
| |------|---------| | ||
| | `stress-test-10k-rows.pdf` | 10,000-row table (167 pages, 4.3MB) | | ||
| | `doc-extreme-bidi-wrapping.pdf` | Extreme BiDi mixed-script text wrapping | | ||
| | `table-heavy-text-overflow.pdf` | Dense table with heavy text overflow | | ||
| | `media-rich-document.pdf` | Media-rich document with multiple images | | ||
| | `tagged-accessibility-complex.pdf` | Complex tagged PDF/A accessibility tree | | ||
| | `layout-extreme-customization.pdf` | Extreme layout customization (margins, columns, colors) | | ||
| ### Edge-Case Stress Tests | ||
| | File | Content | | ||
| |------|---------| | ||
| | `doc-unbreakable-text.pdf` | 1000-char words with no spaces (DNA, URL, Base64) | | ||
| | `table-micro-columns.pdf` | Extreme column fractions (f=0.025, mx=1) | | ||
| | `doc-link-annotation-bomb.pdf` | 500 link annotations across 10 pages | | ||
| | `zero-content-empty-table.pdf` | Table with headers but 0 rows | | ||
| | `zero-content-empty-doc.pdf` | Document with no blocks | | ||
| | `zero-content-empty-strings.pdf` | Empty headings, paragraphs, and list items | | ||
| | `doc-heavy-buffer-5mb.pdf` | 5 MB synthetic JPEG embedded (memory stress) | | ||
| ### Barcode & QR Code Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `barcode-showcase.pdf` | All 5 formats: Code 128, EAN-13, QR Code, Data Matrix, PDF417 | | ||
| | `barcode-alignment-sizing.pdf` | Alignment (left/center/right) and custom size variations | | ||
| | `barcode-tagged-pdfa.pdf` | Barcodes in tagged PDF/A-2b mode (/Figure structure elements) | | ||
| ### SVG Path Rendering Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `svg-basic-shapes.pdf` | Rect, circle, ellipse, line, polyline, polygon | | ||
| | `svg-complex-paths.pdf` | Cubic/quadratic Bézier curves, arcs, combined paths | | ||
| | `svg-tagged-pdfa.pdf` | SVG elements in tagged PDF/A-2b mode | | ||
| ### Form Field Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `form-fields.pdf` | All field types: text, multiline, checkbox, radio, dropdown, listbox | | ||
| | `form-contact.pdf` | Contact form with name, email, message, and submit fields | | ||
| ### Digital Signature Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `sig-rsa-self-signed.pdf` | RSA PKCS#1 v1.5 self-signed signature | | ||
| | `sig-ecdsa-p256.pdf` | ECDSA P-256 digital signature | | ||
| | `sig-multi-field.pdf` | PDF with multiple signature fields | | ||
| ### Streaming Output Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `streaming-document.pdf` | Document streamed via `buildDocumentPDFStream()` | | ||
| | `streaming-table.pdf` | Table streamed via `buildPDFStream()` | | ||
| ### PDF Parser & Modifier Samples | ||
| | File | Content | | ||
| |------|---------| | ||
| | `parser-original.pdf` | Generated → parsed → verified round-trip | | ||
| | `parser-modified.pdf` | Generated → parsed → modified → incremental save | | ||
| | `parser-document.pdf` | Document builder → parser round-trip verification | | ||
| ## API Reference | ||
| ### Core | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `buildPDF(params, layout?)` | Build table-centric PDF as binary string | | ||
| | `buildPDFBytes(params, layout?)` | Build table-centric PDF as `Uint8Array` | | ||
| | `buildDocumentPDF(params, layout?)` | Build free-form document PDF as binary string | | ||
| | `buildDocumentPDFBytes(params, layout?)` | Build free-form document PDF as `Uint8Array` | | ||
| | `wrapText(text, maxWidth, fontSize, enc)` | Word-wrap text into lines | | ||
| | `createPDF(params, options?)` | Smart dispatch (Worker or main thread) | | ||
| | `initNodeCompression()` | Initialize native zlib for ESM (call once before `compress: true`) | | ||
| | `downloadBlob(bytes, filename)` | Trigger browser download | | ||
| | `toBytes(str)` | Convert binary string to `Uint8Array` | | ||
| | `slugify(str)` | Sanitize string for filename | | ||
| ### Image Support | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `parseImage(bytes)` | Auto-detect and parse JPEG or PNG | | ||
| | `parseJPEG(bytes)` | Parse JPEG image (DCTDecode) | | ||
| | `parsePNG(bytes)` | Parse PNG image (FlateDecode) | | ||
| | `detectImageFormat(bytes)` | Detect JPEG or PNG from magic bytes | | ||
| | `buildImageXObject(img, smaskObj?)` | Build PDF Image XObject dictionary | | ||
| | `buildImageOperators(ref, x, y, w, h)` | Build `q cm Do Q` content stream operators | | ||
| ### Link Annotations | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `validateURL(url)` | Validate URL scheme (http/https/mailto only) | | ||
| | `buildLinkAnnotation(annot)` | Build PDF /Link annotation with /URI action | | ||
| | `buildInternalLinkAnnotation(link)` | Build PDF /Link with /GoTo action | | ||
| | `isLinkAnnotation(annot)` | Type guard for LinkAnnotation | | ||
| ### BiDi & Arabic/Hebrew Shaping | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `resolveBidiRuns(text)` | Resolve text into BiDi runs with levels | | ||
| | `containsRTL(text)` | Check if text contains RTL characters | | ||
| | `shapeArabicText(str, fontData)` | Arabic GSUB positional shaping | | ||
| | `containsArabic(text)` | Check for Arabic characters | | ||
| | `containsHebrew(text)` | Check for Hebrew characters | | ||
| ### Barcode & QR Code | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `renderBarcode(format, data, x, y, opts?)` | Unified barcode renderer (dispatches to format-specific function) | | ||
| | `encodeCode128(data)` | Encode data into Code 128 barcode pattern (ISO 15417) | | ||
| | `renderCode128(data, x, y, w, h)` | Render Code 128 barcode as PDF path operators | | ||
| | `ean13CheckDigit(digits)` | Compute EAN-13 check digit (ISO 15420) | | ||
| | `renderEAN13(data, x, y, w, h)` | Render EAN-13 barcode with guard bars and digits | | ||
| | `generateQR(data, ecLevel?)` | Generate QR Code matrix (ISO 18004) | | ||
| | `renderQR(data, x, y, size, ecLevel?)` | Render QR Code as PDF path operators | | ||
| | `generateDataMatrix(data)` | Generate Data Matrix ECC 200 matrix (ISO 16022) | | ||
| | `renderDataMatrix(data, x, y, size)` | Render Data Matrix as PDF path operators | | ||
| | `encodePDF417(data, ecLevel?)` | Encode data into PDF417 codewords (ISO 15438) | | ||
| | `renderPDF417(data, x, y, w, h, ecLevel?)` | Render PDF417 barcode as PDF path operators | | ||
| ### SVG Path Rendering | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `parseSvgPath(d)` | Parse SVG path `d` attribute into segments | | ||
| | `renderSvg(segments, options?)` | Render SVG segments as PDF path operators | | ||
| ### AcroForm Fields | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `buildFormWidget(field, objNum, pageRef)` | Build form field widget annotation + appearance stream | | ||
| | `buildAcroFormDict(fieldRefs)` | Build `/AcroForm` dictionary for catalog | | ||
| | `buildRadioGroupParent(group)` | Build radio button group parent object | | ||
| | `buildAppearanceStreamDict(width, height)` | Build appearance stream dictionary | | ||
| | `defaultFieldHeight(type)` | Default height by field type | | ||
| ### Digital Signatures | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `buildSigDict(options)` | Build `/Sig` dictionary with ByteRange/Contents placeholders | | ||
| | `signPdfBytes(pdf, options)` | Sign a PDF with CMS/PKCS#7 detached signature | | ||
| | `estimateContentsSize(options)` | Estimate hex-encoded `/Contents` size for pre-allocation | | ||
| ### Streaming Output | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `buildDocumentPDFStream(params, layout?, streamOpts?)` | Stream document PDF as `AsyncGenerator<Uint8Array>` | | ||
| | `buildPDFStream(params, layout?, streamOpts?)` | Stream table PDF as `AsyncGenerator<Uint8Array>` | | ||
| | `validateDocumentStreamable(params, layout?)` | Validate document is compatible with streaming (no TOC, no `{pages}`) | | ||
| | `validateTableStreamable(params, layout?)` | Validate table is compatible with streaming | | ||
| | `chunkBinaryString(str, chunkSize)` | Split binary string into `Uint8Array` chunks | | ||
| | `concatChunks(chunks)` | Concatenate `Uint8Array` chunks into one | | ||
| | `streamByteLength(stream)` | Count total bytes from an async stream | | ||
| ### Crypto (Hashing, ASN.1, RSA, ECDSA, X.509, CMS) | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `sha384(data)` / `sha512(data)` | SHA-384 / SHA-512 hash (FIPS 180-4) | | ||
| | `hmacSha256(key, data)` | HMAC-SHA-256 (RFC 2104) | | ||
| | `derDecode(data)` | Decode DER-encoded ASN.1 | | ||
| | `rsaSign(msg, key)` / `rsaVerify(msg, sig, key)` | RSA PKCS#1 v1.5 sign/verify | | ||
| | `ecdsaSign(hash, key)` / `ecdsaVerify(hash, sig, key)` | ECDSA P-256 sign/verify | | ||
| | `parseCertificate(der)` | Parse X.509 DER certificate | | ||
| | `buildCmsSignedData(options)` | Build CMS SignedData (PKCS#7) | | ||
| | `initCrypto()` | Initialize crypto module (lazy load) | | ||
| ### PDF Parser & Modifier | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `openPdf(bytes)` | Parse a PDF `Uint8Array` and return a `PdfReader` | | ||
| | `createModifier(reader)` | Create an incremental `PdfModifier` from a `PdfReader` | | ||
| | `createTokenizer(data, offset?)` | Create a low-level PDF tokenizer | | ||
| | `parseValue(tok)` | Parse a single PDF value from token stream | | ||
| | `parseIndirectObject(tok)` | Parse an indirect object (`N M obj ... endobj`) | | ||
| | `findStartxref(data)` | Find `startxref` offset in PDF bytes | | ||
| | `parseXrefTable(data, offset)` | Parse xref table/stream at given offset | | ||
| | `isRef(v)` / `isDict(v)` / `isArray(v)` / `isStream(v)` | Type guards for parsed PDF values | | ||
| | `dictGet(dict, key)` / `dictGetName(dict, key)` | Dictionary value accessors | | ||
| | `inflateSync(data)` | Decompress FlateDecode data (zlib inflate) | | ||
| ### Document Block Types | ||
| | Type | Description | | ||
| |------|-------------| | ||
| | `HeadingBlock` | H1/H2/H3 with color, auto-wrapped | | ||
| | `ParagraphBlock` | Text with fontSize, lineHeight, align, indent, color | | ||
| | `TableBlock` | Headers + rows using PdfRow/ColumnDef | | ||
| | `ListBlock` | Bullet or numbered items | | ||
| | `ImageBlock` | JPEG/PNG with optional width, height, align, alt text | | ||
| | `LinkBlock` | Hyperlink with URL, blue underline, tagged /Link | | ||
| | `SpacerBlock` | Vertical whitespace | | ||
| | `PageBreakBlock` | Force new page | | ||
| | `TocBlock` | Auto-generated table of contents with /GoTo links | | ||
| | `BarcodeBlock` | Barcode / QR code rendered via PDF path operators | | ||
| | `SvgBlock` | SVG path/shape rendering as native PDF path operators | | ||
| | `FormFieldBlock` | AcroForm interactive fields (text, checkbox, radio, dropdown, listbox) | | ||
| ### Tagged PDF & PDF/A | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `resolvePdfAConfig(tagged)` | Resolve tagged option → PDF/A config (version, part, conformance) | | ||
| | `encodePdfTextString(str)` | Encode string as PDF text (PDFDocEncoding or UTF-16BE hex) | | ||
| ### Encryption | ||
| Encryption is configured via the `encryption` option in layout options. Internal encryption functions are not part of the public API. | ||
| ```typescript | ||
| const pdf = buildPDFBytes(params, { | ||
| encryption: { userPassword: 'secret', ownerPassword: 'admin', permissions: { printing: true } } | ||
| }); | ||
| ``` | ||
| ### Color Utilities | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `parseColor(input)` | Parse hex / tuple / PDF string → validated PDF RGB string | | ||
| | `isValidPdfRgb(str)` | Check if string is valid `"R G B"` format (0.0–1.0) | | ||
| | `normalizeColors(colors)` | Validate and normalize all fields in a PdfColors object | | ||
| ### Compression | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `initNodeCompression()` | Initialize native zlib (async, call once in ESM before `compress: true`) | | ||
| | `setDeflateImpl(fn)` | Inject custom DEFLATE function (e.g. for browser polyfill) | | ||
| **Browser compression** — In browser environments without native zlib, inject a third-party DEFLATE via `setDeflateImpl`: | ||
| ```typescript | ||
| import { setDeflateImpl, buildPDFBytes } from 'pdfnative'; | ||
| import { deflateSync } from 'fflate'; // or pako | ||
| setDeflateImpl(deflateSync); | ||
| const pdf = buildPDFBytes(params, { compress: true }); | ||
| ``` | ||
| ### Fonts | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `registerFont(lang, loader)` | Register a font data loader | | ||
| | `registerFonts(map)` | Register multiple font loaders | | ||
| | `loadFontData(lang)` | Lazy-load font data (cached) | | ||
| | `hasFontLoader(lang)` | Check if loader is registered | | ||
| | `getRegisteredLangs()` | List registered language codes | | ||
| | `createEncodingContext(fontEntries)` | Create encoding context | | ||
| ### Shaping | ||
| | Function | Description | | ||
| |----------|-------------| | ||
| | `shapeThaiText(str, fontData)` | Thai OpenType shaping (GSUB + GPOS) | | ||
| | `shapeBengaliText(str, fontData)` | Bengali GSUB conjuncts + GPOS marks | | ||
| | `shapeTamilText(str, fontData)` | Tamil GSUB + split vowel decomposition | | ||
| | `shapeDevanagariText(str, fontData)` | Devanagari cluster shaping + GSUB/GPOS | | ||
| | `detectFallbackLangs(texts, primaryLang)` | Detect needed fallback fonts | | ||
| | `detectCharLang(codePoint)` | Map codepoint to preferred font language | | ||
| | `splitTextByFont(str, fontEntries)` | Multi-font text run splitting | | ||
| | `needsUnicodeFont(str)` | Check if text needs CIDFont | | ||
| | `containsThai(str)` | Check for Thai characters | | ||
| | `resolveBidiRuns(text)` | Resolve BiDi runs (UAX #9) | | ||
| | `containsRTL(text)` | Detect RTL content | | ||
| | `shapeArabicText(str, fontData)` | Arabic GSUB positional shaping | | ||
| | `containsArabic(text)` | Detect Arabic content | | ||
| | `containsHebrew(text)` | Detect Hebrew content | | ||
| ### Layout Constants | ||
| | Constant | Description | | ||
| |----------|-------------| | ||
| | `PG_W` / `PG_H` | A4 page dimensions (points) | | ||
| | `DEFAULT_MARGINS` | Default margins `{ t, r, b, l }` | | ||
| | `DEFAULT_COLORS` | Default color palette | | ||
| | `DEFAULT_COLUMNS` | Default 5-column layout | | ||
| | `ROW_H` / `TH_H` | Row / header heights | | ||
| | `HEADER_H` | Header zone height (15pt) | | ||
| | `PAGE_SIZES` | Preset page dimensions (A4, Letter, Legal, A3, Tabloid) | | ||
| | `resolveTemplate(tpl, page, pages, title, date)` | Resolve header/footer template placeholders | | ||
| ## Architecture | ||
| ``` | ||
| src/ | ||
| ├── index.ts # Public API — single entry point | ||
| ├── types/ | ||
| │ ├── pdf-types.ts # Core TypeScript type definitions | ||
| │ └── pdf-document-types.ts # Document builder type definitions (blocks, params) | ||
| ├── core/ | ||
| │ ├── pdf-builder.ts # Table-centric PDF assembly + /Info metadata + tagged PDF | ||
| │ ├── pdf-document.ts # Free-form document builder (headings, paragraphs, lists, tables, images) | ||
| │ ├── pdf-assembler.ts # Shared PDF binary assembly primitives (xref, trailer, writer) | ||
| │ ├── encoding-context.ts # Encoding context factory (dependency inversion from fonts/) | ||
| │ ├── pdf-image.ts # JPEG/PNG parsing + PDF Image XObject builder | ||
| │ ├── pdf-text.ts # Text rendering (Latin + CIDFont + shaped + tagged) | ||
| │ ├── pdf-stream.ts # Binary utilities + download | ||
| │ ├── pdf-stream-writer.ts # AsyncGenerator streaming output | ||
| │ ├── pdf-layout.ts # Layout constants & computation | ||
| │ ├── pdf-tags.ts # Tagged PDF: structure tree, XMP metadata, ICC profile | ||
| │ ├── pdf-annot.ts # Link annotations: /URI, /GoTo, URL validation + control-char hardening | ||
| │ ├── pdf-color.ts # Color parsing, validation, normalization | ||
| │ ├── pdf-compress.ts # FlateDecode stream compression (zlib, stored-block fallback) | ||
| │ ├── pdf-watermark.ts # Text/image watermarks with ExtGState transparency | ||
| │ ├── pdf-barcode.ts # Barcode/QR code encoders + PDF path rendering (5 formats) | ||
| │ ├── pdf-svg.ts # SVG path/shape rendering as native PDF operators | ||
| │ ├── pdf-form.ts # AcroForm interactive fields with appearance streams | ||
| │ ├── pdf-signature.ts # CMS/PKCS#7 digital signatures (RSA + ECDSA) | ||
| │ └── pdf-encrypt.ts # AES-128/256 encryption, MD5, SHA-256, key derivation | ||
| ├── crypto/ | ||
| │ ├── sha.ts # SHA-384, SHA-512, HMAC-SHA-256 | ||
| │ ├── asn1.ts # ASN.1 DER encoding/decoding | ||
| │ ├── rsa.ts # RSA PKCS#1 v1.5 sign/verify | ||
| │ ├── ecdsa.ts # ECDSA P-256 sign/verify | ||
| │ ├── x509.ts # X.509 certificate parsing | ||
| │ └── cms.ts # CMS SignedData (PKCS#7) builder | ||
| ├── parser/ | ||
| │ ├── pdf-inflate.ts # DEFLATE decompression (zlib inflate) | ||
| │ ├── pdf-tokenizer.ts # PDF lexical scanner (ISO 32000-1 §7.2) | ||
| │ ├── pdf-object-parser.ts # PDF object parser with type guards | ||
| │ ├── pdf-xref-parser.ts # Cross-reference table/stream parser | ||
| │ ├── pdf-reader.ts # High-level PDF reader (page tree, stream decode) | ||
| │ └── pdf-modifier.ts # Incremental modification (non-destructive save) | ||
| ├── fonts/ | ||
| │ ├── encoding.ts # WinAnsi + CIDFont pure encoding functions (no shaping deps) | ||
| │ ├── font-loader.ts # Configurable font registry + cache | ||
| │ ├── font-subsetter.ts # TTF subsetting engine (with buffer bounds checking) | ||
| │ └── font-embedder.ts # CMap builder + width arrays | ||
| ├── shaping/ | ||
| │ ├── script-registry.ts # Centralized Unicode range constants & script predicates | ||
| │ ├── thai-shaper.ts # Thai GSUB + GPOS shaping pipeline | ||
| │ ├── bengali-shaper.ts # Bengali GSUB conjuncts + GPOS mark positioning | ||
| │ ├── tamil-shaper.ts # Tamil GSUB + split vowel decomposition | ||
| │ ├── script-detect.ts # Unicode script range detection (uses script-registry) | ||
| │ ├── multi-font.ts # Cross-script font run splitting | ||
| │ ├── bidi.ts # Unicode Bidirectional Algorithm (UAX #9) | ||
| │ └── arabic-shaper.ts # Arabic GSUB positional shaping (uses script-registry) | ||
| └── worker/ | ||
| ├── worker-api.ts # Worker/main-thread dispatch | ||
| └── pdf-worker.ts # Self-contained worker entry | ||
| fonts/ # Pre-built font data modules (16 scripts) | ||
| tools/ # CLI: build-font-data.cjs (TTF → JS module) | ||
| scripts/ # Modular sample PDF generation (23 generators, 140+ PDFs) | ||
| tests/ # 1588+ tests (40 files: unit + integration + fuzz + parser) | ||
| bench/ # Performance benchmarks (vitest bench) | ||
| ``` | ||
| ## Development | ||
| ```bash | ||
| git clone https://github.com/Nizoka/pdfnative.git | ||
| cd pdfnative | ||
| npm install | ||
| npm run build # tsup → dist/ (ESM + CJS + .d.ts) | ||
| npm run test # vitest run (1588+ tests) | ||
| npm run test:coverage # vitest with v8 coverage (95%+) | ||
| npm run test:generate # Generate 140+ sample PDFs → test-output/ | ||
| npm run lint # ESLint 9 + typescript-eslint strict | ||
| npm run typecheck # tsc --noEmit (src/) | ||
| npm run typecheck:tests # tsc --project tsconfig.test.json | ||
| npm run typecheck:scripts # tsc --project tsconfig.scripts.json | ||
| npm run typecheck:all # Typecheck src/ + tests/ + scripts/ | ||
| npm run bench # Performance benchmarks (vitest bench) | ||
| ``` | ||
| ### Quality Metrics | ||
| | Metric | Value | | ||
| |--------|-------| | ||
| | Tests | 1588+ (40 files) | | ||
| | Statement coverage | 95.41% | | ||
| | Branch coverage | 87.79% | | ||
| | Function coverage | 98.5% | | ||
| | Fuzz tests | 48 edge-case scenarios | | ||
| | Benchmarks | Latin 500 rows ~10ms, Unicode ~13ms (Apple M1, Node 22) | | ||
| | Dependencies | 0 runtime | | ||
| | CI | Node 22/24 matrix | | ||
| | Provenance | npm signed builds | | ||
| ## Known Limitations — Visual vs. Semantic PDF | ||
| pdfnative generates **visually pixel-perfect** PDFs for all 16 supported scripts. However, PDF is fundamentally a *visual* format (a digital printer), not a *semantic* one. This distinction matters for **text extraction** (copy-paste, `pdftotext`, screen readers): | ||
| ### Complex Text Layout (CTL) scripts | ||
| For scripts with combining marks — **Thai**, **Devanagari**, **Vietnamese tones** — the shaper positions each mark in its own `BT…ET` block with precise GPOS offsets. PDF viewers **render** this correctly, but text extractors reconstruct content by spatial position rather than logical order. This can produce garbled output when copying text from the PDF. | ||
| | Scenario | Visual rendering | Text extraction (Ctrl+C) | | ||
| |----------|:---:|:---:| | ||
| | Latin, Greek, Polish, Turkish | ✅ Perfect | ✅ Perfect | | ||
| | CJK (Japanese, Chinese, Korean) | ✅ Perfect | ✅ Perfect | | ||
| | Vietnamese (combining diacritics) | ✅ Perfect | ⚠️ May show Win-1252 fallback artifacts | | ||
| | Thai (GSUB + GPOS shaping) | ✅ Perfect | ⚠️ Combining marks may be reordered | | ||
| | Devanagari (matras, conjuncts) | ✅ Perfect | ⚠️ Combining marks may be reordered | | ||
| | Bengali (conjuncts, GPOS marks) | ✅ Perfect | ⚠️ Combining marks may be reordered | | ||
| | Tamil (split vowels, GSUB) | ✅ Perfect | ⚠️ Split vowel recomposition may fail | | ||
| ### Why this happens | ||
| This is an inherent limitation of the PDF spec (ISO 32000-1), not a bug in pdfnative. The ToUnicode CMap correctly maps glyph IDs back to Unicode code points, but extractors that rely on spatial reconstruction rather than CMap lookup will produce artifacts. This behavior is shared by most PDF generators that don't use Tagged PDF. | ||
| ### Tagged PDF, /ActualText & PDF/A — Implemented ✅ | ||
| All three roadmap items are now implemented and available via the `tagged` layout option: | ||
| ```ts | ||
| const pdf = buildPDFBytes(params, { tagged: true }); // PDF/A-2b (default) | ||
| const pdf1b = buildPDFBytes(params, { tagged: 'pdfa1b' }); // PDF/A-1b (legacy) | ||
| const pdf2u = buildPDFBytes(params, { tagged: 'pdfa2u' }); // PDF/A-2u (Unicode) | ||
| ``` | ||
| When `tagged` is set, the output includes: | ||
| - **Tagged PDF (PDF/UA)** — full structure tree (`/Document → /Table → /TR → /TH|/TD`, `/H1-H3`, `/P`, `/L → /LI`, `/Figure`, `/Link`) with `/Span` marked content operators and `/StructParents` on every page | ||
| - **/ActualText** — original Unicode string attached as UTF-16BE hex to every `/Span BDC...EMC` sequence, solving text extraction for GPOS-repositioned glyphs (Thai, Arabic, Devanagari) | ||
| - **PDF/A-2b compliance** (default) — PDF 1.7, XMP metadata with `pdfaid:part=2` + `pdfaid:conformance=B`, sRGB ICC OutputIntent (`GTS_PDFA1`), `/MarkInfo << /Marked true >>` on Catalog | ||
| - **PDF/A-1b compatibility** — explicit `tagged: 'pdfa1b'` uses PDF 1.4, `pdfaid:part=1` | ||
| - **PDF/A-2u variant** — `tagged: 'pdfa2u'` uses PDF 1.7, `pdfaid:conformance=U` | ||
| The `tagged` option is backward-compatible — omitting it or setting `false` produces the same output as before. | ||
| ### PDF Encryption — Implemented ✅ | ||
| AES-128 and AES-256 encryption with owner/user passwords and granular permissions: | ||
| ```ts | ||
| const pdf = buildPDFBytes(params, { | ||
| encryption: { | ||
| ownerPassword: 'owner123', // Required — full access password | ||
| userPassword: 'user456', // Optional — password to open the PDF | ||
| algorithm: 'aes128', // 'aes128' (default) or 'aes256' | ||
| permissions: { | ||
| print: true, // Allow printing (default: true) | ||
| copy: false, // Allow copy/paste (default: false) | ||
| modify: false, // Allow modification (default: false) | ||
| extractText: true, // Allow text extraction (default: true) | ||
| }, | ||
| }, | ||
| }); | ||
| ``` | ||
| | Algorithm | PDF Version | Revision | Key Length | CFM | | ||
| |-----------|------------|----------|------------|-----| | ||
| | `aes128` | 1.4 | R4 (V4) | 128-bit | /AESV2 | | ||
| | `aes256` | 1.4 | R6 (V5) | 256-bit | /AESV3 | | ||
| **Note:** PDF/A and encryption are mutually exclusive (ISO 19005-1 §6.3.2). Setting both `tagged` and `encryption` will throw an error. | ||
| ## Typography Convention: En-Dash Separator | ||
| pdfnative uses **en-dash** `–` (U+2013) with surrounding spaces as the standard title and footer separator: | ||
| ``` | ||
| "Arabic Script Coverage – الأبجدية العربية" ✅ recommended | ||
| "Arabic Script Coverage — الأبجدية العربية" ⚠️ works, but wider gap | ||
| ``` | ||
| **Why en-dash?** | ||
| | Property | Em-dash `—` (U+2014) | En-dash `–` (U+2013) | | ||
| |----------|:---:|:---:| | ||
| | Helvetica width | 1000 units (1 em) | 556 units (0.56 em) | | ||
| | Visual gap at 16pt | ~24pt with spaces | ~18pt with spaces | | ||
| | WinAnsi encodable | ✅ (0x97) | ✅ (0x96) | | ||
| | International standard | US English only | ISO / Europe / technical | | ||
| | Cursive script rendering | Disproportionate gap | Balanced spacing | | ||
| The en-dash is **44% narrower** than the em-dash and follows ISO/international typography standards. This eliminates disproportionate visual gaps in cursive scripts (Arabic, Thai) where compact shaped text amplifies the perceived space around wider separators. | ||
| Both em-dash and en-dash are **fully supported** by the library (encoding, width metrics, BiDi classification) — this is a typographic recommendation for the best cross-script visual balance, not a restriction. | ||
| ## Stream Compression (FlateDecode) | ||
| Enable FlateDecode compression for dramatically smaller PDFs: | ||
| ```typescript | ||
| import { initNodeCompression, buildPDFBytes } from 'pdfnative'; | ||
| // Initialize native zlib (required once in ESM context) | ||
| await initNodeCompression(); | ||
| const pdf = buildPDFBytes(params, { compress: true }); | ||
| ``` | ||
| | Stream Type | Compressed? | Typical Reduction | | ||
| |-------------|:-----------:|:-----------------:| | ||
| | Page content (text operators) | ✅ | 80–90% | | ||
| | FontFile2 (TTF subset) | ✅ | 60–80% | | ||
| | ToUnicode CMap | ✅ | 80–90% | | ||
| | ICC sRGB profile | ✅ | 40–60% | | ||
| | XMP metadata | ❌ (tagged mode) | — | | ||
| | JPEG image | ❌ (already DCTDecode) | — | | ||
| | PNG image | ❌ (already FlateDecode) | — | | ||
| ### Compression + Encryption | ||
| Both features compose correctly — compression is applied **before** encryption per ISO 32000-1 §7.3.8: | ||
| ```typescript | ||
| const pdf = buildPDFBytes(params, { | ||
| compress: true, | ||
| encryption: { | ||
| ownerPassword: 'owner123', | ||
| algorithm: 'aes128', | ||
| }, | ||
| }); | ||
| ``` | ||
| ### Platform Support | ||
| | Runtime | Compression Method | Performance | | ||
| |---------|-------------------|-------------| | ||
| | Node.js 22+ | `zlib.deflateSync()` (native C) | Optimal | | ||
| | Browser | Stored-block fallback (valid FlateDecode) | No size reduction | | ||
| | Deno / Bun | CJS require fallback | Depends on compat layer | | ||
| For browser contexts with full compression, call `setDeflateImpl()` with a custom DEFLATE function. | ||
| ## Browser & Runtime Compatibility | ||
| pdfnative targets ES2020 and works in any environment that supports `Uint8Array`, `TextEncoder`, and `crypto.getRandomValues()`. | ||
| | Runtime | Version | Status | Notes | | ||
| |---------|---------|:------:|-------| | ||
| | Node.js | 22, 24+ | ✅ Tested in CI | Full support (ESM + CJS) | | ||
| | Chrome | 80+ | ✅ | ESM via bundler or `<script type="module">` | | ||
| | Firefox | 80+ | ✅ | ESM via bundler or `<script type="module">` | | ||
| | Safari | 14+ | ✅ | ESM via bundler or `<script type="module">` | | ||
| | Edge | 80+ | ✅ | Chromium-based | | ||
| | Deno | 1.0+ | ✅ | Native ESM imports | | ||
| | Bun | 1.0+ | ✅ | Native ESM imports | | ||
| | Web Workers | — | ✅ | Via `pdfnative/worker` entry point | | ||
| | React Native | — | ⚠️ | Requires `TextEncoder` polyfill | | ||
| **Bundle format:** ESM (`dist/index.js`) + CJS (`dist/index.cjs`) + TypeScript declarations (`dist/index.d.ts`). Tree-shakeable with `sideEffects: false`. | ||
| ## Origin | ||
| pdfnative was born inside [**plika.app**](https://plika.app) — a personal finance application where high-quality, multi-language PDF generation (bank statements, transaction reports) was a core requirement. Rather than depending on heavy third-party libraries, the PDF engine was built from scratch with zero dependencies, strict ISO compliance, and native support for 16 Unicode scripts. | ||
| The decision was then made to extract the engine into an independent open-source library so that everyone can benefit from production-grade PDF generation — not just plika.app users. | ||
| > **Where it all started** — the PDF engine that became pdfnative was originally built inside [plika.app](https://plika.app), a personal finance app generating multi-language bank statements and financial summaries across 16 scripts. | ||
| ## Security | ||
| - No `eval()`, `Function()`, or dynamic code execution | ||
| - Input validation at `buildPDF()` and `buildDocumentPDF()` entry: type checks, row/block limits | ||
| - URL validation at `validateURL()`: blocks `javascript:`, `file:`, `data:` URI schemes + control characters (U+0000–U+001F, U+007F–U+009F) | ||
| - RGBA PNG rejection: unsupported color types rejected at parse boundary with descriptive errors | ||
| - PDF string escaping for `\`, `(`, `)` — prevents injection | ||
| - CIDFont hex encoding — no string injection vector | ||
| - TTF subsetting uses typed arrays with bounds checking + compound glyph iteration limits | ||
| - XRef offset guard: validates byte offsets before writing cross-reference table | ||
| - JPEG parser robustness: validates SOF markers and handles edge-case byte sequences | ||
| - PDF encryption: AES-128/256 with per-object keys, random IVs — no ECB mode | ||
| - No external crypto dependencies — pure TypeScript AES, MD5, SHA-256 implementations | ||
| - NPM provenance — signed builds via GitHub Actions OIDC | ||
| For more details, see [SECURITY.md](SECURITY.md). | ||
| ## Contributing | ||
| We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for: | ||
| - Development environment setup | ||
| - Running tests, linting, and type checking | ||
| - Code style requirements (strict TypeScript, pure functions, ESM-first) | ||
| - Branch strategy and PR process | ||
| ## License | ||
| MIT — see [LICENSE](LICENSE). | ||
| Font data files in `fonts/` are licensed under [SIL Open Font License 1.1](https://scripts.sil.org/OFL). |
| #!/usr/bin/env node | ||
| /** | ||
| * BUILD FONT DATA — TTF → JS Data Module Generator | ||
| * ================================================== | ||
| * | ||
| * Parses a TrueType font (.ttf) file and extracts: | ||
| * - cmap: Unicode → Glyph ID mapping (for text encoding) | ||
| * - widths: Glyph ID → Advance Width (for text positioning) | ||
| * - metrics: unitsPerEm, ascent, descent, capHeight, bbox | ||
| * - Raw TTF binary as base64 (for FontFile2 embedding in PDF) | ||
| * | ||
| * Output: ES module exporting pre-built lookup tables | ||
| * | ||
| * Usage: | ||
| * node scripts/build-font-data.cjs <input.ttf> <output.js> [--var-name=fontData] | ||
| * | ||
| * Example: | ||
| * node scripts/build-font-data.cjs assets/fonts/NotoSansThai-Regular.ttf assets/fonts/noto-thai-data.js | ||
| * | ||
| * Dependencies: NONE (pure Node.js Buffer/fs) | ||
| * License: Font files must be OFL (Open Font License) or equivalent | ||
| */ | ||
| 'use strict'; | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| // ── TTF Binary Reader ──────────────────────────────────────────────── | ||
| class TTFReader { | ||
| constructor(buffer) { | ||
| this.buf = buffer; | ||
| this.pos = 0; | ||
| } | ||
| seek(offset) { this.pos = offset; } | ||
| skip(n) { this.pos += n; } | ||
| readUint8() { return this.buf.readUInt8(this.pos++); } | ||
| readUint16() { const v = this.buf.readUInt16BE(this.pos); this.pos += 2; return v; } | ||
| readInt16() { const v = this.buf.readInt16BE(this.pos); this.pos += 2; return v; } | ||
| readUint32() { const v = this.buf.readUInt32BE(this.pos); this.pos += 4; return v; } | ||
| readTag() { | ||
| const t = this.buf.toString('ascii', this.pos, this.pos + 4); | ||
| this.pos += 4; | ||
| return t; | ||
| } | ||
| } | ||
| // ── TTF Table Parser ───────────────────────────────────────────────── | ||
| function parseTTF(buffer) { | ||
| const r = new TTFReader(buffer); | ||
| // Offset table | ||
| const sfVersion = r.readUint32(); | ||
| if (sfVersion !== 0x00010000 && sfVersion !== 0x74727565) { | ||
| throw new Error(`Not a TrueType font (sfVersion: 0x${sfVersion.toString(16)})`); | ||
| } | ||
| const numTables = r.readUint16(); | ||
| r.skip(6); // searchRange, entrySelector, rangeShift | ||
| // Table directory | ||
| const tables = {}; | ||
| for (let i = 0; i < numTables; i++) { | ||
| const tag = r.readTag(); | ||
| r.skip(4); // checksum | ||
| const offset = r.readUint32(); | ||
| const length = r.readUint32(); | ||
| tables[tag] = { offset, length }; | ||
| } | ||
| // ── Parse 'head' table ─────────────────────────────────────────── | ||
| if (!tables['head']) throw new Error('Missing head table'); | ||
| r.seek(tables['head'].offset); | ||
| r.skip(18); // version, fontRevision, checksumAdjust, magic, flags | ||
| const unitsPerEm = r.readUint16(); | ||
| r.skip(16); // created, modified | ||
| const xMin = r.readInt16(); | ||
| const yMin = r.readInt16(); | ||
| const xMax = r.readInt16(); | ||
| const yMax = r.readInt16(); | ||
| // ── Parse 'hhea' table ─────────────────────────────────────────── | ||
| if (!tables['hhea']) throw new Error('Missing hhea table'); | ||
| r.seek(tables['hhea'].offset); | ||
| r.skip(4); // version | ||
| const ascent = r.readInt16(); | ||
| const descent = r.readInt16(); | ||
| r.skip(26); // lineGap + bunch of fields | ||
| const numberOfHMetrics = r.readUint16(); | ||
| // ── Parse 'maxp' table ─────────────────────────────────────────── | ||
| if (!tables['maxp']) throw new Error('Missing maxp table'); | ||
| r.seek(tables['maxp'].offset); | ||
| r.skip(4); // version | ||
| const numGlyphs = r.readUint16(); | ||
| // ── Parse 'OS/2' table (optional, for capHeight) ───────────────── | ||
| let capHeight = Math.round(ascent * 0.7); // fallback | ||
| let stemV = 80; | ||
| if (tables['OS/2']) { | ||
| r.seek(tables['OS/2'].offset); | ||
| const os2Version = r.readUint16(); | ||
| r.skip(30); // xAvgCharWidth, usWeightClass, ... through panose[10] | ||
| // Skip to sTypoAscender (offset 68 from table start) | ||
| r.seek(tables['OS/2'].offset + 68); | ||
| const sTypoAscender = r.readInt16(); | ||
| const sTypoDescender = r.readInt16(); | ||
| r.skip(4); // sTypoLineGap, usWinAscent | ||
| // capHeight at offset 88 (OS/2 v2+) | ||
| if (os2Version >= 2 && tables['OS/2'].length >= 90) { | ||
| r.seek(tables['OS/2'].offset + 88); | ||
| capHeight = r.readInt16(); | ||
| } | ||
| // usWeightClass at offset 4 | ||
| r.seek(tables['OS/2'].offset + 4); | ||
| const weightClass = r.readUint16(); | ||
| stemV = Math.round(weightClass * 0.12); | ||
| } | ||
| // ── Parse 'hmtx' table (glyph widths) ──────────────────────────── | ||
| if (!tables['hmtx']) throw new Error('Missing hmtx table'); | ||
| r.seek(tables['hmtx'].offset); | ||
| const widths = {}; | ||
| let lastWidth = 0; | ||
| for (let i = 0; i < numberOfHMetrics; i++) { | ||
| const advanceWidth = r.readUint16(); | ||
| r.skip(2); // lsb | ||
| widths[i] = advanceWidth; | ||
| lastWidth = advanceWidth; | ||
| } | ||
| // Remaining glyphs use the last advanceWidth | ||
| for (let i = numberOfHMetrics; i < numGlyphs; i++) { | ||
| widths[i] = lastWidth; | ||
| } | ||
| // ── Parse 'cmap' table ─────────────────────────────────────────── | ||
| if (!tables['cmap']) throw new Error('Missing cmap table'); | ||
| const cmapOffset = tables['cmap'].offset; | ||
| r.seek(cmapOffset); | ||
| r.skip(2); // version | ||
| const numSubtables = r.readUint16(); | ||
| // Find best subtable: prefer format 12 (full Unicode), fallback to format 4 (BMP) | ||
| let bestSubtableOffset = -1; | ||
| let bestFormat = 0; | ||
| for (let i = 0; i < numSubtables; i++) { | ||
| const platformID = r.readUint16(); | ||
| const encodingID = r.readUint16(); | ||
| const subtableOffset = r.readUint32(); | ||
| // Platform 3 (Windows) encoding 1 (UCS-2) or 10 (UCS-4) | ||
| // Platform 0 (Unicode) encoding 3 (UCS-2) or 4 (UCS-4) | ||
| if ((platformID === 3 && (encodingID === 1 || encodingID === 10)) || | ||
| (platformID === 0 && (encodingID === 3 || encodingID === 4))) { | ||
| // Check format | ||
| const savedPos = r.pos; | ||
| r.seek(cmapOffset + subtableOffset); | ||
| const format = r.readUint16(); | ||
| r.pos = savedPos; | ||
| if (format === 12 && bestFormat < 12) { | ||
| bestSubtableOffset = cmapOffset + subtableOffset; | ||
| bestFormat = 12; | ||
| } else if (format === 4 && bestFormat < 4) { | ||
| bestSubtableOffset = cmapOffset + subtableOffset; | ||
| bestFormat = 4; | ||
| } | ||
| } | ||
| } | ||
| if (bestSubtableOffset === -1) { | ||
| throw new Error('No suitable cmap subtable found (need format 4 or 12)'); | ||
| } | ||
| const cmap = {}; | ||
| if (bestFormat === 12) { | ||
| // Format 12: Segmented coverage (full Unicode) | ||
| r.seek(bestSubtableOffset); | ||
| r.skip(2); // format | ||
| r.skip(2); // reserved | ||
| r.skip(4); // length | ||
| r.skip(4); // language | ||
| const numGroups = r.readUint32(); | ||
| for (let i = 0; i < numGroups; i++) { | ||
| const startCharCode = r.readUint32(); | ||
| const endCharCode = r.readUint32(); | ||
| const startGlyphID = r.readUint32(); | ||
| for (let c = startCharCode; c <= endCharCode; c++) { | ||
| const gid = startGlyphID + (c - startCharCode); | ||
| if (gid > 0 && gid < numGlyphs) { | ||
| cmap[c] = gid; | ||
| } | ||
| } | ||
| } | ||
| } else { | ||
| // Format 4: Segment mapping to delta values (BMP only) | ||
| r.seek(bestSubtableOffset); | ||
| r.skip(2); // format | ||
| r.skip(2); // length | ||
| r.skip(2); // language | ||
| const segCountX2 = r.readUint16(); | ||
| const segCount = segCountX2 / 2; | ||
| r.skip(6); // searchRange, entrySelector, rangeShift | ||
| const endCodes = []; | ||
| for (let i = 0; i < segCount; i++) endCodes.push(r.readUint16()); | ||
| r.skip(2); // reservedPad | ||
| const startCodes = []; | ||
| for (let i = 0; i < segCount; i++) startCodes.push(r.readUint16()); | ||
| const idDeltas = []; | ||
| for (let i = 0; i < segCount; i++) idDeltas.push(r.readInt16()); | ||
| const idRangeOffsetPos = r.pos; | ||
| const idRangeOffsets = []; | ||
| for (let i = 0; i < segCount; i++) idRangeOffsets.push(r.readUint16()); | ||
| for (let i = 0; i < segCount; i++) { | ||
| if (startCodes[i] === 0xFFFF) break; | ||
| for (let c = startCodes[i]; c <= endCodes[i]; c++) { | ||
| let gid; | ||
| if (idRangeOffsets[i] === 0) { | ||
| gid = (c + idDeltas[i]) & 0xFFFF; | ||
| } else { | ||
| const offset = idRangeOffsetPos + i * 2 + idRangeOffsets[i] + (c - startCodes[i]) * 2; | ||
| r.seek(offset); | ||
| gid = r.readUint16(); | ||
| if (gid !== 0) { | ||
| gid = (gid + idDeltas[i]) & 0xFFFF; | ||
| } | ||
| } | ||
| if (gid > 0 && gid < numGlyphs) { | ||
| cmap[c] = gid; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // ── Compute default width ──────────────────────────────────────── | ||
| // Use width of space (U+0020) or glyph 0 | ||
| const spaceGid = cmap[0x20] || 0; | ||
| const defaultWidth = widths[spaceGid] || widths[0] || 600; | ||
| // ── Parse 'GSUB' table — Single Substitutions for Thai ─────────── | ||
| // We extract LookupType 1 (SingleSubst) corresponding to features: | ||
| // 'abvs' (above-base substitution), 'blwsSub', 'locl', and generic | ||
| // substitutions that remap consonant glyphs when stacked with | ||
| // above/below-base vowels or tone marks. | ||
| // Result: gsub[baseGid] = substituteGid (or vice-versa for above-base forms) | ||
| const gsub = parseTTFGSUB(r, tables); | ||
| // ── Parse 'GSUB' table — Ligature Substitutions for Indic ──────── | ||
| // We extract LookupType 4 (LigatureSubst) for conjunct formation: | ||
| // C + Halant + C → single ligature glyph (Bengali, Tamil, Devanagari) | ||
| const ligatures = parseTTFGSUBLigatures(r, tables); | ||
| // ── Parse 'GPOS' table — MarkToBase + MarkToMark anchor offsets ── | ||
| // LookupType 4 (MarkToBase): mark glyph anchored to base glyph | ||
| // LookupType 6 (MarkToMark): mark glyph anchored to another mark | ||
| // (used for Thai vowel+tone stacking: tone positioned relative to vowel) | ||
| const markAnchors = parseTTFGPOS(r, tables); | ||
| return { | ||
| metrics: { | ||
| unitsPerEm, | ||
| ascent, | ||
| descent, | ||
| capHeight, | ||
| stemV, | ||
| bbox: [xMin, yMin, xMax, yMax], | ||
| defaultWidth, | ||
| numGlyphs | ||
| }, | ||
| cmap, | ||
| widths, | ||
| gsub, | ||
| ligatures, | ||
| markAnchors | ||
| }; | ||
| } | ||
| // ── GSUB Parser — LookupType 1 (SingleSubst) ───────────────────────── | ||
| /** | ||
| * Parse GSUB table and extract SingleSubst (LookupType 1) mappings. | ||
| * These are used for Thai "short" consonant variants (e.g. ป→ป variant) | ||
| * when stacked with above-base marks whose descenders would clash. | ||
| * | ||
| * Returns a sparse object: { fromGid: toGid, ... } | ||
| * Only LookupType 1 (Single Substitution) is extracted. | ||
| * Lookup subtable Format 1 (delta) and Format 2 (explicit mapping) both handled. | ||
| */ | ||
| function parseTTFGSUB(r, tables) { | ||
| const gsub = {}; | ||
| if (!tables['GSUB']) return gsub; | ||
| try { | ||
| const base = tables['GSUB'].offset; | ||
| r.seek(base); | ||
| r.skip(4); // version (major.minor uint16+uint16) | ||
| const scriptListOffset = r.readUint16(); | ||
| const featureListOffset = r.readUint16(); | ||
| const lookupListOffset = r.readUint16(); | ||
| // ── Collect LookupType 1 lookup indices from FeatureList ───── | ||
| // We want ALL SingleSubst lookups regardless of feature tag: | ||
| // Thai uses 'abvm', 'blwm', 'abvs', 'blws', 'calt', 'locl', etc. | ||
| r.seek(base + featureListOffset); | ||
| const featureCount = r.readUint16(); | ||
| const singleSubstLookupIndices = new Set(); | ||
| // Collect per-feature lookup indices | ||
| for (let fi = 0; fi < featureCount; fi++) { | ||
| r.skip(4); // feature tag (4 bytes ASCII) | ||
| const featureOffset = r.readUint16(); | ||
| const savedPos = r.pos; | ||
| r.seek(base + featureListOffset + featureOffset); | ||
| r.skip(2); // featureParamsOffset | ||
| const lookupCount = r.readUint16(); | ||
| const indices = []; | ||
| for (let li = 0; li < lookupCount; li++) { | ||
| indices.push(r.readUint16()); | ||
| } | ||
| r.pos = savedPos; | ||
| // Store indices to check once we read LookupList | ||
| for (const idx of indices) singleSubstLookupIndices.add(idx); | ||
| } | ||
| // ── Read LookupList ────────────────────────────────────────── | ||
| r.seek(base + lookupListOffset); | ||
| const lookupCount = r.readUint16(); | ||
| const lookupOffsets = []; | ||
| for (let i = 0; i < lookupCount; i++) lookupOffsets.push(r.readUint16()); | ||
| for (let li = 0; li < lookupCount; li++) { | ||
| r.seek(base + lookupListOffset + lookupOffsets[li]); | ||
| const lookupType = r.readUint16(); | ||
| r.skip(2); // lookupFlag | ||
| const subtableCount = r.readUint16(); | ||
| const subtableOffsets = []; | ||
| for (let si = 0; si < subtableCount; si++) subtableOffsets.push(r.readUint16()); | ||
| // LookupType 1 = SingleSubst | ||
| if (lookupType !== 1) continue; | ||
| for (const stOffset of subtableOffsets) { | ||
| const stBase = base + lookupListOffset + lookupOffsets[li] + stOffset; | ||
| r.seek(stBase); | ||
| const substFormat = r.readUint16(); | ||
| const coverageOffset = r.readUint16(); | ||
| // Read Coverage table | ||
| const coverageGlyphs = readCoverageTable(r, stBase + coverageOffset); | ||
| if (substFormat === 1) { | ||
| // Format 1: apply delta to all covered glyphs | ||
| const delta = r.readInt16(); | ||
| for (const gid of coverageGlyphs) { | ||
| const sub = (gid + delta) & 0xFFFF; | ||
| if (sub > 0) gsub[gid] = sub; | ||
| } | ||
| } else if (substFormat === 2) { | ||
| // Format 2: explicit list | ||
| const glyphCount = r.readUint16(); | ||
| for (let gi = 0; gi < glyphCount && gi < coverageGlyphs.length; gi++) { | ||
| const sub = r.readUint16(); | ||
| if (sub > 0) gsub[coverageGlyphs[gi]] = sub; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| // Non-fatal: GSUB parsing failure degrades gracefully (no shaping) | ||
| console.warn('[build-font-data] GSUB parse error (non-fatal):', e.message); | ||
| } | ||
| return gsub; | ||
| } | ||
| // ── GSUB Parser — LookupType 4 (LigatureSubst) ─────────────────────── | ||
| /** | ||
| * Parse GSUB table and extract LigatureSubst (LookupType 4) mappings. | ||
| * These are used for Indic conjunct formation: C + Halant + C → ligature glyph. | ||
| * | ||
| * Returns a sparse object keyed by first glyph ID: | ||
| * { [firstGid]: [[resultGid, comp1, comp2, ...], ...] } | ||
| * Each entry is sorted longest-first for greedy matching. | ||
| * Only LookupType 4 (Ligature Substitution) is extracted. | ||
| * | ||
| * OpenType spec: §6.2.4 — LigatureSubstFormat1 | ||
| * Coverage table identifies first glyph; LigatureSet per covered glyph | ||
| * contains Ligature records: substituteGlyph + component[componentCount-1]. | ||
| */ | ||
| function parseTTFGSUBLigatures(r, tables) { | ||
| const ligatures = {}; | ||
| if (!tables['GSUB']) return ligatures; | ||
| try { | ||
| const base = tables['GSUB'].offset; | ||
| r.seek(base); | ||
| r.skip(4); // version (major.minor uint16+uint16) | ||
| r.skip(2); // scriptListOffset | ||
| r.skip(2); // featureListOffset | ||
| const lookupListOffset = r.readUint16(); | ||
| // ── Read LookupList ────────────────────────────────────────── | ||
| r.seek(base + lookupListOffset); | ||
| const lookupCount = r.readUint16(); | ||
| const lookupOffsets = []; | ||
| for (let i = 0; i < lookupCount; i++) lookupOffsets.push(r.readUint16()); | ||
| for (let li = 0; li < lookupCount; li++) { | ||
| const lookupBase = base + lookupListOffset + lookupOffsets[li]; | ||
| r.seek(lookupBase); | ||
| const lookupType = r.readUint16(); | ||
| r.skip(2); // lookupFlag | ||
| const subtableCount = r.readUint16(); | ||
| const subtableOffsets = []; | ||
| for (let si = 0; si < subtableCount; si++) subtableOffsets.push(r.readUint16()); | ||
| // LookupType 4 = LigatureSubst | ||
| if (lookupType !== 4) continue; | ||
| for (const stOffset of subtableOffsets) { | ||
| const stBase = lookupBase + stOffset; | ||
| r.seek(stBase); | ||
| const substFormat = r.readUint16(); | ||
| if (substFormat !== 1) continue; // Only Format 1 defined for LigatureSubst | ||
| const coverageOffset = r.readUint16(); | ||
| const ligSetCount = r.readUint16(); | ||
| const ligSetOffsets = []; | ||
| for (let lsi = 0; lsi < ligSetCount; lsi++) ligSetOffsets.push(r.readUint16()); | ||
| // Read Coverage table — identifies first glyph of each ligature | ||
| const coverageGlyphs = readCoverageTable(r, stBase + coverageOffset); | ||
| for (let lsi = 0; lsi < ligSetCount && lsi < coverageGlyphs.length; lsi++) { | ||
| const firstGid = coverageGlyphs[lsi]; | ||
| const ligSetBase = stBase + ligSetOffsets[lsi]; | ||
| r.seek(ligSetBase); | ||
| const ligCount = r.readUint16(); | ||
| const ligOffsets = []; | ||
| for (let lgi = 0; lgi < ligCount; lgi++) ligOffsets.push(r.readUint16()); | ||
| for (const ligOff of ligOffsets) { | ||
| r.seek(ligSetBase + ligOff); | ||
| const ligatureGlyph = r.readUint16(); | ||
| const componentCount = r.readUint16(); | ||
| // componentCount includes the first glyph, so components array has componentCount-1 entries | ||
| const components = []; | ||
| for (let ci = 0; ci < componentCount - 1; ci++) { | ||
| components.push(r.readUint16()); | ||
| } | ||
| if (!ligatures[firstGid]) ligatures[firstGid] = []; | ||
| // Store as [resultGid, comp1, comp2, ...] | ||
| ligatures[firstGid].push([ligatureGlyph, ...components]); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| // Sort entries for each first glyph: longest component list first (greedy matching) | ||
| for (const gid of Object.keys(ligatures)) { | ||
| ligatures[gid].sort((a, b) => b.length - a.length); | ||
| } | ||
| } catch (e) { | ||
| console.warn('[build-font-data] GSUB ligature parse error (non-fatal):', e.message); | ||
| } | ||
| return ligatures; | ||
| } | ||
| // ── GPOS Parser — LookupType 4 (MarkToBase) + LookupType 6 (MarkToMark) ── | ||
| /** | ||
| * Parse GPOS table and extract MarkToBase (LookupType 4) and | ||
| * MarkToMark (LookupType 6) anchor data. | ||
| * | ||
| * MarkToBase (Type 4): | ||
| * For each mark glyph, record its mark anchor point. | ||
| * For each base glyph, record attachment anchor for each mark class. | ||
| * | ||
| * MarkToMark (Type 6): | ||
| * For each mark2 glyph (the "combining" mark, e.g. a tone above a vowel), | ||
| * record how it attaches to a mark1 glyph (the "base" mark, e.g. an above vowel). | ||
| * Structure identical to MarkToBase but mark1 plays the role of base. | ||
| * | ||
| * Result: markAnchors = { | ||
| * marks: { [markGid]: { classIdx, x, y } }, | ||
| * bases: { [baseGid]: { [classIdx]: { x, y } } }, | ||
| * mark2mark: { mark1Anchors: { [mark1Gid]: { [classIdx]: { x, y } } }, | ||
| * mark2Classes: { [mark2Gid]: { classIdx, x, y } } } | ||
| * } | ||
| */ | ||
| function parseTTFGPOS(r, tables) { | ||
| const result = { marks: {}, bases: {}, mark2mark: { mark1Anchors: {}, mark2Classes: {} } }; | ||
| if (!tables['GPOS']) return result; | ||
| try { | ||
| const base = tables['GPOS'].offset; | ||
| r.seek(base); | ||
| r.skip(4); // version | ||
| r.skip(2); // scriptListOffset | ||
| r.skip(2); // featureListOffset | ||
| const lookupListOffset = r.readUint16(); | ||
| r.seek(base + lookupListOffset); | ||
| const lookupCount = r.readUint16(); | ||
| const lookupOffsets = []; | ||
| for (let i = 0; i < lookupCount; i++) lookupOffsets.push(r.readUint16()); | ||
| for (let li = 0; li < lookupCount; li++) { | ||
| r.seek(base + lookupListOffset + lookupOffsets[li]); | ||
| const lookupType = r.readUint16(); | ||
| r.skip(2); // lookupFlag | ||
| const subtableCount = r.readUint16(); | ||
| const stOffsets = []; | ||
| for (let si = 0; si < subtableCount; si++) stOffsets.push(r.readUint16()); | ||
| // LookupType 4 = MarkToBase, LookupType 6 = MarkToMark | ||
| if (lookupType !== 4 && lookupType !== 6) continue; | ||
| for (const stOff of stOffsets) { | ||
| const stBase = base + lookupListOffset + lookupOffsets[li] + stOff; | ||
| r.seek(stBase); | ||
| r.skip(2); // posFormat (always 1) | ||
| const mark1CoverageOffset = r.readUint16(); // mark coverage (Type 4) or mark2 coverage (Type 6) | ||
| const mark2CoverageOffset = r.readUint16(); // base coverage (Type 4) or mark1 coverage (Type 6) | ||
| const markClassCount = r.readUint16(); | ||
| const mark1ArrayOffset = r.readUint16(); | ||
| const mark2ArrayOffset = r.readUint16(); | ||
| // Read coverage tables | ||
| const mark1Glyphs = readCoverageTable(r, stBase + mark1CoverageOffset); | ||
| const mark2Glyphs = readCoverageTable(r, stBase + mark2CoverageOffset); | ||
| // Read Mark1Array (MarkArray): array of { markClass, markAnchorOffset } | ||
| r.seek(stBase + mark1ArrayOffset); | ||
| const markCount = r.readUint16(); | ||
| const mark1Data = []; // temp storage for mark1 entries | ||
| for (let mi = 0; mi < markCount && mi < mark1Glyphs.length; mi++) { | ||
| const markClass = r.readUint16(); | ||
| const anchorOffset = r.readUint16(); | ||
| const savedPos = r.pos; | ||
| r.seek(stBase + mark1ArrayOffset + anchorOffset); | ||
| const anchorFormat = r.readUint16(); | ||
| const ax = anchorFormat >= 1 ? r.readInt16() : 0; | ||
| const ay = anchorFormat >= 1 ? r.readInt16() : 0; | ||
| r.pos = savedPos; | ||
| mark1Data.push({ gid: mark1Glyphs[mi], classIdx: markClass, x: ax, y: ay }); | ||
| } | ||
| // Read Mark2Array / BaseArray | ||
| r.seek(stBase + mark2ArrayOffset); | ||
| const baseCount = r.readUint16(); | ||
| const baseRecords = []; | ||
| for (let bi = 0; bi < baseCount; bi++) { | ||
| const recs = []; | ||
| for (let mc = 0; mc < markClassCount; mc++) recs.push(r.readUint16()); | ||
| baseRecords.push(recs); | ||
| } | ||
| if (lookupType === 4) { | ||
| // MarkToBase: store in marks + bases | ||
| for (const md of mark1Data) { | ||
| result.marks[md.gid] = { classIdx: md.classIdx, x: md.x, y: md.y }; | ||
| } | ||
| for (let bi = 0; bi < baseCount && bi < mark2Glyphs.length; bi++) { | ||
| const baseGid = mark2Glyphs[bi]; | ||
| result.bases[baseGid] = {}; | ||
| for (let mc = 0; mc < markClassCount; mc++) { | ||
| const anchorOff = baseRecords[bi][mc]; | ||
| if (!anchorOff) continue; | ||
| r.seek(stBase + mark2ArrayOffset + anchorOff); | ||
| r.skip(2); // anchorFormat | ||
| const bx = r.readInt16(); | ||
| const by = r.readInt16(); | ||
| result.bases[baseGid][mc] = { x: bx, y: by }; | ||
| } | ||
| } | ||
| } else { | ||
| // LookupType 6: MarkToMark | ||
| // mark1Glyphs = the "combining" marks (mark2 in spec: tones) | ||
| // mark2Glyphs = the "base" marks (mark1 in spec: vowels) | ||
| for (const md of mark1Data) { | ||
| result.mark2mark.mark2Classes[md.gid] = { classIdx: md.classIdx, x: md.x, y: md.y }; | ||
| } | ||
| for (let bi = 0; bi < baseCount && bi < mark2Glyphs.length; bi++) { | ||
| const m1Gid = mark2Glyphs[bi]; | ||
| result.mark2mark.mark1Anchors[m1Gid] = {}; | ||
| for (let mc = 0; mc < markClassCount; mc++) { | ||
| const anchorOff = baseRecords[bi][mc]; | ||
| if (!anchorOff) continue; | ||
| r.seek(stBase + mark2ArrayOffset + anchorOff); | ||
| r.skip(2); // anchorFormat | ||
| const mx = r.readInt16(); | ||
| const my = r.readInt16(); | ||
| result.mark2mark.mark1Anchors[m1Gid][mc] = { x: mx, y: my }; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| console.warn('[build-font-data] GPOS parse error (non-fatal):', e.message); | ||
| } | ||
| return result; | ||
| } | ||
| // ── Coverage Table Reader (shared by GSUB + GPOS) ──────────────────── | ||
| /** | ||
| * Read an OpenType Coverage table and return the list of covered glyph IDs. | ||
| * Format 1: explicit list. Format 2: ranges. | ||
| * @param {TTFReader} r | ||
| * @param {number} absOffset - Absolute byte offset in the file | ||
| * @returns {number[]} Sorted array of glyph IDs | ||
| */ | ||
| function readCoverageTable(r, absOffset) { | ||
| const glyphs = []; | ||
| r.seek(absOffset); | ||
| const format = r.readUint16(); | ||
| if (format === 1) { | ||
| const count = r.readUint16(); | ||
| for (let i = 0; i < count; i++) glyphs.push(r.readUint16()); | ||
| } else if (format === 2) { | ||
| const rangeCount = r.readUint16(); | ||
| for (let i = 0; i < rangeCount; i++) { | ||
| const start = r.readUint16(); | ||
| const end = r.readUint16(); | ||
| r.skip(2); // startCoverageIndex | ||
| for (let g = start; g <= end; g++) glyphs.push(g); | ||
| } | ||
| } | ||
| return glyphs; | ||
| } | ||
| // ── JS Module Generator ────────────────────────────────────────────── | ||
| function generateModule(fontName, parsed, ttfBase64) { | ||
| const { metrics, cmap, widths, gsub, ligatures, markAnchors } = parsed; | ||
| // Compact cmap: only entries where glyph exists | ||
| const cmapEntries = Object.entries(cmap) | ||
| .map(([k, v]) => `${k}:${v}`) | ||
| .join(','); | ||
| // Compact widths: group consecutive same-width glyphs | ||
| // For simplicity, use sparse format: only glyph IDs with non-default widths | ||
| const defaultW = metrics.defaultWidth; | ||
| const widthEntries = Object.entries(widths) | ||
| .filter(([, w]) => w !== defaultW) | ||
| .map(([k, v]) => `${k}:${v}`) | ||
| .join(','); | ||
| // Compact gsub: sparse object { fromGid: toGid } | ||
| const gsubEntries = Object.entries(gsub || {}) | ||
| .map(([k, v]) => `${k}:${v}`) | ||
| .join(','); | ||
| // Compact ligatures: { firstGid: [[resultGid, comp1, ...], ...] } | ||
| const ligaturesEntries = Object.entries(ligatures || {}) | ||
| .map(([gid, ligs]) => { | ||
| const inner = ligs.map(lig => `[${lig.join(',')}]`).join(','); | ||
| return `${gid}:[${inner}]`; | ||
| }) | ||
| .join(','); | ||
| // Compact markAnchors — split into marks and bases sub-maps | ||
| // marks[gid] = { classIdx, x, y } | ||
| const marksEntries = Object.entries((markAnchors && markAnchors.marks) || {}) | ||
| .map(([gid, a]) => `${gid}:[${a.classIdx},${a.x},${a.y}]`) | ||
| .join(','); | ||
| // bases[gid] = { classIdx: { x, y } } — serialise as { gid: { mc: [x,y] } } | ||
| const basesEntries = Object.entries((markAnchors && markAnchors.bases) || {}) | ||
| .map(([gid, anchors]) => { | ||
| const inner = Object.entries(anchors) | ||
| .map(([mc, a]) => `${mc}:[${a.x},${a.y}]`) | ||
| .join(','); | ||
| return `${gid}:{${inner}}`; | ||
| }) | ||
| .join(','); | ||
| // Compact mark2mark — LookupType 6 (MarkToMark) anchors | ||
| // mark1Anchors[mark1Gid] = { classIdx: [x, y] } (the "base" mark, e.g. above vowel) | ||
| // mark2Classes[mark2Gid] = [classIdx, x, y] (the "combining" mark, e.g. tone) | ||
| const m2m = (markAnchors && markAnchors.mark2mark) || { mark1Anchors: {}, mark2Classes: {} }; | ||
| const m2mMark1Entries = Object.entries(m2m.mark1Anchors) | ||
| .map(([gid, anchors]) => { | ||
| const inner = Object.entries(anchors) | ||
| .map(([mc, a]) => `${mc}:[${a.x},${a.y}]`) | ||
| .join(','); | ||
| return `${gid}:{${inner}}`; | ||
| }) | ||
| .join(','); | ||
| const m2mMark2Entries = Object.entries(m2m.mark2Classes) | ||
| .map(([gid, a]) => `${gid}:[${a.classIdx},${a.x},${a.y}]`) | ||
| .join(','); | ||
| // Build W array for PDF CIDFont /W entry | ||
| // Format: [gid [width]] for each unique width | ||
| // For efficiency, group consecutive glyphs with individual widths | ||
| const wArray = buildPDFWidthArray(widths, metrics.numGlyphs, defaultW); | ||
| return `/** | ||
| * PRE-BUILT FONT DATA — ${fontName} | ||
| * =================================== | ||
| * Generated by: scripts/build-font-data.cjs | ||
| * Source: ${fontName}.ttf | ||
| * License: SIL Open Font License 1.1 | ||
| * | ||
| * DO NOT EDIT — Regenerate with: | ||
| * node scripts/build-font-data.cjs assets/fonts/${fontName}.ttf assets/fonts/${fontName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-data.js | ||
| */ | ||
| // Font metrics | ||
| export const metrics = ${JSON.stringify(metrics)}; | ||
| // Font name for PDF /BaseFont | ||
| export const fontName = '${fontName.replace(/[^A-Za-z0-9-]/g, '')}'; | ||
| // Unicode codepoint → Glyph ID mapping (sparse object, ~O(1) lookup) | ||
| export const cmap = {${cmapEntries}}; | ||
| // Glyph ID → Advance Width (only non-default widths; default = ${defaultW}) | ||
| export const defaultWidth = ${defaultW}; | ||
| export const widths = {${widthEntries}}; | ||
| // GSUB SingleSubst: fromGid → substituteGid | ||
| // Used by the Thai mini-shaper to select below-clash variants of consonants. | ||
| export const gsub = {${gsubEntries}}; | ||
| // GSUB LigatureSubst: firstGid → [[resultGid, comp1, comp2, ...], ...] | ||
| // Used by Indic shapers for conjunct formation (C + Halant + C → ligature). | ||
| // Entries sorted longest-first for greedy matching. | ||
| export const ligatures = {${ligaturesEntries}}; | ||
| // GPOS MarkToBase anchors — used by the Thai mini-shaper for mark positioning. | ||
| // marks[gid] = [classIdx, anchorX, anchorY] (design units) | ||
| // bases[gid] = { classIdx: [anchorX, anchorY] } | ||
| export const markAnchors = { | ||
| marks: {${marksEntries}}, | ||
| bases: {${basesEntries}} | ||
| }; | ||
| // GPOS MarkToMark anchors — used for Thai vowel+tone stacking. | ||
| // mark1Anchors[mark1Gid] = { classIdx: [anchorX, anchorY] } (base mark, e.g. above vowel) | ||
| // mark2Classes[mark2Gid] = [classIdx, anchorX, anchorY] (combining mark, e.g. tone) | ||
| export const mark2mark = { | ||
| mark1Anchors: {${m2mMark1Entries}}, | ||
| mark2Classes: {${m2mMark2Entries}} | ||
| }; | ||
| // PDF /W array string (pre-formatted for CIDFont object) | ||
| export const pdfWidthArray = '${wArray}'; | ||
| // Raw TTF binary as base64 (for PDF FontFile2 embedding) | ||
| export const ttfBase64 = '${ttfBase64}'; | ||
| // Utility: get glyph width | ||
| export function getGlyphWidth(glyphId) { | ||
| return widths[glyphId] !== undefined ? widths[glyphId] : ${defaultW}; | ||
| } | ||
| // Utility: get glyph ID for unicode code point | ||
| export function getGlyphId(codePoint) { | ||
| return cmap[codePoint] || 0; | ||
| } | ||
| `; | ||
| } | ||
| /** | ||
| * Build PDF /W array for CIDFont width definitions. | ||
| * Groups consecutive glyphs: [startGid [w1 w2 w3 ...]] | ||
| */ | ||
| function buildPDFWidthArray(widths, numGlyphs, defaultWidth) { | ||
| // Collect all glyph widths as array | ||
| const allWidths = []; | ||
| for (let i = 0; i < numGlyphs; i++) { | ||
| allWidths.push(widths[i] !== undefined ? widths[i] : defaultWidth); | ||
| } | ||
| // Build grouped /W array entries | ||
| const parts = []; | ||
| let i = 0; | ||
| while (i < numGlyphs) { | ||
| // Skip glyphs with default width | ||
| if (allWidths[i] === defaultWidth) { i++; continue; } | ||
| // Find consecutive run of non-default widths | ||
| let j = i; | ||
| while (j < numGlyphs && allWidths[j] !== defaultWidth) j++; | ||
| // Emit group: startGid [w_i w_{i+1} ... w_{j-1}] | ||
| const ws = allWidths.slice(i, j).join(' '); | ||
| parts.push(`${i} [${ws}]`); | ||
| i = j; | ||
| } | ||
| return parts.join(' '); | ||
| } | ||
| // ── Main ───────────────────────────────────────────────────────────── | ||
| function main() { | ||
| const args = process.argv.slice(2); | ||
| if (args.length < 2) { | ||
| console.error('Usage: node scripts/build-font-data.cjs <input.ttf> <output.js>'); | ||
| console.error('Example: node scripts/build-font-data.cjs assets/fonts/NotoSansThai-Regular.ttf assets/fonts/noto-thai-data.js'); | ||
| process.exit(2); | ||
| } | ||
| const inputPath = path.resolve(args[0]); | ||
| const outputPath = path.resolve(args[1]); | ||
| if (!fs.existsSync(inputPath)) { | ||
| console.error(`❌ Input file not found: ${inputPath}`); | ||
| process.exit(2); | ||
| } | ||
| console.log(`📖 Reading: ${inputPath}`); | ||
| const buffer = fs.readFileSync(inputPath); | ||
| console.log(` Size: ${(buffer.length / 1024).toFixed(1)} KB`); | ||
| console.log('🔍 Parsing TTF tables...'); | ||
| const parsed = parseTTF(buffer); | ||
| console.log(` unitsPerEm: ${parsed.metrics.unitsPerEm}`); | ||
| console.log(` numGlyphs: ${parsed.metrics.numGlyphs}`); | ||
| console.log(` cmap entries: ${Object.keys(parsed.cmap).length}`); | ||
| console.log(` ascent: ${parsed.metrics.ascent}, descent: ${parsed.metrics.descent}`); | ||
| console.log(` capHeight: ${parsed.metrics.capHeight}`); | ||
| console.log(` bbox: [${parsed.metrics.bbox.join(', ')}]`); | ||
| console.log('📦 Encoding TTF to base64...'); | ||
| const ttfBase64 = buffer.toString('base64'); | ||
| console.log(` base64 size: ${(ttfBase64.length / 1024).toFixed(1)} KB`); | ||
| // Derive font name from filename | ||
| const fontName = path.basename(inputPath, '.ttf'); | ||
| console.log('📝 Generating JS module...'); | ||
| const moduleCode = generateModule(fontName, parsed, ttfBase64); | ||
| // Ensure output directory exists | ||
| const outputDir = path.dirname(outputPath); | ||
| if (!fs.existsSync(outputDir)) { | ||
| fs.mkdirSync(outputDir, { recursive: true }); | ||
| } | ||
| fs.writeFileSync(outputPath, moduleCode, 'utf8'); | ||
| console.log(`✅ Written: ${outputPath} (${(moduleCode.length / 1024).toFixed(1)} KB)`); | ||
| console.log(''); | ||
| console.log('📋 Summary:'); | ||
| console.log(` Font: ${fontName}`); | ||
| console.log(` Glyphs: ${parsed.metrics.numGlyphs}`); | ||
| console.log(` Cmap entries: ${Object.keys(parsed.cmap).length}`); | ||
| console.log(` TTF binary: ${(buffer.length / 1024).toFixed(1)} KB`); | ||
| console.log(` Output module: ${(moduleCode.length / 1024).toFixed(1)} KB`); | ||
| } | ||
| main(); |
+103
-6
| { | ||
| "name": "pdfnative", | ||
| "version": "0.0.1", | ||
| "description": "Reserved name for upcoming Local-First PDF engine", | ||
| "main": "index.ts", | ||
| "author": "nizoka", | ||
| "license": "MIT" | ||
| } | ||
| "version": "1.0.0", | ||
| "description": "Zero-dependency native PDF generation library with multi-font Unicode support (Thai shaping, CJK, Greek, Devanagari). Pure JavaScript ISO 32000-1 implementation.", | ||
| "type": "module", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "exports": { | ||
| ".": { | ||
| "import": { | ||
| "types": "./dist/index.d.ts", | ||
| "default": "./dist/index.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/index.d.cts", | ||
| "default": "./dist/index.cjs" | ||
| } | ||
| }, | ||
| "./worker": { | ||
| "import": { | ||
| "types": "./dist/worker/index.d.ts", | ||
| "default": "./dist/worker/index.js" | ||
| }, | ||
| "require": { | ||
| "types": "./dist/worker/index.d.cts", | ||
| "default": "./dist/worker/index.cjs" | ||
| } | ||
| } | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "fonts/*.js", | ||
| "fonts/*.d.ts", | ||
| "tools", | ||
| "LICENSE", | ||
| "README.md" | ||
| ], | ||
| "sideEffects": false, | ||
| "bin": { | ||
| "pdfnative-build-font": "./tools/build-font-data.cjs" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsup", | ||
| "dev": "tsup --watch", | ||
| "test": "vitest run", | ||
| "test:watch": "vitest", | ||
| "test:coverage": "vitest run --coverage", | ||
| "test:visual": "vitest run --config vitest.visual.config.ts", | ||
| "test:generate": "npx tsx scripts/generate-samples.ts", | ||
| "fonts:download": "npx tsx scripts/download-fonts.ts", | ||
| "lint": "eslint src/", | ||
| "typecheck": "tsc --noEmit", | ||
| "typecheck:tests": "tsc --project tsconfig.test.json --noEmit", | ||
| "typecheck:scripts": "tsc --project tsconfig.scripts.json --noEmit", | ||
| "typecheck:all": "npm run typecheck && npm run typecheck:tests && npm run typecheck:scripts", | ||
| "bench": "vitest bench", | ||
| "prepublishOnly": "npm run build" | ||
| }, | ||
| "keywords": [ | ||
| "pdf", | ||
| "pdf-generation", | ||
| "zero-dependency", | ||
| "native-pdf", | ||
| "unicode", | ||
| "thai", | ||
| "cjk", | ||
| "multi-font", | ||
| "opentype", | ||
| "iso-32000", | ||
| "web-worker", | ||
| "streaming", | ||
| "typescript" | ||
| ], | ||
| "author": "Nizoka <hello@pdfnative.dev> (https://pdfnative.dev)", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/Nizoka/pdfnative.git" | ||
| }, | ||
| "homepage": "https://github.com/Nizoka/pdfnative#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/Nizoka/pdfnative/issues" | ||
| }, | ||
| "funding": { | ||
| "type": "individual", | ||
| "url": "https://plika.app" | ||
| }, | ||
| "engines": { | ||
| "node": ">=22" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.0.0", | ||
| "@vitest/coverage-v8": "^2.1.9", | ||
| "eslint": "^9.0.0", | ||
| "tsup": "^8.0.0", | ||
| "tsx": "^4.21.0", | ||
| "typescript": "^5.4.0", | ||
| "typescript-eslint": "^8.57.2", | ||
| "vitest": "^2.1.9" | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Mixed license
LicensePackage contains multiple licenses.
Non-permissive License
LicenseA license not known to be considered permissive was found.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
No website
QualityPackage does not have a website.
61750567
34116235.36%49
2350%55770
Infinity%0
-100%0
-100%0
-100%1083
Infinity%Yes
NaN8
Infinity%2
Infinity%90
-10%25
2400%