@shuji-bonji/pdf-writer-mcp 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.ja.md +156 -0
  3. package/README.md +105 -171
  4. package/dist/config.js +1 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/constants.d.ts +16 -0
  7. package/dist/constants.js +24 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/services/annotation.d.ts +17 -0
  12. package/dist/services/annotation.js +90 -0
  13. package/dist/services/annotation.js.map +1 -0
  14. package/dist/services/builder.js +32 -4
  15. package/dist/services/builder.js.map +1 -1
  16. package/dist/services/editor.d.ts +3 -1
  17. package/dist/services/editor.js +19 -2
  18. package/dist/services/editor.js.map +1 -1
  19. package/dist/services/font-manager.d.ts +1 -1
  20. package/dist/services/font-manager.js +1 -1
  21. package/dist/services/font-manager.js.map +1 -1
  22. package/dist/services/layout.d.ts +17 -1
  23. package/dist/services/layout.js +46 -13
  24. package/dist/services/layout.js.map +1 -1
  25. package/dist/services/outline.d.ts +20 -0
  26. package/dist/services/outline.js +93 -0
  27. package/dist/services/outline.js.map +1 -0
  28. package/dist/services/output.js +2 -2
  29. package/dist/services/output.js.map +1 -1
  30. package/dist/services/renderers/markdown.d.ts +1 -1
  31. package/dist/services/renderers/markdown.js +58 -15
  32. package/dist/services/renderers/markdown.js.map +1 -1
  33. package/dist/services/renderers/table.d.ts +1 -1
  34. package/dist/services/renderers/table.js +30 -16
  35. package/dist/services/renderers/table.js.map +1 -1
  36. package/dist/services/renderers/text.d.ts +1 -1
  37. package/dist/services/renderers/text.js +4 -1
  38. package/dist/services/renderers/text.js.map +1 -1
  39. package/dist/services/struct-tree.d.ts +79 -0
  40. package/dist/services/struct-tree.js +222 -0
  41. package/dist/services/struct-tree.js.map +1 -0
  42. package/dist/services/xmp.d.ts +42 -0
  43. package/dist/services/xmp.js +133 -0
  44. package/dist/services/xmp.js.map +1 -0
  45. package/dist/tools/definitions.d.ts +156 -0
  46. package/dist/tools/definitions.js +89 -0
  47. package/dist/tools/definitions.js.map +1 -1
  48. package/dist/tools/handlers.d.ts +2 -0
  49. package/dist/tools/handlers.js +17 -6
  50. package/dist/tools/handlers.js.map +1 -1
  51. package/dist/types/index.d.ts +54 -0
  52. package/dist/utils/lang.d.ts +18 -0
  53. package/dist/utils/lang.js +30 -0
  54. package/dist/utils/lang.js.map +1 -0
  55. package/dist/utils/validation.d.ts +3 -1
  56. package/dist/utils/validation.js +82 -5
  57. package/dist/utils/validation.js.map +1 -1
  58. package/package.json +14 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,190 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.5.0] - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ - **Tagged PDF / PDF/UA-1 (ISO 14289)** via `tagged: true` on the create tools.
10
+ Output is **verified compliant by veraPDF** (`--flavour ua1`, 106/106 rules) for
11
+ text, Markdown, and table documents.
12
+
13
+ Opt-in by design: existing behaviour is unchanged unless you ask for tagging.
14
+ PDF/UA mandates a document title, so `tagged: true` requires `title`.
15
+
16
+ - Structure tree (`StructTreeRoot`, `StructElem`, `ParentTree`) built from
17
+ pdf-lib's low-level object model — it has no logical-structure API.
18
+ - Every drawn line is wrapped in `/Tag <</MCID n>> BDC … EMC`, and decorations
19
+ (rules, cell borders, code backgrounds) become `/Artifact BMC … EMC` (7.1-3).
20
+ - Markdown maps to structure: headings → `H1`–`H6`, lists → `L`/`LI`/`LBody`,
21
+ tables → `Table`/`TR`/`TH`/`TD`, quotes → `BlockQuote`, code → `Code`.
22
+ - Heading levels are normalised so they start at H1 and never skip (7.4.2).
23
+ A Markdown `# → ###` jump becomes `H1 → H2`; visual sizes are untouched.
24
+ - Table headers carry `/A << /O /Table /Scope /Column >>` (7.5-1).
25
+ - XMP is generated in-house (pdf-lib has no XMP API) with the `pdfuaid`
26
+ declaration **and** the PDF/A extension schema description — veraPDF rejects
27
+ the declaration without it (5-1).
28
+ - `/Lang`, `/ViewerPreferences /DisplayDocTitle`, `/MarkInfo /Marked`.
29
+
30
+ - **`lang` option** (BCP 47). When omitted under `tagged: true` the language is
31
+ inferred from the text and reported via `warnings`. Inference is conservative:
32
+ kana → `ja`, Hangul → `ko`, Han without kana → `ja` *with a warning that it
33
+ could be Chinese*. A wrong `/Lang` makes screen readers mispronounce text, so
34
+ the guess is always surfaced rather than silently applied.
35
+
36
+ ### Fixed
37
+
38
+ - **Bullets in unordered lists rendered as `.notdef`** (blank boxes) — a
39
+ regression from 0.3.0. Font subsetting is driven by the *input* text, but the
40
+ Markdown renderer adds characters of its own: `- item` contains no bullet, yet
41
+ `•` is drawn. Those glyphs were missing from the subset. Renderer-generated
42
+ characters are now always subset in (`RENDERER_GENERATED_CHARS`).
43
+
44
+ Text extraction was unaffected, which is why no existing test caught it;
45
+ veraPDF's 7.21.8-1 (no `.notdef` references) is what surfaced it.
46
+
47
+ - XMP metadata is written as UTF-8 bytes. `context.stream(string)` writes one
48
+ byte per character, which mangled Japanese titles.
49
+
50
+ ## [Unreleased]
51
+
52
+ ### Changed
53
+
54
+ - **biome adopted** for linting and formatting, matching the rest of the family
55
+ (same `biome.json`, same scripts). `npm run check` now runs in CI and in the
56
+ publish workflow. Existing sources were reformatted; the suite is clean.
57
+ - The version is pinned to an exact `2.5.4` rather than a caret range. Biome's
58
+ formatting output changes between minor releases, so a range lets a local
59
+ `npm install` drift ahead of CI and produce spurious diffs.
60
+
61
+ ### Fixed
62
+
63
+ - `handleCreateTablePdf` duplicated the Latin-1 check as a regex containing
64
+ control characters; it now reuses the existing `hasNonLatin1` helper.
65
+ - `src/config.ts` imports `node:module` rather than the bare `module` specifier.
66
+
67
+ ## [0.4.0] - 2026-07-16
68
+
69
+ ### Added
70
+
71
+ - **Editing tools (Tier A, wave 2)**:
72
+ - `add_bookmarks` — set the document outline. Nestable via `children` (up to 8
73
+ levels, 2000 entries). Replaces any existing outline. Titles are written as
74
+ UTF-16BE so CJK round-trips correctly. Open items carry a positive `/Count`,
75
+ collapsed items a negative one, per ISO 32000-1 §12.3.3.
76
+ - `add_annotation` — add a `text` (sticky note), `highlight`, or `square`
77
+ annotation. Coordinates use PDF space (origin bottom-left, points). Colors
78
+ are `#rrggbb`; `contents` and `author` accept CJK.
79
+
80
+ pdf-lib exposes no outline or annotation API, so both are built from the
81
+ low-level object model (`services/outline.ts`, `services/annotation.ts`).
82
+
83
+ ## [0.3.1] - 2026-07-16
84
+
85
+ ### Fixed
86
+
87
+ - **Text extraction broken for digits in Latin context** (regression introduced in
88
+ 0.3.0): `subsetFont` is now called with `noLayoutClosure: true`.
89
+
90
+ pdf-lib's `CustomFontEmbedder` (used with `subset: false`) derives glyphs
91
+ through two independent paths: the CIDs written into the content stream come
92
+ from `font.layout(text)` — **after** GSUB substitution — while the ToUnicode
93
+ CMap is built from `font.characterSet` → `glyphForCodePoint` — **before**
94
+ substitution. Noto Sans JP substitutes ASCII digits with alternate forms in
95
+ Latin context (`layout('English 0')` yields glyph 17460, not 17), so the two
96
+ disagreed and extraction returned wrong characters (`v0.3.0` → `vô.õ.ô`, and
97
+ poppler dropped the digits entirely). Rendering was correct throughout, which
98
+ made this easy to miss.
99
+
100
+ Excluding layout-reachable glyphs from the subset prevents the substitution
101
+ altogether, so `layout()` and `glyphsForString()` agree again. Subsets also got
102
+ smaller as a side effect (9.1 KB → 4.5 KB for a typical page).
103
+
104
+ ### Added
105
+
106
+ - `render.test.ts` now asserts that the CIDs written to the page match the
107
+ ToUnicode CMap, so this class of regression fails the suite.
108
+
109
+ ## [0.3.0] - 2026-07-16
110
+
111
+ ### Fixed
112
+
113
+ - **Japanese text rendered as blank boxes in every viewer** (Chrome, Firefox,
114
+ Acrobat, poppler, Claude Desktop). Font subsetting no longer goes through
115
+ pdf-lib's `embedFont(subset: true)`: fonts are now pre-subset with harfbuzz
116
+ ([subset-font](https://github.com/papandreou/subset-font)) and embedded with
117
+ `subset: false`.
118
+
119
+ fontkit's subsetter drops glyph outlines for CJK fonts. Because the ToUnicode
120
+ CMap stayed correct, text extraction kept working and the existing tests
121
+ passed while the visible output was broken.
122
+
123
+ | Approach | PDF size | Rendering |
124
+ |---|---|---|
125
+ | fontkit `subset: true` (previous) | 24 KB | broken |
126
+ | pdf-lib `subset: false` alone | 3.9 MB | correct |
127
+ | harfbuzz + `subset: false` (current) | 14.5 KB | correct |
128
+
129
+ - The note in the design doc claiming poppler's `Embedded font file may be
130
+ invalid` warning was harmless is retracted — poppler followed it with
131
+ `Couldn't create a font`, i.e. the warning was the breakage.
132
+
133
+ ### Added
134
+
135
+ - `render.test.ts`: extracts the embedded font program from generated PDFs and
136
+ verifies that every rendered character retains a real glyph outline. Verified
137
+ to fail against the pre-0.3.0 implementation.
138
+ - CI fetches Noto Sans JP and sets `TEST_FONT_PATH`, so the font-dependent tests
139
+ no longer skip.
140
+
141
+ ### Changed
142
+
143
+ - `loadFont` split into `openFont` (read + glyph coverage) and `embedFontFor`
144
+ (subset + embed). Subsetting depends on the text being drawn, so embedding is
145
+ deferred until the input is final.
146
+ - **Dependency added**: `subset-font` (harfbuzz/wasm, no native binaries).
147
+
148
+ ## [0.2.1] - 2026-07-16
149
+
150
+ ### Added
151
+
152
+ - **`onMissingGlyph` option** for the create tools. Characters absent from the
153
+ font (e.g. ✔ U+2714, which Noto Sans JP does not include) were previously
154
+ embedded as `.notdef` and rendered as silent blanks.
155
+ - `error` (default) — list the offending characters and fail
156
+ - `replace` — substitute 〓 and report via `warnings`
157
+ - `ignore` — render as before and report via `warnings`
158
+ - `CreateResult.warnings` reports substituted or ignored characters.
159
+
160
+ ## [0.2.0] - 2026-07-16
161
+
162
+ ### Added
163
+
164
+ - **Editing tools (Tier A, wave 1)** — the server now edits existing PDFs in
165
+ addition to creating them:
166
+ - `set_metadata` — update Info dictionary fields, preserving the rest
167
+ - `merge_pdfs` — concatenate in order (metadata inherited from the first file)
168
+ - `split_pdf` — one file per page range
169
+ - `extract_pages` — extract in the requested order (doubles as reordering)
170
+ - `delete_pages` — remove pages (deleting all is rejected)
171
+ - `reorder_pages` — reorder by an explicit permutation
172
+ - `rotate_pages` — rotate clockwise, accumulating over existing rotation
173
+ - **Signature guard**: editing a PDF with `/ByteRange` fails by default, since
174
+ pdf-lib rewrites the whole file and would invalidate existing signatures.
175
+ Pass `allowBreakingSignatures: true` to proceed anyway.
176
+ - Page specs (`"1,3-5,8-"`, 1-based, order-preserving, duplicates removed).
177
+ - CI (`ci.yml`) and npm publish via Trusted Publisher / OIDC (`publish.yml`).
178
+
179
+ ## [0.1.0] - 2026-07-15
180
+
181
+ Initial release.
182
+
183
+ ### Added
184
+
185
+ - `create_text_pdf` — plain text with paragraph breaks, wrapping, pagination
186
+ - `create_markdown_pdf` — headings, lists, code blocks, quotes, rules, tables
187
+ - `create_table_pdf` — ruled tables with automatic column widths and repeated
188
+ headers across page breaks
189
+ - Japanese font embedding, ToUnicode CMap (extractable/searchable text),
190
+ file or base64 output, `asserts`-based input validation.
package/README.ja.md ADDED
@@ -0,0 +1,156 @@
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` / `tagged` / `lang`。
24
+
25
+ ### タグ付き PDF / PDF/UA(v0.5.0)
26
+
27
+ `tagged: true` を指定すると、**PDF/UA-1(ISO 14289)準拠のタグ付き PDF**(アクセシブルな PDF)を生成します。veraPDF(`--flavour ua1`)で 106/106 規則の準拠を確認済みです。
28
+
29
+ ```jsonc
30
+ { "markdown": "# 見出し\n\n本文。", "title": "レポート", "tagged": true, "lang": "ja" }
31
+ ```
32
+
33
+ Markdown は構造木に対応付けられます。見出し → `H1`〜`H6`、リスト → `L`/`LI`/`LBody`、表 → `Table`/`TR`/`TH`/`TD`(ヘッダには `/Scope`)、引用 → `BlockQuote`、コード → `Code`。水平線・罫線・コード背景は Artifact になります。見出しレベルは「H1 始まり・飛ばさない」よう正規化されるため、Markdown の `# → ###` は構造上 `H1 → H2` になります(見た目のサイズは元のままです)。
34
+
35
+ PDF/UA はタイトルを要求するため、`tagged: true` では `title` が必須です。`lang`(BCP 47)は省略すると本文から推定し、`warnings` で報告します。**誤った `/Lang` はスクリーンリーダの誤読を招く**ため、分かっている場合は明示してください。
36
+
37
+ > タグ付けは opt-in です(既定の出力は変わりません)。機械検証は「読み順や代替テキストが**適切か**」までは判定できず、存在の有無しか見られません。人手の確認は依然として必要です。
38
+
39
+ ### 編集
40
+
41
+ | ツール | 役割 |
42
+ |--------|------|
43
+ | `set_metadata` | Info 辞書の更新(`title` / `author` / `subject` / `keywords` / `creator`)。指定フィールドのみ変更し、他は保持 |
44
+ | `merge_pdfs` | 2〜50 個の PDF を指定順に結合。メタデータは先頭ファイルから引き継ぎ |
45
+ | `split_pdf` | ページ範囲ごとに 1 ファイルへ分割 |
46
+ | `extract_pages` | 指定順を保持して抽出(並べ替えを兼ねる) |
47
+ | `delete_pages` | ページ削除(全ページの削除はエラー) |
48
+ | `reorder_pages` | 全ページの順列による並べ替え |
49
+ | `rotate_pages` | 時計回りに回転(90/180/270)。既存の回転に加算 |
50
+ | `add_bookmarks` | しおり(アウトライン)の設定。`children` で入れ子にでき、既存のしおりは置換 |
51
+ | `add_annotation` | 付箋(`text`)/ ハイライト(`highlight`)/ 矩形(`square`)の注釈を追加 |
52
+
53
+ 共通オプション: `outputPath` / `returnBase64` / `allowBreakingSignatures`。
54
+
55
+ ページ指定は `"1,3-5,8-"` 形式(1 始まり。`-3` は先頭から 3 ページまで、`8-` は 8 ページから最終まで)。指定順を保持し、重複は除去します。
56
+
57
+ > **署名について**: pdf-lib は保存時にファイル全体を再構築するため、編集すると既存の電子署名は必ず無効化されます。`/ByteRange` を含む PDF は既定でエラーとし、`allowBreakingSignatures: true` を指定したときのみ続行します。署名を保持する増分更新はロードマップ参照。
58
+
59
+ ## インストール
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "pdf-writer": {
65
+ "command": "npx",
66
+ "args": ["-y", "@shuji-bonji/pdf-writer-mcp"],
67
+ "env": {
68
+ "PDF_WRITER_FONT": "/absolute/path/to/NotoSansJP-Regular.otf"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ `PDF_WRITER_FONT` を設定しておくと、各ツールで `fontPath` を省略しても日本語が出せます。
76
+
77
+ ## フォント
78
+
79
+ 標準 PDF フォント(Helvetica)は **ASCII のみ** です。日本語など非ラテン文字を描画するには、`fontPath` または `PDF_WRITER_FONT` で埋め込み可能な**単一フェイス**のフォント(`.ttf` / `.otf`)を指定してください。
80
+
81
+ - 推奨入手先: [Noto Sans JP (SubsetOTF/JP)](https://github.com/notofonts/noto-cjk/tree/main/Sans/SubsetOTF/JP) — 静的・単一フェイス・SIL OFL。
82
+ - **`.ttc`(TrueTypeCollection)は非対応**です。検知してエラーにするため、単一フェイスを抽出してください:
83
+
84
+ ```bash
85
+ python3 -c "from fontTools.ttLib import TTCollection; \
86
+ TTCollection('NotoSansCJK-Regular.ttc').fonts[0].save('NotoSansCJKjp-Regular.otf')"
87
+ ```
88
+
89
+ ### フォント未収録文字(グリフ欠落)
90
+
91
+ フォントに存在しない文字(例: Noto Sans JP に無い ✔ U+2714)は、そのままだと `.notdef` として埋め込まれ、無警告で空白になります。`onMissingGlyph` でこの挙動を選べます。
92
+
93
+ | 値 | 挙動 |
94
+ |----|------|
95
+ | `error`(既定) | 欠落文字を `"✔" (U+2714)` 形式で列挙してエラー |
96
+ | `replace` | 〓 に置換して生成し、`warnings` で報告 |
97
+ | `ignore` | 空白のまま生成し、`warnings` で報告 |
98
+
99
+ ## 返り値
100
+
101
+ ```jsonc
102
+ {
103
+ "path": "/abs/out.pdf", // outputPath 指定時
104
+ "base64": "JVBERi0xLj...", // returnBase64 指定時、または outputPath 省略時
105
+ "pageCount": 3,
106
+ "bytes": 91788,
107
+ "font": "NotoSansJP-Regular.otf",
108
+ "warnings": ["Replaced 1 unsupported character(s) with \"〓\": \"✔\" (U+2714)"]
109
+ }
110
+ ```
111
+
112
+ 編集ツールは `font` を除いた同じ形を返します。`split_pdf` は `{ files: [...], count }` を返します。
113
+
114
+ ## テキスト抽出
115
+
116
+ 生成される PDF はテキストの選択・コピー・全文検索・スクリーンリーダ読み上げが可能です。埋め込みサブセットフォントでも pdf-lib が ToUnicode CMap を出力するためで、この性質は回帰テスト(`extract.test.ts` / `render.test.ts`)で担保しています。
117
+
118
+ > poppler 系のビューアは、OTF/CFF フォントを CIDFontType0 として埋め込む際に `Mismatch between font type and embedded font file` という警告を表示することがあります。これは無害で、描画・抽出とも正常です。
119
+
120
+ ## 開発
121
+
122
+ ```bash
123
+ npm install
124
+ npm run build # dist/ に出力
125
+ npm test # vitest
126
+ npm run typecheck # tsc --noEmit
127
+ ```
128
+
129
+ 日本語フォント依存のテストは `TEST_FONT_PATH` を指定すると有効になります。
130
+
131
+ ```bash
132
+ TEST_FONT_PATH=/path/to/NotoSansJP-Regular.otf npm test
133
+ ```
134
+
135
+ ## 既知の制約
136
+
137
+ - **インライン装飾**: 太字・斜体はサイズ / 字面のみで、書体としては反映されません(1 文書につき単一フォントを埋め込むため)。
138
+ - **`.ttc` フォント**: 単一フェイスへの抽出が必要です(上記参照)。
139
+ - **サブセット名の接頭辞**: 慣習的な `ABCDEF+` を付けないため、一部ツールがサブセットでないと誤認します。描画・抽出には影響せず、PDF/A の厳密対応時のみ問題になります。
140
+
141
+ ## ロードマップ
142
+
143
+ - [x] 編集系 Tier A 第1波 — メタデータ・ページ操作(v0.2.0)
144
+ - [x] 編集系 Tier A 第2波 — しおり・注釈(v0.4.0)
145
+ - [x] タグ付き PDF / PDF/UA-1 — veraPDF で準拠を確認(v0.5.0)
146
+ - [ ] 編集系 Tier B — フォーム記入 / フラット化、透かし、添付ファイル、ページ番号スタンプ
147
+ - [ ] 画像の代替テキスト(`Figure` + `/Alt`)、タグ付き出力での注釈の `Annot` タグ内包
148
+ - [ ] `.ttc` からのフェイス自動抽出
149
+ - [ ] 見出し用と本文用のフォント分け(太字フェイス埋め込み)
150
+ - [ ] 画像埋め込み、ヘッダー / フッター
151
+ - [ ] Tier C — 署名を保持する増分更新、本文テキスト編集、タグ木の保守
152
+ - [ ] PDF/A 変換
153
+
154
+ ## ライセンス
155
+
156
+ MIT © shuji-bonji