@hidemikimura/receipt-html-to-pdf 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/examples/cdn.html CHANGED
@@ -61,11 +61,11 @@
61
61
 
62
62
  <!--
63
63
  ここが CDN からの読み込み。type="module" が要る。
64
- バージョン(@0.3.0)は必ず固定する。外すと最新版が読み込まれ、更新のたびに挙動が変わりうる。
64
+ バージョン(@0.4.0)は必ず固定する。外すと最新版が読み込まれ、更新のたびに挙動が変わりうる。
65
65
  -->
66
66
  <script type="module">
67
67
  import * as ReceiptHtmlToPdf
68
- from 'https://cdn.jsdelivr.net/npm/@hidemikimura/receipt-html-to-pdf@0.3.0/dist/receipt-html-to-pdf.min.js';
68
+ from 'https://cdn.jsdelivr.net/npm/@hidemikimura/receipt-html-to-pdf@0.4.0/dist/receipt-html-to-pdf.min.js';
69
69
 
70
70
  // type="module" でない普通のスクリプトからも使えるようにグローバルへ載せる
71
71
  window.ReceiptHtmlToPdf = ReceiptHtmlToPdf;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hidemikimura/receipt-html-to-pdf",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Receipt html to pdf — ブラウザ内で HTML/CSS をテキスト選択可能なベクター PDF に変換するライブラリ(日本の領収証・適格請求書向け)",
5
5
  "license": "MIT",
6
6
  "author": "Hidemi Kimura",
@@ -45,7 +45,7 @@ npm を使わない場合は CDN の URL をそのまま `import` できる(`d
45
45
  ```html
46
46
  <script type="module">
47
47
  import { registerFont, htmlToPdf, downloadPdf }
48
- from 'https://cdn.jsdelivr.net/npm/@hidemikimura/receipt-html-to-pdf@0.3.0/dist/receipt-html-to-pdf.min.js';
48
+ from 'https://cdn.jsdelivr.net/npm/@hidemikimura/receipt-html-to-pdf@0.4.0/dist/receipt-html-to-pdf.min.js';
49
49
  </script>
50
50
  ```
51
51
 
@@ -92,6 +92,8 @@ npm を使わない場合は CDN の URL をそのまま `import` できる(`d
92
92
  | `baseUrl` | 現在の文書 | 相対 URL(フォント・画像)の基準 |
93
93
  | `output` | `'blob'` | `'uint8array'` `'dataurl'` |
94
94
  | `onWarning` | — | 未対応 CSS・欠落グリフ・画像失敗の通知。**必ず配線する** |
95
+ | `links` | `true` | `<a href>` を PDF のリンク注釈にする(外部 URL・`mailto:`・文書内 `#id`) |
96
+ | `outline` | `false` | `h1`〜`h6` からしおり(PDF の目次)を作る |
95
97
  | `onProgress` | — | `render` → `walk` → `layout`(`totalPages` 確定)→ `page`(1 ページずつ)→ `done`。長い文書の進捗表示に使う |
96
98
 
97
99
  ## 対応している CSS の要点
@@ -129,6 +131,12 @@ footer: '<div style="text-align:center;font-size:8pt">{{pageNumber}} / {{totalPa
129
131
 
130
132
  `orphans` / `widows` / `@page` は未対応。用紙サイズと余白は `options.page` で指定する。
131
133
 
134
+ ## リンクとしおり
135
+
136
+ `<a href>` は既定で PDF のリンク注釈になる(外部 URL・`mailto:` / `tel:`・文書内 `#id`)。折り返した行やページを跨ぐリンクは分割して注釈を作り、ヘッダー/フッターの中のリンクも各ページに出る。`javascript:` と、飛び先の無い `#id` は注釈にしない。止めるなら `links: false`。
137
+
138
+ `outline: true` を渡すと `h1`〜`h6` の入れ子からしおり(ビューアのサイドバーに出る目次)を作る。長い明細書で便利。既定では作らない。
139
+
132
140
  ## 長い文書
133
141
 
134
142
  変換は途中でイベントループへ戻すので、数百ページでも画面は固まらない(所要時間は 1 割ほど増える)。進捗表示を出すなら `onProgress` を配線する。
package/src/index.js CHANGED
@@ -82,6 +82,8 @@ export { expandPrintMediaCss } from './renderer.js';
82
82
  * @property {'blob'|'uint8array'|'dataurl'} [output='blob']
83
83
  * @property {string} [baseUrl] 相対 URL(フォント・画像)の基準。既定は現在の文書
84
84
  * @property {(warning: ConversionWarning) => void} [onWarning]
85
+ * @property {boolean} [links=true] `<a href>` を PDF のリンク注釈にする
86
+ * @property {boolean} [outline=false] 見出し(h1〜h6)からしおり(PDF の目次)を作る
85
87
  * @property {(progress: ConversionProgress) => void} [onProgress] 進捗通知。長い文書では途中でイベントループへ戻すので、UI を更新できる
86
88
  */
87
89
 
@@ -91,7 +93,7 @@ export { expandPrintMediaCss } from './renderer.js';
91
93
  */
92
94
 
93
95
  /** ライブラリのバージョン(package.json と同期) */
94
- export const version = '0.3.0';
96
+ export const version = '0.4.0';
95
97
 
96
98
  /** モジュール共有のフォントレジストリ */
97
99
  const registry = new FontRegistry();
@@ -176,6 +178,8 @@ export async function htmlToPdf(input, options = {}) {
176
178
  metadata: options.metadata,
177
179
  header,
178
180
  footer,
181
+ links: options.links ?? true,
182
+ outline: options.outline ?? false,
179
183
  pacer,
180
184
  progress,
181
185
  warn,
package/src/page.js CHANGED
@@ -11,6 +11,7 @@ import { PX_TO_PT, PAGE_SIZES, lengthToPt, num } from './units.js';
11
11
  import { paginate } from './paginate.js';
12
12
  import { releasePixels, loadImage } from './walker/image.js';
13
13
  import { buildAxialShading, uniformAlpha, buildAlphaMaskGState } from './pdf/shading.js';
14
+ import { buildLinkAnnot, buildOutline } from './pdf/outline.js';
14
15
 
15
16
  /**
16
17
  * @typedef {object} PageGeometry
@@ -58,7 +59,7 @@ export function resolvePage(page = {}) {
58
59
  /**
59
60
  * @param {import('./walker/walk.js').WalkResult} body
60
61
  * @param {PageGeometry} geo
61
- * @param {{compress: boolean, metadata?: import('./index.js').PdfMetadata, header?: PageDecoration|null, footer?: PageDecoration|null, pacer?: import('./pacer.js').Pacer, progress?: (p: import('./index.js').ConversionProgress) => void, warn?: (w: import('./index.js').ConversionWarning) => void}} opts
62
+ * @param {{compress: boolean, metadata?: import('./index.js').PdfMetadata, header?: PageDecoration|null, footer?: PageDecoration|null, pacer?: import('./pacer.js').Pacer, progress?: (p: import('./index.js').ConversionProgress) => void, warn?: (w: import('./index.js').ConversionWarning) => void, links?: boolean, outline?: boolean}} opts
62
63
  * @returns {Promise<Uint8Array>}
63
64
  */
64
65
  export async function buildPdf(body, geo, opts) {
@@ -177,6 +178,26 @@ export async function buildPdf(body, geo, opts) {
177
178
  const pagesRef = writer.reserve();
178
179
  /** @type {import('./pdf/writer.js').Ref[]} */
179
180
  const pageRefs = [];
181
+ // リンクの飛び先は前後どちらのページにもなりうるので、ページ参照を先に確保しておく
182
+ const pageSlots = Array.from({ length: totalPages }, () => writer.reserve());
183
+ /** @param {number} i @returns {import('./pdf/writer.js').Ref} */
184
+ const slotOf = (i) => /** @type {import('./pdf/writer.js').Ref} */ (pageSlots[i]);
185
+
186
+ /**
187
+ * ドキュメント y が載るページと、そのページでの PDF 座標を求める。
188
+ * @param {number} docY
189
+ * @returns {{page: number, x: number, y: number}|null}
190
+ */
191
+ const locate = (docY) => {
192
+ for (let i = 0; i < totalPages; i++) {
193
+ const r = /** @type {import('./paginate.js').PageRange} */ (ranges[i]);
194
+ if (docY >= r.start - 0.01 && (docY < r.end - 0.01 || i === totalPages - 1)) {
195
+ const top = contentTop - r.headShift * PX_TO_PT;
196
+ return { page: i, x: geo.left, y: top - (docY - r.start) * PX_TO_PT };
197
+ }
198
+ }
199
+ return null;
200
+ };
180
201
 
181
202
  // 4. ページごとに描画
182
203
  opts.progress?.({ phase: 'layout', totalPages });
@@ -236,26 +257,85 @@ export async function buildPdf(body, geo, opts) {
236
257
  cs.restore();
237
258
  }
238
259
 
260
+ // リンク注釈。ページ範囲で切り取ってから用紙座標へ写す
261
+ /** @type {import('./pdf/writer.js').PdfValue[]} */
262
+ const annots = [];
263
+ if (opts.links !== false) {
264
+ /**
265
+ * @param {import('./walker/walk.js').LinkRect[]} list
266
+ * @param {number} pdfTop この帯の上端(PDF 座標)
267
+ * @param {number} docTop その位置に対応するドキュメント y
268
+ * @param {number} docEnd この帯に出せるドキュメント y の終わり
269
+ */
270
+ const addLinks = (list, pdfTop, docTop, docEnd) => {
271
+ for (const link of list) {
272
+ const y0 = Math.max(link.y, docTop);
273
+ const y1 = Math.min(link.y + link.h, docEnd);
274
+ if (y1 - y0 <= 0.01) continue;
275
+ const rect = {
276
+ x: geo.left + link.x * PX_TO_PT,
277
+ y: pdfTop - (y1 - docTop) * PX_TO_PT,
278
+ w: link.w * PX_TO_PT,
279
+ h: (y1 - y0) * PX_TO_PT,
280
+ };
281
+ if (link.fragment === null) {
282
+ annots.push(buildLinkAnnot(writer, rect, { uri: link.href }));
283
+ continue;
284
+ }
285
+ const targetY = body.anchors?.get(link.fragment);
286
+ if (targetY === undefined) continue; // 飛び先が無いリンクは注釈にしない
287
+ const at = locate(targetY);
288
+ if (!at) continue;
289
+ annots.push(buildLinkAnnot(writer, rect, { dest: [slotOf(at.page), new Name('XYZ'), at.x, at.y, null] }));
290
+ }
291
+ };
292
+ addLinks(body.links ?? [], contentTop - bodyShiftPt, range.start, range.end);
293
+ if (header) addLinks(header.links ?? [], geo.height - geo.top, 0, headerPt / PX_TO_PT);
294
+ if (footer) addLinks(footer.links ?? [], geo.bottom + footerPt, 0, footerPt / PX_TO_PT);
295
+ }
296
+
239
297
  const contentRef = await writer.addStream({}, cs.toBytes());
240
- pageRefs.push(
241
- writer.add({
242
- Type: 'Page',
243
- Parent: pagesRef,
244
- MediaBox: [0, 0, geo.width, geo.height],
245
- Resources: {
246
- Font: fontDict,
247
- XObject: xobjDict,
248
- ExtGState: gstateDict,
249
- Shading: shadingDict,
250
- ProcSet: [new Name('PDF'), new Name('Text'), new Name('ImageC')],
251
- },
252
- Contents: contentRef,
298
+ /** @type {{[key: string]: import('./pdf/writer.js').PdfValue}} */
299
+ const pageDict = {
300
+ Type: 'Page',
301
+ Parent: pagesRef,
302
+ MediaBox: [0, 0, geo.width, geo.height],
303
+ Resources: {
304
+ Font: fontDict,
305
+ XObject: xobjDict,
306
+ ExtGState: gstateDict,
307
+ Shading: shadingDict,
308
+ ProcSet: [new Name('PDF'), new Name('Text'), new Name('ImageC')],
309
+ },
310
+ Contents: contentRef,
311
+ };
312
+ if (annots.length) pageDict.Annots = annots;
313
+ const slot = slotOf(p);
314
+ writer.set(slot, pageDict);
315
+ pageRefs.push(slot);
316
+ }
317
+
318
+ writer.set(pagesRef, { Type: 'Pages', Kids: pageRefs, Count: pageRefs.length });
319
+
320
+ // しおり: 見出しから木を作る
321
+ let outlineRef = null;
322
+ if (opts.outline) {
323
+ outlineRef = buildOutline(
324
+ writer,
325
+ (body.headings ?? []).map((h) => {
326
+ const at = locate(h.y);
327
+ return { level: h.level, text: h.text, dest: at ? [slotOf(at.page), new Name('XYZ'), at.x, at.y, null] : null };
253
328
  }),
254
329
  );
255
330
  }
256
331
 
257
- writer.set(pagesRef, { Type: 'Pages', Kids: pageRefs, Count: pageRefs.length });
258
- const catalog = writer.add({ Type: 'Catalog', Pages: pagesRef });
332
+ /** @type {{[key: string]: import('./pdf/writer.js').PdfValue}} */
333
+ const catalogDict = { Type: 'Catalog', Pages: pagesRef };
334
+ if (outlineRef) {
335
+ catalogDict.Outlines = outlineRef;
336
+ catalogDict.PageMode = new Name('UseOutlines');
337
+ }
338
+ const catalog = writer.add(catalogDict);
259
339
 
260
340
  const md = opts.metadata ?? {};
261
341
  /** @type {{[key: string]: import('./pdf/writer.js').PdfValue}} */
@@ -0,0 +1,103 @@
1
+ // @ts-check
2
+ /**
3
+ * しおり(/Outlines)とリンク注釈(/Annots)の組み立て。
4
+ */
5
+ import { Name, pdfString } from './writer.js';
6
+
7
+ /**
8
+ * @typedef {{page: number, x: number, y: number}} DestPoint 飛び先(0 始まりのページ番号と、そのページの PDF 座標)
9
+ */
10
+
11
+ /**
12
+ * リンク注釈を作る。
13
+ * @param {import('./writer.js').PdfWriter} writer
14
+ * @param {{x: number, y: number, w: number, h: number}} rect PDF 座標(左下原点)
15
+ * @param {{uri: string}|{dest: import('./writer.js').PdfValue}} action
16
+ * @returns {import('./writer.js').Ref}
17
+ */
18
+ export function buildLinkAnnot(writer, rect, action) {
19
+ /** @type {{[key: string]: import('./writer.js').PdfValue}} */
20
+ const annot = {
21
+ Type: new Name('Annot'),
22
+ Subtype: new Name('Link'),
23
+ Rect: [rect.x, rect.y, rect.x + rect.w, rect.y + rect.h],
24
+ // 既定の枠線を消す(多くのビューアは描かないが、仕様上は残る)
25
+ Border: [0, 0, 0],
26
+ F: 4, // Print
27
+ };
28
+ if ('uri' in action) annot.A = { S: new Name('URI'), URI: pdfString(action.uri) };
29
+ else annot.Dest = action.dest;
30
+ return writer.add(annot);
31
+ }
32
+
33
+ /**
34
+ * 見出しの並びから、レベルに応じた木構造のしおりを作り、カタログに入れる参照を返す。
35
+ * 飛び先が解決できない見出しは飛ばす。
36
+ *
37
+ * @param {import('./writer.js').PdfWriter} writer
38
+ * @param {{level: number, text: string, dest: import('./writer.js').PdfValue|null}[]} entries
39
+ * @returns {import('./writer.js').Ref|null}
40
+ */
41
+ export function buildOutline(writer, entries) {
42
+ const usable = entries.filter((e) => e.dest !== null);
43
+ if (!usable.length) return null;
44
+
45
+ /**
46
+ * @typedef {{level: number, text: string, dest: import('./writer.js').PdfValue, children: Node[], ref: import('./writer.js').Ref}} Node
47
+ */
48
+ /** @type {Node[]} */
49
+ const roots = [];
50
+ /** @type {Node[]} 現在の祖先チェーン */
51
+ const stack = [];
52
+ for (const e of usable) {
53
+ /** @type {Node} */
54
+ const node = { level: e.level, text: e.text, dest: /** @type {import('./writer.js').PdfValue} */ (e.dest), children: [], ref: writer.reserve() };
55
+ while (stack.length && /** @type {Node} */ (stack[stack.length - 1]).level >= node.level) stack.pop();
56
+ const parent = stack[stack.length - 1];
57
+ if (parent) parent.children.push(node);
58
+ else roots.push(node);
59
+ stack.push(node);
60
+ }
61
+
62
+ const outlineRef = writer.reserve();
63
+
64
+ /**
65
+ * 兄弟の並びを書き出し、開いている項目数(Count 用)を返す。
66
+ * @param {Node[]} nodes
67
+ * @param {import('./writer.js').Ref} parentRef
68
+ * @returns {number}
69
+ */
70
+ const emit = (nodes, parentRef) => {
71
+ let total = 0;
72
+ nodes.forEach((node, i) => {
73
+ const childCount = emit(node.children, node.ref);
74
+ /** @type {{[key: string]: import('./writer.js').PdfValue}} */
75
+ const dict = {
76
+ Title: pdfString(node.text),
77
+ Parent: parentRef,
78
+ Dest: node.dest,
79
+ };
80
+ const prev = nodes[i - 1];
81
+ const next = nodes[i + 1];
82
+ if (prev) dict.Prev = prev.ref;
83
+ if (next) dict.Next = next.ref;
84
+ if (node.children.length) {
85
+ dict.First = /** @type {Node} */ (node.children[0]).ref;
86
+ dict.Last = /** @type {Node} */ (node.children[node.children.length - 1]).ref;
87
+ dict.Count = childCount; // 正の値 = 既定で開いた状態
88
+ }
89
+ writer.set(node.ref, dict);
90
+ total += 1 + childCount;
91
+ });
92
+ return total;
93
+ };
94
+
95
+ const count = emit(roots, outlineRef);
96
+ writer.set(outlineRef, {
97
+ Type: new Name('Outlines'),
98
+ First: /** @type {Node} */ (roots[0]).ref,
99
+ Last: /** @type {Node} */ (roots[roots.length - 1]).ref,
100
+ Count: count,
101
+ });
102
+ return outlineRef;
103
+ }
@@ -32,7 +32,9 @@ import { shapeToPath } from './svg-path.js';
32
32
  * @typedef {{top: number, bottom: number}} Atom ページ境界を跨いではいけない縦範囲(行・表の行・画像・break-inside: avoid)
33
33
  * @typedef {{top: number, bottom: number, headTop: number, headBottom: number, headItems: DisplayItem[], footTop: number, footBottom: number, footItems: DisplayItem[]}} TableInfo
34
34
  * @typedef {{start: number, end: number, pullTo: number}} Join break-before/after: avoid — [start, end] に境界を置かず、置きそうなら pullTo まで戻す
35
- * @typedef {{items: DisplayItem[], atoms: Atom[], breaks: number[], joins: Join[], tables: TableInfo[], height: number}} WalkResult
35
+ * @typedef {{x: number, y: number, w: number, h: number, href: string, fragment: string|null}} LinkRect <a href> の 1 行ぶんの当たり判定(ドキュメント px)
36
+ * @typedef {{level: number, text: string, y: number}} Heading しおり用の見出し
37
+ * @typedef {{items: DisplayItem[], atoms: Atom[], breaks: number[], joins: Join[], tables: TableInfo[], links: LinkRect[], anchors: Map<string, number>, headings: Heading[], height: number}} WalkResult
36
38
  */
37
39
 
38
40
  /**
@@ -107,6 +109,12 @@ export async function walk(root, ctx) {
107
109
  const breaks = [];
108
110
  /** @type {Join[]} */
109
111
  const joins = [];
112
+ /** @type {LinkRect[]} */
113
+ const links = [];
114
+ /** @type {Map<string, number>} 文書内リンクの飛び先: id → ドキュメント y */
115
+ const anchors = new Map();
116
+ /** @type {Heading[]} */
117
+ const headings = [];
110
118
  /** @type {TableInfo[]} */
111
119
  const tables = [];
112
120
  /** @type {TableInfo|null} 走査中のテーブル(thead の描画命令を記録する先) */
@@ -223,6 +231,8 @@ export async function walk(root, ctx) {
223
231
  }
224
232
  }
225
233
  }
234
+ collectLinkAndOutline(el, style);
235
+
226
236
  /** @type {TableInfo|null} */
227
237
  let openedTable = null;
228
238
  if (style.display === 'table' || style.display === 'inline-table') {
@@ -758,6 +768,53 @@ export async function walk(root, ctx) {
758
768
  };
759
769
  }
760
770
 
771
+ /**
772
+ * リンク注釈としおりの材料を集める。
773
+ *
774
+ * 描画命令ではないので DisplayList には入れず、WalkResult に別で持つ。
775
+ * `getClientRects()` は transform 適用後の矩形を返すので、変形の中のリンクも
776
+ * そのまま外接矩形として扱える(PDF の注釈は軸並行の矩形しか持てない)。
777
+ *
778
+ * @param {Element} el
779
+ * @param {CSSStyleDeclaration} style
780
+ */
781
+ function collectLinkAndOutline(el, style) {
782
+ // 飛び先になりうる id を記録する(<a name> も含む)
783
+ const id = el.id || (el.tagName === 'A' ? el.getAttribute('name') : null);
784
+ if (id && !anchors.has(id)) {
785
+ const r = el.getBoundingClientRect();
786
+ anchors.set(id, r.top + sy);
787
+ }
788
+
789
+ if (/^H[1-6]$/.test(el.tagName)) {
790
+ const text = (el.textContent ?? '').trim().replace(/\s+/g, ' ');
791
+ if (text) headings.push({ level: Number(el.tagName[1]), text, y: el.getBoundingClientRect().top + sy });
792
+ }
793
+
794
+ if (el.tagName !== 'A') return;
795
+ const href = el.getAttribute('href');
796
+ if (!href) return;
797
+ if (style.visibility !== 'visible') return;
798
+ // 同じ文書内へのリンクは飛び先の id を覚えておき、ページが決まってから解決する
799
+ const fragment = href.startsWith('#') ? decodeURIComponent(href.slice(1)) : null;
800
+ /** @type {string} */
801
+ let uri = href;
802
+ if (!fragment) {
803
+ try {
804
+ uri = new URL(href, el.ownerDocument.baseURI).href;
805
+ } catch {
806
+ return; // 解決できない href は注釈にしない
807
+ }
808
+ // javascript: などは注釈にしない
809
+ if (!/^(https?|mailto|tel|ftp|file):/i.test(uri)) return;
810
+ }
811
+ // インラインで折り返していると行ごとに矩形が返る
812
+ for (const r of el.getClientRects()) {
813
+ if (r.width <= 0 || r.height <= 0) continue;
814
+ links.push({ x: r.left + sx, y: r.top + sy, w: r.width, h: r.height, href: uri, fragment });
815
+ }
816
+ }
817
+
761
818
  /**
762
819
  * overflow クリップを閉じる。範囲外の命令は捨てる(クリップで消えた文字が抽出テキストに残らないように)。
763
820
  * @param {Box} clipBox
@@ -834,7 +891,7 @@ export async function walk(root, ctx) {
834
891
  return null;
835
892
  }
836
893
 
837
- return { items: rootItems, atoms, breaks, joins, tables, height };
894
+ return { items: rootItems, atoms, breaks, joins, tables, links, anchors, headings, height };
838
895
  }
839
896
 
840
897
  /** @param {DisplayItem[]} items */
package/types/index.d.ts CHANGED
@@ -95,6 +95,8 @@ export { expandPrintMediaCss } from "./renderer.js";
95
95
  * @property {'blob'|'uint8array'|'dataurl'} [output='blob']
96
96
  * @property {string} [baseUrl] 相対 URL(フォント・画像)の基準。既定は現在の文書
97
97
  * @property {(warning: ConversionWarning) => void} [onWarning]
98
+ * @property {boolean} [links=true] `<a href>` を PDF のリンク注釈にする
99
+ * @property {boolean} [outline=false] 見出し(h1〜h6)からしおり(PDF の目次)を作る
98
100
  * @property {(progress: ConversionProgress) => void} [onProgress] 進捗通知。長い文書では途中でイベントループへ戻すので、UI を更新できる
99
101
  */
100
102
  /**
@@ -102,7 +104,7 @@ export { expandPrintMediaCss } from "./renderer.js";
102
104
  * @typedef {Element|string} ConvertInput
103
105
  */
104
106
  /** ライブラリのバージョン(package.json と同期) */
105
- export const version: "0.3.0";
107
+ export const version: "0.4.0";
106
108
  /**
107
109
  * 登録するフォントの定義。
108
110
  * `src` は TrueType アウトライン(glyf)を持つ静的 TTF のみ対応。
@@ -214,6 +216,14 @@ export type ConvertOptions = {
214
216
  */
215
217
  baseUrl?: string | undefined;
216
218
  onWarning?: ((warning: ConversionWarning) => void) | undefined;
219
+ /**
220
+ * `<a href>` を PDF のリンク注釈にする
221
+ */
222
+ links?: boolean | undefined;
223
+ /**
224
+ * 見出し(h1〜h6)からしおり(PDF の目次)を作る
225
+ */
226
+ outline?: boolean | undefined;
217
227
  /**
218
228
  * 進捗通知。長い文書では途中でイベントループへ戻すので、UI を更新できる
219
229
  */
package/types/page.d.ts CHANGED
@@ -20,7 +20,7 @@ export function resolvePage(page?: import("./index.js").PageOptions | undefined)
20
20
  /**
21
21
  * @param {import('./walker/walk.js').WalkResult} body
22
22
  * @param {PageGeometry} geo
23
- * @param {{compress: boolean, metadata?: import('./index.js').PdfMetadata, header?: PageDecoration|null, footer?: PageDecoration|null, pacer?: import('./pacer.js').Pacer, progress?: (p: import('./index.js').ConversionProgress) => void, warn?: (w: import('./index.js').ConversionWarning) => void}} opts
23
+ * @param {{compress: boolean, metadata?: import('./index.js').PdfMetadata, header?: PageDecoration|null, footer?: PageDecoration|null, pacer?: import('./pacer.js').Pacer, progress?: (p: import('./index.js').ConversionProgress) => void, warn?: (w: import('./index.js').ConversionWarning) => void, links?: boolean, outline?: boolean}} opts
24
24
  * @returns {Promise<Uint8Array>}
25
25
  */
26
26
  export function buildPdf(body: import("./walker/walk.js").WalkResult, geo: PageGeometry, opts: {
@@ -31,6 +31,8 @@ export function buildPdf(body: import("./walker/walk.js").WalkResult, geo: PageG
31
31
  pacer?: import("./pacer.js").Pacer;
32
32
  progress?: (p: import("./index.js").ConversionProgress) => void;
33
33
  warn?: (w: import("./index.js").ConversionWarning) => void;
34
+ links?: boolean;
35
+ outline?: boolean;
34
36
  }): Promise<Uint8Array>;
35
37
  export type PageGeometry = {
36
38
  /**
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @typedef {{page: number, x: number, y: number}} DestPoint 飛び先(0 始まりのページ番号と、そのページの PDF 座標)
3
+ */
4
+ /**
5
+ * リンク注釈を作る。
6
+ * @param {import('./writer.js').PdfWriter} writer
7
+ * @param {{x: number, y: number, w: number, h: number}} rect PDF 座標(左下原点)
8
+ * @param {{uri: string}|{dest: import('./writer.js').PdfValue}} action
9
+ * @returns {import('./writer.js').Ref}
10
+ */
11
+ export function buildLinkAnnot(writer: import("./writer.js").PdfWriter, rect: {
12
+ x: number;
13
+ y: number;
14
+ w: number;
15
+ h: number;
16
+ }, action: {
17
+ uri: string;
18
+ } | {
19
+ dest: import("./writer.js").PdfValue;
20
+ }): import("./writer.js").Ref;
21
+ /**
22
+ * 見出しの並びから、レベルに応じた木構造のしおりを作り、カタログに入れる参照を返す。
23
+ * 飛び先が解決できない見出しは飛ばす。
24
+ *
25
+ * @param {import('./writer.js').PdfWriter} writer
26
+ * @param {{level: number, text: string, dest: import('./writer.js').PdfValue|null}[]} entries
27
+ * @returns {import('./writer.js').Ref|null}
28
+ */
29
+ export function buildOutline(writer: import("./writer.js").PdfWriter, entries: {
30
+ level: number;
31
+ text: string;
32
+ dest: import("./writer.js").PdfValue | null;
33
+ }[]): import("./writer.js").Ref | null;
34
+ /**
35
+ * 飛び先(0 始まりのページ番号と、そのページの PDF 座標)
36
+ */
37
+ export type DestPoint = {
38
+ page: number;
39
+ x: number;
40
+ y: number;
41
+ };
@@ -179,12 +179,34 @@ export type Join = {
179
179
  end: number;
180
180
  pullTo: number;
181
181
  };
182
+ /**
183
+ * <a href> の 1 行ぶんの当たり判定(ドキュメント px)
184
+ */
185
+ export type LinkRect = {
186
+ x: number;
187
+ y: number;
188
+ w: number;
189
+ h: number;
190
+ href: string;
191
+ fragment: string | null;
192
+ };
193
+ /**
194
+ * しおり用の見出し
195
+ */
196
+ export type Heading = {
197
+ level: number;
198
+ text: string;
199
+ y: number;
200
+ };
182
201
  export type WalkResult = {
183
202
  items: DisplayItem[];
184
203
  atoms: Atom[];
185
204
  breaks: number[];
186
205
  joins: Join[];
187
206
  tables: TableInfo[];
207
+ links: LinkRect[];
208
+ anchors: Map<string, number>;
209
+ headings: Heading[];
188
210
  height: number;
189
211
  };
190
212
  export type WalkContext = {