@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
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
|
+
}
|
package/src/color/cie.ts
ADDED
|
@@ -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 }
|
package/src/color/icc.ts
ADDED
|
@@ -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
|
+
}
|