@ultimat3/core 1.0.0
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/LICENSE +21 -0
- package/README.md +171 -0
- package/package.json +33 -0
- package/src/actor.ts +81 -0
- package/src/assert.ts +44 -0
- package/src/clock.ts +51 -0
- package/src/config.ts +265 -0
- package/src/context.ts +210 -0
- package/src/cursor.ts +116 -0
- package/src/env.ts +259 -0
- package/src/error-codes.ts +131 -0
- package/src/errors.ts +159 -0
- package/src/ids.ts +132 -0
- package/src/image/color.ts +34 -0
- package/src/image/errors.ts +58 -0
- package/src/image/fixtures.ts +263 -0
- package/src/image/jpeg-decode.ts +283 -0
- package/src/image/jpeg-encode.ts +463 -0
- package/src/image/jpeg-headers.ts +267 -0
- package/src/image/jpeg-huffman.ts +202 -0
- package/src/image/jpeg-tables.ts +117 -0
- package/src/image/pipeline.ts +117 -0
- package/src/image/png-bytes.ts +91 -0
- package/src/image/png.ts +433 -0
- package/src/image/probe-svg.ts +85 -0
- package/src/image/probe.ts +302 -0
- package/src/image/raster.ts +71 -0
- package/src/image/resize.ts +320 -0
- package/src/index.ts +256 -0
- package/src/lifecycle.ts +242 -0
- package/src/listeners.ts +80 -0
- package/src/logger.ts +146 -0
- package/src/registrar.ts +94 -0
- package/src/result.ts +78 -0
- package/src/roles.ts +66 -0
- package/src/service.ts +61 -0
- package/src/telemetry.ts +290 -0
- package/src/version.ts +37 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Single responsibility: byte-exact image fixtures produced by an INDEPENDENT encoder
|
|
2
|
+
// (Pillow 10.2 and ffmpeg), plus the pixels they must decode to. A codec that only
|
|
3
|
+
// round-trips against itself proves nothing — these bytes are the outside reference, so a
|
|
4
|
+
// decoder bug shows up as a pixel mismatch rather than a mutually agreed hallucination.
|
|
5
|
+
|
|
6
|
+
export interface ImageFixture {
|
|
7
|
+
/** The encoded file, base64. */
|
|
8
|
+
readonly base64: string;
|
|
9
|
+
readonly width: number;
|
|
10
|
+
readonly height: number;
|
|
11
|
+
/** Expected RGBA decode, row-major. Omitted for probe-only and lossy fixtures. */
|
|
12
|
+
readonly pixels?: readonly number[] | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const fixtureBytes = (fixture: ImageFixture): Uint8Array =>
|
|
16
|
+
Uint8Array.from(atob(fixture.base64), (c) => c.charCodeAt(0));
|
|
17
|
+
|
|
18
|
+
/** Truecolour + alpha (PNG colour type 6), including fully transparent and premature zeros. */
|
|
19
|
+
export const PNG_RGBA_4X4: ImageFixture = {
|
|
20
|
+
base64:
|
|
21
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAQUlEQVR4nAXBKQGAABAAweU5Po1GE4I4hEBeCpIQ' +
|
|
22
|
+
'gjjoM8sMgIJIgqC6rNv+3qdN2/VkpoqookREVFUd1/PN0zj84AIej7U060EAAAAASUVORK5CYII=',
|
|
23
|
+
width: 4,
|
|
24
|
+
height: 4,
|
|
25
|
+
pixels: [
|
|
26
|
+
0, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 255, 255, 255, 255, 10, 20, 30, 40,
|
|
27
|
+
200, 150, 100, 255, 1, 2, 3, 4, 128, 128, 128, 255, 255, 255, 0, 255, 0, 255, 255, 255, 255, 0,
|
|
28
|
+
255, 255, 5, 5, 5, 5, 250, 250, 250, 250, 60, 120, 180, 240, 9, 8, 7, 6,
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Truecolour, no alpha (colour type 2) — alpha must be synthesised as 255. */
|
|
33
|
+
export const PNG_RGB_3X2: ImageFixture = {
|
|
34
|
+
base64:
|
|
35
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAIAAAASFvFNAAAAF0lEQVR4nGP4z8DAAMFcInInUowYGBgANh8EmGKC' +
|
|
36
|
+
'N2kAAAAASUVORK5CYII=',
|
|
37
|
+
width: 3,
|
|
38
|
+
height: 2,
|
|
39
|
+
pixels: [
|
|
40
|
+
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 10, 20, 30, 255, 200, 100, 50, 255, 0, 0, 0,
|
|
41
|
+
255,
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** Greyscale (colour type 0) — one channel replicated across RGB. */
|
|
46
|
+
export const PNG_GRAY_2X2: ImageFixture = {
|
|
47
|
+
base64:
|
|
48
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAAAAABX3VL4AAAADklEQVR4nGNgCGVY9R8AA60B/2f7ygkAAAAASUVO' +
|
|
49
|
+
'RK5CYII=',
|
|
50
|
+
width: 2,
|
|
51
|
+
height: 2,
|
|
52
|
+
pixels: [0, 0, 0, 255, 85, 85, 85, 255, 170, 170, 170, 255, 255, 255, 255, 255],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Indexed colour (type 3) with a `tRNS` alpha table — the entry PNG readers forget. */
|
|
56
|
+
export const PNG_PALETTE_4X1: ImageFixture = {
|
|
57
|
+
base64:
|
|
58
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAQAAAABCAMAAADO4v//AAADAFBMVEX/AAAA/wAAAP///wAAAAAAAAAAAAAAAAAA' +
|
|
59
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
60
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
61
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
62
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
63
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
64
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
65
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
66
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
67
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
68
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
69
|
+
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' +
|
|
70
|
+
'AAAAAAAAAAAAAAAAAAAAAAAX3fWLAAAABHRSTlP/gAD/oaGUZgAAAA1JREFUeJxjYGBkYgYAAA8AB4SOmW0AAAAA' +
|
|
71
|
+
'SUVORK5CYII=',
|
|
72
|
+
width: 4,
|
|
73
|
+
height: 1,
|
|
74
|
+
pixels: [255, 0, 0, 255, 0, 255, 0, 128, 0, 0, 255, 0, 255, 255, 0, 255],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Greyscale + alpha (colour type 4). */
|
|
78
|
+
export const PNG_GRAY_ALPHA_2X2: ImageFixture = {
|
|
79
|
+
base64:
|
|
80
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAQAAADYv8WvAAAAEklEQVR4nGNk+N/QyHCC4b8DABPwBAkrfI8bAAAA' +
|
|
81
|
+
'AElFTkSuQmCC',
|
|
82
|
+
width: 2,
|
|
83
|
+
height: 2,
|
|
84
|
+
pixels: [0, 0, 0, 255, 128, 128, 128, 128, 200, 200, 200, 0, 255, 255, 255, 64],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** 16 bits per channel — must be reduced to 8 by taking the high byte. */
|
|
88
|
+
export const PNG_GRAY16_2X2: ImageFixture = {
|
|
89
|
+
base64:
|
|
90
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAIAAAACEAAAAAAHTY67AAAAEklEQVR4nGNgYHBgYGhg+P8fAAbHAr89RY6RAAAA' +
|
|
91
|
+
'AElFTkSuQmCC',
|
|
92
|
+
width: 2,
|
|
93
|
+
height: 2,
|
|
94
|
+
pixels: [0, 0, 0, 255, 64, 64, 64, 255, 128, 128, 128, 255, 255, 255, 255, 255],
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Big enough that Pillow picks a different row filter per scanline — all five must work. */
|
|
98
|
+
export const PNG_GRADIENT_32X24: ImageFixture = {
|
|
99
|
+
base64:
|
|
100
|
+
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAYCAIAAAAUMWhjAAAAuklEQVR4nLXSyw6DIBCF4WNrr/aiaNHamL7/W3aB' +
|
|
101
|
+
'GFDEgQ7JtxjGxR/UDMAppRwFgCydKbBLxAzsU5gFcnbLwIGXM3BktBZg+1E9gTMLf+Dyv83ANdagBkqgCNHPNsTA' +
|
|
102
|
+
'jaBz7umB+zrpeRoUeCw0rqUlNPA01PbRLSJQAiUg9LAhLlDRRQQEUAGC5hsaqPUNBFB7DWoICjTGK1KBxuVjHqO/' +
|
|
103
|
+
'wRR42frZhh6Q6wGpvY15xHIDCbRAB7RLPwPnHDcTUr+XAAAAAElFTkSuQmCC',
|
|
104
|
+
width: 32,
|
|
105
|
+
height: 24,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** The exact source of `PNG_GRADIENT_32X24` and the JPEG fixtures, as a formula. */
|
|
109
|
+
export const gradientPixel = (x: number, y: number): readonly [number, number, number] => [
|
|
110
|
+
(x * 7) % 256,
|
|
111
|
+
(y * 11) % 256,
|
|
112
|
+
(x * y) % 256,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A genuine Adam7 interlaced PNG (written by ImageMagick — Pillow's writer silently ignores
|
|
117
|
+
* `interlace=True`, which is why this one is not generated with the others). The pipeline
|
|
118
|
+
* refuses it with a coded error rather than garbling it.
|
|
119
|
+
*/
|
|
120
|
+
export const PNG_INTERLACED_8X8: ImageFixture = {
|
|
121
|
+
base64:
|
|
122
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAAE8ahlKAAAAS0lEQVQI12NgYGBgqGFQYWQoUalhUGGwYxDaxWDG' +
|
|
123
|
+
'wlCiwsCgwshgJWTHAEUQMSEIYpRn4ESSsRKCy+DmMDLIcsozYEEQVZyYiHQJAE1EC4Be7arJAAAAAElFTkSuQmCC',
|
|
124
|
+
width: 8,
|
|
125
|
+
height: 8,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Baseline JPEG, 4:4:4, quality 95 — the closest a lossy codec gets to exact. */
|
|
129
|
+
export const JPEG_444_16X16: ImageFixture = {
|
|
130
|
+
base64:
|
|
131
|
+
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJ' +
|
|
132
|
+
'BwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoK' +
|
|
133
|
+
'CgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wAARCAAQABADAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAA' +
|
|
134
|
+
'AAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAk' +
|
|
135
|
+
'M2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKT' +
|
|
136
|
+
'lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QA' +
|
|
137
|
+
'HwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdh' +
|
|
138
|
+
'cRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp' +
|
|
139
|
+
'anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk' +
|
|
140
|
+
'5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD83/gx+yH/AKr/AIlfp/BRg8Z5h4eeIfwe+fW3wX/ZDz5X/Er9' +
|
|
141
|
+
'P4K+qweM21P7r8PPEP4PfPpT4L/sh48r/iV+n8FfleDxm2p/hT4eeIfwe+fW3wY/ZD/1X/Er9P4K+qweM8z+6/Dz' +
|
|
142
|
+
'xD+D3z//2Q==',
|
|
143
|
+
width: 16,
|
|
144
|
+
height: 16,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/** Baseline JPEG, 4:2:0 — chroma is half resolution and must be upsampled. */
|
|
148
|
+
export const JPEG_420_16X16: ImageFixture = {
|
|
149
|
+
base64:
|
|
150
|
+
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4R' +
|
|
151
|
+
'DgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' +
|
|
152
|
+
'FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wAARCAAQABADASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAA' +
|
|
153
|
+
'AAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAk' +
|
|
154
|
+
'M2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKT' +
|
|
155
|
+
'lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QA' +
|
|
156
|
+
'HwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdh' +
|
|
157
|
+
'cRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp' +
|
|
158
|
+
'anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk' +
|
|
159
|
+
'5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD4/wDBfwh/1f7j9K958F/CH/V/uP0r2PwX8If9X+4/SvefBfwh' +
|
|
160
|
+
'/wBX+4/SjB4zbUPDzxD+D3z/2Q==',
|
|
161
|
+
width: 16,
|
|
162
|
+
height: 16,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** The exact source of the 16x16 JPEG fixtures, as a formula. */
|
|
166
|
+
export const jpegPixel = (x: number, y: number): readonly [number, number, number] => [
|
|
167
|
+
(x * 16) % 256,
|
|
168
|
+
(y * 16) % 256,
|
|
169
|
+
((x + y) * 8) % 256,
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
/** Odd dimensions with 4:2:0: the last MCU column and row are padded and must be cropped. */
|
|
173
|
+
export const JPEG_420_ODD_33X17: ImageFixture = {
|
|
174
|
+
base64:
|
|
175
|
+
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAQDAwMDAgQDAwMEBAQFBgoGBgUFBgwICQcKDgwPDg4MDQ0PERYTDxAV' +
|
|
176
|
+
'EQ0NExoTFRcYGRkZDxIbHRsYHRYYGRj/2wBDAQQEBAYFBgsGBgsYEA0QGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgY' +
|
|
177
|
+
'GBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGBj/wAARCAARACEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAA' +
|
|
178
|
+
'AAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAk' +
|
|
179
|
+
'M2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKT' +
|
|
180
|
+
'lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QA' +
|
|
181
|
+
'HwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdh' +
|
|
182
|
+
'cRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hp' +
|
|
183
|
+
'anN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk' +
|
|
184
|
+
'5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD5Q0fwT5u39z+ld1pvw/iSISTIqL6nvXp/h3weiW/nSRfKoyeO' +
|
|
185
|
+
'tdhpvg/di6uot8jcJGBjPsPQf561rgMVg4YSnWrQU5zV9X7sY931d3oktZO6Tvv4XDfG0nJJTPLNO8D2h4hs5JTx' +
|
|
186
|
+
'g7dqn+v6VR+JHgq2Hgu0AtIUYX6AgkyEfu5OwGRX0dZeEGLhZ4ix2gmJDtRQOef59/5VQ+InhLb4LtDs+UXyZ8v5' +
|
|
187
|
+
'F/1cnAPU9OlZ0M4w6xkI8kY66+7HTfpZyVutpuS+0lu/1vNuNH/q7ivf+x380fHP/CERf8+8H/fl6K96/wCEIi/5' +
|
|
188
|
+
'94P+/L0V9J/bf/Til/4Gfz7/AK6f32eh6N/yA3/4D/6EK7LTv+Qlb/8AXIfzNFFfmMP4OE/wUv8A0uofC8M7/f8A' +
|
|
189
|
+
'kjpNG/5Ab/8AAf8A0IVB8RP+QBp3/XxH/wCgy0UV8/k/8bAf4aX/AKXUP2DOf+Sexv8Ahf6HjNFFFfmh+Hn/2Q==',
|
|
190
|
+
width: 33,
|
|
191
|
+
height: 17,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** The exact source of `JPEG_420_ODD_33X17`, as a formula. */
|
|
195
|
+
export const oddJpegPixel = (x: number, y: number): readonly [number, number, number] => [
|
|
196
|
+
(x * 9) % 256,
|
|
197
|
+
(y * 13) % 256,
|
|
198
|
+
(x * y * 3) % 256,
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
/** Single-component (greyscale) JPEG. */
|
|
202
|
+
export const JPEG_GRAY_16X16: ImageFixture = {
|
|
203
|
+
base64:
|
|
204
|
+
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsO' +
|
|
205
|
+
'CwkJDRENDg8QEBEQCgwSExIQEw8QEBD/wAALCAAQABABAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcI' +
|
|
206
|
+
'CQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcY' +
|
|
207
|
+
'GRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKj' +
|
|
208
|
+
'pKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/APzK' +
|
|
209
|
+
'8P8Ah/8AtXb8mc+1el+H/gt/au3/AETOf9mj4LeH/wC1fI+TOcdq+9fgt8Fv7V8j/RM5x/DX/9k=',
|
|
210
|
+
width: 16,
|
|
211
|
+
height: 16,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Progressive JPEG — refused with a coded error, never decoded as noise. */
|
|
215
|
+
export const JPEG_PROGRESSIVE_16X16: ImageFixture = {
|
|
216
|
+
base64:
|
|
217
|
+
'/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4R' +
|
|
218
|
+
'DgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' +
|
|
219
|
+
'FBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAA' +
|
|
220
|
+
'Bgf/xAAUAQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAGPvGTw/8QAFhAAAwAAAAAAAAAAAAAAAAAAAAQF' +
|
|
221
|
+
'/9oACAEBAAEFAkpAlIEpAlIP/8QAFxEBAAMAAAAAAAAAAAAAAAAABgAhMf/aAAgBAwEBPwE8hy5//8QAFREBAQAA' +
|
|
222
|
+
'AAAAAAAAAAAAAAAAAwD/2gAIAQIBAT8BFr//xAAWEAADAAAAAAAAAAAAAAAAAAAAASH/2gAIAQEABj8CUFBQUP/E' +
|
|
223
|
+
'ABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEAAT8hCqq//9oADAMBAAIAAwAAABBT/8QAFBEBAAAAAAAAAAAAAAAA' +
|
|
224
|
+
'AAAAAP/aAAgBAwEBPxB//8QAFhEAAwAAAAAAAAAAAAAAAAAAACEx/9oACAECAQE/EIM//8QAFRABAQAAAAAAAAAA' +
|
|
225
|
+
'AAAAAAAAAPH/2gAIAQEAAT8QgoKCgv/Z',
|
|
226
|
+
width: 16,
|
|
227
|
+
height: 16,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** Probe-only: GIF87a/89a header. */
|
|
231
|
+
export const GIF_5X7: ImageFixture = {
|
|
232
|
+
base64: 'R0lGODdhBQAHAIEAAAAAAAAAAAAAAAAAACwAAAAABQAHAAAIDAABCBxIsKDBgwcDAgA7',
|
|
233
|
+
width: 5,
|
|
234
|
+
height: 7,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** Probe-only: lossy VP8 inside RIFF/WEBP. */
|
|
238
|
+
export const WEBP_9X11: ImageFixture = {
|
|
239
|
+
base64: 'UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoJAAsAAUAmJaQAA3AA/v02aAA=',
|
|
240
|
+
width: 9,
|
|
241
|
+
height: 11,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** Probe-only: AVIF, dimensions read from the `ispe` property box. */
|
|
245
|
+
export const AVIF_12X16: ImageFixture = {
|
|
246
|
+
base64:
|
|
247
|
+
'AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAAD5bWV0YQAAAAAAAAAvaGRscgAAAAAAAAAAcGljdAAA' +
|
|
248
|
+
'AAAAAAAAAAAAAFBpY3R1cmVIYW5kbGVyAAAAAA5waXRtAAAAAAABAAAAHmlsb2MAAAAARAAAAQABAAAAAQAAASEA' +
|
|
249
|
+
'AAAiAAAAKGlpbmYAAAAAAAEAAAAaaW5mZQIAAAAAAQAAYXYwMUNvbG9yAAAAAGppcHJwAAAAS2lwY28AAAAUaXNw' +
|
|
250
|
+
'ZQAAAAAAAAAMAAAAEAAAABBwaXhpAAAAAAMICAgAAAAMYXYxQ4EADAAAAAATY29scm5jbHgAAgACAAIAAAAAF2lw' +
|
|
251
|
+
'bWEAAAAAAAAAAQABBAECgwQAAAAqbWRhdAoKAAAAAZ35//MAgDIUEADAAAACgAAAAKmObLaqsca2hUA=',
|
|
252
|
+
width: 12,
|
|
253
|
+
height: 16,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
/** Probe-only: SVG is vector — it has a declared box but no raster to decode. */
|
|
257
|
+
export const SVG_120X45: ImageFixture = {
|
|
258
|
+
base64:
|
|
259
|
+
'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iNDUiIHZp' +
|
|
260
|
+
'ZXdCb3g9IjAgMCAxMjAgNDUiPjwvc3ZnPg==',
|
|
261
|
+
width: 120,
|
|
262
|
+
height: 45,
|
|
263
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Single responsibility: turning a baseline or extended sequential Huffman JPEG (SOF0/SOF1) into
|
|
2
|
+
// RGBA — the marker walk, the entropy-coded scan, the inverse DCT and the sample planes. What each
|
|
3
|
+
// segment DECLARES, and which codings are refused by name, is `jpeg-headers.ts`; this file is the
|
|
4
|
+
// algorithm those declarations describe.
|
|
5
|
+
|
|
6
|
+
import { imageDecodeFailed } from './errors';
|
|
7
|
+
import {
|
|
8
|
+
assertSupportedCoding,
|
|
9
|
+
type Component,
|
|
10
|
+
type Frame,
|
|
11
|
+
hex,
|
|
12
|
+
isAdobe,
|
|
13
|
+
readFrame,
|
|
14
|
+
readQuantTables,
|
|
15
|
+
readScanHeader,
|
|
16
|
+
readU16,
|
|
17
|
+
type ScanComponent,
|
|
18
|
+
} from './jpeg-headers';
|
|
19
|
+
import { type HuffmanTable, JpegBitReader, readHuffmanTables } from './jpeg-huffman';
|
|
20
|
+
import { ycbcrToRgb, ZIGZAG } from './jpeg-tables';
|
|
21
|
+
import { type Raster, rasterFrom } from './raster';
|
|
22
|
+
|
|
23
|
+
const SQRT2 = Math.SQRT2;
|
|
24
|
+
|
|
25
|
+
/** Reused across every block of every image: the decoder is synchronous and single-threaded. */
|
|
26
|
+
const COEF = new Int32Array(64);
|
|
27
|
+
const WORK = new Float32Array(64);
|
|
28
|
+
|
|
29
|
+
/** libjpeg's AAN float butterfly, in place over the 8 samples `step` apart from `base`. */
|
|
30
|
+
function idct1d(v: Float32Array, base: number, step: number): void {
|
|
31
|
+
const s0 = v[base] ?? 0;
|
|
32
|
+
const s1 = v[base + step] ?? 0;
|
|
33
|
+
const s2 = v[base + step * 2] ?? 0;
|
|
34
|
+
const s3 = v[base + step * 3] ?? 0;
|
|
35
|
+
const s4 = v[base + step * 4] ?? 0;
|
|
36
|
+
const s5 = v[base + step * 5] ?? 0;
|
|
37
|
+
const s6 = v[base + step * 6] ?? 0;
|
|
38
|
+
const s7 = v[base + step * 7] ?? 0;
|
|
39
|
+
const e10 = s0 + s4;
|
|
40
|
+
const e11 = s0 - s4;
|
|
41
|
+
const e13 = s2 + s6;
|
|
42
|
+
const e12 = (s2 - s6) * SQRT2 - e13;
|
|
43
|
+
const t0 = e10 + e13;
|
|
44
|
+
const t3 = e10 - e13;
|
|
45
|
+
const t1 = e11 + e12;
|
|
46
|
+
const t2 = e11 - e12;
|
|
47
|
+
const z13 = s5 + s3;
|
|
48
|
+
const z10 = s5 - s3;
|
|
49
|
+
const z11 = s1 + s7;
|
|
50
|
+
const z12 = s1 - s7;
|
|
51
|
+
const t7 = z11 + z13;
|
|
52
|
+
const t11 = (z11 - z13) * SQRT2;
|
|
53
|
+
const z5 = (z10 + z12) * 1.847759065;
|
|
54
|
+
const t10 = 1.0823922 * z12 - z5;
|
|
55
|
+
const t12 = -2.61312593 * z10 + z5;
|
|
56
|
+
const t6 = t12 - t7;
|
|
57
|
+
const t5 = t11 - t6;
|
|
58
|
+
const t4 = t10 + t5;
|
|
59
|
+
v[base] = t0 + t7;
|
|
60
|
+
v[base + step * 7] = t0 - t7;
|
|
61
|
+
v[base + step] = t1 + t6;
|
|
62
|
+
v[base + step * 6] = t1 - t6;
|
|
63
|
+
v[base + step * 2] = t2 + t5;
|
|
64
|
+
v[base + step * 5] = t2 - t5;
|
|
65
|
+
v[base + step * 4] = t3 + t4;
|
|
66
|
+
v[base + step * 3] = t3 - t4;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** `Uint8ClampedArray` is the level shift: it rounds and clamps to 0-255 on every store. */
|
|
70
|
+
function writeBlock(comp: Component, at: number, flat: boolean): void {
|
|
71
|
+
const { samples, stride } = comp;
|
|
72
|
+
if (flat) {
|
|
73
|
+
const value = (COEF[0] ?? 0) * (comp.dequant[0] ?? 0) + 128;
|
|
74
|
+
for (let r = 0; r < 8; r += 1) samples.fill(value, at + r * stride, at + r * stride + 8);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < 64; i += 1) WORK[i] = (COEF[i] ?? 0) * (comp.dequant[i] ?? 0);
|
|
78
|
+
for (let c = 0; c < 8; c += 1) idct1d(WORK, c, 8);
|
|
79
|
+
for (let r = 0; r < 8; r += 1) idct1d(WORK, r * 8, 1);
|
|
80
|
+
for (let r = 0; r < 8; r += 1) {
|
|
81
|
+
const row = at + r * stride;
|
|
82
|
+
for (let c = 0; c < 8; c += 1) samples[row + c] = (WORK[r * 8 + c] ?? 0) + 128;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function decodeBlock(reader: JpegBitReader, scan: ScanComponent, row: number, col: number): void {
|
|
87
|
+
const { comp } = scan;
|
|
88
|
+
if (row >= comp.blocksPerColumn || col >= comp.blocksPerLine) {
|
|
89
|
+
throw imageDecodeFailed(`block ${col},${row} falls outside component ${comp.id}`, { row, col });
|
|
90
|
+
}
|
|
91
|
+
COEF.fill(0);
|
|
92
|
+
const size = reader.decode(scan.dc);
|
|
93
|
+
if (size > 15) {
|
|
94
|
+
throw imageDecodeFailed(`a DC coefficient claims ${size} magnitude bits, over the 15 allowed`);
|
|
95
|
+
}
|
|
96
|
+
comp.pred += reader.receiveAndExtend(size);
|
|
97
|
+
COEF[0] = comp.pred;
|
|
98
|
+
let k = 1;
|
|
99
|
+
let last = 0;
|
|
100
|
+
while (k < 64) {
|
|
101
|
+
const rs = reader.decode(scan.ac);
|
|
102
|
+
const bits = rs & 15;
|
|
103
|
+
const run = rs >> 4;
|
|
104
|
+
if (bits === 0) {
|
|
105
|
+
if (run !== 15) break; // 0x00 is end-of-block; 0xF0 is a run of 16 zeros
|
|
106
|
+
k += 16;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
k += run;
|
|
110
|
+
if (k > 63) {
|
|
111
|
+
throw imageDecodeFailed(`an AC run overruns block ${col},${row} of component ${comp.id}`, {
|
|
112
|
+
row,
|
|
113
|
+
col,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
COEF[ZIGZAG[k] ?? 0] = reader.receiveAndExtend(bits);
|
|
117
|
+
last = k;
|
|
118
|
+
k += 1;
|
|
119
|
+
}
|
|
120
|
+
writeBlock(comp, row * 8 * comp.stride + col * 8, last === 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Leaves the walk at the next marker, or at the end when the file carries no EOI. */
|
|
124
|
+
function skipToMarker(bytes: Uint8Array, from: number): number {
|
|
125
|
+
for (let at = from; at + 1 < bytes.length; at += 1) {
|
|
126
|
+
if ((bytes[at] ?? 0) === 0xff && (bytes[at + 1] ?? 0) !== 0x00) return at;
|
|
127
|
+
}
|
|
128
|
+
return bytes.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** One scan's entropy-coded data, from the byte after its header to the marker that ends it. */
|
|
132
|
+
function decodeScan(
|
|
133
|
+
bytes: Uint8Array,
|
|
134
|
+
start: number,
|
|
135
|
+
seg: Uint8Array,
|
|
136
|
+
frame: Frame,
|
|
137
|
+
quant: ReadonlyArray<Float32Array | undefined>,
|
|
138
|
+
dcTables: ReadonlyArray<HuffmanTable | undefined>,
|
|
139
|
+
acTables: ReadonlyArray<HuffmanTable | undefined>,
|
|
140
|
+
restartInterval: number,
|
|
141
|
+
): number {
|
|
142
|
+
const scan: readonly ScanComponent[] = readScanHeader(seg, frame, quant, dcTables, acTables);
|
|
143
|
+
const reader = new JpegBitReader(bytes, start);
|
|
144
|
+
const single = scan.length === 1 ? scan[0] : undefined;
|
|
145
|
+
// A non-interleaved scan walks the component's own blocks, which for a subsampled component is
|
|
146
|
+
// fewer than its MCU-padded plane holds; an interleaved one walks whole MCUs.
|
|
147
|
+
const perLine =
|
|
148
|
+
single === undefined
|
|
149
|
+
? frame.mcusPerLine
|
|
150
|
+
: Math.ceil(Math.ceil((frame.width * single.comp.h) / frame.maxH) / 8);
|
|
151
|
+
const perColumn =
|
|
152
|
+
single === undefined
|
|
153
|
+
? frame.mcusPerColumn
|
|
154
|
+
: Math.ceil(Math.ceil((frame.height * single.comp.v) / frame.maxV) / 8);
|
|
155
|
+
for (let n = 0; n < perLine * perColumn; n += 1) {
|
|
156
|
+
if (restartInterval > 0 && n > 0 && n % restartInterval === 0) {
|
|
157
|
+
if (!reader.restart()) {
|
|
158
|
+
throw imageDecodeFailed(`the scan omits the restart marker due after ${n} units`, { n });
|
|
159
|
+
}
|
|
160
|
+
for (const entry of scan) entry.comp.pred = 0;
|
|
161
|
+
}
|
|
162
|
+
const row = (n / perLine) | 0;
|
|
163
|
+
const col = n % perLine;
|
|
164
|
+
if (single !== undefined) {
|
|
165
|
+
decodeBlock(reader, single, row, col);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const entry of scan) {
|
|
169
|
+
for (let v = 0; v < entry.comp.v; v += 1) {
|
|
170
|
+
for (let h = 0; h < entry.comp.h; h += 1) {
|
|
171
|
+
decodeBlock(reader, entry, row * entry.comp.v + v, col * entry.comp.h + h);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return skipToMarker(bytes, reader.position);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sample planes to RGBA, cropped to the declared size: the MCU-padded edge columns and rows exist
|
|
181
|
+
* only so the last block is whole, and a decoder that returns them reports the wrong dimensions.
|
|
182
|
+
* Chroma is upsampled by replication, which is what `h`/`v` below the maxima mean.
|
|
183
|
+
*/
|
|
184
|
+
function toRaster(frame: Frame, adobeTransform: number): Raster {
|
|
185
|
+
const { width, height, components, maxH, maxV } = frame;
|
|
186
|
+
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
187
|
+
const luma = components[0];
|
|
188
|
+
if (luma === undefined) throw imageDecodeFailed('the frame declares no components');
|
|
189
|
+
const cb = components[1];
|
|
190
|
+
const cr = components[2];
|
|
191
|
+
// Adobe transform 0 over three components means the samples already ARE R, G and B.
|
|
192
|
+
const alreadyRgb = adobeTransform === 0;
|
|
193
|
+
for (let y = 0; y < height; y += 1) {
|
|
194
|
+
const lumaRow = (((y * luma.v) / maxV) | 0) * luma.stride;
|
|
195
|
+
let out = y * width * 4;
|
|
196
|
+
if (cb === undefined || cr === undefined) {
|
|
197
|
+
for (let x = 0; x < width; x += 1) {
|
|
198
|
+
const grey = luma.samples[lumaRow + (((x * luma.h) / maxH) | 0)] ?? 0;
|
|
199
|
+
pixels[out] = grey;
|
|
200
|
+
pixels[out + 1] = grey;
|
|
201
|
+
pixels[out + 2] = grey;
|
|
202
|
+
pixels[out + 3] = 255;
|
|
203
|
+
out += 4;
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const cbRow = (((y * cb.v) / maxV) | 0) * cb.stride;
|
|
208
|
+
const crRow = (((y * cr.v) / maxV) | 0) * cr.stride;
|
|
209
|
+
for (let x = 0; x < width; x += 1) {
|
|
210
|
+
const a = luma.samples[lumaRow + (((x * luma.h) / maxH) | 0)] ?? 0;
|
|
211
|
+
const b = cb.samples[cbRow + (((x * cb.h) / maxH) | 0)] ?? 0;
|
|
212
|
+
const c = cr.samples[crRow + (((x * cr.h) / maxH) | 0)] ?? 0;
|
|
213
|
+
if (alreadyRgb) {
|
|
214
|
+
pixels[out] = a;
|
|
215
|
+
pixels[out + 1] = b;
|
|
216
|
+
pixels[out + 2] = c;
|
|
217
|
+
} else {
|
|
218
|
+
const [r, g, blue] = ycbcrToRgb(a, b, c);
|
|
219
|
+
pixels[out] = r;
|
|
220
|
+
pixels[out + 1] = g;
|
|
221
|
+
pixels[out + 2] = blue;
|
|
222
|
+
}
|
|
223
|
+
pixels[out + 3] = 255;
|
|
224
|
+
out += 4;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return rasterFrom(width, height, pixels);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** JPEG bytes to RGBA. Baseline and extended sequential only; everything else is named and refused. */
|
|
231
|
+
export function decodeJpeg(bytes: Uint8Array): Raster {
|
|
232
|
+
if ((bytes[0] ?? 0) !== 0xff || (bytes[1] ?? 0) !== 0xd8) {
|
|
233
|
+
throw imageDecodeFailed('the bytes do not open with a JPEG SOI marker (FF D8)', {
|
|
234
|
+
first: `${hex(bytes[0])} ${hex(bytes[1])}`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const quant: Array<Float32Array | undefined> = [];
|
|
238
|
+
const dcTables: Array<HuffmanTable | undefined> = [];
|
|
239
|
+
const acTables: Array<HuffmanTable | undefined> = [];
|
|
240
|
+
let frame: Frame | undefined;
|
|
241
|
+
let restartInterval = 0;
|
|
242
|
+
let adobeTransform = -1;
|
|
243
|
+
let offset = 2;
|
|
244
|
+
while (offset + 1 < bytes.length) {
|
|
245
|
+
if ((bytes[offset] ?? 0) !== 0xff) {
|
|
246
|
+
throw imageDecodeFailed(
|
|
247
|
+
`expected a marker at byte ${offset}, found 0x${hex(bytes[offset])}`,
|
|
248
|
+
{
|
|
249
|
+
offset,
|
|
250
|
+
},
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
while (bytes[offset + 1] === 0xff) offset += 1; // fill bytes between segments
|
|
254
|
+
const marker = bytes[offset + 1];
|
|
255
|
+
if (marker === undefined) throw imageDecodeFailed('the file ends inside a marker');
|
|
256
|
+
offset += 2;
|
|
257
|
+
if (marker === 0xd9) break; // EOI
|
|
258
|
+
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue; // no payload
|
|
259
|
+
assertSupportedCoding(marker);
|
|
260
|
+
const length = readU16(bytes, offset);
|
|
261
|
+
if (length < 2 || offset + length > bytes.length) {
|
|
262
|
+
throw imageDecodeFailed(
|
|
263
|
+
`segment FF${hex(marker)} declares ${length} bytes but ${bytes.length - offset} remain`,
|
|
264
|
+
{ marker: `FF${hex(marker)}`, length },
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
const seg = bytes.subarray(offset + 2, offset + length);
|
|
268
|
+
offset += length;
|
|
269
|
+
if (marker === 0xdb) readQuantTables(seg, quant);
|
|
270
|
+
else if (marker === 0xc4) readHuffmanTables(seg, dcTables, acTables);
|
|
271
|
+
else if (marker === 0xc0 || marker === 0xc1) frame = readFrame(seg, marker);
|
|
272
|
+
else if (marker === 0xdd) restartInterval = readU16(seg, 0);
|
|
273
|
+
else if (marker === 0xee && isAdobe(seg)) adobeTransform = seg[11] ?? adobeTransform;
|
|
274
|
+
else if (marker === 0xda) {
|
|
275
|
+
if (frame === undefined) {
|
|
276
|
+
throw imageDecodeFailed('a scan (SOS) arrives before any frame header (SOF)');
|
|
277
|
+
}
|
|
278
|
+
offset = decodeScan(bytes, offset, seg, frame, quant, dcTables, acTables, restartInterval);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (frame === undefined) throw imageDecodeFailed('the file carries no frame header (SOF)');
|
|
282
|
+
return toRaster(frame, adobeTransform);
|
|
283
|
+
}
|