forked from Hopding/pdf-lib
-
Notifications
You must be signed in to change notification settings - Fork 3
/
JpegEmbedder.ts
125 lines (105 loc) · 3.24 KB
/
JpegEmbedder.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { PDFRef } from 'src/core/objects/PDFRef';
import type { PDFContext } from 'src/core/PDFContext';
// prettier-ignore
const MARKERS = [
0xffc0, 0xffc1, 0xffc2,
0xffc3, 0xffc5, 0xffc6,
0xffc7, 0xffc8, 0xffc9,
0xffca, 0xffcb, 0xffcc,
0xffcd, 0xffce, 0xffcf,
];
enum ColorSpace {
DeviceGray = 'DeviceGray',
DeviceRGB = 'DeviceRGB',
DeviceCMYK = 'DeviceCMYK',
}
const ChannelToColorSpace: { [idx: number]: ColorSpace | undefined } = {
1: ColorSpace.DeviceGray,
3: ColorSpace.DeviceRGB,
4: ColorSpace.DeviceCMYK,
};
/**
* A note of thanks to the developers of https://github.com/foliojs/pdfkit, as
* this class borrows from:
* https://github.com/foliojs/pdfkit/blob/a6af76467ce06bd6a2af4aa7271ccac9ff152a7d/lib/image/jpeg.js
*/
export class JpegEmbedder {
static async for(imageData: Uint8Array) {
const dataView = new DataView(imageData.buffer);
const soi = dataView.getUint16(0);
if (soi !== 0xffd8) throw new Error('SOI not found in JPEG');
let pos = 2;
let marker: number;
while (pos < dataView.byteLength) {
marker = dataView.getUint16(pos);
pos += 2;
if (MARKERS.includes(marker)) break;
pos += dataView.getUint16(pos);
}
if (!MARKERS.includes(marker!)) throw new Error('Invalid JPEG');
pos += 2;
const bitsPerComponent = dataView.getUint8(pos++);
const height = dataView.getUint16(pos);
pos += 2;
const width = dataView.getUint16(pos);
pos += 2;
const channelByte = dataView.getUint8(pos++);
const channelName = ChannelToColorSpace[channelByte];
if (!channelName) throw new Error('Unknown JPEG channel.');
const colorSpace = channelName;
return new JpegEmbedder(
imageData,
bitsPerComponent,
width,
height,
colorSpace,
);
}
readonly bitsPerComponent: number;
readonly height: number;
readonly width: number;
readonly colorSpace: ColorSpace;
private readonly imageData: Uint8Array;
private constructor(
imageData: Uint8Array,
bitsPerComponent: number,
width: number,
height: number,
colorSpace: ColorSpace,
) {
this.imageData = imageData;
this.bitsPerComponent = bitsPerComponent;
this.width = width;
this.height = height;
this.colorSpace = colorSpace;
}
async embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
const xObject = context.stream(this.imageData, {
Type: 'XObject',
Subtype: 'Image',
BitsPerComponent: this.bitsPerComponent,
Width: this.width,
Height: this.height,
ColorSpace: this.colorSpace,
Filter: 'DCTDecode',
// CMYK JPEG streams in PDF are typically stored complemented,
// with 1 as 'off' and 0 as 'on' (PDF 32000-1:2008, 8.6.4.4).
//
// Standalone CMYK JPEG (usually exported by Photoshop) are
// stored inverse, with 0 as 'off' and 1 as 'on', like RGB.
//
// Applying a swap here as a hedge that most bytes passing
// through this method will benefit from it.
Decode:
this.colorSpace === ColorSpace.DeviceCMYK
? [1, 0, 1, 0, 1, 0, 1, 0]
: undefined,
});
if (ref) {
context.assign(ref, xObject);
return ref;
} else {
return context.register(xObject);
}
}
}