@shuji-bonji/pdf-writer-mcp 0.3.1 → 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/CHANGELOG.md ADDED
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.4.0] - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ - **Editing tools (Tier A, wave 2)**:
10
+ - `add_bookmarks` — set the document outline. Nestable via `children` (up to 8
11
+ levels, 2000 entries). Replaces any existing outline. Titles are written as
12
+ UTF-16BE so CJK round-trips correctly. Open items carry a positive `/Count`,
13
+ collapsed items a negative one, per ISO 32000-1 §12.3.3.
14
+ - `add_annotation` — add a `text` (sticky note), `highlight`, or `square`
15
+ annotation. Coordinates use PDF space (origin bottom-left, points). Colors
16
+ are `#rrggbb`; `contents` and `author` accept CJK.
17
+
18
+ pdf-lib exposes no outline or annotation API, so both are built from the
19
+ low-level object model (`services/outline.ts`, `services/annotation.ts`).
20
+
21
+ ## [0.3.1] - 2026-07-16
22
+
23
+ ### Fixed
24
+
25
+ - **Text extraction broken for digits in Latin context** (regression introduced in
26
+ 0.3.0): `subsetFont` is now called with `noLayoutClosure: true`.
27
+
28
+ pdf-lib's `CustomFontEmbedder` (used with `subset: false`) derives glyphs
29
+ through two independent paths: the CIDs written into the content stream come
30
+ from `font.layout(text)` — **after** GSUB substitution — while the ToUnicode
31
+ CMap is built from `font.characterSet` → `glyphForCodePoint` — **before**
32
+ substitution. Noto Sans JP substitutes ASCII digits with alternate forms in
33
+ Latin context (`layout('English 0')` yields glyph 17460, not 17), so the two
34
+ disagreed and extraction returned wrong characters (`v0.3.0` → `vô.õ.ô`, and
35
+ poppler dropped the digits entirely). Rendering was correct throughout, which
36
+ made this easy to miss.
37
+
38
+ Excluding layout-reachable glyphs from the subset prevents the substitution
39
+ altogether, so `layout()` and `glyphsForString()` agree again. Subsets also got
40
+ smaller as a side effect (9.1 KB → 4.5 KB for a typical page).
41
+
42
+ ### Added
43
+
44
+ - `render.test.ts` now asserts that the CIDs written to the page match the
45
+ ToUnicode CMap, so this class of regression fails the suite.
46
+
47
+ ## [0.3.0] - 2026-07-16
48
+
49
+ ### Fixed
50
+
51
+ - **Japanese text rendered as blank boxes in every viewer** (Chrome, Firefox,
52
+ Acrobat, poppler, Claude Desktop). Font subsetting no longer goes through
53
+ pdf-lib's `embedFont(subset: true)`: fonts are now pre-subset with harfbuzz
54
+ ([subset-font](https://github.com/papandreou/subset-font)) and embedded with
55
+ `subset: false`.
56
+
57
+ fontkit's subsetter drops glyph outlines for CJK fonts. Because the ToUnicode
58
+ CMap stayed correct, text extraction kept working and the existing tests
59
+ passed while the visible output was broken.
60
+
61
+ | Approach | PDF size | Rendering |
62
+ |---|---|---|
63
+ | fontkit `subset: true` (previous) | 24 KB | broken |
64
+ | pdf-lib `subset: false` alone | 3.9 MB | correct |
65
+ | harfbuzz + `subset: false` (current) | 14.5 KB | correct |
66
+
67
+ - The note in the design doc claiming poppler's `Embedded font file may be
68
+ invalid` warning was harmless is retracted — poppler followed it with
69
+ `Couldn't create a font`, i.e. the warning was the breakage.
70
+
71
+ ### Added
72
+
73
+ - `render.test.ts`: extracts the embedded font program from generated PDFs and
74
+ verifies that every rendered character retains a real glyph outline. Verified
75
+ to fail against the pre-0.3.0 implementation.
76
+ - CI fetches Noto Sans JP and sets `TEST_FONT_PATH`, so the font-dependent tests
77
+ no longer skip.
78
+
79
+ ### Changed
80
+
81
+ - `loadFont` split into `openFont` (read + glyph coverage) and `embedFontFor`
82
+ (subset + embed). Subsetting depends on the text being drawn, so embedding is
83
+ deferred until the input is final.
84
+ - **Dependency added**: `subset-font` (harfbuzz/wasm, no native binaries).
85
+
86
+ ## [0.2.1] - 2026-07-16
87
+
88
+ ### Added
89
+
90
+ - **`onMissingGlyph` option** for the create tools. Characters absent from the
91
+ font (e.g. ✔ U+2714, which Noto Sans JP does not include) were previously
92
+ embedded as `.notdef` and rendered as silent blanks.
93
+ - `error` (default) — list the offending characters and fail
94
+ - `replace` — substitute 〓 and report via `warnings`
95
+ - `ignore` — render as before and report via `warnings`
96
+ - `CreateResult.warnings` reports substituted or ignored characters.
97
+
98
+ ## [0.2.0] - 2026-07-16
99
+
100
+ ### Added
101
+
102
+ - **Editing tools (Tier A, wave 1)** — the server now edits existing PDFs in
103
+ addition to creating them:
104
+ - `set_metadata` — update Info dictionary fields, preserving the rest
105
+ - `merge_pdfs` — concatenate in order (metadata inherited from the first file)
106
+ - `split_pdf` — one file per page range
107
+ - `extract_pages` — extract in the requested order (doubles as reordering)
108
+ - `delete_pages` — remove pages (deleting all is rejected)
109
+ - `reorder_pages` — reorder by an explicit permutation
110
+ - `rotate_pages` — rotate clockwise, accumulating over existing rotation
111
+ - **Signature guard**: editing a PDF with `/ByteRange` fails by default, since
112
+ pdf-lib rewrites the whole file and would invalidate existing signatures.
113
+ Pass `allowBreakingSignatures: true` to proceed anyway.
114
+ - Page specs (`"1,3-5,8-"`, 1-based, order-preserving, duplicates removed).
115
+ - CI (`ci.yml`) and npm publish via Trusted Publisher / OIDC (`publish.yml`).
116
+
117
+ ## [0.1.0] - 2026-07-15
118
+
119
+ Initial release.
120
+
121
+ ### Added
122
+
123
+ - `create_text_pdf` — plain text with paragraph breaks, wrapping, pagination
124
+ - `create_markdown_pdf` — headings, lists, code blocks, quotes, rules, tables
125
+ - `create_table_pdf` — ruled tables with automatic column widths and repeated
126
+ headers across page breaks
127
+ - Japanese font embedding, ToUnicode CMap (extractable/searchable text),
128
+ file or base64 output, `asserts`-based input validation.
package/README.ja.md ADDED
@@ -0,0 +1,141 @@
1
+ # pdf-writer-mcp
2
+
3
+ [![CI](https://github.com/shuji-bonji/pdf-writer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/shuji-bonji/pdf-writer-mcp/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@shuji-bonji/pdf-writer-mcp.svg)](https://www.npmjs.com/package/@shuji-bonji/pdf-writer-mcp)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
6
+
7
+ [English](./README.md)
8
+
9
+ テキスト / Markdown / 表データからの **PDF 生成** と、既存 PDF の **編集**(メタデータ・ページ操作)を行う MCP (Model Context Protocol) サーバです。[pdf-lib](https://pdf-lib.js.org/) をコアに、harfbuzz サブセットによる日本語フォント埋め込みに対応します。
10
+
11
+ [pdf-reader-mcp](https://github.com/shuji-bonji/pdf-reader-mcp)(構造解析)、[pdf-verify-mcp](https://github.com/shuji-bonji/pdf-verify-mcp)(真正性検証)と同じ PDF family の一員です。`pdf-reader-mcp` が「何があるか」を読み、`pdf-verify-mcp` が「本物か」を検証するのに対し、`pdf-writer-mcp` は「それを書く」役割を担います。
12
+
13
+ ## ツール
14
+
15
+ ### 生成
16
+
17
+ | ツール | 役割 |
18
+ |--------|------|
19
+ | `create_text_pdf` | プレーンテキスト — `\n` で改行、空行で段落区切り、長い行は自動折り返し |
20
+ | `create_markdown_pdf` | Markdown — 見出し / 段落 / 箇条書き・番号リスト / コードブロック / 引用 / 水平線 / 表 |
21
+ | `create_table_pdf` | 罫線付き表 — 列幅の自動算出、セル内折り返し、改ページ時のヘッダ再描画 |
22
+
23
+ 共通オプション: `outputPath` / `returnBase64` / `fontPath` / `fontSize` / `pageSize`(A4/A3/A5/LETTER/LEGAL)/ `margin` / `title` / `author` / `onMissingGlyph`。
24
+
25
+ ### 編集
26
+
27
+ | ツール | 役割 |
28
+ |--------|------|
29
+ | `set_metadata` | Info 辞書の更新(`title` / `author` / `subject` / `keywords` / `creator`)。指定フィールドのみ変更し、他は保持 |
30
+ | `merge_pdfs` | 2〜50 個の PDF を指定順に結合。メタデータは先頭ファイルから引き継ぎ |
31
+ | `split_pdf` | ページ範囲ごとに 1 ファイルへ分割 |
32
+ | `extract_pages` | 指定順を保持して抽出(並べ替えを兼ねる) |
33
+ | `delete_pages` | ページ削除(全ページの削除はエラー) |
34
+ | `reorder_pages` | 全ページの順列による並べ替え |
35
+ | `rotate_pages` | 時計回りに回転(90/180/270)。既存の回転に加算 |
36
+ | `add_bookmarks` | しおり(アウトライン)の設定。`children` で入れ子にでき、既存のしおりは置換 |
37
+ | `add_annotation` | 付箋(`text`)/ ハイライト(`highlight`)/ 矩形(`square`)の注釈を追加 |
38
+
39
+ 共通オプション: `outputPath` / `returnBase64` / `allowBreakingSignatures`。
40
+
41
+ ページ指定は `"1,3-5,8-"` 形式(1 始まり。`-3` は先頭から 3 ページまで、`8-` は 8 ページから最終まで)。指定順を保持し、重複は除去します。
42
+
43
+ > **署名について**: pdf-lib は保存時にファイル全体を再構築するため、編集すると既存の電子署名は必ず無効化されます。`/ByteRange` を含む PDF は既定でエラーとし、`allowBreakingSignatures: true` を指定したときのみ続行します。署名を保持する増分更新はロードマップ参照。
44
+
45
+ ## インストール
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "pdf-writer": {
51
+ "command": "npx",
52
+ "args": ["-y", "@shuji-bonji/pdf-writer-mcp"],
53
+ "env": {
54
+ "PDF_WRITER_FONT": "/absolute/path/to/NotoSansJP-Regular.otf"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ `PDF_WRITER_FONT` を設定しておくと、各ツールで `fontPath` を省略しても日本語が出せます。
62
+
63
+ ## フォント
64
+
65
+ 標準 PDF フォント(Helvetica)は **ASCII のみ** です。日本語など非ラテン文字を描画するには、`fontPath` または `PDF_WRITER_FONT` で埋め込み可能な**単一フェイス**のフォント(`.ttf` / `.otf`)を指定してください。
66
+
67
+ - 推奨入手先: [Noto Sans JP (SubsetOTF/JP)](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP) — 静的・単一フェイス・SIL OFL。
68
+ - **`.ttc`(TrueTypeCollection)は非対応**です。検知してエラーにするため、単一フェイスを抽出してください:
69
+
70
+ ```bash
71
+ python3 -c "from fontTools.ttLib import TTCollection; \
72
+ TTCollection('NotoSansCJK-Regular.ttc').fonts[0].save('NotoSansCJKjp-Regular.otf')"
73
+ ```
74
+
75
+ ### フォント未収録文字(グリフ欠落)
76
+
77
+ フォントに存在しない文字(例: Noto Sans JP に無い ✔ U+2714)は、そのままだと `.notdef` として埋め込まれ、無警告で空白になります。`onMissingGlyph` でこの挙動を選べます。
78
+
79
+ | 値 | 挙動 |
80
+ |----|------|
81
+ | `error`(既定) | 欠落文字を `"✔" (U+2714)` 形式で列挙してエラー |
82
+ | `replace` | 〓 に置換して生成し、`warnings` で報告 |
83
+ | `ignore` | 空白のまま生成し、`warnings` で報告 |
84
+
85
+ ## 返り値
86
+
87
+ ```jsonc
88
+ {
89
+ "path": "/abs/out.pdf", // outputPath 指定時
90
+ "base64": "JVBERi0xLj...", // returnBase64 指定時、または outputPath 省略時
91
+ "pageCount": 3,
92
+ "bytes": 91788,
93
+ "font": "NotoSansJP-Regular.otf",
94
+ "warnings": ["Replaced 1 unsupported character(s) with \"〓\": \"✔\" (U+2714)"]
95
+ }
96
+ ```
97
+
98
+ 編集ツールは `font` を除いた同じ形を返します。`split_pdf` は `{ files: [...], count }` を返します。
99
+
100
+ ## テキスト抽出
101
+
102
+ 生成される PDF はテキストの選択・コピー・全文検索・スクリーンリーダ読み上げが可能です。埋め込みサブセットフォントでも pdf-lib が ToUnicode CMap を出力するためで、この性質は回帰テスト(`extract.test.ts` / `render.test.ts`)で担保しています。
103
+
104
+ > poppler 系のビューアは、OTF/CFF フォントを CIDFontType0 として埋め込む際に `Mismatch between font type and embedded font file` という警告を表示することがあります。これは無害で、描画・抽出とも正常です。
105
+
106
+ ## 開発
107
+
108
+ ```bash
109
+ npm install
110
+ npm run build # dist/ に出力
111
+ npm test # vitest
112
+ npm run typecheck # tsc --noEmit
113
+ ```
114
+
115
+ 日本語フォント依存のテストは `TEST_FONT_PATH` を指定すると有効になります。
116
+
117
+ ```bash
118
+ TEST_FONT_PATH=/path/to/NotoSansJP-Regular.otf npm test
119
+ ```
120
+
121
+ ## 既知の制約
122
+
123
+ - **インライン装飾**: 太字・斜体はサイズ / 字面のみで、書体としては反映されません(1 文書につき単一フォントを埋め込むため)。
124
+ - **`.ttc` フォント**: 単一フェイスへの抽出が必要です(上記参照)。
125
+ - **サブセット名の接頭辞**: 慣習的な `ABCDEF+` を付けないため、一部ツールがサブセットでないと誤認します。描画・抽出には影響せず、PDF/A の厳密対応時のみ問題になります。
126
+
127
+ ## ロードマップ
128
+
129
+ - [x] 編集系 Tier A 第1波 — メタデータ・ページ操作(v0.2.0)
130
+ - [x] 編集系 Tier A 第2波 — しおり・注釈(v0.4.0)
131
+ - [ ] 編集系 Tier B — フォーム記入 / フラット化、透かし、添付ファイル、ページ番号スタンプ
132
+ - [ ] タグ付き PDF / PDF/UA(スクリーンリーダ向けの構造タグ)
133
+ - [ ] `.ttc` からのフェイス自動抽出
134
+ - [ ] 見出し用と本文用のフォント分け(太字フェイス埋め込み)
135
+ - [ ] 画像埋め込み、ヘッダー / フッター
136
+ - [ ] Tier C — 署名を保持する増分更新、本文テキスト編集、タグ木の保守
137
+ - [ ] PDF/A 変換
138
+
139
+ ## ライセンス
140
+
141
+ MIT © shuji-bonji
package/README.md CHANGED
@@ -1,73 +1,55 @@
1
1
  # pdf-writer-mcp
2
2
 
3
- テキスト / Markdown / 表データからの **PDF 生成** と、既存 PDF の **編集**(メタデータ・ページ操作)を行う
4
- MCP (Model Context Protocol) サーバです。
5
- [pdf-lib](https://pdf-lib.js.org/) をコアに、日本語フォントの埋め込み(サブセット)に対応します。
6
-
7
- ## 位置づけ
8
-
9
- `shuji-bonji` の PDF 系 MCP エコシステムにおける「生成(write)」担当です。
10
- 読み取り・検証系とは責務を分けています。
11
-
12
- ```mermaid
13
- graph LR
14
- subgraph 生成
15
- W[pdf-writer-mcp<br/>text/md/table→PDF]
16
- end
17
- subgraph 読取
18
- R[pdf-reader-mcp<br/>抽出・検査]
19
- end
20
- subgraph 検証
21
- V[pdf-verify-mcp<br/>署名・準拠性]
22
- end
23
- subgraph 仕様参照
24
- S[pdf-spec-mcp<br/>ISO 32000]
25
- end
26
- W --> R
27
- R --> V
28
- S -.仕様根拠.-> W
29
- S -.仕様根拠.-> V
30
- ```
3
+ [![CI](https://github.com/shuji-bonji/pdf-writer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/shuji-bonji/pdf-writer-mcp/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@shuji-bonji/pdf-writer-mcp.svg)](https://www.npmjs.com/package/@shuji-bonji/pdf-writer-mcp)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
31
6
 
32
- ## 特徴
7
+ [日本語](./README.ja.md)
33
8
 
34
- - **3 つの生成ツール**: プレーンテキスト / Markdown /
35
- - **7 つの編集ツール**: メタデータ更新、結合・分割・抽出・削除・並べ替え・回転
36
- - **署名ガード**: 電子署名済み PDF(`/ByteRange` 検知)の編集は既定でエラーにし、
37
- 署名の意図しない無効化を防止(`allowBreakingSignatures: true` で明示的に続行可能)
38
- - **日本語対応**: `.ttf` / `.otf` フォントを harfbuzz でサブセット埋め込み(4.5MB のフォントでも出力は数十 KB)
39
- - **抽出・検索・コピー可能**: 埋め込みフォントでも ToUnicode CMap が付与されるため、
40
- 生成した PDF のテキストは選択・コピー・全文検索・スクリーンリーダ読み上げが可能
41
- - **自動レイアウト**: テキスト折り返し(日英混在・長語の強制分割)、改ページ、表の改ページ時ヘッダ再描画
42
- - **出力先の柔軟性**: ファイル保存 / base64 返却の両対応
43
- - **堅牢な入力検査**: `asserts` によるバリデーションで不正値を早期に弾く
9
+ MCP server that **creates** PDFs from text, Markdown, or tabular data and **edits** existing ones (metadata and page operations). Built on [pdf-lib](https://pdf-lib.js.org/), with CJK font embedding via harfbuzz subsetting.
44
10
 
45
- ## MCP クライアント設定
11
+ Part of the PDF family alongside [pdf-reader-mcp](https://github.com/shuji-bonji/pdf-reader-mcp) (structure analysis) and [pdf-verify-mcp](https://github.com/shuji-bonji/pdf-verify-mcp) (authenticity verification). Where `pdf-reader-mcp` tells you *what is in* a PDF and `pdf-verify-mcp` tells you *whether it is genuine*, `pdf-writer-mcp` is the one that *writes it*.
46
12
 
47
- `claude_desktop_config.json`(例、npm 公開版を npx で起動):
13
+ ## Tools
48
14
 
49
- ```json
50
- {
51
- "mcpServers": {
52
- "pdf-writer": {
53
- "command": "npx",
54
- "args": ["-y", "@shuji-bonji/pdf-writer-mcp"],
55
- "env": {
56
- "PDF_WRITER_FONT": "/absolute/path/to/NotoSansJP-Regular.otf"
57
- }
58
- }
59
- }
60
- }
61
- ```
15
+ ### Creation
16
+
17
+ | Tool | Purpose |
18
+ |------|---------|
19
+ | `create_text_pdf` | Plain text — honours `\n`, blank lines separate paragraphs, long lines wrap |
20
+ | `create_markdown_pdf` | Markdown — headings, paragraphs, bullet/ordered lists, code blocks, quotes, rules, tables |
21
+ | `create_table_pdf` | Ruled tables — automatic column widths, cell wrapping, headers repeated across page breaks |
62
22
 
63
- ローカルのソースから起動する場合:
23
+ Shared options: `outputPath`, `returnBase64`, `fontPath`, `fontSize`, `pageSize` (A4/A3/A5/LETTER/LEGAL), `margin`, `title`, `author`, `onMissingGlyph`.
24
+
25
+ ### Editing
26
+
27
+ | Tool | Purpose |
28
+ |------|---------|
29
+ | `set_metadata` | Update Info dictionary fields (`title` / `author` / `subject` / `keywords` / `creator`), preserving the rest |
30
+ | `merge_pdfs` | Concatenate 2–50 PDFs in order; metadata inherited from the first file |
31
+ | `split_pdf` | One output file per page range |
32
+ | `extract_pages` | Extract pages in the requested order (doubles as reordering) |
33
+ | `delete_pages` | Remove pages (deleting every page is rejected) |
34
+ | `reorder_pages` | Reorder by an explicit permutation of all pages |
35
+ | `rotate_pages` | Rotate clockwise (90/180/270), accumulating over existing rotation |
36
+ | `add_bookmarks` | Set the outline (bookmarks); nestable via `children`, replaces any existing outline |
37
+ | `add_annotation` | Add a sticky note (`text`), `highlight`, or `square` annotation to a page |
38
+
39
+ Shared options: `outputPath`, `returnBase64`, `allowBreakingSignatures`.
40
+
41
+ Page specs use `"1,3-5,8-"` (1-based; `-3` means up to page 3, `8-` means page 8 to the end). Order is preserved and duplicates are removed.
42
+
43
+ > **Signatures**: pdf-lib rewrites the whole file on save, so editing always invalidates existing signatures. PDFs containing `/ByteRange` are rejected by default; pass `allowBreakingSignatures: true` to proceed anyway. Signature-preserving incremental updates are on the roadmap.
44
+
45
+ ## Install
64
46
 
65
47
  ```json
66
48
  {
67
49
  "mcpServers": {
68
50
  "pdf-writer": {
69
- "command": "node",
70
- "args": ["/absolute/path/to/pdf-writer-mcp/dist/index.js"],
51
+ "command": "npx",
52
+ "args": ["-y", "@shuji-bonji/pdf-writer-mcp"],
71
53
  "env": {
72
54
  "PDF_WRITER_FONT": "/absolute/path/to/NotoSansJP-Regular.otf"
73
55
  }
@@ -76,147 +58,84 @@ graph LR
76
58
  }
77
59
  ```
78
60
 
79
- `PDF_WRITER_FONT` を設定しておくと、各ツールで `fontPath` を省略しても日本語が出せます。
80
-
81
- ## 開発
61
+ `PDF_WRITER_FONT` lets every tool omit `fontPath` and still render CJK text.
82
62
 
83
- ```bash
84
- npm install
85
- npm run build # dist/ に出力
86
- npm test # vitest
87
- npm run typecheck # tsc --noEmit
88
- ```
63
+ ## Fonts
89
64
 
90
- ## フォントについて(重要)
65
+ The standard PDF font (Helvetica) covers **ASCII only**. To render Japanese or any non-Latin text, point `fontPath` or `PDF_WRITER_FONT` at an embeddable **single-face** font (`.ttf` / `.otf`).
91
66
 
92
- - 標準フォント(Helvetica)は **ASCII のみ**。日本語を含む場合は `fontPath` `PDF_WRITER_FONT`
93
- 埋め込み可能な **単一フォント(`.ttf` / `.otf`)** を指定してください。
94
- - **`.ttc`(TrueTypeCollection)は非対応**です(pdf-lib がサブセット化できないため、検知してエラーにします)。
95
- `.ttc` しか手元にない場合は、単一フェイスを抽出してください。
67
+ - Recommended source: [Noto Sans JP (SubsetOTF/JP)](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP) — static, single-face, SIL OFL.
68
+ - **`.ttc` (TrueType Collection) is not supported** — the file is detected and rejected. Extract a single face first:
96
69
 
97
70
  ```bash
98
- # 例: Noto Sans CJK の .ttc から日本語フェイス(index 0)を .otf として取り出す
99
71
  python3 -c "from fontTools.ttLib import TTCollection; \
100
72
  TTCollection('NotoSansCJK-Regular.ttc').fonts[0].save('NotoSansCJKjp-Regular.otf')"
101
73
  ```
102
74
 
103
- - 日本語フォントの入手先の例:
104
- [notofonts/noto-cjk の日本語サブセット OTF](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP)(SIL OFL、静的・単一フェイス)。
75
+ ### Missing glyphs
105
76
 
106
- ### フォント未収録文字(グリフ欠落)の扱い
77
+ Characters absent from the font (e.g. ✔ U+2714, which Noto Sans JP does not include) would otherwise be embedded as `.notdef` and render as silent blanks. `onMissingGlyph` controls this:
107
78
 
108
- 指定フォントに存在しない文字(例: Noto Sans JP に無い ✔ U+2714 や絵文字)を検知した場合の
109
- 挙動を `onMissingGlyph` で選べます(v0.2.1〜):
79
+ | Value | Behaviour |
80
+ |-------|-----------|
81
+ | `error` (default) | Fail, listing the offending characters as `"✔" (U+2714)` |
82
+ | `replace` | Substitute 〓 and report via `warnings` |
83
+ | `ignore` | Render as blanks and report via `warnings` |
110
84
 
111
- | 値 | 挙動 |
112
- |----|------|
113
- | `error`(既定) | 欠落文字を `"✔" (U+2714)` 形式で列挙してエラー。無警告の空白出力を防ぐ |
114
- | `replace` | 〓(下駄記号)に置換して生成し、`warnings` で報告 |
115
- | `ignore` | そのまま生成(該当文字は空白になる)し、`warnings` で報告 |
116
-
117
- ## ツール
118
-
119
- ### `create_text_pdf`
120
-
121
- プレーンテキスト → PDF。`\n` で改行、空行で段落区切り。長い行は自動折り返し。
122
-
123
- | 引数 | 型 | 必須 | 説明 |
124
- |------|----|:---:|------|
125
- | `text` | string | ✓ | 本文 |
126
- | `outputPath` | string | | 保存先。省略時は base64 を返す |
127
- | `returnBase64` | boolean | | 保存に加えて base64 も返す |
128
- | `fontPath` | string | | 埋め込むフォント(日本語は必須) |
129
- | `fontSize` | number | | 本文サイズ pt(既定 11、4〜96) |
130
- | `pageSize` | enum | | A4/A3/A5/LETTER/LEGAL(既定 A4) |
131
- | `margin` | number | | 余白 pt(既定 56、0〜300) |
132
- | `title` | string | | タイトル(メタデータ+冒頭見出し) |
133
- | `author` | string | | 作成者(メタデータ) |
134
- | `onMissingGlyph` | enum | | フォント未収録文字の扱い: error(既定)/ replace / ignore |
135
-
136
- ### `create_markdown_pdf`
137
-
138
- Markdown → PDF。対応要素: 見出し / 段落 / 箇条書き・番号リスト / コードブロック / 引用 / 水平線 / 表。
139
- インライン装飾(`**bold**` など)は単一フォントのため **記号を除去して字面のみ** 反映します。
140
-
141
- 引数は `text` の代わりに `markdown`(string, 必須)。その他は共通。
142
-
143
- ### `create_table_pdf`
144
-
145
- ヘッダ + 行データ → 罫線付きの表 PDF。列幅は内容から自動算出、セル内は折り返し、改ページ時はヘッダを再描画。
146
-
147
- | 引数 | 型 | 必須 | 説明 |
148
- |------|----|:---:|------|
149
- | `headers` | string[] | ✓ | 列見出し |
150
- | `rows` | string[][] | ✓ | データ行(各行は headers と同数の列を推奨) |
151
-
152
- その他は共通引数。
153
-
154
- ## 編集ツール(v0.2.0〜)
155
-
156
- 既存 PDF に対する操作です。共通引数: `outputPath`(省略時は base64 返却)/ `returnBase64` /
157
- `allowBreakingSignatures`(署名済み PDF の編集続行フラグ、既定 false)。
158
-
159
- ページ指定は `"1,3-5,8-"` 形式(1 始まり。`-3` = 先頭から 3 まで、`8-` = 8 から最終まで)。
160
-
161
- | ツール | 主な引数 | 説明 |
162
- |--------|----------|------|
163
- | `set_metadata` | `inputPath`, `title` / `author` / `subject` / `keywords` / `creator` | 指定フィールドのみ更新、他は保持 |
164
- | `merge_pdfs` | `inputPaths: string[]` | 指定順に結合(2〜50 ファイル) |
165
- | `split_pdf` | `inputPath`, `ranges: string[]`, `outputDir`, `prefix?` | 範囲ごとに連番ファイルへ分割 |
166
- | `extract_pages` | `inputPath`, `pages` | 指定順を保持して抽出(並べ替えを兼ねる) |
167
- | `delete_pages` | `inputPath`, `pages` | ページ削除(全削除はエラー) |
168
- | `reorder_pages` | `inputPath`, `order: number[]` | 全ページの順列で並べ替え |
169
- | `rotate_pages` | `inputPath`, `rotation: 90\|180\|270`, `pages?` | 時計回りに回転(既存回転に加算) |
170
-
171
- > **署名について**: pdf-lib はファイル全体を再構築して保存するため、編集すると既存の電子署名は
172
- > 必ず無効化されます。署名(`/ByteRange`)を検知した場合は既定でエラーとし、
173
- > `allowBreakingSignatures: true` を指定したときのみ続行します。
174
- > 署名を保持する増分更新は将来対応(ロードマップ参照)。
175
-
176
- ### 返り値(共通)
85
+ ## Result
177
86
 
178
87
  ```jsonc
179
88
  {
180
- "path": "/abs/out.pdf", // outputPath 指定時
181
- "base64": "JVBERi0xLj...", // returnBase64 or outputPath 未指定時
89
+ "path": "/abs/out.pdf", // when outputPath is given
90
+ "base64": "JVBERi0xLj...", // when returnBase64, or outputPath is omitted
182
91
  "pageCount": 3,
183
92
  "bytes": 91788,
184
- "font": "NotoSansJP-Regular.otf"
93
+ "font": "NotoSansJP-Regular.otf",
94
+ "warnings": ["Replaced 1 unsupported character(s) with \"〓\": \"✔\" (U+2714)"]
185
95
  }
186
96
  ```
187
97
 
188
- ## テキスト抽出・検索について
98
+ Editing tools return the same shape without `font`; `split_pdf` returns `{ files: [...], count }`.
99
+
100
+ ## Text extraction
101
+
102
+ Generated PDFs are selectable, copyable, searchable, and screen-reader accessible: pdf-lib emits a ToUnicode CMap even for embedded subset fonts. This is covered by regression tests (`extract.test.ts`, `render.test.ts`).
103
+
104
+ > Some poppler-based viewers print `Mismatch between font type and embedded font file` for OTF/CFF fonts embedded as CIDFontType0. This is harmless — rendering and extraction are both correct.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ npm install
110
+ npm run build # emits dist/
111
+ npm test # vitest
112
+ npm run typecheck # tsc --noEmit
113
+ ```
189
114
 
190
- 生成される PDF **テキストの選択・コピー・全文検索が可能** です。
191
- 埋め込みフォント使用時も pdf-lib が ToUnicode CMap(CID→Unicode の逆引き)を出力するため、
192
- `pdftotext` 等で本文を抽出できます。この性質は `tests/extract.test.ts` の回帰テストで担保しています
193
- (ToUnicode に `日 → U+65E5` のマッピングが含まれることを検証)。
115
+ Font-dependent tests activate when `TEST_FONT_PATH` points at a CJK font:
194
116
 
195
- > **v0.3.0 の重要な修正**: v0.2.1 以前は pdf-lib(fontkit)のサブセッタを使っており、
196
- > 日本語フォントのグリフが破壊されて**すべてのビューアで文字が豆腐・空白になる**不具合がありました
197
- > (テキスト抽出だけは正常なため気づきにくい状態でした)。v0.3.0 で harfbuzz による事前サブセットに
198
- > 切り替え、サイズはより小さく(実測 24KB → 14.5KB)、描画は正常になっています。
117
+ ```bash
118
+ TEST_FONT_PATH=/path/to/NotoSansJP-Regular.otf npm test
119
+ ```
199
120
 
200
- ## 既知の制約
121
+ ## Known limitations
201
122
 
202
- - **インライン装飾**: 太字・斜体などはサイズ/字面のみで、書体としては反映されません
203
- (見出し/本文/太字を別フェイスで埋め分ける対応はロードマップ参照)。
204
- - **`.ttc` 非対応**: 単一フェイスへの抽出が必要です(上記参照)。
205
- - **サブセット名の接頭辞**: pdf-lib は慣習的な `ABCDEF+` 接頭辞を付けないため、
206
- 一部ツールがサブセットでないと誤認します(表示・抽出には無影響。PDF/A 厳密対応時の項目)。
123
+ - **Inline styling**: bold/italic affect size and glyph text only, not typeface — a single font is embedded per document.
124
+ - **`.ttc` fonts** require extracting a single face (see above).
125
+ - **Subset name prefix**: the conventional `ABCDEF+` prefix is not applied, so some tools report the font as non-subset. No effect on rendering or extraction; relevant only for strict PDF/A work.
207
126
 
208
- ## ロードマップ
127
+ ## Roadmap
209
128
 
210
- - [x] 編集系 Tier A 1波(メタデータ・ページ操作)— v0.2.0
211
- - [ ] 編集系 Tier A 2波(しおり・注釈)
212
- - [ ] 編集系 Tier B(フォーム記入・フラット化 / 透かし / 添付ファイル / ページ番号スタンプ)
213
- - [ ] タグ付き PDF / PDF/UA 対応(スクリーンリーダ向けの構造タグ。抽出可否とは別レイヤの本格対応)
214
- - [ ] `.ttc` からのフェイス自動抽出(Node だけで完結)
215
- - [ ] 見出し用と本文用のフォント分け(太字フェイス埋め込み)
216
- - [ ] 画像埋め込み / ヘッダー・フッター
217
- - [ ] 署名を保持する増分更新・本文テキスト編集・タグ木保守(Tier C
218
- - [ ] PDF/A 変換(サブセット命名の正規化を含む・外部ツール連携)
129
+ - [x] Editing Tier A wave 1 — metadata and page operations (v0.2.0)
130
+ - [x] Editing Tier A wave 2 — bookmarks and annotations (v0.4.0)
131
+ - [ ] Editing Tier B form filling/flattening, watermarks, attachments, page-number stamping
132
+ - [ ] Tagged PDF / PDF/UA (structure tags for screen readers)
133
+ - [ ] Automatic face extraction from `.ttc`
134
+ - [ ] Separate faces for headings and body (bold face embedding)
135
+ - [ ] Image embedding, headers/footers
136
+ - [ ] Tier C — signature-preserving incremental updates, body text editing, tag tree maintenance
137
+ - [ ] PDF/A conversion
219
138
 
220
- ## ライセンス
139
+ ## License
221
140
 
222
141
  MIT © shuji-bonji
@@ -33,7 +33,14 @@ export declare const LIMITS: {
33
33
  readonly MERGE_MAX_INPUTS: 50;
34
34
  /** split の最大分割数。暴走防止 */
35
35
  readonly SPLIT_MAX_PARTS: 200;
36
+ /** しおりの最大総数・最大ネスト深さ */
37
+ readonly BOOKMARK_MAX_TOTAL: 2000;
38
+ readonly BOOKMARK_MAX_DEPTH: 8;
36
39
  };
40
+ /** add_annotation が受け付ける注釈種別 */
41
+ export declare const ANNOTATION_TYPES: readonly ['text', 'highlight', 'square'];
42
+ /** text 注釈のアイコン名(ISO 32000-1 Table 172 の一般的な値) */
43
+ export declare const ANNOTATION_ICONS: readonly ['Note', 'Comment', 'Key', 'Help', 'NewParagraph', 'Paragraph', 'Insert'];
37
44
  /** rotate_pages が受け付ける回転角(時計回り・度) */
38
45
  export declare const ROTATION_ANGLES: readonly [90, 180, 270];
39
46
  /**
package/dist/constants.js CHANGED
@@ -32,7 +32,22 @@ export const LIMITS = {
32
32
  MERGE_MAX_INPUTS: 50,
33
33
  /** split の最大分割数。暴走防止 */
34
34
  SPLIT_MAX_PARTS: 200,
35
+ /** しおりの最大総数・最大ネスト深さ */
36
+ BOOKMARK_MAX_TOTAL: 2_000,
37
+ BOOKMARK_MAX_DEPTH: 8,
35
38
  };
39
+ /** add_annotation が受け付ける注釈種別 */
40
+ export const ANNOTATION_TYPES = ['text', 'highlight', 'square'];
41
+ /** text 注釈のアイコン名(ISO 32000-1 Table 172 の一般的な値) */
42
+ export const ANNOTATION_ICONS = [
43
+ 'Note',
44
+ 'Comment',
45
+ 'Key',
46
+ 'Help',
47
+ 'NewParagraph',
48
+ 'Paragraph',
49
+ 'Insert',
50
+ ];
36
51
  /** rotate_pages が受け付ける回転角(時計回り・度) */
37
52
  export const ROTATION_ANGLES = [90, 180, 270];
38
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IACrB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC;CACV,CAAC;AAIX;;GAEG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,qBAAqB;IACrB,aAAa,EAAE,CAAC;IAChB,aAAa,EAAE,EAAE;IACjB,sCAAsC;IACtC,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,GAAG;IACf,6CAA6C;IAC7C,eAAe,EAAE,OAAO;IACxB,+BAA+B;IAC/B,cAAc,EAAE,EAAE;IAClB,cAAc,EAAE,KAAK;IACrB,4BAA4B;IAC5B,gBAAgB,EAAE,EAAE;IACpB,wBAAwB;IACxB,eAAe,EAAE,GAAG;CACZ,CAAC;AAEX,qCAAqC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,mCAAmC;IACnC,GAAG,EAAE,MAAM;CACH,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;IACrB,EAAE,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC;IAClB,KAAK,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC;CACV,CAAC;AAIX;;GAEG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,qBAAqB;IACrB,aAAa,EAAE,CAAC;IAChB,aAAa,EAAE,EAAE;IACjB,sCAAsC;IACtC,UAAU,EAAE,CAAC;IACb,UAAU,EAAE,GAAG;IACf,6CAA6C;IAC7C,eAAe,EAAE,OAAO;IACxB,+BAA+B;IAC/B,cAAc,EAAE,EAAE;IAClB,cAAc,EAAE,KAAK;IACrB,4BAA4B;IAC5B,gBAAgB,EAAE,EAAE;IACpB,wBAAwB;IACxB,eAAe,EAAE,GAAG;IACpB,uBAAuB;IACvB,kBAAkB,EAAE,KAAK;IACzB,kBAAkB,EAAE,CAAC;CACb,CAAC;AAEX,gCAAgC;AAChC,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAEzE,kDAAkD;AAClD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,MAAM;IACN,SAAS;IACT,KAAK;IACL,MAAM;IACN,cAAc;IACd,WAAW;IACX,QAAQ;CACA,CAAC;AAEX,qCAAqC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,CAAU,CAAC;AAEvD;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,mCAAmC;IACnC,GAAG,EAAE,MAAM;CACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Annotation
3
+ * pdf-lib の低レベル API で注釈辞書を組み立て、ページの /Annots に追加する。
4
+ *
5
+ * 対応(ISO 32000-1 §12.5.6):
6
+ * - text : 付箋(Text annotation)。/Rect の左上にアイコン表示
7
+ * - highlight : ハイライト(Highlight)。/QuadPoints が必須
8
+ * - square : 矩形(Square)
9
+ *
10
+ * 座標系は PDF 準拠(左下原点・pt)。生成系レンダラの top 基準とは異なる点に注意。
11
+ */
12
+ import { PDFDocument, type RGB } from 'pdf-lib';
13
+ import type { AddAnnotationArgs } from '../types/index.js';
14
+ /** #rrggbb / #rgb を pdf-lib の RGB へ */
15
+ export declare function parseHexColor(hex: string): RGB;
16
+ /** 注釈をページに追加する。@returns 追加後のそのページの注釈数 */
17
+ export declare function addAnnotation(doc: PDFDocument, args: AddAnnotationArgs): number;