@strav/pdf 0.4.17
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/README.md +79 -0
- package/package.json +51 -0
- package/src/color/cie.ts +61 -0
- package/src/color/color.ts +77 -0
- package/src/color/conversion.ts +26 -0
- package/src/color/device.ts +37 -0
- package/src/color/devicen.ts +74 -0
- package/src/color/icc.ts +103 -0
- package/src/color/index.ts +15 -0
- package/src/color/separation.ts +94 -0
- package/src/color/space.ts +47 -0
- package/src/content/content_stream.ts +373 -0
- package/src/content/graphics_state.ts +64 -0
- package/src/content/index.ts +16 -0
- package/src/content/operators.ts +70 -0
- package/src/content/path.ts +51 -0
- package/src/content/resources.ts +119 -0
- package/src/content/text_object.ts +140 -0
- package/src/document/catalog.ts +16 -0
- package/src/document/index.ts +13 -0
- package/src/document/object_table.ts +67 -0
- package/src/document/page.ts +74 -0
- package/src/document/page_tree.ts +78 -0
- package/src/document/pdf_document.ts +310 -0
- package/src/document/types.ts +65 -0
- package/src/document/xref.ts +68 -0
- package/src/ext-gstate/ext_gstate.ts +69 -0
- package/src/ext-gstate/index.ts +2 -0
- package/src/fonts/cff.ts +123 -0
- package/src/fonts/cid_encoding.ts +45 -0
- package/src/fonts/cmap_table.ts +180 -0
- package/src/fonts/font.ts +342 -0
- package/src/fonts/glyf.ts +59 -0
- package/src/fonts/hmtx.ts +21 -0
- package/src/fonts/index.ts +20 -0
- package/src/fonts/name_table.ts +50 -0
- package/src/fonts/os2.ts +41 -0
- package/src/fonts/sfnt.ts +224 -0
- package/src/fonts/standard_14.ts +132 -0
- package/src/fonts/subset.ts +221 -0
- package/src/fonts/to_unicode.ts +82 -0
- package/src/fonts/win_ansi.ts +69 -0
- package/src/images/image.ts +111 -0
- package/src/images/index.ts +6 -0
- package/src/images/jpeg.ts +103 -0
- package/src/images/png.ts +239 -0
- package/src/images/smask.ts +24 -0
- package/src/index.ts +57 -0
- package/src/metadata/index.ts +3 -0
- package/src/metadata/info_dict.ts +28 -0
- package/src/metadata/xmp.ts +110 -0
- package/src/objects/encode.ts +77 -0
- package/src/objects/index.ts +43 -0
- package/src/objects/indirect_ref.ts +17 -0
- package/src/objects/name.ts +50 -0
- package/src/objects/number.ts +43 -0
- package/src/objects/string.ts +136 -0
- package/src/objects/types.ts +86 -0
- package/src/output/buffer_sink.ts +40 -0
- package/src/output/byte_sink.ts +12 -0
- package/src/output/index.ts +3 -0
- package/src/output/stream_sink.ts +62 -0
- package/src/patterns/index.ts +10 -0
- package/src/patterns/shading.ts +162 -0
- package/src/patterns/tiling_pattern.ts +68 -0
- package/src/standards/context.ts +10 -0
- package/src/standards/index.ts +23 -0
- package/src/standards/pdf_a.ts +23 -0
- package/src/standards/pdf_x.ts +31 -0
- package/src/streams/ascii85.ts +61 -0
- package/src/streams/ascii_hex.ts +33 -0
- package/src/streams/flate.ts +17 -0
- package/src/streams/index.ts +9 -0
- package/src/streams/stream.ts +66 -0
- package/src/util/ascii.ts +63 -0
- package/src/util/binary.ts +71 -0
- package/src/util/errors.ts +61 -0
- package/src/util/index.ts +10 -0
- package/src/util/units.ts +24 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WinAnsiEncoding (spec §10.4) — the encoding used for Standard-14 simple
|
|
3
|
+
* text fonts. It is Latin-1 except for the 0x80–0x9F band, which holds the
|
|
4
|
+
* CP1252 printable characters instead of C1 controls.
|
|
5
|
+
*
|
|
6
|
+
* `encodeWinAnsi` maps a JS string (iterated by code point) to the single-byte
|
|
7
|
+
* sequence that goes inside a PDF `(...)` / `<...>` string for `Tj`/`TJ`.
|
|
8
|
+
* Unrepresentable characters throw — silent substitution corrupts text.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PdfGenError } from '../util/errors.ts'
|
|
12
|
+
|
|
13
|
+
/** Unicode code point → byte for the CP1252 0x80–0x9F band. */
|
|
14
|
+
const CP1252_HIGH: ReadonlyArray<[number, number]> = [
|
|
15
|
+
[0x20ac, 0x80], // €
|
|
16
|
+
[0x201a, 0x82], // ‚
|
|
17
|
+
[0x0192, 0x83], // ƒ
|
|
18
|
+
[0x201e, 0x84], // „
|
|
19
|
+
[0x2026, 0x85], // …
|
|
20
|
+
[0x2020, 0x86], // †
|
|
21
|
+
[0x2021, 0x87], // ‡
|
|
22
|
+
[0x02c6, 0x88], // ˆ
|
|
23
|
+
[0x2030, 0x89], // ‰
|
|
24
|
+
[0x0160, 0x8a], // Š
|
|
25
|
+
[0x2039, 0x8b], // ‹
|
|
26
|
+
[0x0152, 0x8c], // Œ
|
|
27
|
+
[0x017d, 0x8e], // Ž
|
|
28
|
+
[0x2018, 0x91], // ‘
|
|
29
|
+
[0x2019, 0x92], // ’
|
|
30
|
+
[0x201c, 0x93], // “
|
|
31
|
+
[0x201d, 0x94], // ”
|
|
32
|
+
[0x2022, 0x95], // •
|
|
33
|
+
[0x2013, 0x96], // –
|
|
34
|
+
[0x2014, 0x97], // —
|
|
35
|
+
[0x02dc, 0x98], // ˜
|
|
36
|
+
[0x2122, 0x99], // ™
|
|
37
|
+
[0x0161, 0x9a], // š
|
|
38
|
+
[0x203a, 0x9b], // ›
|
|
39
|
+
[0x0153, 0x9c], // œ
|
|
40
|
+
[0x017e, 0x9e], // ž
|
|
41
|
+
[0x0178, 0x9f], // Ÿ
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const HIGH = new Map<number, number>(CP1252_HIGH)
|
|
45
|
+
|
|
46
|
+
/** Map one Unicode code point to its WinAnsi byte, or `null` if unmappable. */
|
|
47
|
+
export function winAnsiByte(cp: number): number | null {
|
|
48
|
+
// ASCII and the Latin-1 upper half map identically.
|
|
49
|
+
if ((cp >= 0x20 && cp <= 0x7e) || (cp >= 0xa0 && cp <= 0xff)) return cp
|
|
50
|
+
return HIGH.get(cp) ?? null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Encode a string to WinAnsi bytes; throws on the first unmappable char. */
|
|
54
|
+
export function encodeWinAnsi(text: string): Uint8Array {
|
|
55
|
+
const out: number[] = []
|
|
56
|
+
for (const ch of text) {
|
|
57
|
+
const cp = ch.codePointAt(0)!
|
|
58
|
+
const b = winAnsiByte(cp)
|
|
59
|
+
if (b === null) {
|
|
60
|
+
throw new PdfGenError(
|
|
61
|
+
'PDF_TEXT_ENCODING',
|
|
62
|
+
`U+${cp.toString(16).toUpperCase().padStart(4, '0')} (${JSON.stringify(ch)}) ` +
|
|
63
|
+
'is not representable in WinAnsiEncoding; embed a Unicode font (milestone 5)'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
out.push(b)
|
|
67
|
+
}
|
|
68
|
+
return Uint8Array.from(out)
|
|
69
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PdfImage` (spec §11, §16). The public image handle.
|
|
3
|
+
*
|
|
4
|
+
* - `PdfImage.fromJpeg(bytes)` — embedded verbatim as a `/DCTDecode` XObject.
|
|
5
|
+
* - `PdfImage.fromPng(bytes)` — decoded and re-emitted as a `/FlateDecode`
|
|
6
|
+
* XObject; alpha becomes an `/SMask`, a palette an `/Indexed` color space.
|
|
7
|
+
*
|
|
8
|
+
* Like {@link PdfFont}, an image registers itself into the object table via
|
|
9
|
+
* `register(table)` and returns the XObject reference for the page resources.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { IndirectRef, PdfObject } from '../objects/types.ts'
|
|
13
|
+
import { arr, name, num } from '../objects/types.ts'
|
|
14
|
+
import { hexBytes } from '../objects/string.ts'
|
|
15
|
+
import type { ObjectTable } from '../document/object_table.ts'
|
|
16
|
+
import { makeStream } from '../streams/stream.ts'
|
|
17
|
+
import { parseJpeg } from './jpeg.ts'
|
|
18
|
+
import { parsePng, type PngColorSpace } from './png.ts'
|
|
19
|
+
import { buildSMask } from './smask.ts'
|
|
20
|
+
|
|
21
|
+
export abstract class PdfImage {
|
|
22
|
+
/** Pixel dimensions (independent of the drawn size). */
|
|
23
|
+
abstract readonly width: number
|
|
24
|
+
abstract readonly height: number
|
|
25
|
+
|
|
26
|
+
/** Add the image (and any soft mask) to the table; return the XObject ref. */
|
|
27
|
+
abstract register(table: ObjectTable): IndirectRef
|
|
28
|
+
|
|
29
|
+
/** A baseline/progressive JPEG, embedded without re-encoding. */
|
|
30
|
+
static fromJpeg(bytes: Uint8Array): PdfImage {
|
|
31
|
+
return new JpegImage(bytes)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** A non-interlaced PNG, decoded and re-encoded as FlateDecode. */
|
|
35
|
+
static fromPng(bytes: Uint8Array): PdfImage {
|
|
36
|
+
return new PngImage(bytes)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function deviceColorSpace(components: 1 | 3 | 4): PdfObject {
|
|
41
|
+
return name(components === 1 ? 'DeviceGray' : components === 3 ? 'DeviceRGB' : 'DeviceCMYK')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class JpegImage extends PdfImage {
|
|
45
|
+
readonly width: number
|
|
46
|
+
readonly height: number
|
|
47
|
+
private readonly info: ReturnType<typeof parseJpeg>
|
|
48
|
+
|
|
49
|
+
constructor(bytes: Uint8Array) {
|
|
50
|
+
super()
|
|
51
|
+
this.info = parseJpeg(bytes)
|
|
52
|
+
this.width = this.info.width
|
|
53
|
+
this.height = this.info.height
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
register(table: ObjectTable): IndirectRef {
|
|
57
|
+
const extra: Record<string, PdfObject> = {
|
|
58
|
+
Type: name('XObject'),
|
|
59
|
+
Subtype: name('Image'),
|
|
60
|
+
Width: num(this.info.width),
|
|
61
|
+
Height: num(this.info.height),
|
|
62
|
+
ColorSpace: deviceColorSpace(this.info.components),
|
|
63
|
+
BitsPerComponent: num(8),
|
|
64
|
+
Filter: name('DCTDecode'),
|
|
65
|
+
}
|
|
66
|
+
// Photoshop CMYK JPEGs store inverted samples (Adobe APP14).
|
|
67
|
+
if (this.info.adobeInverted) {
|
|
68
|
+
extra.Decode = arr([1, 0, 1, 0, 1, 0, 1, 0].map(num))
|
|
69
|
+
}
|
|
70
|
+
// filter:'none' → bytes embedded verbatim; /Filter set above.
|
|
71
|
+
return table.add(makeStream(this.info.data, { filter: 'none', extra }))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function pngColorSpaceObject(cs: PngColorSpace): PdfObject {
|
|
76
|
+
if (cs.kind === 'Indexed') {
|
|
77
|
+
return arr([name('Indexed'), name('DeviceRGB'), num(cs.hival), hexBytes(cs.palette)])
|
|
78
|
+
}
|
|
79
|
+
return name(cs.kind)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class PngImage extends PdfImage {
|
|
83
|
+
readonly width: number
|
|
84
|
+
readonly height: number
|
|
85
|
+
private readonly png: ReturnType<typeof parsePng>
|
|
86
|
+
|
|
87
|
+
constructor(bytes: Uint8Array) {
|
|
88
|
+
super()
|
|
89
|
+
this.png = parsePng(bytes)
|
|
90
|
+
this.width = this.png.width
|
|
91
|
+
this.height = this.png.height
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
register(table: ObjectTable): IndirectRef {
|
|
95
|
+
let smaskRef: IndirectRef | undefined
|
|
96
|
+
if (this.png.alpha) {
|
|
97
|
+
smaskRef = table.add(buildSMask(this.png.alpha, this.png.width, this.png.height))
|
|
98
|
+
}
|
|
99
|
+
const extra: Record<string, PdfObject> = {
|
|
100
|
+
Type: name('XObject'),
|
|
101
|
+
Subtype: name('Image'),
|
|
102
|
+
Width: num(this.png.width),
|
|
103
|
+
Height: num(this.png.height),
|
|
104
|
+
ColorSpace: pngColorSpaceObject(this.png.colorSpace),
|
|
105
|
+
BitsPerComponent: num(this.png.bitsPerComponent),
|
|
106
|
+
}
|
|
107
|
+
if (smaskRef) extra.SMask = smaskRef
|
|
108
|
+
if (this.png.colorKey) extra.Mask = arr(this.png.colorKey.map(num))
|
|
109
|
+
return table.add(makeStream(this.png.samples, { filter: 'FlateDecode', extra }))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JPEG (JFIF/EXIF) parsing (spec §11.1).
|
|
3
|
+
*
|
|
4
|
+
* JPEG image data is embedded **verbatim** with `/DCTDecode` — we never
|
|
5
|
+
* re-encode. We only walk the marker segments to learn the dimensions,
|
|
6
|
+
* component count and bit depth, and whether an Adobe APP14 marker is present
|
|
7
|
+
* (Photoshop CMYK JPEGs store inverted samples → a `/Decode` inversion).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { InvalidImageError } from '../util/errors.ts'
|
|
11
|
+
|
|
12
|
+
export interface JpegInfo {
|
|
13
|
+
width: number
|
|
14
|
+
height: number
|
|
15
|
+
/** 1 = Gray, 3 = RGB/YCbCr, 4 = CMYK/YCCK. */
|
|
16
|
+
components: 1 | 3 | 4
|
|
17
|
+
bitsPerComponent: number
|
|
18
|
+
/** Adobe APP14 present — 4-component data is inverted (set `/Decode`). */
|
|
19
|
+
adobeInverted: boolean
|
|
20
|
+
/** The original JPEG bytes, embedded as-is. */
|
|
21
|
+
data: Uint8Array
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function parseJpeg(data: Uint8Array): JpegInfo {
|
|
25
|
+
if (data.length < 4 || data[0] !== 0xff || data[1] !== 0xd8) {
|
|
26
|
+
throw new InvalidImageError('Not a JPEG (missing SOI marker)')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let hasAdobe = false
|
|
30
|
+
let width = 0
|
|
31
|
+
let height = 0
|
|
32
|
+
let components: 1 | 3 | 4 = 3
|
|
33
|
+
let precision = 8
|
|
34
|
+
let sawSOF = false
|
|
35
|
+
|
|
36
|
+
let i = 2
|
|
37
|
+
while (i + 1 < data.length) {
|
|
38
|
+
if (data[i] !== 0xff) {
|
|
39
|
+
i++
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
let marker = data[i + 1]!
|
|
43
|
+
// Skip fill bytes (0xFF padding).
|
|
44
|
+
while (marker === 0xff && i + 2 < data.length) {
|
|
45
|
+
i++
|
|
46
|
+
marker = data[i + 1]!
|
|
47
|
+
}
|
|
48
|
+
i += 2
|
|
49
|
+
// Standalone markers (no length): RSTn, SOI, EOI, TEM.
|
|
50
|
+
if (marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (i + 1 >= data.length) break
|
|
54
|
+
const segLen = (data[i]! << 8) | data[i + 1]!
|
|
55
|
+
const segStart = i + 2
|
|
56
|
+
const segEnd = i + segLen
|
|
57
|
+
|
|
58
|
+
if (marker === 0xee) {
|
|
59
|
+
// APP14 "Adobe"
|
|
60
|
+
if (
|
|
61
|
+
segLen >= 7 &&
|
|
62
|
+
data[segStart] === 0x41 && // 'A'
|
|
63
|
+
data[segStart + 1] === 0x64 && // 'd'
|
|
64
|
+
data[segStart + 2] === 0x6f // 'o'
|
|
65
|
+
) {
|
|
66
|
+
hasAdobe = true
|
|
67
|
+
}
|
|
68
|
+
} else if (
|
|
69
|
+
// SOF0..SOF15 except DHT(C4), DAC(CC), and the JPG marker (C8).
|
|
70
|
+
marker >= 0xc0 &&
|
|
71
|
+
marker <= 0xcf &&
|
|
72
|
+
marker !== 0xc4 &&
|
|
73
|
+
marker !== 0xc8 &&
|
|
74
|
+
marker !== 0xcc
|
|
75
|
+
) {
|
|
76
|
+
precision = data[segStart]!
|
|
77
|
+
height = (data[segStart + 1]! << 8) | data[segStart + 2]!
|
|
78
|
+
width = (data[segStart + 3]! << 8) | data[segStart + 4]!
|
|
79
|
+
components = data[segStart + 5]! as 1 | 3 | 4
|
|
80
|
+
sawSOF = true
|
|
81
|
+
} else if (marker === 0xda) {
|
|
82
|
+
break // SOS — entropy-coded data follows; stop scanning.
|
|
83
|
+
}
|
|
84
|
+
i = segEnd
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!sawSOF) throw new InvalidImageError('JPEG has no Start-Of-Frame marker')
|
|
88
|
+
if (precision !== 8) {
|
|
89
|
+
throw new InvalidImageError(`Unsupported JPEG precision ${precision} (only 8-bit is supported)`)
|
|
90
|
+
}
|
|
91
|
+
if (components !== 1 && components !== 3 && components !== 4) {
|
|
92
|
+
throw new InvalidImageError(`Unsupported JPEG component count ${components}`)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
components,
|
|
99
|
+
bitsPerComponent: 8,
|
|
100
|
+
adobeInverted: components === 4 && hasAdobe,
|
|
101
|
+
data,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PNG parsing (spec §11.2).
|
|
3
|
+
*
|
|
4
|
+
* PDF has no PNG filter, so we decode: parse chunks, inflate IDAT, undo the
|
|
5
|
+
* PNG line filters, and hand back raw samples (re-encoded as FlateDecode by
|
|
6
|
+
* image.ts, with no PDF predictor in v1). Alpha (color type 4/6 or `tRNS`) is
|
|
7
|
+
* split into a separate soft-mask plane; an indexed PNG keeps its palette.
|
|
8
|
+
*
|
|
9
|
+
* Limits (v1): interlaced PNGs and 16-bit depth are rejected; an embedded
|
|
10
|
+
* `iCCP` profile is ignored (device color space) until ICC support (M9).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { InvalidImageError } from '../util/errors.ts'
|
|
14
|
+
import { flateDecode } from '../streams/flate.ts'
|
|
15
|
+
|
|
16
|
+
export type PngColorSpace =
|
|
17
|
+
| { kind: 'DeviceGray' }
|
|
18
|
+
| { kind: 'DeviceRGB' }
|
|
19
|
+
| { kind: 'Indexed'; palette: Uint8Array; hival: number }
|
|
20
|
+
|
|
21
|
+
export interface PngImageData {
|
|
22
|
+
width: number
|
|
23
|
+
height: number
|
|
24
|
+
bitsPerComponent: number
|
|
25
|
+
colorSpace: PngColorSpace
|
|
26
|
+
/** Color samples, line-filters removed (no alpha). */
|
|
27
|
+
samples: Uint8Array
|
|
28
|
+
/** 8-bit alpha plane (width×height), if the image has transparency. */
|
|
29
|
+
alpha?: Uint8Array
|
|
30
|
+
/** Color-key mask `[min max …]` for `tRNS` on gray/RGB images. */
|
|
31
|
+
colorKey?: number[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const SIG = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
|
35
|
+
|
|
36
|
+
function u32(b: Uint8Array, o: number): number {
|
|
37
|
+
return ((b[o]! << 24) | (b[o + 1]! << 16) | (b[o + 2]! << 8) | b[o + 3]!) >>> 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function paeth(a: number, b: number, c: number): number {
|
|
41
|
+
const p = a + b - c
|
|
42
|
+
const pa = Math.abs(p - a)
|
|
43
|
+
const pb = Math.abs(p - b)
|
|
44
|
+
const pc = Math.abs(p - c)
|
|
45
|
+
if (pa <= pb && pa <= pc) return a
|
|
46
|
+
return pb <= pc ? b : c
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Reverse the PNG line filters, returning `height` rows of `rowBytes`. */
|
|
50
|
+
function unfilter(
|
|
51
|
+
raw: Uint8Array,
|
|
52
|
+
height: number,
|
|
53
|
+
rowBytes: number,
|
|
54
|
+
bpp: number
|
|
55
|
+
): Uint8Array {
|
|
56
|
+
const out = new Uint8Array(height * rowBytes)
|
|
57
|
+
let pos = 0
|
|
58
|
+
for (let y = 0; y < height; y++) {
|
|
59
|
+
const filter = raw[pos++]!
|
|
60
|
+
const o = y * rowBytes
|
|
61
|
+
const p = (y - 1) * rowBytes
|
|
62
|
+
for (let x = 0; x < rowBytes; x++) {
|
|
63
|
+
const v = raw[pos++]!
|
|
64
|
+
const a = x >= bpp ? out[o + x - bpp]! : 0
|
|
65
|
+
const b = y > 0 ? out[p + x]! : 0
|
|
66
|
+
const c = y > 0 && x >= bpp ? out[p + x - bpp]! : 0
|
|
67
|
+
let s: number
|
|
68
|
+
switch (filter) {
|
|
69
|
+
case 0:
|
|
70
|
+
s = v
|
|
71
|
+
break
|
|
72
|
+
case 1:
|
|
73
|
+
s = v + a
|
|
74
|
+
break
|
|
75
|
+
case 2:
|
|
76
|
+
s = v + b
|
|
77
|
+
break
|
|
78
|
+
case 3:
|
|
79
|
+
s = v + ((a + b) >> 1)
|
|
80
|
+
break
|
|
81
|
+
case 4:
|
|
82
|
+
s = v + paeth(a, b, c)
|
|
83
|
+
break
|
|
84
|
+
default:
|
|
85
|
+
throw new InvalidImageError(`Unknown PNG filter type ${filter}`)
|
|
86
|
+
}
|
|
87
|
+
out[o + x] = s & 0xff
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parsePng(data: Uint8Array): PngImageData {
|
|
94
|
+
for (let i = 0; i < 8; i++) {
|
|
95
|
+
if (data[i] !== SIG[i]) throw new InvalidImageError('Not a PNG (bad signature)')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let width = 0
|
|
99
|
+
let height = 0
|
|
100
|
+
let bitDepth = 8
|
|
101
|
+
let colorType = 0
|
|
102
|
+
let palette: Uint8Array | undefined
|
|
103
|
+
let trns: Uint8Array | undefined
|
|
104
|
+
const idat: Uint8Array[] = []
|
|
105
|
+
|
|
106
|
+
let pos = 8
|
|
107
|
+
while (pos + 8 <= data.length) {
|
|
108
|
+
const len = u32(data, pos)
|
|
109
|
+
const type = String.fromCharCode(
|
|
110
|
+
data[pos + 4]!,
|
|
111
|
+
data[pos + 5]!,
|
|
112
|
+
data[pos + 6]!,
|
|
113
|
+
data[pos + 7]!
|
|
114
|
+
)
|
|
115
|
+
const body = data.subarray(pos + 8, pos + 8 + len)
|
|
116
|
+
if (type === 'IHDR') {
|
|
117
|
+
width = u32(body, 0)
|
|
118
|
+
height = u32(body, 4)
|
|
119
|
+
bitDepth = body[8]!
|
|
120
|
+
colorType = body[9]!
|
|
121
|
+
if (body[12] !== 0) {
|
|
122
|
+
throw new InvalidImageError('Interlaced PNGs are not supported (de-interlace first)')
|
|
123
|
+
}
|
|
124
|
+
} else if (type === 'PLTE') {
|
|
125
|
+
palette = body.slice()
|
|
126
|
+
} else if (type === 'tRNS') {
|
|
127
|
+
trns = body.slice()
|
|
128
|
+
} else if (type === 'IDAT') {
|
|
129
|
+
idat.push(body.slice())
|
|
130
|
+
} else if (type === 'IEND') {
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
pos += 12 + len // length + type + data + CRC
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (bitDepth === 16) {
|
|
137
|
+
throw new InvalidImageError('16-bit PNGs are not supported in v1 (downsample to 8-bit)')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const channels = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 }[colorType]
|
|
141
|
+
if (channels === undefined) {
|
|
142
|
+
throw new InvalidImageError(`Unsupported PNG color type ${colorType}`)
|
|
143
|
+
}
|
|
144
|
+
if (colorType !== 0 && colorType !== 3 && bitDepth !== 8) {
|
|
145
|
+
throw new InvalidImageError(`Unsupported PNG bit depth ${bitDepth} for color type ${colorType}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Concatenate + inflate IDAT, then undo line filters.
|
|
149
|
+
let total = 0
|
|
150
|
+
for (const c of idat) total += c.length
|
|
151
|
+
const comp = new Uint8Array(total)
|
|
152
|
+
{
|
|
153
|
+
let o = 0
|
|
154
|
+
for (const c of idat) {
|
|
155
|
+
comp.set(c, o)
|
|
156
|
+
o += c.length
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const rowBytes = Math.ceil((width * channels * bitDepth) / 8)
|
|
160
|
+
const bpp = Math.max(1, Math.ceil((channels * bitDepth) / 8))
|
|
161
|
+
const raw = unfilter(flateDecode(comp), height, rowBytes, bpp)
|
|
162
|
+
|
|
163
|
+
// Split colour vs. alpha and pick the PDF color space.
|
|
164
|
+
if (colorType === 0) {
|
|
165
|
+
const res: PngImageData = {
|
|
166
|
+
width,
|
|
167
|
+
height,
|
|
168
|
+
bitsPerComponent: bitDepth,
|
|
169
|
+
colorSpace: { kind: 'DeviceGray' },
|
|
170
|
+
samples: raw,
|
|
171
|
+
}
|
|
172
|
+
if (trns && trns.length >= 2) res.colorKey = [trns[1]!, trns[1]!]
|
|
173
|
+
return res
|
|
174
|
+
}
|
|
175
|
+
if (colorType === 2) {
|
|
176
|
+
const res: PngImageData = {
|
|
177
|
+
width,
|
|
178
|
+
height,
|
|
179
|
+
bitsPerComponent: 8,
|
|
180
|
+
colorSpace: { kind: 'DeviceRGB' },
|
|
181
|
+
samples: raw,
|
|
182
|
+
}
|
|
183
|
+
if (trns && trns.length >= 6) {
|
|
184
|
+
res.colorKey = [trns[1]!, trns[1]!, trns[3]!, trns[3]!, trns[5]!, trns[5]!]
|
|
185
|
+
}
|
|
186
|
+
return res
|
|
187
|
+
}
|
|
188
|
+
if (colorType === 3) {
|
|
189
|
+
if (!palette) throw new InvalidImageError('Indexed PNG missing PLTE chunk')
|
|
190
|
+
const res: PngImageData = {
|
|
191
|
+
width,
|
|
192
|
+
height,
|
|
193
|
+
bitsPerComponent: bitDepth,
|
|
194
|
+
colorSpace: { kind: 'Indexed', palette, hival: palette.length / 3 - 1 },
|
|
195
|
+
samples: raw,
|
|
196
|
+
}
|
|
197
|
+
if (trns) {
|
|
198
|
+
// Per-palette-entry alpha → an 8-bit soft mask plane.
|
|
199
|
+
const alpha = new Uint8Array(width * height)
|
|
200
|
+
const perRow = rowBytes
|
|
201
|
+
let k = 0
|
|
202
|
+
for (let y = 0; y < height; y++) {
|
|
203
|
+
for (let x = 0; x < width; x++) {
|
|
204
|
+
const idx = readIndex(raw, y * perRow, x, bitDepth)
|
|
205
|
+
alpha[k++] = idx < trns.length ? trns[idx]! : 255
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
res.alpha = alpha
|
|
209
|
+
}
|
|
210
|
+
return res
|
|
211
|
+
}
|
|
212
|
+
// colorType 4 (gray+alpha) or 6 (RGBA) — 8-bit, split the alpha channel.
|
|
213
|
+
const colorCh = colorType === 4 ? 1 : 3
|
|
214
|
+
const px = width * height
|
|
215
|
+
const samples = new Uint8Array(px * colorCh)
|
|
216
|
+
const alpha = new Uint8Array(px)
|
|
217
|
+
for (let i = 0; i < px; i++) {
|
|
218
|
+
const src = i * (colorCh + 1)
|
|
219
|
+
for (let ch = 0; ch < colorCh; ch++) samples[i * colorCh + ch] = raw[src + ch]!
|
|
220
|
+
alpha[i] = raw[src + colorCh]!
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
width,
|
|
224
|
+
height,
|
|
225
|
+
bitsPerComponent: 8,
|
|
226
|
+
colorSpace: colorType === 4 ? { kind: 'DeviceGray' } : { kind: 'DeviceRGB' },
|
|
227
|
+
samples,
|
|
228
|
+
alpha,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Read the palette index of pixel `x` in a sub-byte-packed scanline. */
|
|
233
|
+
function readIndex(raw: Uint8Array, rowOff: number, x: number, bitDepth: number): number {
|
|
234
|
+
if (bitDepth === 8) return raw[rowOff + x]!
|
|
235
|
+
const perByte = 8 / bitDepth
|
|
236
|
+
const byte = raw[rowOff + Math.floor(x / perByte)]!
|
|
237
|
+
const shift = 8 - bitDepth * ((x % perByte) + 1)
|
|
238
|
+
return (byte >> shift) & ((1 << bitDepth) - 1)
|
|
239
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Soft mask for image alpha (spec §11.2). An 8-bit alpha plane becomes a
|
|
3
|
+
* standalone DeviceGray image XObject referenced from the parent image's
|
|
4
|
+
* `/SMask` entry.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { name, num } from '../objects/types.ts'
|
|
8
|
+
import type { PdfStream } from '../objects/types.ts'
|
|
9
|
+
import { makeStream } from '../streams/stream.ts'
|
|
10
|
+
|
|
11
|
+
/** Build the soft-mask image XObject stream from an alpha plane. */
|
|
12
|
+
export function buildSMask(alpha: Uint8Array, width: number, height: number): PdfStream {
|
|
13
|
+
return makeStream(alpha, {
|
|
14
|
+
filter: 'FlateDecode',
|
|
15
|
+
extra: {
|
|
16
|
+
Type: name('XObject'),
|
|
17
|
+
Subtype: name('Image'),
|
|
18
|
+
Width: num(width),
|
|
19
|
+
Height: num(height),
|
|
20
|
+
ColorSpace: name('DeviceGray'),
|
|
21
|
+
BitsPerComponent: num(8),
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@strav/pdf` — low-level, zero-dependency PDF generation (write side).
|
|
3
|
+
*
|
|
4
|
+
* This effort delivers spec milestones 1–3: object model & serialization,
|
|
5
|
+
* pages + content streams + device color, and stream filters. Fonts, images,
|
|
6
|
+
* color management, transparency and conformance modes are later milestones.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export { PdfDocument } from './document/pdf_document.ts'
|
|
10
|
+
export { Page } from './document/page.ts'
|
|
11
|
+
export type {
|
|
12
|
+
PageSize,
|
|
13
|
+
Rect,
|
|
14
|
+
ConformanceLevel,
|
|
15
|
+
DocumentInfo,
|
|
16
|
+
CreateOptions,
|
|
17
|
+
AddPageOptions,
|
|
18
|
+
} from './document/types.ts'
|
|
19
|
+
|
|
20
|
+
export { BufferSink } from './output/buffer_sink.ts'
|
|
21
|
+
export { StreamSink } from './output/stream_sink.ts'
|
|
22
|
+
export type { ByteSink } from './output/byte_sink.ts'
|
|
23
|
+
|
|
24
|
+
export { ContentStream } from './content/content_stream.ts'
|
|
25
|
+
export type { TextObject, RunPart } from './content/text_object.ts'
|
|
26
|
+
|
|
27
|
+
export { PdfFont } from './fonts/font.ts'
|
|
28
|
+
export type { StandardFontName, TrueTypeOptions } from './fonts/font.ts'
|
|
29
|
+
|
|
30
|
+
export { PdfImage } from './images/image.ts'
|
|
31
|
+
|
|
32
|
+
export { Color, rgb, cmyk, gray } from './color/color.ts'
|
|
33
|
+
export type { Color as ColorValue, ColorSpace, DeviceColor } from './color/color.ts'
|
|
34
|
+
export { rgbToGray, cmykToRgb, rgbToCmyk } from './color/conversion.ts'
|
|
35
|
+
export { separation } from './color/separation.ts'
|
|
36
|
+
export { iccBased, parseIccProfile } from './color/icc.ts'
|
|
37
|
+
export { deviceN } from './color/devicen.ts'
|
|
38
|
+
export { calGray, calRGB, lab } from './color/cie.ts'
|
|
39
|
+
export type { ManagedColorSpace } from './color/space.ts'
|
|
40
|
+
export type { OutputIntentConfig } from './document/types.ts'
|
|
41
|
+
|
|
42
|
+
export { extGState } from './ext-gstate/ext_gstate.ts'
|
|
43
|
+
export type { ExtGState, ExtGStateOptions, BlendMode } from './ext-gstate/ext_gstate.ts'
|
|
44
|
+
export { tilingPattern } from './patterns/tiling_pattern.ts'
|
|
45
|
+
export type { TilingPattern, TilingPatternOptions } from './patterns/tiling_pattern.ts'
|
|
46
|
+
export { axialShading, radialShading, shadingPattern } from './patterns/shading.ts'
|
|
47
|
+
export type { Shading, ShadingPattern, ColorStop } from './patterns/shading.ts'
|
|
48
|
+
|
|
49
|
+
export { mm, cm, inch, pt } from './util/units.ts'
|
|
50
|
+
|
|
51
|
+
export {
|
|
52
|
+
PdfGenError,
|
|
53
|
+
ConformanceError,
|
|
54
|
+
UnsupportedFontError,
|
|
55
|
+
InvalidImageError,
|
|
56
|
+
} from './util/errors.ts'
|
|
57
|
+
export type { PdfGenErrorCode } from './util/errors.ts'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The legacy Info dictionary (spec §14.1). Kept for old readers and the
|
|
3
|
+
* trailer `/Info`; XMP (metadata/xmp.ts) is the authoritative source and is
|
|
4
|
+
* built from the same values so the two stay in sync.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { dict } from '../objects/types.ts'
|
|
8
|
+
import type { PdfDictionary, PdfObject } from '../objects/types.ts'
|
|
9
|
+
import { textString, dateString } from '../objects/string.ts'
|
|
10
|
+
import type { DocumentInfo } from '../document/types.ts'
|
|
11
|
+
|
|
12
|
+
export function buildInfoDict(
|
|
13
|
+
info: DocumentInfo,
|
|
14
|
+
creationDate: Date,
|
|
15
|
+
producer: string
|
|
16
|
+
): PdfDictionary {
|
|
17
|
+
const d = dateString(creationDate)
|
|
18
|
+
const entries: Record<string, PdfObject> = {}
|
|
19
|
+
if (info.title) entries.Title = textString(info.title)
|
|
20
|
+
if (info.author) entries.Author = textString(info.author)
|
|
21
|
+
if (info.subject) entries.Subject = textString(info.subject)
|
|
22
|
+
if (info.keywords) entries.Keywords = textString(info.keywords)
|
|
23
|
+
if (info.creator) entries.Creator = textString(info.creator)
|
|
24
|
+
entries.Producer = textString(producer)
|
|
25
|
+
entries.CreationDate = d
|
|
26
|
+
entries.ModDate = d
|
|
27
|
+
return dict(entries)
|
|
28
|
+
}
|