spine-rigc 0.5.0 → 0.6.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/NOTICE.md +21 -0
- package/README.md +323 -224
- package/bin/rigc.cjs +40 -0
- package/cli.ts +558 -49
- package/docs/AUTHORING.md +60 -23
- package/docs/PROMPTING.md +106 -0
- package/docs/SPEC_COVERAGE.md +16 -14
- package/package.json +4 -2
- package/src/compile.ts +189 -142
- package/src/emit.ts +88 -0
- package/src/json-position.ts +253 -0
- package/src/png.ts +72 -7
- package/src/preview.ts +243 -0
- package/src/render.ts +58 -0
- package/src/types.ts +8 -0
- package/src/validate.ts +58 -13
- package/tools/plate.ts +164 -21
package/src/types.ts
CHANGED
|
@@ -573,6 +573,14 @@ export interface CompiledImage {
|
|
|
573
573
|
absPath: string;
|
|
574
574
|
width: number;
|
|
575
575
|
height: number;
|
|
576
|
+
/**
|
|
577
|
+
* A per-pixel alpha channel, and only that — colour types 4 and 6.
|
|
578
|
+
*
|
|
579
|
+
* ⚠️ Not "this part can be transparent": indexed and greyscale art keeps its
|
|
580
|
+
* transparency in a `tRNS` chunk and reads `false` here. Anything asking
|
|
581
|
+
* whether the art can draw a transparent pixel wants `PngInfo.hasTransparency`
|
|
582
|
+
* ([`src/png.ts`](png.ts)), which is the distinction A19 got wrong (#215).
|
|
583
|
+
*/
|
|
576
584
|
hasAlpha: boolean;
|
|
577
585
|
isBase: boolean;
|
|
578
586
|
}
|
package/src/validate.ts
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
SkeletonJson,
|
|
30
30
|
TextureAtlas,
|
|
31
31
|
} from '@esotericsoftware/spine-core';
|
|
32
|
-
import { readPngInfo } from './png.ts';
|
|
32
|
+
import { colourTypeName, readPngInfo } from './png.ts';
|
|
33
33
|
import { CHANNELS_BY_KIND, KEY_TIME_EPSILON, walkTimelines } from './timelines.ts';
|
|
34
34
|
import type { RigInfo } from './types.ts';
|
|
35
35
|
|
|
@@ -53,15 +53,31 @@ export interface Failure {
|
|
|
53
53
|
* - `spine` — is this valid Spine 4.3 that any runtime will play correctly?
|
|
54
54
|
* - `spine-html` — the above, plus this project's renderer and archetype policy.
|
|
55
55
|
*
|
|
56
|
-
* ⚠️ `
|
|
57
|
-
*
|
|
58
|
-
*
|
|
56
|
+
* ⚠️ `validate()` has NO default profile — `ValidateInput.profile` is required,
|
|
57
|
+
* and that is deliberate. A silent default here can only be wrong in one of two
|
|
58
|
+
* directions: loosen it and a caller who did not ask gets a weaker gate than the
|
|
59
|
+
* one they think they ran; tighten it and foreign data is refused by a policy the
|
|
60
|
+
* caller has no stake in. Issue #221 flipped the CLI to `spine` while several
|
|
61
|
+
* internal callers still wanted `spine-html`, at which point one constant could
|
|
62
|
+
* no longer honestly serve both — so the choice is made at every call site now,
|
|
63
|
+
* by the caller who knows which question they are asking.
|
|
59
64
|
*/
|
|
60
65
|
export type ValidateProfile = 'spine' | 'spine-html';
|
|
61
66
|
|
|
62
67
|
export const VALIDATE_PROFILES: readonly ValidateProfile[] = ['spine', 'spine-html'];
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
/**
|
|
70
|
+
* What the CLI uses when `--profile` is absent — and ONLY the CLI. This is not
|
|
71
|
+
* `validate()`'s default; that function has none (above).
|
|
72
|
+
*
|
|
73
|
+
* `spine` since issue #221. The published package's pitch is "the output imports
|
|
74
|
+
* into the Spine editor", which is exactly the question `spine` asks, and a
|
|
75
|
+
* stranger's first build was being judged instead against one renderer's policy
|
|
76
|
+
* and one project's canvas budget — 14 rules they have no stake in, with the
|
|
77
|
+
* escape hatch documented only in prose. Defaults beat prose. `spine-html` is
|
|
78
|
+
* still one flag away, and every report names the profile that judged it.
|
|
79
|
+
*/
|
|
80
|
+
export const CLI_DEFAULT_PROFILE: ValidateProfile = 'spine';
|
|
65
81
|
|
|
66
82
|
/**
|
|
67
83
|
* What kind of rule each assertion is. Every assertion has an entry, and
|
|
@@ -136,8 +152,11 @@ export interface ValidateInput {
|
|
|
136
152
|
* stats line says `rig=absent` so a green run cannot be mistaken for a full one.
|
|
137
153
|
*/
|
|
138
154
|
rig?: RigInfo;
|
|
139
|
-
/**
|
|
140
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Which body of rules to apply. Required, and deliberately so — there is no
|
|
157
|
+
* default to fall into. See ValidateProfile.
|
|
158
|
+
*/
|
|
159
|
+
profile: ValidateProfile;
|
|
141
160
|
}
|
|
142
161
|
|
|
143
162
|
export interface ValidateReport {
|
|
@@ -215,7 +234,7 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
215
234
|
const skipped: ValidateReport['skipped'] = [];
|
|
216
235
|
const profileSkipped: ValidateReport['profileSkipped'] = [];
|
|
217
236
|
const stats: Record<string, number | string> = {};
|
|
218
|
-
const profile = input.profile
|
|
237
|
+
const profile = input.profile;
|
|
219
238
|
/** True when this profile's rulebook includes the policy layer. */
|
|
220
239
|
const policy = profile === 'spine-html';
|
|
221
240
|
|
|
@@ -1297,11 +1316,25 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1297
1316
|
}
|
|
1298
1317
|
}
|
|
1299
1318
|
});
|
|
1300
|
-
// An overlay part must
|
|
1301
|
-
// would paint
|
|
1302
|
-
// formation's whole claim is that the still frame has no seam. The base
|
|
1319
|
+
// An overlay part must be able to draw a transparent pixel or it cannot be an
|
|
1320
|
+
// overlay: it would paint a solid rectangle over the untouched base, and an
|
|
1321
|
+
// overlay formation's whole claim is that the still frame has no seam. The base
|
|
1303
1322
|
// plate itself is the one page allowed to be opaque, and it identifies
|
|
1304
1323
|
// itself structurally — it is the region that covers the whole stage.
|
|
1324
|
+
//
|
|
1325
|
+
// ⭐ Transparency is not the same thing as an alpha CHANNEL, and this assertion
|
|
1326
|
+
// used to conflate them (#215). Colour types 4 and 6 store alpha per pixel;
|
|
1327
|
+
// types 0, 2 and 3 store it in a `tRNS` chunk instead, and indexed+tRNS is the
|
|
1328
|
+
// ordinary output of ImageMagick, Photoshop's PNG-8 export, GIMP's indexed
|
|
1329
|
+
// mode, aseprite and pngquant. Judging on the colour type alone refused art
|
|
1330
|
+
// that was never broken — seven of one author's nine hand-drawn parts, on their
|
|
1331
|
+
// first build — and told them, untruthfully, that the file held no transparency
|
|
1332
|
+
// at all. That audit is also why the CLI's default is now `spine`, which does
|
|
1333
|
+
// not run this rule at all (#221): a stranger reaches this refusal only by
|
|
1334
|
+
// asking for `spine-html`. It still has to be both TRUE and actionable when it
|
|
1335
|
+
// fires, and it now has to name the profile it belongs to as well as the one
|
|
1336
|
+
// that does not ask — the reader opted in, and the message is where they find
|
|
1337
|
+
// out what they opted into.
|
|
1305
1338
|
check('A19_OVERLAY_PNGS_HAVE_ALPHA', () => {
|
|
1306
1339
|
if (!atlas) return;
|
|
1307
1340
|
const stageW = skeletonData?.width ?? 0;
|
|
@@ -1313,15 +1346,27 @@ export function validate(input: ValidateInput): ValidateReport {
|
|
|
1313
1346
|
if (region) basePages.add(region.page.name);
|
|
1314
1347
|
}
|
|
1315
1348
|
}
|
|
1349
|
+
// The escape hatch is only worth naming when it is reachable: with no stage
|
|
1350
|
+
// size to measure against, `basePages` is empty and no image can qualify, so
|
|
1351
|
+
// pointing at it would send the reader after a door that is not there.
|
|
1352
|
+
const exemption =
|
|
1353
|
+
stageW && stageH
|
|
1354
|
+
? `Only the one image big enough to cover the whole stage (${stageW}x${stageH}) may be opaque.`
|
|
1355
|
+
: 'The one image that covers the whole stage may be opaque, but this skeleton declares no stage size, so ' +
|
|
1356
|
+
'nothing here qualifies.';
|
|
1316
1357
|
for (const page of atlas.pages) {
|
|
1317
1358
|
const abs = resolve(input.atlasDir, page.name);
|
|
1318
1359
|
if (!existsSync(abs)) continue;
|
|
1319
1360
|
const info = readPngInfo(abs);
|
|
1320
|
-
if (info.
|
|
1361
|
+
if (info.hasTransparency) continue;
|
|
1321
1362
|
if (basePages.has(page.name)) continue; // full-stage base plate: opaque is correct
|
|
1322
1363
|
fail(
|
|
1323
1364
|
'A19_OVERLAY_PNGS_HAVE_ALPHA',
|
|
1324
|
-
`
|
|
1365
|
+
`part image "${page.name}" cannot be transparent anywhere: it is colour type ${info.colourType} ` +
|
|
1366
|
+
`(${colourTypeName(info.colourType)}) with no tRNS chunk, so it would paint a solid rectangle over ` +
|
|
1367
|
+
'whatever is drawn behind it. Re-export it with transparency — as RGBA, or as an indexed or greyscale ' +
|
|
1368
|
+
`PNG that keeps its tRNS chunk. ${exemption} This is renderer policy, and it belongs to --profile ` +
|
|
1369
|
+
'spine-html: the default --profile spine does not run this check.',
|
|
1325
1370
|
);
|
|
1326
1371
|
}
|
|
1327
1372
|
});
|
package/tools/plate.ts
CHANGED
|
@@ -169,7 +169,19 @@ function crc32(buf: Uint8Array): number {
|
|
|
169
169
|
return (c ^ 0xffffffff) >>> 0;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
/** The eight bytes every PNG opens with. */
|
|
173
|
+
export const PNG_SIGNATURE = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* One PNG chunk: length, type, body, CRC.
|
|
177
|
+
*
|
|
178
|
+
* Exported because this writer only ever emits colour type 6, and the selftest
|
|
179
|
+
* has to produce the colour types it does NOT — indexed and greyscale, with and
|
|
180
|
+
* without a `tRNS` chunk — to have anything for `A19` to judge. Building those by
|
|
181
|
+
* hand needs the CRC table, and a second copy of it in the test would be a second
|
|
182
|
+
* thing that can be wrong.
|
|
183
|
+
*/
|
|
184
|
+
export function pngChunk(type: string, body: Uint8Array): Uint8Array {
|
|
173
185
|
const out = new Uint8Array(body.length + 12);
|
|
174
186
|
const view = new DataView(out.buffer);
|
|
175
187
|
view.setUint32(0, body.length);
|
|
@@ -197,10 +209,10 @@ export function encodePng(width: number, height: number, rgba: Uint8Array): Uint
|
|
|
197
209
|
ihdr[11] = 0;
|
|
198
210
|
ihdr[12] = 0;
|
|
199
211
|
const parts = [
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
212
|
+
PNG_SIGNATURE,
|
|
213
|
+
pngChunk('IHDR', ihdr),
|
|
214
|
+
pngChunk('IDAT', new Uint8Array(deflateSync(raw, { level: 9 }))),
|
|
215
|
+
pngChunk('IEND', new Uint8Array(0)),
|
|
204
216
|
];
|
|
205
217
|
const total = parts.reduce((n, p) => n + p.length, 0);
|
|
206
218
|
const out = new Uint8Array(total);
|
|
@@ -216,48 +228,127 @@ export function encodePng(width: number, height: number, rgba: Uint8Array): Uint
|
|
|
216
228
|
// PNG decoding
|
|
217
229
|
// ---------------------------------------------------------------------------
|
|
218
230
|
|
|
231
|
+
/** Samples per pixel for each PNG colour type. Anything else is not a colour type. */
|
|
232
|
+
const CHANNELS: Record<number, number> = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
233
|
+
|
|
234
|
+
/** The bit depths the PNG spec allows for each colour type. */
|
|
235
|
+
const BIT_DEPTHS: Record<number, number[]> = {
|
|
236
|
+
0: [1, 2, 4, 8, 16],
|
|
237
|
+
2: [8, 16],
|
|
238
|
+
3: [1, 2, 4, 8],
|
|
239
|
+
4: [8, 16],
|
|
240
|
+
6: [8, 16],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Read one sample out of an unfiltered scanline, at the file's own bit depth.
|
|
245
|
+
*
|
|
246
|
+
* Returns the RAW value — 0..1 for a 1-bit file, 0..65535 for a 16-bit one — not
|
|
247
|
+
* a scaled byte. Two callers need it that way: a palette index IS the raw value,
|
|
248
|
+
* and a `tRNS` colour is declared at the file's depth and has to be compared
|
|
249
|
+
* against samples at that depth, not against something already rounded to 8 bits.
|
|
250
|
+
*/
|
|
251
|
+
function sampleAt(line: Uint8Array, index: number, bitDepth: number): number {
|
|
252
|
+
if (bitDepth === 8) return line[index];
|
|
253
|
+
if (bitDepth === 16) return (line[index * 2] << 8) | line[index * 2 + 1];
|
|
254
|
+
const perByte = 8 / bitDepth;
|
|
255
|
+
const byte = line[Math.floor(index / perByte)];
|
|
256
|
+
// Sub-byte samples are packed most-significant first.
|
|
257
|
+
const shift = 8 - bitDepth * ((index % perByte) + 1);
|
|
258
|
+
return (byte >> shift) & ((1 << bitDepth) - 1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A raw sample scaled to 0..255, the range a `Plate` stores. */
|
|
262
|
+
function toByte(sample: number, bitDepth: number): number {
|
|
263
|
+
if (bitDepth === 8) return sample;
|
|
264
|
+
if (bitDepth === 16) return sample >> 8;
|
|
265
|
+
return Math.round((sample * 255) / ((1 << bitDepth) - 1));
|
|
266
|
+
}
|
|
267
|
+
|
|
219
268
|
/**
|
|
220
|
-
* Read
|
|
269
|
+
* Read a PNG back into a `Plate`, expanding whatever it is to straight RGBA.
|
|
221
270
|
*
|
|
222
271
|
* `src/png.ts` parses 26 bytes because that is all the COMPILER needs, and it
|
|
223
272
|
* stays that way: the compiler never re-measures art. This
|
|
224
273
|
* decoder is for the measuring tools, which is where art measurement belongs -
|
|
225
274
|
* the same division that puts `mesh.center` in the manifest as a measured number
|
|
226
275
|
* rather than as something the compiler derives at build time.
|
|
276
|
+
*
|
|
277
|
+
* ⭐ **Every colour type the gate accepts is decodable here** (issue #226). It
|
|
278
|
+
* used to read colour types 2 and 6 only, which was the compiler's own old blind
|
|
279
|
+
* spot rebuilt one step later: `A19` learned in #215 that indexed and greyscale
|
|
280
|
+
* art carrying a `tRNS` chunk is ordinary transparent art — ImageMagick, PNG-8
|
|
281
|
+
* export, GIMP indexed, aseprite and pngquant all write it — so such a part
|
|
282
|
+
* builds and validates green, and would then have been refused by the one command
|
|
283
|
+
* whose whole job is to show the author what they built. A wall removed at the
|
|
284
|
+
* gate and rebuilt at the picture is not a wall removed.
|
|
285
|
+
*
|
|
286
|
+
* So the expansion happens here, at decode: a palette index becomes its `PLTE`
|
|
287
|
+
* entry with its `tRNS` alpha, a greyscale sample becomes three equal channels,
|
|
288
|
+
* and a `tRNS` colour on a type 0 or 2 file becomes alpha 0 on the pixels that
|
|
289
|
+
* match it. Every caller above this line keeps seeing exactly one thing, RGBA.
|
|
290
|
+
*
|
|
291
|
+
* Interlaced files are still refused: Adam7 is a second sample layout rather than
|
|
292
|
+
* a second sample format, and no tool in the corpus writes one.
|
|
227
293
|
*/
|
|
228
294
|
export function decodePng(buf: Uint8Array): Plate {
|
|
229
295
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
230
296
|
let at = 8;
|
|
231
297
|
let width = 0;
|
|
232
298
|
let height = 0;
|
|
299
|
+
let bitDepth = 8;
|
|
233
300
|
let colourType = 6;
|
|
301
|
+
let palette: Uint8Array | null = null;
|
|
302
|
+
let trns: Uint8Array | null = null;
|
|
234
303
|
const idat: Uint8Array[] = [];
|
|
235
|
-
while (at
|
|
304
|
+
while (at + 8 <= buf.length) {
|
|
236
305
|
const len = view.getUint32(at);
|
|
237
306
|
const type = String.fromCharCode(buf[at + 4], buf[at + 5], buf[at + 6], buf[at + 7]);
|
|
238
307
|
const body = buf.subarray(at + 8, at + 8 + len);
|
|
239
308
|
if (type === 'IHDR') {
|
|
240
309
|
width = view.getUint32(at + 8);
|
|
241
310
|
height = view.getUint32(at + 12);
|
|
242
|
-
|
|
311
|
+
bitDepth = body[8];
|
|
243
312
|
colourType = body[9];
|
|
244
313
|
if (body[12] !== 0) throw new Error('interlaced PNG');
|
|
245
|
-
} else if (type === '
|
|
314
|
+
} else if (type === 'PLTE') palette = body.slice();
|
|
315
|
+
else if (type === 'tRNS') trns = body.slice();
|
|
316
|
+
else if (type === 'IDAT') idat.push(body);
|
|
246
317
|
at += 12 + len;
|
|
247
318
|
}
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
319
|
+
const channels = CHANNELS[colourType];
|
|
320
|
+
if (channels === undefined) throw new Error(`unsupported colour type ${colourType}`);
|
|
321
|
+
if (!BIT_DEPTHS[colourType].includes(bitDepth)) {
|
|
322
|
+
throw new Error(`bit depth ${bitDepth} is not one PNG allows for colour type ${colourType}`);
|
|
323
|
+
}
|
|
324
|
+
if (colourType === 3 && palette === null) throw new Error('indexed PNG with no PLTE chunk');
|
|
325
|
+
// Const copies so the null checks above narrow inside the per-pixel loop below,
|
|
326
|
+
// where `palette` and `trns` are still the chunk loop's mutable bindings.
|
|
327
|
+
const pal = palette ?? new Uint8Array(0);
|
|
328
|
+
const alphaTable = trns;
|
|
329
|
+
|
|
250
330
|
const raw = new Uint8Array(inflateSync(Buffer.concat(idat.map((c) => Buffer.from(c)))));
|
|
251
|
-
|
|
331
|
+
// The filter operates on BYTES, and its "left neighbour" is one whole pixel
|
|
332
|
+
// back — rounded up to a byte, so sub-byte depths compare against the byte
|
|
333
|
+
// immediately to the left.
|
|
334
|
+
const filterUnit = Math.max(1, Math.ceil((channels * bitDepth) / 8));
|
|
335
|
+
const rowBytes = Math.ceil((width * channels * bitDepth) / 8);
|
|
336
|
+
const stride = rowBytes + 1;
|
|
252
337
|
const plate = new Plate(width, height);
|
|
253
|
-
let prev = new Uint8Array(
|
|
338
|
+
let prev = new Uint8Array(rowBytes);
|
|
339
|
+
// On a type 0 or 2 file a `tRNS` chunk names ONE invisible colour, one
|
|
340
|
+
// big-endian 16-bit sample per channel whatever the file's own depth. (For an
|
|
341
|
+
// indexed file the same chunk is something else entirely: one alpha byte per
|
|
342
|
+
// palette entry, read directly below.)
|
|
343
|
+
const trnsSample = (i: number): number =>
|
|
344
|
+
alphaTable === null ? -1 : (alphaTable[i * 2] << 8) | alphaTable[i * 2 + 1];
|
|
254
345
|
for (let y = 0; y < height; y++) {
|
|
255
346
|
const filter = raw[y * stride];
|
|
256
347
|
const line = raw.slice(y * stride + 1, (y + 1) * stride);
|
|
257
|
-
for (let x = 0; x <
|
|
258
|
-
const a = x >=
|
|
348
|
+
for (let x = 0; x < rowBytes; x++) {
|
|
349
|
+
const a = x >= filterUnit ? line[x - filterUnit] : 0;
|
|
259
350
|
const b = prev[x];
|
|
260
|
-
const c = x >=
|
|
351
|
+
const c = x >= filterUnit ? prev[x - filterUnit] : 0;
|
|
261
352
|
if (filter === 1) line[x] = (line[x] + a) & 255;
|
|
262
353
|
else if (filter === 2) line[x] = (line[x] + b) & 255;
|
|
263
354
|
else if (filter === 3) line[x] = (line[x] + ((a + b) >> 1)) & 255;
|
|
@@ -271,16 +362,68 @@ export function decodePng(buf: Uint8Array): Plate {
|
|
|
271
362
|
}
|
|
272
363
|
for (let x = 0; x < width; x++) {
|
|
273
364
|
const o = (y * width + x) * 4;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
365
|
+
const first = x * channels;
|
|
366
|
+
if (colourType === 3) {
|
|
367
|
+
const index = sampleAt(line, first, bitDepth);
|
|
368
|
+
plate.data[o] = pal[index * 3];
|
|
369
|
+
plate.data[o + 1] = pal[index * 3 + 1];
|
|
370
|
+
plate.data[o + 2] = pal[index * 3 + 2];
|
|
371
|
+
// A tRNS shorter than the palette leaves the entries past its end opaque,
|
|
372
|
+
// which is what the spec says and what pngquant relies on.
|
|
373
|
+
plate.data[o + 3] = alphaTable !== null && index < alphaTable.length ? alphaTable[index] : 255;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (colourType === 0 || colourType === 4) {
|
|
377
|
+
const grey = sampleAt(line, first, bitDepth);
|
|
378
|
+
const g = toByte(grey, bitDepth);
|
|
379
|
+
plate.data[o] = g;
|
|
380
|
+
plate.data[o + 1] = g;
|
|
381
|
+
plate.data[o + 2] = g;
|
|
382
|
+
plate.data[o + 3] =
|
|
383
|
+
colourType === 4
|
|
384
|
+
? toByte(sampleAt(line, first + 1, bitDepth), bitDepth)
|
|
385
|
+
: alphaTable !== null && alphaTable.length >= 2 && trnsSample(0) === grey
|
|
386
|
+
? 0
|
|
387
|
+
: 255;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const r = sampleAt(line, first, bitDepth);
|
|
391
|
+
const g = sampleAt(line, first + 1, bitDepth);
|
|
392
|
+
const b = sampleAt(line, first + 2, bitDepth);
|
|
393
|
+
plate.data[o] = toByte(r, bitDepth);
|
|
394
|
+
plate.data[o + 1] = toByte(g, bitDepth);
|
|
395
|
+
plate.data[o + 2] = toByte(b, bitDepth);
|
|
396
|
+
plate.data[o + 3] =
|
|
397
|
+
colourType === 6
|
|
398
|
+
? toByte(sampleAt(line, first + 3, bitDepth), bitDepth)
|
|
399
|
+
: alphaTable !== null &&
|
|
400
|
+
alphaTable.length >= 6 &&
|
|
401
|
+
trnsSample(0) === r &&
|
|
402
|
+
trnsSample(1) === g &&
|
|
403
|
+
trnsSample(2) === b
|
|
404
|
+
? 0
|
|
405
|
+
: 255;
|
|
278
406
|
}
|
|
279
407
|
prev = line;
|
|
280
408
|
}
|
|
281
409
|
return plate;
|
|
282
410
|
}
|
|
283
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Decode the PNG at `path`, naming the file if it cannot be decoded.
|
|
414
|
+
*
|
|
415
|
+
* The bare message says what is wrong with the bytes and nothing about which
|
|
416
|
+
* bytes; a rig has as many pages as it has parts, and "unsupported colour type 5"
|
|
417
|
+
* with no path is a search rather than a fix.
|
|
418
|
+
*/
|
|
284
419
|
export function readPlate(path: string): Plate {
|
|
285
|
-
|
|
420
|
+
// Only the DECODE is wrapped. A missing file already says which one, and
|
|
421
|
+
// rewording an ENOENT as a decoding problem would mislabel the commonest
|
|
422
|
+
// failure of all.
|
|
423
|
+
const buf = readFileSync(path);
|
|
424
|
+
try {
|
|
425
|
+
return decodePng(buf);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
throw new Error(`cannot decode PNG ${path}: ${(err as Error).message}`);
|
|
428
|
+
}
|
|
286
429
|
}
|