@hidemikimura/receipt-html-to-pdf 0.1.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 +44 -0
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/dist/receipt-html-to-pdf.min.js +39 -0
- package/dist/receipt-html-to-pdf.min.js.map +7 -0
- package/examples/lit.js +52 -0
- package/examples/react.jsx +56 -0
- package/examples/vanilla.html +55 -0
- package/package.json +82 -0
- package/src/font/cid.js +187 -0
- package/src/font/parse.js +336 -0
- package/src/font/registry.js +168 -0
- package/src/font/subset.js +211 -0
- package/src/index.js +239 -0
- package/src/page.js +429 -0
- package/src/paginate.js +119 -0
- package/src/pdf/compress.js +30 -0
- package/src/pdf/content.js +192 -0
- package/src/pdf/image.js +33 -0
- package/src/pdf/writer.js +253 -0
- package/src/renderer.js +328 -0
- package/src/units.js +127 -0
- package/src/walker/image.js +264 -0
- package/src/walker/text.js +199 -0
- package/src/walker/walk.js +670 -0
- package/types/font/cid.d.ts +40 -0
- package/types/font/parse.d.ts +113 -0
- package/types/font/registry.d.ts +63 -0
- package/types/font/subset.d.ts +26 -0
- package/types/index.d.ts +198 -0
- package/types/page.d.ts +64 -0
- package/types/paginate.d.ts +59 -0
- package/types/pdf/compress.d.ts +13 -0
- package/types/pdf/content.d.ts +65 -0
- package/types/pdf/image.d.ts +9 -0
- package/types/pdf/writer.d.ts +125 -0
- package/types/renderer.d.ts +55 -0
- package/types/units.d.ts +53 -0
- package/types/walker/image.d.ts +72 -0
- package/types/walker/text.d.ts +38 -0
- package/types/walker/walk.d.ts +150 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* ページコンテンツストリームのビルダー。
|
|
4
|
+
* 座標は PDF 座標系(左下原点、pt)で受け取る。CSS 座標からの変換は呼び出し側(page.js)が行う。
|
|
5
|
+
*/
|
|
6
|
+
import { num } from '../units.js';
|
|
7
|
+
|
|
8
|
+
export class ContentStream {
|
|
9
|
+
constructor() {
|
|
10
|
+
/** @type {string[]} */
|
|
11
|
+
this.ops = [];
|
|
12
|
+
this.depth = 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
save() {
|
|
16
|
+
this.ops.push('q');
|
|
17
|
+
this.depth++;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
restore() {
|
|
22
|
+
if (this.depth <= 0) throw new Error('ContentStream: unbalanced Q');
|
|
23
|
+
this.ops.push('Q');
|
|
24
|
+
this.depth--;
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {number} a @param {number} b @param {number} c @param {number} d @param {number} e @param {number} f */
|
|
29
|
+
transform(a, b, c, d, e, f) {
|
|
30
|
+
this.ops.push(`${num(a)} ${num(b)} ${num(c)} ${num(d)} ${num(e)} ${num(f)} cm`);
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @param {string} gsName ExtGState リソース名 */
|
|
35
|
+
setGState(gsName) {
|
|
36
|
+
this.ops.push(`/${gsName} gs`);
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** @param {number} r @param {number} g @param {number} b */
|
|
41
|
+
fillColor(r, g, b) {
|
|
42
|
+
this.ops.push(`${num(r)} ${num(g)} ${num(b)} rg`);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @param {number} r @param {number} g @param {number} b */
|
|
47
|
+
strokeColor(r, g, b) {
|
|
48
|
+
this.ops.push(`${num(r)} ${num(g)} ${num(b)} RG`);
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** @param {number} w */
|
|
53
|
+
lineWidth(w) {
|
|
54
|
+
this.ops.push(`${num(w)} w`);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {number[]} pattern @param {number} [phase] */
|
|
59
|
+
dash(pattern, phase = 0) {
|
|
60
|
+
this.ops.push(`[${pattern.map(num).join(' ')}] ${num(phase)} d`);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** @param {0|1|2} cap */
|
|
65
|
+
lineCap(cap) {
|
|
66
|
+
this.ops.push(`${cap} J`);
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {number} x @param {number} y @param {number} w @param {number} h */
|
|
71
|
+
rect(x, y, w, h) {
|
|
72
|
+
this.ops.push(`${num(x)} ${num(y)} ${num(w)} ${num(h)} re`);
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {number} x @param {number} y */
|
|
77
|
+
moveTo(x, y) {
|
|
78
|
+
this.ops.push(`${num(x)} ${num(y)} m`);
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** @param {number} x @param {number} y */
|
|
83
|
+
lineTo(x, y) {
|
|
84
|
+
this.ops.push(`${num(x)} ${num(y)} l`);
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 3 次ベジェ曲線
|
|
90
|
+
* @param {number} x1 @param {number} y1 @param {number} x2 @param {number} y2 @param {number} x3 @param {number} y3
|
|
91
|
+
*/
|
|
92
|
+
curveTo(x1, y1, x2, y2, x3, y3) {
|
|
93
|
+
this.ops.push(`${num(x1)} ${num(y1)} ${num(x2)} ${num(y2)} ${num(x3)} ${num(y3)} c`);
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
closePath() {
|
|
98
|
+
this.ops.push('h');
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 角丸矩形のパスを追加する(PDF 座標: y 上向き、(x, y) は左下)。
|
|
104
|
+
* @param {number} x @param {number} y @param {number} w @param {number} h
|
|
105
|
+
* @param {[number, number, number, number]} r [左上, 右上, 右下, 左下] の半径(pt)。CSS の順序
|
|
106
|
+
*/
|
|
107
|
+
roundedRect(x, y, w, h, r) {
|
|
108
|
+
const k = 0.5523; // 4/3·(√2−1)
|
|
109
|
+
const max = Math.min(w, h) / 2;
|
|
110
|
+
const clamp = (/** @type {number} */ v) => Math.max(0, Math.min(v, max));
|
|
111
|
+
const tl = clamp(r[0]);
|
|
112
|
+
const tr = clamp(r[1]);
|
|
113
|
+
const br = clamp(r[2]);
|
|
114
|
+
const bl = clamp(r[3]);
|
|
115
|
+
const top = y + h;
|
|
116
|
+
const right = x + w;
|
|
117
|
+
// 左上から時計回り(PDF 座標では上辺が y+h)
|
|
118
|
+
this.moveTo(x + tl, top);
|
|
119
|
+
this.lineTo(right - tr, top);
|
|
120
|
+
if (tr) this.curveTo(right - tr + tr * k, top, right, top - tr + tr * k, right, top - tr);
|
|
121
|
+
this.lineTo(right, y + br);
|
|
122
|
+
if (br) this.curveTo(right, y + br - br * k, right - br + br * k, y, right - br, y);
|
|
123
|
+
this.lineTo(x + bl, y);
|
|
124
|
+
if (bl) this.curveTo(x + bl - bl * k, y, x, y + bl - bl * k, x, y + bl);
|
|
125
|
+
this.lineTo(x, top - tl);
|
|
126
|
+
if (tl) this.curveTo(x, top - tl + tl * k, x + tl - tl * k, top, x + tl, top);
|
|
127
|
+
return this.closePath();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 画像 XObject を (x, y) を左下として w×h で描く。
|
|
132
|
+
* @param {string} name @param {number} x @param {number} y @param {number} w @param {number} h
|
|
133
|
+
*/
|
|
134
|
+
image(name, x, y, w, h) {
|
|
135
|
+
this.ops.push(`q ${num(w)} 0 0 ${num(h)} ${num(x)} ${num(y)} cm /${name} Do Q`);
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fill() {
|
|
140
|
+
this.ops.push('f');
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
stroke() {
|
|
145
|
+
this.ops.push('S');
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 現在のパスでクリップして新しいパスを開始する */
|
|
150
|
+
clip() {
|
|
151
|
+
this.ops.push('W n');
|
|
152
|
+
return this;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @param {number} x @param {number} y @param {number} w @param {number} h */
|
|
156
|
+
fillRect(x, y, w, h) {
|
|
157
|
+
return this.rect(x, y, w, h).fill();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* テキストを描画する。
|
|
162
|
+
* @param {string} fontName リソース名(F1 など)
|
|
163
|
+
* @param {number} size フォントサイズ (pt)
|
|
164
|
+
* @param {number} x ベースライン始点 x (pt)
|
|
165
|
+
* @param {number} y ベースライン y (pt)
|
|
166
|
+
* @param {Array<string|number>} tj TJ 配列。文字列は 16 進 CID 列(<...> なし)、数値は 1/1000 単位の位置調整
|
|
167
|
+
* @param {{charSpacing?: number, rise?: number}} [opts]
|
|
168
|
+
*/
|
|
169
|
+
text(fontName, size, x, y, tj, opts = {}) {
|
|
170
|
+
const parts = [];
|
|
171
|
+
for (const t of tj) {
|
|
172
|
+
if (typeof t === 'string') {
|
|
173
|
+
if (t.length) parts.push(`<${t}>`);
|
|
174
|
+
} else if (t !== 0) parts.push(num(t));
|
|
175
|
+
}
|
|
176
|
+
if (!parts.length) return this;
|
|
177
|
+
this.ops.push('BT');
|
|
178
|
+
this.ops.push(`/${fontName} ${num(size)} Tf`);
|
|
179
|
+
if (opts.charSpacing) this.ops.push(`${num(opts.charSpacing)} Tc`);
|
|
180
|
+
if (opts.rise) this.ops.push(`${num(opts.rise)} Ts`);
|
|
181
|
+
this.ops.push(`1 0 0 1 ${num(x)} ${num(y)} Tm`);
|
|
182
|
+
this.ops.push(`[${parts.join(' ')}] TJ`);
|
|
183
|
+
this.ops.push('ET');
|
|
184
|
+
return this;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** @returns {Uint8Array} */
|
|
188
|
+
toBytes() {
|
|
189
|
+
if (this.depth !== 0) throw new Error(`ContentStream: ${this.depth} unclosed q`);
|
|
190
|
+
return new TextEncoder().encode(this.ops.join('\n') + '\n');
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/pdf/image.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* デコード済み画像を PDF の Image XObject として書き出す。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {import('../pdf/writer.js').PdfWriter} writer
|
|
8
|
+
* @param {import('../walker/image.js').DecodedImage} img
|
|
9
|
+
* @returns {Promise<import('../pdf/writer.js').Ref>}
|
|
10
|
+
*/
|
|
11
|
+
export async function embedImage(writer, img) {
|
|
12
|
+
/** @type {{[key: string]: import('../pdf/writer.js').PdfValue}} */
|
|
13
|
+
const dict = {
|
|
14
|
+
Type: 'XObject',
|
|
15
|
+
Subtype: 'Image',
|
|
16
|
+
Width: img.width,
|
|
17
|
+
Height: img.height,
|
|
18
|
+
ColorSpace: 'DeviceRGB',
|
|
19
|
+
BitsPerComponent: 8,
|
|
20
|
+
};
|
|
21
|
+
if (img.jpeg) {
|
|
22
|
+
dict.Filter = 'DCTDecode';
|
|
23
|
+
return writer.addStream(dict, img.jpeg, { compress: false });
|
|
24
|
+
}
|
|
25
|
+
if (!img.rgb) throw new Error('embedImage: image has no pixel data');
|
|
26
|
+
if (img.alpha) {
|
|
27
|
+
dict.SMask = await writer.addStream(
|
|
28
|
+
{ Type: 'XObject', Subtype: 'Image', Width: img.width, Height: img.height, ColorSpace: 'DeviceGray', BitsPerComponent: 8 },
|
|
29
|
+
img.alpha,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return writer.addStream(dict, img.rgb);
|
|
33
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* 最小限の PDF ライター。
|
|
4
|
+
* オブジェクトをメモリ上に保持し、build() で xref / trailer を含むバイト列を組み立てる。
|
|
5
|
+
*/
|
|
6
|
+
import { deflate } from './compress.js';
|
|
7
|
+
import { num } from '../units.js';
|
|
8
|
+
|
|
9
|
+
/** 間接オブジェクト参照 */
|
|
10
|
+
export class Ref {
|
|
11
|
+
/** @param {number} id */
|
|
12
|
+
constructor(id) {
|
|
13
|
+
this.id = id;
|
|
14
|
+
}
|
|
15
|
+
toString() {
|
|
16
|
+
return `${this.id} 0 R`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** PDF 名前オブジェクト(/Name) */
|
|
21
|
+
export class Name {
|
|
22
|
+
/** @param {string} name */
|
|
23
|
+
constructor(name) {
|
|
24
|
+
this.name = name;
|
|
25
|
+
}
|
|
26
|
+
toString() {
|
|
27
|
+
// 区切り文字・非 ASCII は #xx でエスケープ
|
|
28
|
+
let out = '/';
|
|
29
|
+
for (const ch of this.name) {
|
|
30
|
+
const c = ch.charCodeAt(0);
|
|
31
|
+
if (c < 0x21 || c > 0x7e || '#/%()<>[]{}'.includes(ch)) {
|
|
32
|
+
for (const b of new TextEncoder().encode(ch)) out += '#' + b.toString(16).padStart(2, '0');
|
|
33
|
+
} else out += ch;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** バイト列そのまま(既にシリアライズ済みの断片) */
|
|
40
|
+
export class Raw {
|
|
41
|
+
/** @param {string} text */
|
|
42
|
+
constructor(text) {
|
|
43
|
+
this.text = text;
|
|
44
|
+
}
|
|
45
|
+
toString() {
|
|
46
|
+
return this.text;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* PDF テキスト文字列。ASCII のみならリテラル文字列、そうでなければ UTF-16BE (BOM 付き) の 16 進文字列。
|
|
52
|
+
* @param {string} s
|
|
53
|
+
* @returns {Raw}
|
|
54
|
+
*/
|
|
55
|
+
export function pdfString(s) {
|
|
56
|
+
// eslint-disable-next-line no-control-regex
|
|
57
|
+
if (/^[\x20-\x7e]*$/.test(s)) {
|
|
58
|
+
return new Raw('(' + s.replace(/[\\()]/g, (c) => '\\' + c) + ')');
|
|
59
|
+
}
|
|
60
|
+
let hex = 'FEFF';
|
|
61
|
+
for (let i = 0; i < s.length; i++) hex += s.charCodeAt(i).toString(16).padStart(4, '0');
|
|
62
|
+
return new Raw('<' + hex + '>');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* PDF 日付文字列 (D:YYYYMMDDHHmmSS+HH'mm')
|
|
67
|
+
* @param {Date} d
|
|
68
|
+
* @returns {Raw}
|
|
69
|
+
*/
|
|
70
|
+
export function pdfDate(d) {
|
|
71
|
+
const p = (/** @type {number} */ n) => String(n).padStart(2, '0');
|
|
72
|
+
const tz = -d.getTimezoneOffset();
|
|
73
|
+
const sign = tz >= 0 ? '+' : '-';
|
|
74
|
+
const tzh = p(Math.floor(Math.abs(tz) / 60));
|
|
75
|
+
const tzm = p(Math.abs(tz) % 60);
|
|
76
|
+
return new Raw(
|
|
77
|
+
`(D:${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}${sign}${tzh}'${tzm}')`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {number|string|boolean|null|Ref|Name|Raw|PdfArray|PdfDict} PdfValue
|
|
83
|
+
*/
|
|
84
|
+
/** @typedef {PdfValue[]} PdfArray */
|
|
85
|
+
/** @typedef {{[key: string]: PdfValue}} PdfDict */
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 値を PDF 構文にシリアライズする。
|
|
89
|
+
* - number → 数値、boolean → true/false、null → null
|
|
90
|
+
* - string → 名前 (/Foo)。テキスト文字列は pdfString() で Raw にして渡す
|
|
91
|
+
* - 配列 → [ ... ]、プレーンオブジェクト → << /Key value ... >>
|
|
92
|
+
* @param {PdfValue} v
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function serialize(v) {
|
|
96
|
+
if (v === null) return 'null';
|
|
97
|
+
if (typeof v === 'number') return num(v);
|
|
98
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
99
|
+
if (typeof v === 'string') return new Name(v).toString();
|
|
100
|
+
if (v instanceof Ref || v instanceof Name || v instanceof Raw) return v.toString();
|
|
101
|
+
if (Array.isArray(v)) return '[' + v.map(serialize).join(' ') + ']';
|
|
102
|
+
const parts = [];
|
|
103
|
+
for (const [k, val] of Object.entries(v)) {
|
|
104
|
+
if (val === undefined) continue;
|
|
105
|
+
parts.push(new Name(k).toString() + ' ' + serialize(val));
|
|
106
|
+
}
|
|
107
|
+
return '<< ' + parts.join(' ') + ' >>';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @typedef {object} PdfObject
|
|
112
|
+
* @property {PdfValue} value
|
|
113
|
+
* @property {Uint8Array} [stream]
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
export class PdfWriter {
|
|
117
|
+
/**
|
|
118
|
+
* @param {{compress?: boolean, version?: string}} [options]
|
|
119
|
+
*/
|
|
120
|
+
constructor(options = {}) {
|
|
121
|
+
/** @type {(PdfObject|null)[]} 0 番はフリーオブジェクト */
|
|
122
|
+
this.objects = [null];
|
|
123
|
+
this.compress = options.compress ?? true;
|
|
124
|
+
this.version = options.version ?? '1.7';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 間接オブジェクトを予約する(後で set で中身を入れる)。
|
|
129
|
+
* @returns {Ref}
|
|
130
|
+
*/
|
|
131
|
+
reserve() {
|
|
132
|
+
this.objects.push({ value: null });
|
|
133
|
+
return new Ref(this.objects.length - 1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {Ref} ref
|
|
138
|
+
* @param {PdfValue} value
|
|
139
|
+
* @param {Uint8Array} [stream]
|
|
140
|
+
*/
|
|
141
|
+
set(ref, value, stream) {
|
|
142
|
+
this.objects[ref.id] = { value, stream };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {PdfValue} value
|
|
147
|
+
* @returns {Ref}
|
|
148
|
+
*/
|
|
149
|
+
add(value) {
|
|
150
|
+
const ref = this.reserve();
|
|
151
|
+
this.set(ref, value);
|
|
152
|
+
return ref;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* ストリームオブジェクトを追加する。compress が有効なら FlateDecode を試みる。
|
|
157
|
+
* @param {{[key: string]: PdfValue}} dict
|
|
158
|
+
* @param {Uint8Array} bytes
|
|
159
|
+
* @param {{compress?: boolean}} [opts]
|
|
160
|
+
* @returns {Promise<Ref>}
|
|
161
|
+
*/
|
|
162
|
+
async addStream(dict, bytes, opts = {}) {
|
|
163
|
+
const ref = this.reserve();
|
|
164
|
+
await this.setStream(ref, dict, bytes, opts);
|
|
165
|
+
return ref;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {Ref} ref
|
|
170
|
+
* @param {{[key: string]: PdfValue}} dict
|
|
171
|
+
* @param {Uint8Array} bytes
|
|
172
|
+
* @param {{compress?: boolean}} [opts]
|
|
173
|
+
*/
|
|
174
|
+
async setStream(ref, dict, bytes, opts = {}) {
|
|
175
|
+
let data = bytes;
|
|
176
|
+
const d = { ...dict };
|
|
177
|
+
const doCompress = opts.compress ?? this.compress;
|
|
178
|
+
if (doCompress && !('Filter' in d)) {
|
|
179
|
+
const z = await deflate(bytes);
|
|
180
|
+
if (z && z.length < bytes.length) {
|
|
181
|
+
data = z;
|
|
182
|
+
d.Filter = 'FlateDecode';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
d.Length = data.length;
|
|
186
|
+
this.set(ref, d, data);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ドキュメント全体を組み立てる。
|
|
191
|
+
* @param {Ref} rootRef /Catalog への参照
|
|
192
|
+
* @param {Ref} [infoRef] /Info への参照
|
|
193
|
+
* @returns {Uint8Array}
|
|
194
|
+
*/
|
|
195
|
+
build(rootRef, infoRef) {
|
|
196
|
+
const enc = new TextEncoder();
|
|
197
|
+
/** @type {Uint8Array[]} */
|
|
198
|
+
const chunks = [];
|
|
199
|
+
let offset = 0;
|
|
200
|
+
const push = (/** @type {Uint8Array|string} */ c) => {
|
|
201
|
+
const b = typeof c === 'string' ? enc.encode(c) : c;
|
|
202
|
+
chunks.push(b);
|
|
203
|
+
offset += b.length;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ヘッダー。2 行目のバイナリコメントはファイルがバイナリであることを転送系に知らせる慣例
|
|
207
|
+
push(concat([enc.encode(`%PDF-${this.version}\n%`), new Uint8Array([0xe2, 0xe3, 0xcf, 0xd3]), enc.encode('\n')]));
|
|
208
|
+
|
|
209
|
+
/** @type {number[]} */
|
|
210
|
+
const offsets = [];
|
|
211
|
+
for (let i = 1; i < this.objects.length; i++) {
|
|
212
|
+
const obj = this.objects[i];
|
|
213
|
+
if (!obj) throw new Error(`PDF object ${i} was reserved but never set`);
|
|
214
|
+
offsets[i] = offset;
|
|
215
|
+
push(`${i} 0 obj\n${serialize(obj.value)}\n`);
|
|
216
|
+
if (obj.stream) {
|
|
217
|
+
push('stream\n');
|
|
218
|
+
push(obj.stream);
|
|
219
|
+
push('\nendstream\n');
|
|
220
|
+
}
|
|
221
|
+
push('endobj\n');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const xrefOffset = offset;
|
|
225
|
+
let xref = `xref\n0 ${this.objects.length}\n0000000000 65535 f \n`;
|
|
226
|
+
for (let i = 1; i < this.objects.length; i++) {
|
|
227
|
+
xref += String(offsets[i]).padStart(10, '0') + ' 00000 n \n';
|
|
228
|
+
}
|
|
229
|
+
push(xref);
|
|
230
|
+
/** @type {{[key: string]: PdfValue}} */
|
|
231
|
+
const trailer = { Size: this.objects.length, Root: rootRef };
|
|
232
|
+
if (infoRef) trailer.Info = infoRef;
|
|
233
|
+
push(`trailer\n${serialize(trailer)}\nstartxref\n${xrefOffset}\n%%EOF\n`);
|
|
234
|
+
|
|
235
|
+
return concat(chunks);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {Uint8Array[]} chunks
|
|
241
|
+
* @returns {Uint8Array}
|
|
242
|
+
*/
|
|
243
|
+
export function concat(chunks) {
|
|
244
|
+
let len = 0;
|
|
245
|
+
for (const c of chunks) len += c.length;
|
|
246
|
+
const out = new Uint8Array(len);
|
|
247
|
+
let o = 0;
|
|
248
|
+
for (const c of chunks) {
|
|
249
|
+
out.set(c, o);
|
|
250
|
+
o += c.length;
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|