@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
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @strav/pdf
2
+
3
+ Low-level, **zero-dependency** PDF generation (the *write* side of PDF) for
4
+ the Strav ecosystem. Produces conformant PDF 1.7 byte streams — it does not
5
+ parse, render or display PDFs. No `@strav/*` dependency and no npm runtime
6
+ dependency; only Node/Bun built-ins.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ bun add @strav/pdf
12
+ ```
13
+
14
+ ## Quick start
15
+
16
+ ```typescript
17
+ import { PdfDocument, PdfFont, rgb, mm } from '@strav/pdf'
18
+
19
+ const doc = PdfDocument.create({ info: { title: 'Hello', author: 'Acme' } })
20
+ const page = doc.addPage({ size: { widthPt: mm(210), heightPt: mm(297) } })
21
+
22
+ page.content()
23
+ .save()
24
+ .setFillColor(rgb(0.1, 0.1, 0.4))
25
+ .rect(mm(20), mm(240), mm(60), mm(30)).fill()
26
+ .text((t) =>
27
+ t.setFont(PdfFont.standard('Helvetica-Bold'), 24)
28
+ .moveTo(mm(20), mm(210))
29
+ .show('Hello, print world.'),
30
+ )
31
+ .restore()
32
+
33
+ await Bun.write('out.pdf', await doc.save())
34
+ ```
35
+
36
+ ## Streaming output
37
+
38
+ `save()` buffers and returns a `Uint8Array`. For large documents or HTTP
39
+ responses, stream straight to a Node `Writable` instead — nothing is buffered:
40
+
41
+ ```typescript
42
+ import { createWriteStream } from 'node:fs'
43
+
44
+ await doc.saveToStream(createWriteStream('out.pdf'))
45
+ ```
46
+
47
+ `saveToStream` resolves once the stream has flushed; it rejects on a stream
48
+ error or a build/conformance error, exactly like `save()`.
49
+
50
+ ## What's supported
51
+
52
+ Object model & serialization, pages, the full content-stream operator set,
53
+ device + ICC/Separation/DeviceN/CIE color, FlateDecode/ASCII85/ASCIIHex
54
+ filters, Standard-14 and embedded (subsetted) TrueType + OpenType/CFF fonts
55
+ with ToUnicode, JPEG/PNG images with alpha, transparency (ExtGState) and
56
+ tiling/shading patterns, XMP metadata, and PDF/A-2b / PDF/X-4 conformance
57
+ validation. Output is byte-deterministic with a fixed creation date and id.
58
+
59
+ Browser builds, encryption, signatures, forms, and reading/parsing PDFs are
60
+ out of scope.
61
+
62
+ ## Documentation
63
+
64
+ Full guides live in [`docs/pdf`](../../docs/pdf/pdf.md): the content builder,
65
+ fonts, images, color, transparency/patterns, and conformance.
66
+
67
+ ## Examples
68
+
69
+ Runnable under Bun (`packages/pdf/examples/`):
70
+
71
+ ```bash
72
+ bun packages/pdf/examples/basic_page.ts
73
+ bun packages/pdf/examples/multi_font.ts
74
+ bun packages/pdf/examples/print_ready_pdfx4.ts <font.ttf> <cmyk.icc>
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@strav/pdf",
3
+ "version": "0.4.17",
4
+ "type": "module",
5
+ "description": "Low-level, zero-dependency PDF generation (write side) for the Strav ecosystem",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "bun",
9
+ "pdf",
10
+ "typescript",
11
+ "strav"
12
+ ],
13
+ "files": [
14
+ "src/",
15
+ "README.md",
16
+ "package.json",
17
+ "tsconfig.json"
18
+ ],
19
+ "exports": {
20
+ ".": "./src/index.ts",
21
+ "./objects": "./src/objects/index.ts",
22
+ "./objects/*": "./src/objects/*.ts",
23
+ "./streams": "./src/streams/index.ts",
24
+ "./streams/*": "./src/streams/*.ts",
25
+ "./content": "./src/content/index.ts",
26
+ "./content/*": "./src/content/*.ts",
27
+ "./fonts": "./src/fonts/index.ts",
28
+ "./fonts/*": "./src/fonts/*.ts",
29
+ "./images": "./src/images/index.ts",
30
+ "./images/*": "./src/images/*.ts",
31
+ "./ext-gstate": "./src/ext-gstate/index.ts",
32
+ "./ext-gstate/*": "./src/ext-gstate/*.ts",
33
+ "./patterns": "./src/patterns/index.ts",
34
+ "./patterns/*": "./src/patterns/*.ts",
35
+ "./metadata": "./src/metadata/index.ts",
36
+ "./metadata/*": "./src/metadata/*.ts",
37
+ "./standards": "./src/standards/index.ts",
38
+ "./standards/*": "./src/standards/*.ts",
39
+ "./color": "./src/color/index.ts",
40
+ "./color/*": "./src/color/*.ts",
41
+ "./document": "./src/document/index.ts",
42
+ "./document/*": "./src/document/*.ts"
43
+ },
44
+ "scripts": {
45
+ "test": "bun test tests/",
46
+ "typecheck": "tsc --noEmit"
47
+ },
48
+ "devDependencies": {
49
+ "bun-types": "^1.3.9"
50
+ }
51
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * CIE-based color spaces (spec §9.1): CalGray, CalRGB and Lab. These are
3
+ * device-independent array color spaces with a parameter dictionary; no
4
+ * profile is embedded. D50 is the default white point.
5
+ */
6
+
7
+ import { arr, dict, name, num } from '../objects/types.ts'
8
+ import type { PdfObject } from '../objects/types.ts'
9
+ import type { ManagedColorSpace } from './space.ts'
10
+ import { managedColor } from './space.ts'
11
+ import type { Color } from './color.ts'
12
+
13
+ const D50: [number, number, number] = [0.9505, 1.0, 1.089]
14
+
15
+ class ArrayColorSpace implements ManagedColorSpace {
16
+ constructor(
17
+ readonly id: string,
18
+ readonly components: number,
19
+ private readonly obj: PdfObject
20
+ ) {}
21
+ build(): PdfObject {
22
+ return this.obj
23
+ }
24
+ color(...comps: number[]): Color {
25
+ return managedColor(this, comps)
26
+ }
27
+ }
28
+
29
+ let cieCounter = 0
30
+
31
+ export function calGray(opts: { whitePoint?: [number, number, number]; gamma?: number } = {}) {
32
+ const params = dict({ WhitePoint: arr((opts.whitePoint ?? D50).map(num)) })
33
+ if (opts.gamma !== undefined) params.entries.set('Gamma', num(opts.gamma))
34
+ return new ArrayColorSpace(`calgray:${cieCounter++}`, 1, arr([name('CalGray'), params]))
35
+ }
36
+
37
+ export function calRGB(
38
+ opts: {
39
+ whitePoint?: [number, number, number]
40
+ gamma?: [number, number, number]
41
+ matrix?: number[]
42
+ } = {}
43
+ ) {
44
+ const params = dict({ WhitePoint: arr((opts.whitePoint ?? D50).map(num)) })
45
+ if (opts.gamma) params.entries.set('Gamma', arr(opts.gamma.map(num)))
46
+ if (opts.matrix) params.entries.set('Matrix', arr(opts.matrix.map(num)))
47
+ return new ArrayColorSpace(`calrgb:${cieCounter++}`, 3, arr([name('CalRGB'), params]))
48
+ }
49
+
50
+ /** Lab color space. `range` bounds the a and b axes (default [-100,100,-100,100]). */
51
+ export function lab(
52
+ opts: { whitePoint?: [number, number, number]; range?: number[] } = {}
53
+ ) {
54
+ const params = dict({
55
+ WhitePoint: arr((opts.whitePoint ?? D50).map(num)),
56
+ Range: arr((opts.range ?? [-100, 100, -100, 100]).map(num)),
57
+ })
58
+ return new ArrayColorSpace(`lab:${cieCounter++}`, 3, arr([name('Lab'), params]))
59
+ }
60
+
61
+ export type { ArrayColorSpace }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Color values and factories (spec §9).
3
+ *
4
+ * M1–M3 ship the three device color spaces. CIE-based, ICCBased, Indexed,
5
+ * Separation, DeviceN and Pattern arrive in M9 — the union is designed to grow
6
+ * without breaking existing callers.
7
+ */
8
+
9
+ import { PdfGenError } from '../util/errors.ts'
10
+ import type { ManagedColor } from './space.ts'
11
+
12
+ /** Device color space names. */
13
+ export type ColorSpace = 'DeviceGray' | 'DeviceRGB' | 'DeviceCMYK'
14
+
15
+ export type DeviceColor =
16
+ | { space: 'DeviceGray'; g: number }
17
+ | { space: 'DeviceRGB'; r: number; g: number; b: number }
18
+ | { space: 'DeviceCMYK'; c: number; m: number; y: number; k: number }
19
+
20
+ /** A color in any space — device or managed (ICCBased/Separation/…). */
21
+ export type Color = DeviceColor | ManagedColor
22
+
23
+ /** Components of a device color, in PDF operand order. */
24
+ export function deviceComponents(c: DeviceColor): number[] {
25
+ switch (c.space) {
26
+ case 'DeviceGray':
27
+ return [c.g]
28
+ case 'DeviceRGB':
29
+ return [c.r, c.g, c.b]
30
+ case 'DeviceCMYK':
31
+ return [c.c, c.m, c.y, c.k]
32
+ }
33
+ }
34
+
35
+ /** The device color-space name for a device color. */
36
+ export function deviceSpaceName(c: DeviceColor): ColorSpace {
37
+ return c.space
38
+ }
39
+
40
+ function unit(name: string, v: number): number {
41
+ if (!Number.isFinite(v) || v < 0 || v > 1) {
42
+ throw new PdfGenError(
43
+ 'PDF_INVALID_COLOR',
44
+ `${name} component must be in [0, 1], got ${v}`
45
+ )
46
+ }
47
+ return v
48
+ }
49
+
50
+ /** DeviceGray. `g` in [0,1] (0 = black, 1 = white). */
51
+ export function gray(g: number): Color {
52
+ return { space: 'DeviceGray', g: unit('Gray', g) }
53
+ }
54
+
55
+ /** DeviceRGB. Components in [0,1]. */
56
+ export function rgb(r: number, g: number, b: number): Color {
57
+ return {
58
+ space: 'DeviceRGB',
59
+ r: unit('R', r),
60
+ g: unit('G', g),
61
+ b: unit('B', b),
62
+ }
63
+ }
64
+
65
+ /** DeviceCMYK. Components in [0,1]. */
66
+ export function cmyk(c: number, m: number, y: number, k: number): Color {
67
+ return {
68
+ space: 'DeviceCMYK',
69
+ c: unit('C', c),
70
+ m: unit('M', m),
71
+ y: unit('Y', y),
72
+ k: unit('K', k),
73
+ }
74
+ }
75
+
76
+ /** Namespace form of the factories (spec §16 exports `Color` as a value). */
77
+ export const Color = { gray, rgb, cmyk } as const
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Naïve device-space color conversions (spec §9.4).
3
+ *
4
+ * ⚠️ Preview only — NOT color-accurate. Callers needing accurate conversion
5
+ * must use a real CMM externally and pass pre-converted values.
6
+ */
7
+
8
+ import type { Color } from './color.ts'
9
+ import { gray, rgb, cmyk } from './color.ts'
10
+
11
+ /** Rec.601 luma. */
12
+ export function rgbToGray(r: number, g: number, b: number): Color {
13
+ return gray(0.299 * r + 0.587 * g + 0.114 * b)
14
+ }
15
+
16
+ /** Simple CMYK → RGB. */
17
+ export function cmykToRgb(c: number, m: number, y: number, k: number): Color {
18
+ return rgb((1 - c) * (1 - k), (1 - m) * (1 - k), (1 - y) * (1 - k))
19
+ }
20
+
21
+ /** Simple RGB → CMYK with black generation. */
22
+ export function rgbToCmyk(r: number, g: number, b: number): Color {
23
+ const k = 1 - Math.max(r, g, b)
24
+ if (k >= 1) return cmyk(0, 0, 0, 1)
25
+ return cmyk((1 - r - k) / (1 - k), (1 - g - k) / (1 - k), (1 - b - k) / (1 - k), k)
26
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Device color-space operator emission (spec §9.1). Maps a {@link Color} to
3
+ * the shortcut color operators (`g`/`rg`/`k` for fill, `G`/`RG`/`K` for
4
+ * stroke). Numbers go through the shared serializer for determinism (§8.5).
5
+ */
6
+
7
+ import { formatNumber } from '../objects/number.ts'
8
+ import { OP } from '../content/operators.ts'
9
+ import type { DeviceColor } from './color.ts'
10
+
11
+ function n(v: number): string {
12
+ return formatNumber(v)
13
+ }
14
+
15
+ /** Operator line that sets the non-stroking (fill) color. */
16
+ export function fillColorOp(c: DeviceColor): string {
17
+ switch (c.space) {
18
+ case 'DeviceGray':
19
+ return `${n(c.g)} ${OP.fillGray}`
20
+ case 'DeviceRGB':
21
+ return `${n(c.r)} ${n(c.g)} ${n(c.b)} ${OP.fillRGB}`
22
+ case 'DeviceCMYK':
23
+ return `${n(c.c)} ${n(c.m)} ${n(c.y)} ${n(c.k)} ${OP.fillCMYK}`
24
+ }
25
+ }
26
+
27
+ /** Operator line that sets the stroking color. */
28
+ export function strokeColorOp(c: DeviceColor): string {
29
+ switch (c.space) {
30
+ case 'DeviceGray':
31
+ return `${n(c.g)} ${OP.strokeGray}`
32
+ case 'DeviceRGB':
33
+ return `${n(c.r)} ${n(c.g)} ${n(c.b)} ${OP.strokeRGB}`
34
+ case 'DeviceCMYK':
35
+ return `${n(c.c)} ${n(c.m)} ${n(c.y)} ${n(c.k)} ${OP.strokeCMYK}`
36
+ }
37
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * DeviceN color space (spec §9.5) — N named colorants mapped to an alternate
3
+ * device space by a tint transform. The transform is an N→M Type 4 PostScript
4
+ * function supplied by the caller (multi-input, so Type 2 doesn't apply).
5
+ */
6
+
7
+ import { arr, name, num } from '../objects/types.ts'
8
+ import type { PdfObject } from '../objects/types.ts'
9
+ import type { ObjectTable } from '../document/object_table.ts'
10
+ import type { ManagedColorSpace } from './space.ts'
11
+ import { managedColor } from './space.ts'
12
+ import type { Color } from './color.ts'
13
+ import type { ColorSpace } from './color.ts'
14
+ import { type4Function } from './separation.ts'
15
+
16
+ class DeviceNColorSpace implements ManagedColorSpace {
17
+ readonly id: string
18
+ readonly components: number
19
+
20
+ constructor(
21
+ private readonly names: string[],
22
+ private readonly alternate: ColorSpace,
23
+ private readonly fn: (t: ObjectTable) => PdfObject,
24
+ tag: number
25
+ ) {
26
+ this.components = names.length
27
+ this.id = `devn:${names.join(',')}:${tag}`
28
+ }
29
+
30
+ build(table: ObjectTable): PdfObject {
31
+ return arr([
32
+ name('DeviceN'),
33
+ arr(this.names.map(name)),
34
+ name(this.alternate),
35
+ this.fn(table),
36
+ ])
37
+ }
38
+
39
+ /** A color with one tint per colorant (each in [0,1]). */
40
+ color(...tints: number[]): Color {
41
+ return managedColor(this, tints)
42
+ }
43
+ }
44
+
45
+ let devnCounter = 0
46
+
47
+ /**
48
+ * A DeviceN color space. `postscript` is a Type 4 calculator program mapping
49
+ * the N colorant tints to the alternate space's components.
50
+ *
51
+ * ```ts
52
+ * const duotone = deviceN(['Black', 'PANTONE 877 C'], 'DeviceCMYK',
53
+ * '{ exch dup 0 0 4 1 roll }') // illustrative
54
+ * ```
55
+ */
56
+ export function deviceN(
57
+ names: string[],
58
+ alternate: ColorSpace,
59
+ postscript: string
60
+ ): DeviceNColorSpace {
61
+ const altComps = alternate === 'DeviceGray' ? 1 : alternate === 'DeviceRGB' ? 3 : 4
62
+ const domain: number[] = []
63
+ for (let i = 0; i < names.length; i++) domain.push(0, 1)
64
+ const range: number[] = []
65
+ for (let i = 0; i < altComps; i++) range.push(0, 1)
66
+ return new DeviceNColorSpace(
67
+ names,
68
+ alternate,
69
+ type4Function(domain, range, postscript),
70
+ devnCounter++
71
+ )
72
+ }
73
+
74
+ export type { DeviceNColorSpace }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * ICC profile reading + embedding (spec §9.2).
3
+ *
4
+ * We do **no** color transforms. The profile is an opaque blob embedded for
5
+ * the consumer (RIP, Acrobat). We parse only the 128-byte header to learn the
6
+ * data color space (→ component count) and validate the `acsp` signature.
7
+ */
8
+
9
+ import { PdfGenError } from '../util/errors.ts'
10
+ import { arr, name, num } from '../objects/types.ts'
11
+ import type { PdfObject } from '../objects/types.ts'
12
+ import type { ObjectTable } from '../document/object_table.ts'
13
+ import { makeStream } from '../streams/stream.ts'
14
+ import type { ManagedColorSpace } from './space.ts'
15
+ import { managedColor } from './space.ts'
16
+ import type { Color } from './color.ts'
17
+
18
+ export type IccColorSpace = 'GRAY' | 'RGB' | 'CMYK' | 'Lab'
19
+
20
+ export interface IccProfile {
21
+ /** Data color space (from header offset 16). */
22
+ colorSpace: IccColorSpace
23
+ /** Component count: GRAY 1, RGB/Lab 3, CMYK 4. */
24
+ components: 1 | 3 | 4
25
+ /** Profile/device class fourcc (offset 12), e.g. `prtr`, `mntr`. */
26
+ profileClass: string
27
+ /** Profile connection space (offset 20), e.g. `XYZ ` or `Lab `. */
28
+ pcs: string
29
+ bytes: Uint8Array
30
+ }
31
+
32
+ function fourcc(b: Uint8Array, o: number): string {
33
+ return String.fromCharCode(b[o]!, b[o + 1]!, b[o + 2]!, b[o + 3]!)
34
+ }
35
+
36
+ export function parseIccProfile(bytes: Uint8Array): IccProfile {
37
+ if (bytes.length < 128) {
38
+ throw new PdfGenError('PDF_INVALID_COLOR', 'ICC profile too short (need ≥128-byte header)')
39
+ }
40
+ if (fourcc(bytes, 36) !== 'acsp') {
41
+ throw new PdfGenError('PDF_INVALID_COLOR', "Not an ICC profile (missing 'acsp' signature)")
42
+ }
43
+ const sig = fourcc(bytes, 16)
44
+ const map: Record<string, { cs: IccColorSpace; n: 1 | 3 | 4 }> = {
45
+ 'GRAY': { cs: 'GRAY', n: 1 },
46
+ 'RGB ': { cs: 'RGB', n: 3 },
47
+ 'CMYK': { cs: 'CMYK', n: 4 },
48
+ 'Lab ': { cs: 'Lab', n: 3 },
49
+ }
50
+ const m = map[sig]
51
+ if (!m) {
52
+ throw new PdfGenError('PDF_INVALID_COLOR', `Unsupported ICC data color space '${sig}'`)
53
+ }
54
+ return {
55
+ colorSpace: m.cs,
56
+ components: m.n,
57
+ profileClass: fourcc(bytes, 12),
58
+ pcs: fourcc(bytes, 20),
59
+ bytes,
60
+ }
61
+ }
62
+
63
+ function altName(components: 1 | 3 | 4): string {
64
+ return components === 1 ? 'DeviceGray' : components === 3 ? 'DeviceRGB' : 'DeviceCMYK'
65
+ }
66
+
67
+ class IccBasedColorSpace implements ManagedColorSpace {
68
+ readonly id: string
69
+ readonly components: number
70
+
71
+ constructor(
72
+ private readonly profile: IccProfile,
73
+ private readonly tag: number
74
+ ) {
75
+ this.components = profile.components
76
+ this.id = `icc:${profile.colorSpace}:${tag}`
77
+ }
78
+
79
+ build(table: ObjectTable): PdfObject {
80
+ const stream = makeStream(this.profile.bytes, {
81
+ filter: 'FlateDecode',
82
+ extra: {
83
+ N: num(this.profile.components),
84
+ Alternate: name(altName(this.profile.components)),
85
+ },
86
+ })
87
+ return arr([name('ICCBased'), table.add(stream)])
88
+ }
89
+
90
+ /** A color in this profile's space (components in [0,1]). */
91
+ color(...comps: number[]): Color {
92
+ return managedColor(this, comps)
93
+ }
94
+ }
95
+
96
+ let iccCounter = 0
97
+
98
+ /** An ICCBased color space from raw ICC profile bytes (`.icc`/`.icm`). */
99
+ export function iccBased(profileBytes: Uint8Array): IccBasedColorSpace {
100
+ return new IccBasedColorSpace(parseIccProfile(profileBytes), iccCounter++)
101
+ }
102
+
103
+ export type { IccBasedColorSpace }
@@ -0,0 +1,15 @@
1
+ export type { ColorSpace, Color as ColorValue, DeviceColor } from './color.ts'
2
+ // A named re-export carries both the type and value meanings of `Color`.
3
+ export { Color, gray, rgb, cmyk, deviceComponents, deviceSpaceName } from './color.ts'
4
+ export { rgbToGray, cmykToRgb, rgbToCmyk } from './conversion.ts'
5
+ export { fillColorOp, strokeColorOp } from './device.ts'
6
+
7
+ export type { ManagedColorSpace, ManagedColor } from './space.ts'
8
+ export { managedColor } from './space.ts'
9
+ export { parseIccProfile, iccBased } from './icc.ts'
10
+ export type { IccProfile, IccColorSpace, IccBasedColorSpace } from './icc.ts'
11
+ export { separation, type4Function } from './separation.ts'
12
+ export type { SeparationColorSpace } from './separation.ts'
13
+ export { deviceN } from './devicen.ts'
14
+ export type { DeviceNColorSpace } from './devicen.ts'
15
+ export { calGray, calRGB, lab } from './cie.ts'
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Separation color space (spec §9.5) — a single spot colorant with an
3
+ * alternate space and a tint transform.
4
+ *
5
+ * The high-level helper emits a Type 2 (exponential) function interpolating
6
+ * between the alternate's zero color at tint 0 and the supplied full-strength
7
+ * color at tint 1 (`N = 1`, linear). Type 4 PostScript tint transforms are
8
+ * available via {@link deviceN} for advanced callers.
9
+ */
10
+
11
+ import { arr, dict, name, num } from '../objects/types.ts'
12
+ import type { PdfObject } from '../objects/types.ts'
13
+ import { ascii } from '../util/ascii.ts'
14
+ import type { ObjectTable } from '../document/object_table.ts'
15
+ import type { ManagedColorSpace } from './space.ts'
16
+ import { managedColor } from './space.ts'
17
+ import type { Color, DeviceColor } from './color.ts'
18
+ import { deviceComponents, deviceSpaceName } from './color.ts'
19
+
20
+ class SeparationColorSpace implements ManagedColorSpace {
21
+ readonly id: string
22
+ readonly components = 1
23
+ private readonly altName: string
24
+ private readonly c1: number[]
25
+
26
+ constructor(
27
+ private readonly colorant: string,
28
+ full: DeviceColor,
29
+ private readonly tag: number
30
+ ) {
31
+ this.altName = deviceSpaceName(full)
32
+ this.c1 = deviceComponents(full)
33
+ this.id = `sep:${colorant}:${tag}`
34
+ }
35
+
36
+ build(table: ObjectTable): PdfObject {
37
+ const c0 = this.c1.map(() => 0)
38
+ const tintFn = table.add(
39
+ dict({
40
+ FunctionType: num(2),
41
+ Domain: arr([num(0), num(1)]),
42
+ C0: arr(c0.map(num)),
43
+ C1: arr(this.c1.map(num)),
44
+ N: num(1),
45
+ })
46
+ )
47
+ return arr([
48
+ name('Separation'),
49
+ name(this.colorant),
50
+ name(this.altName),
51
+ tintFn,
52
+ ])
53
+ }
54
+
55
+ /** A color at tint `t` in [0,1] (0 = no ink, 1 = full strength). */
56
+ tint(t: number): Color {
57
+ return managedColor(this, [t])
58
+ }
59
+ }
60
+
61
+ let sepCounter = 0
62
+
63
+ /**
64
+ * A Separation (spot) color space. `full` is the colour the colorant prints
65
+ * at 100% tint, given in a device space (usually CMYK).
66
+ *
67
+ * ```ts
68
+ * const pantone = separation('PANTONE 185 C', cmyk(0, 0.91, 0.76, 0))
69
+ * c.setFillColor(pantone.tint(0.6))
70
+ * ```
71
+ */
72
+ export function separation(colorant: string, full: DeviceColor): SeparationColorSpace {
73
+ return new SeparationColorSpace(colorant, full, sepCounter++)
74
+ }
75
+
76
+ export type { SeparationColorSpace }
77
+
78
+ /** A PostScript-calculator (Type 4) function stream, for DeviceN/advanced use. */
79
+ export function type4Function(
80
+ domain: number[],
81
+ range: number[],
82
+ postscript: string
83
+ ): (table: ObjectTable) => PdfObject {
84
+ return table =>
85
+ table.add({
86
+ kind: 'stream',
87
+ dict: dict({
88
+ FunctionType: num(4),
89
+ Domain: arr(domain.map(num)),
90
+ Range: arr(range.map(num)),
91
+ }),
92
+ data: ascii(postscript),
93
+ })
94
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Managed (non-device) color spaces (spec §9.1).
3
+ *
4
+ * DeviceGray/RGB/CMYK use the shortcut operators and need no resources. The
5
+ * remaining spaces — ICCBased, Separation, DeviceN, CalGray/CalRGB/Lab — are
6
+ * objects (a name or array, sometimes with indirect children) registered in
7
+ * the page `/Resources /ColorSpace` map and selected with `cs`/`CS` + `scn`.
8
+ *
9
+ * Each implementation builds its PDF object via `build(table)` and produces
10
+ * tinted {@link Color} values via `color(...)`.
11
+ */
12
+
13
+ import { PdfGenError } from '../util/errors.ts'
14
+ import type { ObjectTable } from '../document/object_table.ts'
15
+ import type { PdfObject } from '../objects/types.ts'
16
+
17
+ export interface ManagedColorSpace {
18
+ /** Stable identity for resource deduplication. */
19
+ readonly id: string
20
+ /** Number of colour components (operands for `scn`/`SCN`). */
21
+ readonly components: number
22
+ /** Build the color-space object, adding any child objects to the table. */
23
+ build(table: ObjectTable): PdfObject
24
+ }
25
+
26
+ /** A color value in a managed color space (selected via `cs`/`CS`). */
27
+ export type ManagedColor = {
28
+ space: 'Managed'
29
+ cs: ManagedColorSpace
30
+ comps: number[]
31
+ }
32
+
33
+ /** Validate + build a {@link ManagedColor} for a managed space. */
34
+ export function managedColor(cs: ManagedColorSpace, comps: number[]): ManagedColor {
35
+ if (comps.length !== cs.components) {
36
+ throw new PdfGenError(
37
+ 'PDF_INVALID_COLOR',
38
+ `${cs.id} expects ${cs.components} component(s), got ${comps.length}`
39
+ )
40
+ }
41
+ for (const v of comps) {
42
+ if (!Number.isFinite(v)) {
43
+ throw new PdfGenError('PDF_INVALID_COLOR', `Non-finite color component in ${cs.id}`)
44
+ }
45
+ }
46
+ return { space: 'Managed', cs, comps }
47
+ }