@shuji-bonji/pdf-writer-mcp 0.2.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/LICENSE +21 -0
- package/README.md +208 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +38 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +46 -0
- package/dist/constants.js +46 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/services/builder.d.ts +10 -0
- package/dist/services/builder.js +40 -0
- package/dist/services/builder.js.map +1 -0
- package/dist/services/editor.d.ts +24 -0
- package/dist/services/editor.js +194 -0
- package/dist/services/editor.js.map +1 -0
- package/dist/services/font-manager.d.ts +18 -0
- package/dist/services/font-manager.js +59 -0
- package/dist/services/font-manager.js.map +1 -0
- package/dist/services/layout.d.ts +62 -0
- package/dist/services/layout.js +198 -0
- package/dist/services/layout.js.map +1 -0
- package/dist/services/output.d.ts +13 -0
- package/dist/services/output.js +60 -0
- package/dist/services/output.js.map +1 -0
- package/dist/services/renderers/markdown.d.ts +8 -0
- package/dist/services/renderers/markdown.js +132 -0
- package/dist/services/renderers/markdown.js.map +1 -0
- package/dist/services/renderers/table.d.ts +7 -0
- package/dist/services/renderers/table.js +89 -0
- package/dist/services/renderers/table.js.map +1 -0
- package/dist/services/renderers/text.d.ts +9 -0
- package/dist/services/renderers/text.js +29 -0
- package/dist/services/renderers/text.js.map +1 -0
- package/dist/tools/definitions.d.ts +388 -0
- package/dist/tools/definitions.js +241 -0
- package/dist/tools/definitions.js.map +1 -0
- package/dist/tools/handlers.d.ts +23 -0
- package/dist/tools/handlers.js +82 -0
- package/dist/tools/handlers.js.map +1 -0
- package/dist/types/index.d.ts +135 -0
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.js +28 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/page-spec.d.ts +12 -0
- package/dist/utils/page-spec.js +55 -0
- package/dist/utils/page-spec.js.map +1 -0
- package/dist/utils/validation.d.ts +24 -0
- package/dist/utils/validation.js +187 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor
|
|
3
|
+
* 既存 PDF の編集(Tier A: メタデータ・ページ操作)を担う。
|
|
4
|
+
*
|
|
5
|
+
* create 系(builder → font → layout → renderer)とはフローが異なり、
|
|
6
|
+
* 読込(loadForEdit: 署名ガード込み)→ 操作 → 保存(saveEdited)
|
|
7
|
+
* の 3 段で完結する。
|
|
8
|
+
*
|
|
9
|
+
* 署名保全について(specs/05 §3-1):
|
|
10
|
+
* pdf-lib の save() はファイル全体を再構築するため、既存の電子署名は必ず無効化される。
|
|
11
|
+
* 署名(/ByteRange)を検知した場合は既定でエラーとし、明示フラグ
|
|
12
|
+
* allowBreakingSignatures: true があるときのみ続行する。
|
|
13
|
+
* 署名を保持する増分更新(incremental_save)は Tier C の課題。
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from 'node:fs/promises';
|
|
16
|
+
import { resolve, basename, extname, join } from 'node:path';
|
|
17
|
+
import { PDFDocument, degrees } from 'pdf-lib';
|
|
18
|
+
import { LIMITS } from '../constants.js';
|
|
19
|
+
import { logger } from '../utils/logger.js';
|
|
20
|
+
import { parsePageSpec } from '../utils/page-spec.js';
|
|
21
|
+
import { saveEdited } from './output.js';
|
|
22
|
+
/** 入力バイト列に電子署名(/ByteRange)が含まれるかの軽量検査 */
|
|
23
|
+
export function containsSignature(bytes) {
|
|
24
|
+
// "/ByteRange" の ASCII 検索(署名辞書は非圧縮で現れるのが通例)
|
|
25
|
+
const needle = [0x2f, 0x42, 0x79, 0x74, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65]; // "/ByteRange"
|
|
26
|
+
outer: for (let i = 0; i <= bytes.length - needle.length; i++) {
|
|
27
|
+
for (let j = 0; j < needle.length; j++) {
|
|
28
|
+
if (bytes[i + j] !== needle[j])
|
|
29
|
+
continue outer;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
/** PDF を読み込み、署名ガードを通す */
|
|
36
|
+
async function loadForEdit(filePath, opts) {
|
|
37
|
+
const absPath = resolve(filePath);
|
|
38
|
+
let bytes;
|
|
39
|
+
try {
|
|
40
|
+
bytes = await readFile(absPath);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`Cannot read PDF file: ${absPath}`);
|
|
44
|
+
}
|
|
45
|
+
if (containsSignature(bytes) && !opts.allowBreakingSignatures) {
|
|
46
|
+
throw new Error(`"${absPath}" appears to be digitally signed (/ByteRange found). ` +
|
|
47
|
+
'Editing will invalidate existing signatures because pdf-lib rewrites the whole file. ' +
|
|
48
|
+
'Pass "allowBreakingSignatures": true to proceed anyway. ' +
|
|
49
|
+
'(Signature-preserving incremental update is a future Tier C feature.)');
|
|
50
|
+
}
|
|
51
|
+
let doc;
|
|
52
|
+
try {
|
|
53
|
+
// updateMetadata: false — 読込時に Producer/ModDate を書き換えない
|
|
54
|
+
doc = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
throw new Error(`Failed to parse PDF "${absPath}" (encrypted or corrupted?): ${e instanceof Error ? e.message : String(e)}`);
|
|
58
|
+
}
|
|
59
|
+
return { doc, absPath };
|
|
60
|
+
}
|
|
61
|
+
/** 基本メタデータを src から dst へ引き継ぐ(copyPages は文書情報を運ばないため) */
|
|
62
|
+
function copyDocumentInfo(src, dst) {
|
|
63
|
+
const title = src.getTitle();
|
|
64
|
+
const author = src.getAuthor();
|
|
65
|
+
const subject = src.getSubject();
|
|
66
|
+
const keywords = src.getKeywords();
|
|
67
|
+
const creator = src.getCreator();
|
|
68
|
+
const producer = src.getProducer();
|
|
69
|
+
const creationDate = src.getCreationDate();
|
|
70
|
+
if (title !== undefined)
|
|
71
|
+
dst.setTitle(title);
|
|
72
|
+
if (author !== undefined)
|
|
73
|
+
dst.setAuthor(author);
|
|
74
|
+
if (subject !== undefined)
|
|
75
|
+
dst.setSubject(subject);
|
|
76
|
+
if (keywords !== undefined)
|
|
77
|
+
dst.setKeywords([keywords]);
|
|
78
|
+
if (creator !== undefined)
|
|
79
|
+
dst.setCreator(creator);
|
|
80
|
+
if (producer !== undefined)
|
|
81
|
+
dst.setProducer(producer);
|
|
82
|
+
if (creationDate !== undefined)
|
|
83
|
+
dst.setCreationDate(creationDate);
|
|
84
|
+
}
|
|
85
|
+
/** src の指定ページ(1 始まり配列・指定順)だけを持つ新規文書を作る */
|
|
86
|
+
async function copyIntoNewDoc(src, pages1) {
|
|
87
|
+
const dst = await PDFDocument.create();
|
|
88
|
+
const copied = await dst.copyPages(src, pages1.map((n) => n - 1));
|
|
89
|
+
for (const p of copied)
|
|
90
|
+
dst.addPage(p);
|
|
91
|
+
copyDocumentInfo(src, dst);
|
|
92
|
+
return dst;
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Tier A ツール本体
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
export async function setMetadata(args) {
|
|
98
|
+
const { doc } = await loadForEdit(args.inputPath, args);
|
|
99
|
+
if (args.title !== undefined)
|
|
100
|
+
doc.setTitle(args.title);
|
|
101
|
+
if (args.author !== undefined)
|
|
102
|
+
doc.setAuthor(args.author);
|
|
103
|
+
if (args.subject !== undefined)
|
|
104
|
+
doc.setSubject(args.subject);
|
|
105
|
+
if (args.keywords !== undefined)
|
|
106
|
+
doc.setKeywords(args.keywords);
|
|
107
|
+
if (args.creator !== undefined)
|
|
108
|
+
doc.setCreator(args.creator);
|
|
109
|
+
return saveEdited(doc, args);
|
|
110
|
+
}
|
|
111
|
+
export async function mergePdfs(inputPaths, opts) {
|
|
112
|
+
const dst = await PDFDocument.create();
|
|
113
|
+
for (const p of inputPaths) {
|
|
114
|
+
const { doc: src } = await loadForEdit(p, opts);
|
|
115
|
+
const copied = await dst.copyPages(src, src.getPageIndices());
|
|
116
|
+
for (const page of copied)
|
|
117
|
+
dst.addPage(page);
|
|
118
|
+
}
|
|
119
|
+
// 文書情報は先頭ファイルから引き継ぐ
|
|
120
|
+
const { doc: first } = await loadForEdit(inputPaths[0], opts);
|
|
121
|
+
copyDocumentInfo(first, dst);
|
|
122
|
+
logger.info('Editor', `Merged ${inputPaths.length} PDFs (${dst.getPageCount()} pages)`);
|
|
123
|
+
return saveEdited(dst, opts);
|
|
124
|
+
}
|
|
125
|
+
export async function extractPages(inputPath, pages, opts) {
|
|
126
|
+
const { doc: src } = await loadForEdit(inputPath, opts);
|
|
127
|
+
const pageNums = parsePageSpec(pages, src.getPageCount());
|
|
128
|
+
const dst = await copyIntoNewDoc(src, pageNums);
|
|
129
|
+
return saveEdited(dst, opts);
|
|
130
|
+
}
|
|
131
|
+
export async function deletePages(inputPath, pages, opts) {
|
|
132
|
+
const { doc: src } = await loadForEdit(inputPath, opts);
|
|
133
|
+
const total = src.getPageCount();
|
|
134
|
+
const del = new Set(parsePageSpec(pages, total));
|
|
135
|
+
if (del.size >= total) {
|
|
136
|
+
throw new Error(`Cannot delete all ${total} page(s) — the result would be an empty PDF`);
|
|
137
|
+
}
|
|
138
|
+
const keep = [];
|
|
139
|
+
for (let n = 1; n <= total; n++)
|
|
140
|
+
if (!del.has(n))
|
|
141
|
+
keep.push(n);
|
|
142
|
+
const dst = await copyIntoNewDoc(src, keep);
|
|
143
|
+
return saveEdited(dst, opts);
|
|
144
|
+
}
|
|
145
|
+
export async function reorderPages(inputPath, order, opts) {
|
|
146
|
+
const { doc: src } = await loadForEdit(inputPath, opts);
|
|
147
|
+
const total = src.getPageCount();
|
|
148
|
+
if (order.length !== total) {
|
|
149
|
+
throw new Error(`order must list all ${total} page(s) exactly once, got ${order.length}`);
|
|
150
|
+
}
|
|
151
|
+
const seen = new Set();
|
|
152
|
+
for (const n of order) {
|
|
153
|
+
if (!Number.isInteger(n) || n < 1 || n > total) {
|
|
154
|
+
throw new Error(`order contains an invalid page number ${n} (1..${total})`);
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(n)) {
|
|
157
|
+
throw new Error(`order contains page ${n} more than once`);
|
|
158
|
+
}
|
|
159
|
+
seen.add(n);
|
|
160
|
+
}
|
|
161
|
+
const dst = await copyIntoNewDoc(src, order);
|
|
162
|
+
return saveEdited(dst, opts);
|
|
163
|
+
}
|
|
164
|
+
export async function rotatePages(inputPath, rotation, pages, opts) {
|
|
165
|
+
const { doc } = await loadForEdit(inputPath, opts);
|
|
166
|
+
const targets = pages
|
|
167
|
+
? parsePageSpec(pages, doc.getPageCount())
|
|
168
|
+
: Array.from({ length: doc.getPageCount() }, (_, i) => i + 1);
|
|
169
|
+
for (const n of targets) {
|
|
170
|
+
const page = doc.getPage(n - 1);
|
|
171
|
+
const current = page.getRotation().angle;
|
|
172
|
+
page.setRotation(degrees((((current + rotation) % 360) + 360) % 360));
|
|
173
|
+
}
|
|
174
|
+
return saveEdited(doc, opts);
|
|
175
|
+
}
|
|
176
|
+
export async function splitPdf(inputPath, ranges, outputDir, prefix, opts) {
|
|
177
|
+
if (ranges.length > LIMITS.SPLIT_MAX_PARTS) {
|
|
178
|
+
throw new Error(`Too many split parts (${ranges.length}, max ${LIMITS.SPLIT_MAX_PARTS})`);
|
|
179
|
+
}
|
|
180
|
+
const { doc: src, absPath } = await loadForEdit(inputPath, opts);
|
|
181
|
+
const total = src.getPageCount();
|
|
182
|
+
const base = prefix ?? `${basename(absPath, extname(absPath))}-part`;
|
|
183
|
+
const files = [];
|
|
184
|
+
for (const [i, range] of ranges.entries()) {
|
|
185
|
+
const pageNums = parsePageSpec(range, total, `ranges[${i}]`);
|
|
186
|
+
const dst = await copyIntoNewDoc(src, pageNums);
|
|
187
|
+
const outputPath = join(outputDir, `${base}${i + 1}.pdf`);
|
|
188
|
+
const saved = await saveEdited(dst, { outputPath });
|
|
189
|
+
files.push({ path: saved.path, pageCount: saved.pageCount, bytes: saved.bytes });
|
|
190
|
+
}
|
|
191
|
+
logger.info('Editor', `Split "${absPath}" into ${files.length} file(s)`);
|
|
192
|
+
return { files, count: files.length };
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.js","sourceRoot":"","sources":["../../src/services/editor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font Manager
|
|
3
|
+
* カスタムフォント(.ttf/.otf)の埋め込みと、標準フォントへのフォールバックを担う。
|
|
4
|
+
* .ttc(TrueTypeCollection)は pdf-lib がサブセット化できないため検知して弾く。
|
|
5
|
+
*/
|
|
6
|
+
import { PDFDocument, type PDFFont } from 'pdf-lib';
|
|
7
|
+
export interface LoadedFont {
|
|
8
|
+
font: PDFFont;
|
|
9
|
+
/** 表示用フォント名 */
|
|
10
|
+
name: string;
|
|
11
|
+
/** 標準フォント(英数字のみ)か否か。true の場合、日本語描画は不可 */
|
|
12
|
+
isStandard: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* フォントを読み込んで doc に埋め込む。
|
|
16
|
+
* @param fontPath 明示指定のフォントパス(優先)。未指定なら環境変数 → 標準フォント。
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadFont(doc: PDFDocument, fontPath?: string): Promise<LoadedFont>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font Manager
|
|
3
|
+
* カスタムフォント(.ttf/.otf)の埋め込みと、標準フォントへのフォールバックを担う。
|
|
4
|
+
* .ttc(TrueTypeCollection)は pdf-lib がサブセット化できないため検知して弾く。
|
|
5
|
+
*/
|
|
6
|
+
import { readFile } from 'node:fs/promises';
|
|
7
|
+
import { basename } from 'node:path';
|
|
8
|
+
import { StandardFonts } from 'pdf-lib';
|
|
9
|
+
import fontkit from '@pdf-lib/fontkit';
|
|
10
|
+
import { ENV_KEYS } from '../config.js';
|
|
11
|
+
import { FONT_MAGIC } from '../constants.js';
|
|
12
|
+
import { logger } from '../utils/logger.js';
|
|
13
|
+
const CTX = 'FontManager';
|
|
14
|
+
/**
|
|
15
|
+
* 先頭 4 bytes を ASCII として読む
|
|
16
|
+
*/
|
|
17
|
+
function magic4(bytes) {
|
|
18
|
+
return String.fromCharCode(bytes[0], bytes[1], bytes[2], bytes[3]);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* フォントを読み込んで doc に埋め込む。
|
|
22
|
+
* @param fontPath 明示指定のフォントパス(優先)。未指定なら環境変数 → 標準フォント。
|
|
23
|
+
*/
|
|
24
|
+
export async function loadFont(doc, fontPath) {
|
|
25
|
+
const resolvedPath = fontPath ?? process.env[ENV_KEYS.DEFAULT_FONT];
|
|
26
|
+
if (!resolvedPath) {
|
|
27
|
+
const font = await doc.embedFont(StandardFonts.Helvetica);
|
|
28
|
+
logger.info(CTX, 'No fontPath given; using StandardFonts.Helvetica (ASCII only)');
|
|
29
|
+
return { font, name: 'Helvetica', isStandard: true };
|
|
30
|
+
}
|
|
31
|
+
let bytes;
|
|
32
|
+
try {
|
|
33
|
+
bytes = await readFile(resolvedPath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Font file not found or unreadable: ${resolvedPath}`);
|
|
37
|
+
}
|
|
38
|
+
if (bytes.length < 4) {
|
|
39
|
+
throw new Error(`Font file is too small to be valid: ${resolvedPath}`);
|
|
40
|
+
}
|
|
41
|
+
if (magic4(bytes) === FONT_MAGIC.TTC) {
|
|
42
|
+
throw new Error(`Font file is a TrueTypeCollection (.ttc): ${resolvedPath}. ` +
|
|
43
|
+
`pdf-lib cannot subset .ttc directly. Extract a single face to .otf/.ttf first ` +
|
|
44
|
+
`(e.g. Python fonttools: TTCollection(path).fonts[i].save('out.otf')).`);
|
|
45
|
+
}
|
|
46
|
+
doc.registerFontkit(fontkit);
|
|
47
|
+
let font;
|
|
48
|
+
try {
|
|
49
|
+
font = await doc.embedFont(bytes, { subset: true });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
53
|
+
throw new Error(`Failed to embed font ${resolvedPath}: ${msg}`);
|
|
54
|
+
}
|
|
55
|
+
const name = basename(resolvedPath);
|
|
56
|
+
logger.info(CTX, `Embedded custom font: ${name}`);
|
|
57
|
+
return { font, name, isStandard: false };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=font-manager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-manager.js","sourceRoot":"","sources":["../../src/services/font-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAe,aAAa,EAAgB,MAAM,SAAS,CAAC;AACnE,OAAO,OAAO,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAU5C,MAAM,GAAG,GAAG,aAAa,CAAC;AAE1B;;GAEG;AACH,SAAS,MAAM,CAAC,KAAiB;IAC/B,OAAO,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACrE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAgB,EAAE,QAAiB;IAChE,MAAM,YAAY,GAAG,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;IAEpE,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAC1D,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,+DAA+D,CAAC,CAAC;QAClF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACvD,CAAC;IAED,IAAI,KAAiB,CAAC;IACtB,IAAI,CAAC;QACH,KAAK,GAAG,MAAM,QAAQ,CAAC,YAAY,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,sCAAsC,YAAY,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,uCAAuC,YAAY,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,GAAG,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,6CAA6C,YAAY,IAAI;YAC3D,gFAAgF;YAChF,uEAAuE,CAC1E,CAAC;IACJ,CAAC;IAED,GAAG,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IAC7B,IAAI,IAAa,CAAC;IAClB,IAAI,CAAC;QACH,IAAI,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,wBAAwB,YAAY,KAAK,GAAG,EAAE,CAAC,CAAC;IAClE,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,yBAAyB,IAAI,EAAE,CAAC,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAC3C,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Engine
|
|
3
|
+
* pdf-lib の低レベル API の上に、テキスト折り返し・改ページ・カーソル管理を載せる薄い層。
|
|
4
|
+
* 座標は「上端(top)基準」で管理し、見出し・本文・表でサイズが変わっても一貫して積み上げる。
|
|
5
|
+
*/
|
|
6
|
+
import { type PDFDocument, type PDFPage, type PDFFont, type RGB } from 'pdf-lib';
|
|
7
|
+
export interface LayoutOptions {
|
|
8
|
+
pageWidth: number;
|
|
9
|
+
pageHeight: number;
|
|
10
|
+
margin: number;
|
|
11
|
+
font: PDFFont;
|
|
12
|
+
fontSize: number;
|
|
13
|
+
lineHeight: number;
|
|
14
|
+
}
|
|
15
|
+
export interface DrawTextOptions {
|
|
16
|
+
font?: PDFFont;
|
|
17
|
+
size?: number;
|
|
18
|
+
color?: RGB;
|
|
19
|
+
lineHeight?: number;
|
|
20
|
+
/** 左インデント(pt) */
|
|
21
|
+
leftIndent?: number;
|
|
22
|
+
/** 段落末尾に追加する余白(pt) */
|
|
23
|
+
spaceAfter?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* WinAnsi で表現できない文字(コードポイント > 0xFF)が含まれるか。
|
|
27
|
+
* 標準フォントでは日本語などが描画できないため、事前判定に使う。
|
|
28
|
+
*/
|
|
29
|
+
export declare function hasNonLatin1(text: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* テキストを maxWidth に収まる行配列に折り返す。
|
|
32
|
+
* 明示的な \n は改行として尊重し、空行も保持する。
|
|
33
|
+
*/
|
|
34
|
+
export declare function wrapText(text: string, font: PDFFont, size: number, maxWidth: number): string[];
|
|
35
|
+
export declare class LayoutEngine {
|
|
36
|
+
readonly doc: PDFDocument;
|
|
37
|
+
private opts;
|
|
38
|
+
private _page;
|
|
39
|
+
private _top;
|
|
40
|
+
constructor(doc: PDFDocument, opts: LayoutOptions);
|
|
41
|
+
get page(): PDFPage;
|
|
42
|
+
get leftX(): number;
|
|
43
|
+
get bottomY(): number;
|
|
44
|
+
get contentWidth(): number;
|
|
45
|
+
get defaultFont(): PDFFont;
|
|
46
|
+
get defaultSize(): number;
|
|
47
|
+
get cursorTop(): number;
|
|
48
|
+
set cursorTop(v: number);
|
|
49
|
+
newPage(): void;
|
|
50
|
+
/** height 分の余白が下端まで残っていなければ改ページ */
|
|
51
|
+
ensureSpace(height: number): void;
|
|
52
|
+
moveDown(amount: number): void;
|
|
53
|
+
/** 折り返し + 改ページしながら段落を描画。消費した高さ分 cursorTop が下がる。 */
|
|
54
|
+
drawParagraph(text: string, options?: DrawTextOptions): void;
|
|
55
|
+
/** 水平線 */
|
|
56
|
+
drawRule(options?: {
|
|
57
|
+
color?: RGB;
|
|
58
|
+
thickness?: number;
|
|
59
|
+
spaceBefore?: number;
|
|
60
|
+
spaceAfter?: number;
|
|
61
|
+
}): void;
|
|
62
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout Engine
|
|
3
|
+
* pdf-lib の低レベル API の上に、テキスト折り返し・改ページ・カーソル管理を載せる薄い層。
|
|
4
|
+
* 座標は「上端(top)基準」で管理し、見出し・本文・表でサイズが変わっても一貫して積み上げる。
|
|
5
|
+
*/
|
|
6
|
+
import { rgb, } from 'pdf-lib';
|
|
7
|
+
/** ベースライン近似係数(グリフ上端からベースラインまで ≒ size * この値) */
|
|
8
|
+
const ASCENT_RATIO = 0.8;
|
|
9
|
+
/**
|
|
10
|
+
* WinAnsi で表現できない文字(コードポイント > 0xFF)が含まれるか。
|
|
11
|
+
* 標準フォントでは日本語などが描画できないため、事前判定に使う。
|
|
12
|
+
*/
|
|
13
|
+
export function hasNonLatin1(text) {
|
|
14
|
+
for (const ch of text) {
|
|
15
|
+
if (ch.codePointAt(0) > 0xff)
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const CJK_RE = /[\u2E80-\u9FFF\u3000-\u303F\uFF00-\uFFEF\u3400-\u4DBF]/;
|
|
21
|
+
/**
|
|
22
|
+
* 折り返し用トークン分割:
|
|
23
|
+
* - 空白は ' ' マーカー
|
|
24
|
+
* - CJK は 1 文字ずつ(どこでも改行可)
|
|
25
|
+
* - それ以外(ラテン単語など)は連続した塊
|
|
26
|
+
*/
|
|
27
|
+
function splitTokens(line) {
|
|
28
|
+
const tokens = [];
|
|
29
|
+
let buf = '';
|
|
30
|
+
for (const ch of line) {
|
|
31
|
+
if (/\s/.test(ch)) {
|
|
32
|
+
if (buf) {
|
|
33
|
+
tokens.push(buf);
|
|
34
|
+
buf = '';
|
|
35
|
+
}
|
|
36
|
+
tokens.push(' ');
|
|
37
|
+
}
|
|
38
|
+
else if (CJK_RE.test(ch)) {
|
|
39
|
+
if (buf) {
|
|
40
|
+
tokens.push(buf);
|
|
41
|
+
buf = '';
|
|
42
|
+
}
|
|
43
|
+
tokens.push(ch);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
buf += ch;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (buf)
|
|
50
|
+
tokens.push(buf);
|
|
51
|
+
return tokens;
|
|
52
|
+
}
|
|
53
|
+
/** 1 トークンが maxWidth を超える場合に文字単位で強制分割 */
|
|
54
|
+
function breakLongToken(token, font, size, maxWidth) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
let cur = '';
|
|
57
|
+
for (const ch of token) {
|
|
58
|
+
const trial = cur + ch;
|
|
59
|
+
if (cur && font.widthOfTextAtSize(trial, size) > maxWidth) {
|
|
60
|
+
parts.push(cur);
|
|
61
|
+
cur = ch;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
cur = trial;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (cur)
|
|
68
|
+
parts.push(cur);
|
|
69
|
+
return parts;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* テキストを maxWidth に収まる行配列に折り返す。
|
|
73
|
+
* 明示的な \n は改行として尊重し、空行も保持する。
|
|
74
|
+
*/
|
|
75
|
+
export function wrapText(text, font, size, maxWidth) {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const rawLine of text.split('\n')) {
|
|
78
|
+
if (rawLine.trim() === '') {
|
|
79
|
+
out.push('');
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const tokens = splitTokens(rawLine);
|
|
83
|
+
let line = '';
|
|
84
|
+
for (const tk of tokens) {
|
|
85
|
+
if (tk === ' ' && line === '')
|
|
86
|
+
continue; // 行頭空白は捨てる
|
|
87
|
+
const candidate = line + tk;
|
|
88
|
+
if (line !== '' && font.widthOfTextAtSize(candidate, size) > maxWidth) {
|
|
89
|
+
out.push(line.replace(/\s+$/, ''));
|
|
90
|
+
line = tk === ' ' ? '' : tk;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
line = candidate;
|
|
94
|
+
}
|
|
95
|
+
// 単一トークンだけで既に幅超過 → 強制分割
|
|
96
|
+
if (line !== '' && font.widthOfTextAtSize(line, size) > maxWidth) {
|
|
97
|
+
const parts = breakLongToken(line, font, size, maxWidth);
|
|
98
|
+
for (let i = 0; i < parts.length - 1; i++)
|
|
99
|
+
out.push(parts[i]);
|
|
100
|
+
line = parts[parts.length - 1] ?? '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
out.push(line.replace(/\s+$/, ''));
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
export class LayoutEngine {
|
|
108
|
+
doc;
|
|
109
|
+
opts;
|
|
110
|
+
_page;
|
|
111
|
+
_top;
|
|
112
|
+
constructor(doc, opts) {
|
|
113
|
+
this.doc = doc;
|
|
114
|
+
this.opts = opts;
|
|
115
|
+
this._page = doc.addPage([opts.pageWidth, opts.pageHeight]);
|
|
116
|
+
this._top = opts.pageHeight - opts.margin;
|
|
117
|
+
}
|
|
118
|
+
get page() {
|
|
119
|
+
return this._page;
|
|
120
|
+
}
|
|
121
|
+
get leftX() {
|
|
122
|
+
return this.opts.margin;
|
|
123
|
+
}
|
|
124
|
+
get bottomY() {
|
|
125
|
+
return this.opts.margin;
|
|
126
|
+
}
|
|
127
|
+
get contentWidth() {
|
|
128
|
+
return this.opts.pageWidth - this.opts.margin * 2;
|
|
129
|
+
}
|
|
130
|
+
get defaultFont() {
|
|
131
|
+
return this.opts.font;
|
|
132
|
+
}
|
|
133
|
+
get defaultSize() {
|
|
134
|
+
return this.opts.fontSize;
|
|
135
|
+
}
|
|
136
|
+
get cursorTop() {
|
|
137
|
+
return this._top;
|
|
138
|
+
}
|
|
139
|
+
set cursorTop(v) {
|
|
140
|
+
this._top = v;
|
|
141
|
+
}
|
|
142
|
+
newPage() {
|
|
143
|
+
this._page = this.doc.addPage([this.opts.pageWidth, this.opts.pageHeight]);
|
|
144
|
+
this._top = this.opts.pageHeight - this.opts.margin;
|
|
145
|
+
}
|
|
146
|
+
/** height 分の余白が下端まで残っていなければ改ページ */
|
|
147
|
+
ensureSpace(height) {
|
|
148
|
+
if (this._top - height < this.bottomY) {
|
|
149
|
+
this.newPage();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
moveDown(amount) {
|
|
153
|
+
this._top -= amount;
|
|
154
|
+
}
|
|
155
|
+
/** 折り返し + 改ページしながら段落を描画。消費した高さ分 cursorTop が下がる。 */
|
|
156
|
+
drawParagraph(text, options = {}) {
|
|
157
|
+
const font = options.font ?? this.opts.font;
|
|
158
|
+
const size = options.size ?? this.opts.fontSize;
|
|
159
|
+
const lineHeight = options.lineHeight ?? this.opts.lineHeight;
|
|
160
|
+
const color = options.color ?? rgb(0.1, 0.1, 0.1);
|
|
161
|
+
const leftIndent = options.leftIndent ?? 0;
|
|
162
|
+
const leading = size * lineHeight;
|
|
163
|
+
const maxWidth = this.contentWidth - leftIndent;
|
|
164
|
+
const lines = wrapText(text, font, size, maxWidth);
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
this.ensureSpace(leading);
|
|
167
|
+
if (line !== '') {
|
|
168
|
+
this._page.drawText(line, {
|
|
169
|
+
x: this.leftX + leftIndent,
|
|
170
|
+
y: this._top - size * ASCENT_RATIO,
|
|
171
|
+
size,
|
|
172
|
+
font,
|
|
173
|
+
color,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
this._top -= leading;
|
|
177
|
+
}
|
|
178
|
+
if (options.spaceAfter)
|
|
179
|
+
this._top -= options.spaceAfter;
|
|
180
|
+
}
|
|
181
|
+
/** 水平線 */
|
|
182
|
+
drawRule(options = {}) {
|
|
183
|
+
const thickness = options.thickness ?? 0.75;
|
|
184
|
+
const color = options.color ?? rgb(0.75, 0.75, 0.75);
|
|
185
|
+
const spaceBefore = options.spaceBefore ?? 4;
|
|
186
|
+
const spaceAfter = options.spaceAfter ?? 8;
|
|
187
|
+
this.moveDown(spaceBefore);
|
|
188
|
+
this.ensureSpace(thickness + spaceAfter);
|
|
189
|
+
this._page.drawLine({
|
|
190
|
+
start: { x: this.leftX, y: this._top },
|
|
191
|
+
end: { x: this.leftX + this.contentWidth, y: this._top },
|
|
192
|
+
thickness,
|
|
193
|
+
color,
|
|
194
|
+
});
|
|
195
|
+
this.moveDown(spaceAfter);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/services/layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output
|
|
3
|
+
* PDF のメタデータ付与・保存・base64 化を共通化する。
|
|
4
|
+
*/
|
|
5
|
+
import type { PDFDocument } from 'pdf-lib';
|
|
6
|
+
import type { CommonCreateOptions, CommonEditOptions, CreateResult, EditResult } from '../types/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* 編集済み PDF の保存・base64 化。
|
|
9
|
+
* create 系の finalizePdf と異なり、既存メタデータ(Title/Producer/CreationDate 等)を
|
|
10
|
+
* 尊重し、ModificationDate のみ更新する。
|
|
11
|
+
*/
|
|
12
|
+
export declare function saveEdited(doc: PDFDocument, opts: CommonEditOptions): Promise<EditResult>;
|
|
13
|
+
export declare function finalizePdf(doc: PDFDocument, opts: CommonCreateOptions, fontName: string): Promise<CreateResult>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output
|
|
3
|
+
* PDF のメタデータ付与・保存・base64 化を共通化する。
|
|
4
|
+
*/
|
|
5
|
+
import { writeFile, mkdir } from 'node:fs/promises';
|
|
6
|
+
import { resolve, dirname } from 'node:path';
|
|
7
|
+
import { PACKAGE_INFO } from '../config.js';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* 編集済み PDF の保存・base64 化。
|
|
11
|
+
* create 系の finalizePdf と異なり、既存メタデータ(Title/Producer/CreationDate 等)を
|
|
12
|
+
* 尊重し、ModificationDate のみ更新する。
|
|
13
|
+
*/
|
|
14
|
+
export async function saveEdited(doc, opts) {
|
|
15
|
+
doc.setModificationDate(new Date());
|
|
16
|
+
const bytes = await doc.save();
|
|
17
|
+
const result = {
|
|
18
|
+
pageCount: doc.getPageCount(),
|
|
19
|
+
bytes: bytes.length,
|
|
20
|
+
};
|
|
21
|
+
if (opts.outputPath) {
|
|
22
|
+
const abs = resolve(opts.outputPath);
|
|
23
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
24
|
+
await writeFile(abs, bytes);
|
|
25
|
+
result.path = abs;
|
|
26
|
+
logger.info('Output', `Saved PDF: ${abs} (${bytes.length} bytes, ${result.pageCount} pages)`);
|
|
27
|
+
}
|
|
28
|
+
if (opts.returnBase64 || !opts.outputPath) {
|
|
29
|
+
result.base64 = Buffer.from(bytes).toString('base64');
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
export async function finalizePdf(doc, opts, fontName) {
|
|
34
|
+
if (opts.title)
|
|
35
|
+
doc.setTitle(opts.title);
|
|
36
|
+
if (opts.author)
|
|
37
|
+
doc.setAuthor(opts.author);
|
|
38
|
+
doc.setProducer(`${PACKAGE_INFO.name}/${PACKAGE_INFO.version}`);
|
|
39
|
+
doc.setCreationDate(new Date());
|
|
40
|
+
doc.setModificationDate(new Date());
|
|
41
|
+
const bytes = await doc.save();
|
|
42
|
+
const result = {
|
|
43
|
+
pageCount: doc.getPageCount(),
|
|
44
|
+
bytes: bytes.length,
|
|
45
|
+
font: fontName,
|
|
46
|
+
};
|
|
47
|
+
if (opts.outputPath) {
|
|
48
|
+
const abs = resolve(opts.outputPath);
|
|
49
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
50
|
+
await writeFile(abs, bytes);
|
|
51
|
+
result.path = abs;
|
|
52
|
+
logger.info('Output', `Saved PDF: ${abs} (${bytes.length} bytes, ${result.pageCount} pages)`);
|
|
53
|
+
}
|
|
54
|
+
// 保存先がない場合、または明示要求時は base64 で返す
|
|
55
|
+
if (opts.returnBase64 || !opts.outputPath) {
|
|
56
|
+
result.base64 = Buffer.from(bytes).toString('base64');
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.js","sourceRoot":"","sources":["../../src/services/output.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Renderer
|
|
3
|
+
* marked でブロックトークン化し、pdf-lib で描画する。
|
|
4
|
+
* インライン装飾(太字/斜体)は単一フォントのため字面のみ反映(記号は除去)。
|
|
5
|
+
*/
|
|
6
|
+
import { LayoutEngine } from '../layout.js';
|
|
7
|
+
import type { LoadedFont } from '../font-manager.js';
|
|
8
|
+
export declare function renderMarkdown(engine: LayoutEngine, markdown: string, loaded: LoadedFont): void;
|