@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.
Files changed (80) hide show
  1. package/README.md +79 -0
  2. package/package.json +51 -0
  3. package/src/color/cie.ts +61 -0
  4. package/src/color/color.ts +77 -0
  5. package/src/color/conversion.ts +26 -0
  6. package/src/color/device.ts +37 -0
  7. package/src/color/devicen.ts +74 -0
  8. package/src/color/icc.ts +103 -0
  9. package/src/color/index.ts +15 -0
  10. package/src/color/separation.ts +94 -0
  11. package/src/color/space.ts +47 -0
  12. package/src/content/content_stream.ts +373 -0
  13. package/src/content/graphics_state.ts +64 -0
  14. package/src/content/index.ts +16 -0
  15. package/src/content/operators.ts +70 -0
  16. package/src/content/path.ts +51 -0
  17. package/src/content/resources.ts +119 -0
  18. package/src/content/text_object.ts +140 -0
  19. package/src/document/catalog.ts +16 -0
  20. package/src/document/index.ts +13 -0
  21. package/src/document/object_table.ts +67 -0
  22. package/src/document/page.ts +74 -0
  23. package/src/document/page_tree.ts +78 -0
  24. package/src/document/pdf_document.ts +310 -0
  25. package/src/document/types.ts +65 -0
  26. package/src/document/xref.ts +68 -0
  27. package/src/ext-gstate/ext_gstate.ts +69 -0
  28. package/src/ext-gstate/index.ts +2 -0
  29. package/src/fonts/cff.ts +123 -0
  30. package/src/fonts/cid_encoding.ts +45 -0
  31. package/src/fonts/cmap_table.ts +180 -0
  32. package/src/fonts/font.ts +342 -0
  33. package/src/fonts/glyf.ts +59 -0
  34. package/src/fonts/hmtx.ts +21 -0
  35. package/src/fonts/index.ts +20 -0
  36. package/src/fonts/name_table.ts +50 -0
  37. package/src/fonts/os2.ts +41 -0
  38. package/src/fonts/sfnt.ts +224 -0
  39. package/src/fonts/standard_14.ts +132 -0
  40. package/src/fonts/subset.ts +221 -0
  41. package/src/fonts/to_unicode.ts +82 -0
  42. package/src/fonts/win_ansi.ts +69 -0
  43. package/src/images/image.ts +111 -0
  44. package/src/images/index.ts +6 -0
  45. package/src/images/jpeg.ts +103 -0
  46. package/src/images/png.ts +239 -0
  47. package/src/images/smask.ts +24 -0
  48. package/src/index.ts +57 -0
  49. package/src/metadata/index.ts +3 -0
  50. package/src/metadata/info_dict.ts +28 -0
  51. package/src/metadata/xmp.ts +110 -0
  52. package/src/objects/encode.ts +77 -0
  53. package/src/objects/index.ts +43 -0
  54. package/src/objects/indirect_ref.ts +17 -0
  55. package/src/objects/name.ts +50 -0
  56. package/src/objects/number.ts +43 -0
  57. package/src/objects/string.ts +136 -0
  58. package/src/objects/types.ts +86 -0
  59. package/src/output/buffer_sink.ts +40 -0
  60. package/src/output/byte_sink.ts +12 -0
  61. package/src/output/index.ts +3 -0
  62. package/src/output/stream_sink.ts +62 -0
  63. package/src/patterns/index.ts +10 -0
  64. package/src/patterns/shading.ts +162 -0
  65. package/src/patterns/tiling_pattern.ts +68 -0
  66. package/src/standards/context.ts +10 -0
  67. package/src/standards/index.ts +23 -0
  68. package/src/standards/pdf_a.ts +23 -0
  69. package/src/standards/pdf_x.ts +31 -0
  70. package/src/streams/ascii85.ts +61 -0
  71. package/src/streams/ascii_hex.ts +33 -0
  72. package/src/streams/flate.ts +17 -0
  73. package/src/streams/index.ts +9 -0
  74. package/src/streams/stream.ts +66 -0
  75. package/src/util/ascii.ts +63 -0
  76. package/src/util/binary.ts +71 -0
  77. package/src/util/errors.ts +61 -0
  78. package/src/util/index.ts +10 -0
  79. package/src/util/units.ts +24 -0
  80. package/tsconfig.json +5 -0
@@ -0,0 +1,10 @@
1
+ export { tilingPattern } from './tiling_pattern.ts'
2
+ export type { TilingPattern, TilingPatternOptions } from './tiling_pattern.ts'
3
+ export { axialShading, radialShading, shadingPattern } from './shading.ts'
4
+ export type {
5
+ Shading,
6
+ ShadingPattern,
7
+ ColorStop,
8
+ AxialOptions,
9
+ RadialOptions,
10
+ } from './shading.ts'
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Axial (type 2) and radial (type 3) shadings (spec §12.2). The color ramp is
3
+ * a Type-2 exponential function for two stops, or a Type-3 stitching function
4
+ * for more. Types 1 and 4–7 are reachable by adding raw objects.
5
+ *
6
+ * A `Shading` paints via the `sh` operator; wrap it with `shadingPattern` to
7
+ * use it as a fill/stroke pattern (PatternType 2).
8
+ */
9
+
10
+ import { PdfGenError } from '../util/errors.ts'
11
+ import { arr, bool, dict, name, num } from '../objects/types.ts'
12
+ import type { PdfObject } from '../objects/types.ts'
13
+ import type { ObjectTable } from '../document/object_table.ts'
14
+ import type { DeviceColor } from '../color/color.ts'
15
+ import { deviceComponents, deviceSpaceName } from '../color/color.ts'
16
+
17
+ export interface ColorStop {
18
+ /** Position in [0,1]. */
19
+ offset: number
20
+ color: DeviceColor
21
+ }
22
+
23
+ function buildFunction(stops: ColorStop[]): PdfObject {
24
+ if (stops.length < 2) {
25
+ throw new PdfGenError('PDF_INVALID_COLOR', 'A shading needs at least two color stops')
26
+ }
27
+ const space = stops[0]!.color.space
28
+ for (const s of stops) {
29
+ if (s.color.space !== space) {
30
+ throw new PdfGenError('PDF_INVALID_COLOR', 'All shading stops must share one color space')
31
+ }
32
+ }
33
+ const seg = (a: DeviceColor, b: DeviceColor): PdfObject =>
34
+ dict({
35
+ FunctionType: num(2),
36
+ Domain: arr([num(0), num(1)]),
37
+ C0: arr(deviceComponents(a).map(num)),
38
+ C1: arr(deviceComponents(b).map(num)),
39
+ N: num(1),
40
+ })
41
+
42
+ if (stops.length === 2) return seg(stops[0]!.color, stops[1]!.color)
43
+
44
+ // Type-3 stitching of consecutive Type-2 segments.
45
+ const fns: PdfObject[] = []
46
+ const bounds: PdfObject[] = []
47
+ const encode: PdfObject[] = []
48
+ for (let i = 0; i < stops.length - 1; i++) {
49
+ fns.push(seg(stops[i]!.color, stops[i + 1]!.color))
50
+ encode.push(num(0), num(1))
51
+ if (i > 0) bounds.push(num(stops[i]!.offset))
52
+ }
53
+ return dict({
54
+ FunctionType: num(3),
55
+ Domain: arr([num(0), num(1)]),
56
+ Functions: arr(fns),
57
+ Bounds: arr(bounds),
58
+ Encode: arr(encode),
59
+ })
60
+ }
61
+
62
+ function colorSpaceName(stops: ColorStop[]): string {
63
+ return deviceSpaceName(stops[0]!.color)
64
+ }
65
+
66
+ function normalizeStops(colors: DeviceColor[] | ColorStop[]): ColorStop[] {
67
+ const first = colors[0]
68
+ if (first && 'color' in first) return colors as ColorStop[]
69
+ const cs = colors as DeviceColor[]
70
+ return cs.map((color, i) => ({ offset: i / (cs.length - 1), color }))
71
+ }
72
+
73
+ class Shading {
74
+ readonly id: string
75
+ constructor(
76
+ private readonly shadingType: 2 | 3,
77
+ private readonly coords: number[],
78
+ private readonly stops: ColorStop[],
79
+ private readonly extend: [boolean, boolean],
80
+ tag: number
81
+ ) {
82
+ this.id = `sh${shadingType}:${tag}`
83
+ }
84
+
85
+ build(_table: ObjectTable): PdfObject {
86
+ return dict({
87
+ ShadingType: num(this.shadingType),
88
+ ColorSpace: name(colorSpaceName(this.stops)),
89
+ Coords: arr(this.coords.map(num)),
90
+ Function: buildFunction(this.stops),
91
+ Extend: arr([bool(this.extend[0]), bool(this.extend[1])]),
92
+ })
93
+ }
94
+ }
95
+
96
+ let shCounter = 0
97
+
98
+ export interface AxialOptions {
99
+ from: [number, number]
100
+ to: [number, number]
101
+ colors: DeviceColor[] | ColorStop[]
102
+ extend?: [boolean, boolean]
103
+ }
104
+
105
+ /** A type-2 (axial / linear-gradient) shading. */
106
+ export function axialShading(o: AxialOptions): Shading {
107
+ return new Shading(
108
+ 2,
109
+ [o.from[0], o.from[1], o.to[0], o.to[1]],
110
+ normalizeStops(o.colors),
111
+ o.extend ?? [true, true],
112
+ shCounter++
113
+ )
114
+ }
115
+
116
+ export interface RadialOptions {
117
+ from: { x: number; y: number; r: number }
118
+ to: { x: number; y: number; r: number }
119
+ colors: DeviceColor[] | ColorStop[]
120
+ extend?: [boolean, boolean]
121
+ }
122
+
123
+ /** A type-3 (radial) shading. */
124
+ export function radialShading(o: RadialOptions): Shading {
125
+ return new Shading(
126
+ 3,
127
+ [o.from.x, o.from.y, o.from.r, o.to.x, o.to.y, o.to.r],
128
+ normalizeStops(o.colors),
129
+ o.extend ?? [true, true],
130
+ shCounter++
131
+ )
132
+ }
133
+
134
+ class ShadingPattern {
135
+ readonly id: string
136
+ constructor(
137
+ private readonly shading: Shading,
138
+ private readonly matrix: number[] | undefined,
139
+ tag: number
140
+ ) {
141
+ this.id = `shp:${shading.id}:${tag}`
142
+ }
143
+
144
+ build(table: ObjectTable): PdfObject {
145
+ const d = dict({
146
+ Type: name('Pattern'),
147
+ PatternType: num(2),
148
+ Shading: this.shading.build(table),
149
+ })
150
+ if (this.matrix) d.entries.set('Matrix', arr(this.matrix.map(num)))
151
+ return table.add(d)
152
+ }
153
+ }
154
+
155
+ let shpCounter = 0
156
+
157
+ /** Wrap a shading as a pattern (use with `setFillPattern`/`setStrokePattern`). */
158
+ export function shadingPattern(shading: Shading, matrix?: number[]): ShadingPattern {
159
+ return new ShadingPattern(shading, matrix, shpCounter++)
160
+ }
161
+
162
+ export type { Shading, ShadingPattern }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Tiling pattern (spec §12.1). The draw callback receives a content-stream
3
+ * builder scoped to the pattern cell; its own resources (colors, images,
4
+ * fonts) are collected into the pattern's `/Resources`. A colored pattern
5
+ * (PaintType 1) carries its own color; an uncolored one (PaintType 2) is
6
+ * tinted by the caller's current color.
7
+ */
8
+
9
+ import { arr, name, num } from '../objects/types.ts'
10
+ import type { PdfObject } from '../objects/types.ts'
11
+ import type { ObjectTable } from '../document/object_table.ts'
12
+ import { makeStream } from '../streams/stream.ts'
13
+ import { ContentStream } from '../content/content_stream.ts'
14
+
15
+ export interface TilingPatternOptions {
16
+ /** Pattern cell bounding box `[llx lly urx ury]`. */
17
+ bbox: [number, number, number, number]
18
+ xStep: number
19
+ yStep: number
20
+ /** `'colored'` (default) carries its own color; `'uncolored'` is tinted. */
21
+ paintType?: 'colored' | 'uncolored'
22
+ /** Optional pattern→page transform. */
23
+ matrix?: number[]
24
+ draw: (c: ContentStream) => void
25
+ }
26
+
27
+ class TilingPattern {
28
+ readonly id: string
29
+ /** `'colored'` patterns set color in the cell; `'uncolored'` are tinted. */
30
+ readonly paintType: 'colored' | 'uncolored'
31
+
32
+ constructor(
33
+ private readonly opts: TilingPatternOptions,
34
+ tag: number
35
+ ) {
36
+ this.paintType = opts.paintType ?? 'colored'
37
+ this.id = `tile:${tag}`
38
+ }
39
+
40
+ build(table: ObjectTable): PdfObject {
41
+ const cell = new ContentStream()
42
+ this.opts.draw(cell)
43
+ cell.assertBalanced()
44
+ const resources = cell.buildResources(table) // children added to table
45
+ const [x0, y0, x1, y1] = this.opts.bbox
46
+ const extra: Record<string, PdfObject> = {
47
+ Type: name('Pattern'),
48
+ PatternType: num(1),
49
+ PaintType: num(this.paintType === 'colored' ? 1 : 2),
50
+ TilingType: num(1),
51
+ BBox: arr([x0, y0, x1, y1].map(num)),
52
+ XStep: num(this.opts.xStep),
53
+ YStep: num(this.opts.yStep),
54
+ Resources: resources,
55
+ }
56
+ if (this.opts.matrix) extra.Matrix = arr(this.opts.matrix.map(num))
57
+ return table.add(makeStream(cell.toBytes(), { filter: 'FlateDecode', extra }))
58
+ }
59
+ }
60
+
61
+ let tileCounter = 0
62
+
63
+ /** Create a tiling pattern (use with `ContentStream.setFillPattern`). */
64
+ export function tilingPattern(opts: TilingPatternOptions): TilingPattern {
65
+ return new TilingPattern(opts, tileCounter++)
66
+ }
67
+
68
+ export type { TilingPattern }
@@ -0,0 +1,10 @@
1
+ /** Inputs to the conformance validators (gathered by PdfDocument at save). */
2
+ export interface ConformanceContext {
3
+ /** One entry per page; whether it has a TrimBox or ArtBox set. */
4
+ pages: { hasTrimOrArt: boolean }[]
5
+ outputIntent: {
6
+ present: boolean
7
+ /** ICC data color space of the destination profile, if present. */
8
+ profileColorSpace?: 'GRAY' | 'RGB' | 'CMYK' | 'Lab'
9
+ }
10
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Conformance dispatch (spec §15). `setConformance(...)` opts in; validation
3
+ * runs at `save()` and a non-empty result becomes a `ConformanceError`.
4
+ */
5
+
6
+ import type { ConformanceLevel } from '../document/types.ts'
7
+ import type { ConformanceContext } from './context.ts'
8
+ import { validatePdfA } from './pdf_a.ts'
9
+ import { validatePdfX } from './pdf_x.ts'
10
+
11
+ export type { ConformanceContext } from './context.ts'
12
+ export { validatePdfA } from './pdf_a.ts'
13
+ export { validatePdfX } from './pdf_x.ts'
14
+
15
+ /** Returns all conformance violations for `level` (empty = conforms). */
16
+ export function validateConformance(
17
+ level: ConformanceLevel,
18
+ ctx: ConformanceContext
19
+ ): string[] {
20
+ if (level === 'PDF/A-2b') return validatePdfA(ctx)
21
+ if (level === 'PDF/X-4') return validatePdfX(ctx)
22
+ return []
23
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * PDF/A-2b conformance checks (spec §15.1). Returns the list of violations
3
+ * (empty = conforms). Font rules — all fonts embedded, ToUnicode present, no
4
+ * Standard-14 — are enforced earlier: a Standard-14 font under a conformance
5
+ * mode throws `UnsupportedFontError`, and embedded fonts always carry a
6
+ * ToUnicode CMap and are subsetted, so they need no check here.
7
+ *
8
+ * Not checked (out of scope / not producible by this library): encryption,
9
+ * LZWDecode, JavaScript, external references — none are ever emitted.
10
+ */
11
+
12
+ import type { ConformanceContext } from './context.ts'
13
+
14
+ export function validatePdfA(ctx: ConformanceContext): string[] {
15
+ const v: string[] = []
16
+ if (!ctx.outputIntent.present) {
17
+ v.push(
18
+ 'PDF/A-2b requires an OutputIntent (the all-DeviceGray exception is not ' +
19
+ 'auto-detected — call setOutputIntent)'
20
+ )
21
+ }
22
+ return v
23
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * PDF/X-4 conformance checks (spec §15.2). Returns the list of violations.
3
+ *
4
+ * Enforced here: an OutputIntent with a CMYK/Gray destination profile, and a
5
+ * TrimBox or ArtBox on every page (MediaBox always exists). Embedded-and-
6
+ * subsetted fonts and the `/ID` array are guaranteed by the library; the
7
+ * no-Standard-14 rule throws `UnsupportedFontError` earlier.
8
+ */
9
+
10
+ import type { ConformanceContext } from './context.ts'
11
+
12
+ export function validatePdfX(ctx: ConformanceContext): string[] {
13
+ const v: string[] = []
14
+
15
+ if (!ctx.outputIntent.present) {
16
+ v.push('PDF/X-4 requires an OutputIntent — call setOutputIntent()')
17
+ } else {
18
+ const cs = ctx.outputIntent.profileColorSpace
19
+ if (cs !== 'CMYK' && cs !== 'GRAY') {
20
+ v.push(`PDF/X-4 output intent profile must be CMYK or Gray (got ${cs ?? 'unknown'})`)
21
+ }
22
+ }
23
+
24
+ ctx.pages.forEach((p, i) => {
25
+ if (!p.hasTrimOrArt) {
26
+ v.push(`PDF/X-4 page ${i + 1} requires a TrimBox or ArtBox (MediaBox alone is not enough)`)
27
+ }
28
+ })
29
+
30
+ return v
31
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ASCII85Decode (spec §7.1). Available; not used by default. Adobe variant:
3
+ * 4-byte groups → 5 chars offset by '!' (0x21); an all-zero group is 'z';
4
+ * the stream ends with the `~>` EOD marker.
5
+ */
6
+
7
+ export function ascii85Encode(data: Uint8Array): Uint8Array {
8
+ const out: number[] = []
9
+ for (let i = 0; i < data.length; i += 4) {
10
+ const len = Math.min(4, data.length - i)
11
+ let word = 0
12
+ for (let j = 0; j < 4; j++) {
13
+ word = (word * 256 + (j < len ? data[i + j]! : 0)) >>> 0
14
+ }
15
+ if (len === 4 && word === 0) {
16
+ out.push(0x7a) // 'z'
17
+ continue
18
+ }
19
+ const group = [0, 0, 0, 0, 0]
20
+ for (let k = 4; k >= 0; k--) {
21
+ group[k] = word % 85
22
+ word = Math.floor(word / 85)
23
+ }
24
+ for (let k = 0; k < len + 1; k++) out.push(group[k]! + 0x21)
25
+ }
26
+ out.push(0x7e, 0x3e) // ~>
27
+ return Uint8Array.from(out)
28
+ }
29
+
30
+ export function ascii85Decode(data: Uint8Array): Uint8Array {
31
+ const out: number[] = []
32
+ const group: number[] = []
33
+ for (let i = 0; i < data.length; i++) {
34
+ const b = data[i]!
35
+ if (b === 0x7e) break // ~ (start of ~>)
36
+ if (b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d || b === 0x0c) continue
37
+ if (b === 0x7a) {
38
+ // 'z' — only valid between groups
39
+ if (group.length !== 0) throw new Error('Unexpected z in ASCII85 group')
40
+ out.push(0, 0, 0, 0)
41
+ continue
42
+ }
43
+ group.push(b - 0x21)
44
+ if (group.length === 5) {
45
+ let word = 0
46
+ for (const g of group) word = word * 85 + g
47
+ out.push((word >>> 24) & 0xff, (word >>> 16) & 0xff, (word >>> 8) & 0xff, word & 0xff)
48
+ group.length = 0
49
+ }
50
+ }
51
+ if (group.length === 1) throw new Error('Truncated ASCII85 group')
52
+ if (group.length > 1) {
53
+ const n = group.length
54
+ while (group.length < 5) group.push(84)
55
+ let word = 0
56
+ for (const g of group) word = word * 85 + g
57
+ const bytes = [(word >>> 24) & 0xff, (word >>> 16) & 0xff, (word >>> 8) & 0xff, word & 0xff]
58
+ for (let k = 0; k < n - 1; k++) out.push(bytes[k]!)
59
+ }
60
+ return Uint8Array.from(out)
61
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ASCIIHexDecode (spec §7.1). Available; not used by default. Each byte → two
3
+ * uppercase hex digits, terminated by the `>` EOD marker.
4
+ */
5
+
6
+ const HEX = '0123456789ABCDEF'
7
+
8
+ export function asciiHexEncode(data: Uint8Array): Uint8Array {
9
+ const out = new Uint8Array(data.length * 2 + 1)
10
+ let i = 0
11
+ for (const b of data) {
12
+ out[i++] = HEX.charCodeAt(b >> 4)
13
+ out[i++] = HEX.charCodeAt(b & 0x0f)
14
+ }
15
+ out[i] = 0x3e // >
16
+ return out
17
+ }
18
+
19
+ export function asciiHexDecode(data: Uint8Array): Uint8Array {
20
+ const nibbles: number[] = []
21
+ for (const b of data) {
22
+ if (b === 0x3e) break // > EOD
23
+ if (b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d || b === 0x0c) continue
24
+ const c = String.fromCharCode(b)
25
+ const v = parseInt(c, 16)
26
+ if (Number.isNaN(v)) throw new Error(`Invalid ASCIIHex digit: ${c}`)
27
+ nibbles.push(v)
28
+ }
29
+ if (nibbles.length % 2 === 1) nibbles.push(0) // odd → last nibble is 0
30
+ const out = new Uint8Array(nibbles.length / 2)
31
+ for (let j = 0; j < out.length; j++) out[j] = (nibbles[2 * j]! << 4) | nibbles[2 * j + 1]!
32
+ return out
33
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * FlateDecode (spec §7.2). Node/Bun build only — `node:zlib` at level 9 for
3
+ * deterministic output (level affects the byte sequence). No browser fallback
4
+ * (project decision); no PNG/TIFF predictor in v1.
5
+ */
6
+
7
+ import { deflateSync, inflateSync } from 'node:zlib'
8
+
9
+ /** Deflate (zlib) encode at level 9. */
10
+ export function flateEncode(data: Uint8Array): Uint8Array {
11
+ return new Uint8Array(deflateSync(data, { level: 9 }))
12
+ }
13
+
14
+ /** Inflate — not used in production output; provided for round-trip tests. */
15
+ export function flateDecode(data: Uint8Array): Uint8Array {
16
+ return new Uint8Array(inflateSync(data))
17
+ }
@@ -0,0 +1,9 @@
1
+ export {
2
+ makeStream,
3
+ makeContentStream,
4
+ MIN_FILTER_BYTES,
5
+ } from './stream.ts'
6
+ export type { FilterName, MakeStreamOptions } from './stream.ts'
7
+ export { flateEncode, flateDecode } from './flate.ts'
8
+ export { ascii85Encode, ascii85Decode } from './ascii85.ts'
9
+ export { asciiHexEncode, asciiHexDecode } from './ascii_hex.ts'
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Stream construction (spec §7). Filters are applied *here*, at construction
3
+ * time: the resulting {@link PdfStream}'s `data` is always the final on-disk
4
+ * bytes and `/Length` is its filtered length. The library never stores
5
+ * unfiltered data alongside a filter chain.
6
+ */
7
+
8
+ import type { PdfObject, PdfStream } from '../objects/types.ts'
9
+ import { dict, name, num } from '../objects/types.ts'
10
+ import { flateEncode } from './flate.ts'
11
+ import { ascii85Encode } from './ascii85.ts'
12
+ import { asciiHexEncode } from './ascii_hex.ts'
13
+
14
+ export type FilterName = 'FlateDecode' | 'ASCII85Decode' | 'ASCIIHexDecode'
15
+
16
+ /** Streams below this size aren't worth filtering (spec §7.3). */
17
+ export const MIN_FILTER_BYTES = 64
18
+
19
+ export interface MakeStreamOptions {
20
+ /**
21
+ * `'auto'` (default) FlateDecodes when worthwhile; `'none'` never filters;
22
+ * or force a specific filter.
23
+ */
24
+ filter?: 'auto' | 'none' | FilterName
25
+ /** Data is already compressed (e.g. JPEG/DCT) — never re-filter (§7.3). */
26
+ alreadyCompressed?: boolean
27
+ /** Extra dictionary entries (e.g. /Type, /Subtype for XObjects). */
28
+ extra?: Record<string, PdfObject>
29
+ }
30
+
31
+ function applyFilter(data: Uint8Array, f: FilterName): Uint8Array {
32
+ switch (f) {
33
+ case 'FlateDecode':
34
+ return flateEncode(data)
35
+ case 'ASCII85Decode':
36
+ return ascii85Encode(data)
37
+ case 'ASCIIHexDecode':
38
+ return asciiHexEncode(data)
39
+ }
40
+ }
41
+
42
+ export function makeStream(raw: Uint8Array, opts: MakeStreamOptions = {}): PdfStream {
43
+ const mode = opts.filter ?? 'auto'
44
+
45
+ let filter: FilterName | null
46
+ if (mode === 'none' || opts.alreadyCompressed) {
47
+ filter = null
48
+ } else if (mode === 'auto') {
49
+ filter = raw.length >= MIN_FILTER_BYTES ? 'FlateDecode' : null
50
+ } else {
51
+ filter = mode
52
+ }
53
+
54
+ const data = filter ? applyFilter(raw, filter) : raw
55
+
56
+ const entries: Record<string, PdfObject> = { ...opts.extra }
57
+ if (filter) entries.Filter = name(filter)
58
+ entries.Length = num(data.length)
59
+
60
+ return { kind: 'stream', dict: dict(entries), data }
61
+ }
62
+
63
+ /** Page/Form content stream: auto-compressed (spec §7.1 default). */
64
+ export function makeContentStream(raw: Uint8Array): PdfStream {
65
+ return makeStream(raw, { filter: 'auto' })
66
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ASCII byte helpers. PDF tokens are ASCII; we work at the byte level to keep
3
+ * serialization deterministic and free of platform text-encoding surprises.
4
+ */
5
+
6
+ const encoder = new TextEncoder()
7
+
8
+ /** Encode an ASCII/Latin-1 string to bytes. Throws on non-Latin-1 input. */
9
+ export function ascii(s: string): Uint8Array {
10
+ const out = new Uint8Array(s.length)
11
+ for (let i = 0; i < s.length; i++) {
12
+ const c = s.charCodeAt(i)
13
+ if (c > 0xff) {
14
+ throw new Error(`Non-Latin-1 character at index ${i}: U+${c.toString(16)}`)
15
+ }
16
+ out[i] = c
17
+ }
18
+ return out
19
+ }
20
+
21
+ /** Encode a string as UTF-8 bytes (used for XMP, ToUnicode CMap text, etc.). */
22
+ export function utf8(s: string): Uint8Array {
23
+ return encoder.encode(s)
24
+ }
25
+
26
+ /** Concatenate byte chunks into a single Uint8Array. */
27
+ export function concatBytes(chunks: readonly Uint8Array[]): Uint8Array {
28
+ let total = 0
29
+ for (const c of chunks) total += c.length
30
+ const out = new Uint8Array(total)
31
+ let offset = 0
32
+ for (const c of chunks) {
33
+ out.set(c, offset)
34
+ offset += c.length
35
+ }
36
+ return out
37
+ }
38
+
39
+ // Common single bytes used by the encoder.
40
+ export const SPACE = 0x20
41
+ export const LF = 0x0a
42
+ export const CR = 0x0d
43
+
44
+ /** True for PDF whitespace bytes (ISO 32000-1 Table 1). */
45
+ export function isWhitespace(b: number): boolean {
46
+ return b === 0x00 || b === 0x09 || b === 0x0a || b === 0x0c || b === 0x0d || b === 0x20
47
+ }
48
+
49
+ /** True for PDF delimiter bytes (ISO 32000-1 Table 2). */
50
+ export function isDelimiter(b: number): boolean {
51
+ return (
52
+ b === 0x28 || // (
53
+ b === 0x29 || // )
54
+ b === 0x3c || // <
55
+ b === 0x3e || // >
56
+ b === 0x5b || // [
57
+ b === 0x5d || // ]
58
+ b === 0x7b || // {
59
+ b === 0x7d || // }
60
+ b === 0x2f || // /
61
+ b === 0x25 // %
62
+ )
63
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Big-endian binary readers and fixed-point helpers.
3
+ *
4
+ * Minimal in M1–M3 (only needed by document ID hashing helpers and future
5
+ * font/image parsers). SFNT/JPEG/PNG parsing in later milestones builds on
6
+ * this without restructuring.
7
+ */
8
+
9
+ export class BinaryReader {
10
+ private readonly view: DataView
11
+ private offset = 0
12
+
13
+ constructor(
14
+ private readonly bytes: Uint8Array,
15
+ offset = 0
16
+ ) {
17
+ this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
18
+ this.offset = offset
19
+ }
20
+
21
+ get position(): number {
22
+ return this.offset
23
+ }
24
+
25
+ seek(offset: number): void {
26
+ this.offset = offset
27
+ }
28
+
29
+ u8(): number {
30
+ return this.view.getUint8(this.offset++)
31
+ }
32
+
33
+ u16(): number {
34
+ const v = this.view.getUint16(this.offset, false)
35
+ this.offset += 2
36
+ return v
37
+ }
38
+
39
+ i16(): number {
40
+ const v = this.view.getInt16(this.offset, false)
41
+ this.offset += 2
42
+ return v
43
+ }
44
+
45
+ u32(): number {
46
+ const v = this.view.getUint32(this.offset, false)
47
+ this.offset += 4
48
+ return v
49
+ }
50
+
51
+ /** Read `n` raw bytes as a subarray (no copy). */
52
+ bytesOf(n: number): Uint8Array {
53
+ const out = this.bytes.subarray(this.offset, this.offset + n)
54
+ this.offset += n
55
+ return out
56
+ }
57
+
58
+ get remaining(): number {
59
+ return this.bytes.length - this.offset
60
+ }
61
+ }
62
+
63
+ /** Convert a signed 16.16 fixed-point value to a float. */
64
+ export function fixed1616(raw: number): number {
65
+ return raw / 65536
66
+ }
67
+
68
+ /** Convert a signed 2.14 fixed-point value (F2Dot14) to a float. */
69
+ export function f2dot14(raw: number): number {
70
+ return (raw << 16) / (1 << 30)
71
+ }