@penpair/reflection-check 0.3.7 → 0.4.1
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 +3 -2
- package/dist/image-evidence.js +7 -92
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,8 +40,9 @@ Evidence formats are detected from bytes, not filenames: PNG and static lossless
|
|
|
40
40
|
WebP (`VP8L`, including a matching `VP8X` canvas) are accepted. Lossy or animated
|
|
41
41
|
WebP and malformed/truncated containers are rejected. The adapter inspects image
|
|
42
42
|
metadata and hashes; the project provider owns actual decoding and pixel comparison.
|
|
43
|
-
Existing PNG baseline approvals retain their exact bytes and hashes.
|
|
44
|
-
|
|
43
|
+
Existing PNG baseline approvals retain their exact bytes and hashes. New core
|
|
44
|
+
baseline/approval images default to lossless WebP; public Penpot PNG exports remain
|
|
45
|
+
PNG. Existing approved references are never silently converted.
|
|
45
46
|
|
|
46
47
|
The browser starter saves `.webp` evidence by default. Supply `encodeLosslessWebp`
|
|
47
48
|
from the project's pinned codec, for example Sharp with `lossless: true` and
|
package/dist/image-evidence.js
CHANGED
|
@@ -1,98 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { resolveProjectInput } from "@penpair/core";
|
|
2
|
+
import { readImageEvidence as readCoreImageEvidence } from "@penpair/core";
|
|
4
3
|
export function hashImageBytes(bytes) {
|
|
5
4
|
return createHash("sha256").update(bytes).digest("hex");
|
|
6
5
|
}
|
|
7
|
-
function losslessWebpSize(bytes) {
|
|
8
|
-
const malformed = () => new Error("Malformed static lossless WebP evidence");
|
|
9
|
-
if (bytes.readUInt32LE(4) + 8 !== bytes.length) {
|
|
10
|
-
throw malformed();
|
|
11
|
-
}
|
|
12
|
-
let imageSize;
|
|
13
|
-
let canvasSize;
|
|
14
|
-
let offset = 12;
|
|
15
|
-
while (offset < bytes.length) {
|
|
16
|
-
if (offset + 8 > bytes.length) {
|
|
17
|
-
throw malformed();
|
|
18
|
-
}
|
|
19
|
-
const chunk = bytes.subarray(offset, offset + 4).toString("latin1");
|
|
20
|
-
const size = bytes.readUInt32LE(offset + 4);
|
|
21
|
-
const payload = offset + 8;
|
|
22
|
-
const end = payload + size;
|
|
23
|
-
const paddedEnd = end + (size % 2);
|
|
24
|
-
if (paddedEnd > bytes.length || (size % 2 !== 0 && bytes[end] !== 0)) {
|
|
25
|
-
throw malformed();
|
|
26
|
-
}
|
|
27
|
-
if (chunk === "VP8 " || chunk === "ANIM" || chunk === "ANMF" || chunk === "ALPH") {
|
|
28
|
-
throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
|
|
29
|
-
}
|
|
30
|
-
if (chunk === "VP8X") {
|
|
31
|
-
if (offset !== 12 ||
|
|
32
|
-
canvasSize ||
|
|
33
|
-
size !== 10 ||
|
|
34
|
-
(bytes[payload] & 0xc3) !== 0 ||
|
|
35
|
-
bytes.readUIntLE(payload + 1, 3) !== 0) {
|
|
36
|
-
throw malformed();
|
|
37
|
-
}
|
|
38
|
-
canvasSize = {
|
|
39
|
-
width: bytes.readUIntLE(payload + 4, 3) + 1,
|
|
40
|
-
height: bytes.readUIntLE(payload + 7, 3) + 1,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
else if (chunk === "VP8L") {
|
|
44
|
-
if (imageSize || size <= 5 || bytes[payload] !== 0x2f) {
|
|
45
|
-
throw malformed();
|
|
46
|
-
}
|
|
47
|
-
const dimensions = bytes.readUInt32LE(payload + 1);
|
|
48
|
-
if (dimensions >>> 29 !== 0) {
|
|
49
|
-
throw malformed();
|
|
50
|
-
}
|
|
51
|
-
imageSize = {
|
|
52
|
-
width: (dimensions & 0x3fff) + 1,
|
|
53
|
-
height: ((dimensions >>> 14) & 0x3fff) + 1,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
else if (offset === 12) {
|
|
57
|
-
throw malformed();
|
|
58
|
-
}
|
|
59
|
-
offset = paddedEnd;
|
|
60
|
-
}
|
|
61
|
-
if (!imageSize ||
|
|
62
|
-
(canvasSize && (canvasSize.width !== imageSize.width || canvasSize.height !== imageSize.height))) {
|
|
63
|
-
throw malformed();
|
|
64
|
-
}
|
|
65
|
-
return imageSize;
|
|
66
|
-
}
|
|
67
6
|
export async function readImageEvidence(root, file) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw new Error("Image exceeds 64 MiB");
|
|
75
|
-
}
|
|
76
|
-
if (bytes.length >= 24 &&
|
|
77
|
-
bytes.subarray(0, 8).toString("hex") === "89504e470d0a1a0a" &&
|
|
78
|
-
bytes.subarray(12, 16).toString("latin1") === "IHDR") {
|
|
79
|
-
const width = bytes.readUInt32BE(16);
|
|
80
|
-
const height = bytes.readUInt32BE(20);
|
|
81
|
-
if (width > 0 && height > 0) {
|
|
82
|
-
return {
|
|
83
|
-
bytes,
|
|
84
|
-
width,
|
|
85
|
-
height,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else if (bytes.length >= 12 &&
|
|
90
|
-
bytes.subarray(0, 4).toString("latin1") === "RIFF" &&
|
|
91
|
-
bytes.subarray(8, 12).toString("latin1") === "WEBP") {
|
|
92
|
-
return {
|
|
93
|
-
bytes,
|
|
94
|
-
...losslessWebpSize(bytes),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
throw new Error("Provider evidence must reference PNG or static lossless WebP bytes");
|
|
7
|
+
const { bytes, width, height } = await readCoreImageEvidence(root, file);
|
|
8
|
+
return {
|
|
9
|
+
bytes,
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
};
|
|
98
13
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penpair/reflection-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Explicit design case planning and verified implementation evidence for penpair baselines.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"jiti": "^2.4.2",
|
|
44
|
-
"@penpair/core": "0.
|
|
44
|
+
"@penpair/core": "0.4.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc -p tsconfig.json",
|