@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 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. Penpot's core
44
- export/approval workflow remains PNG; no approved reference is silently converted.
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
@@ -1,98 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { readFile, stat } from "node:fs/promises";
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 input = await resolveProjectInput(root, file);
69
- if ((await stat(input)).size > 64 * 1024 * 1024) {
70
- throw new Error("Image exceeds 64 MiB");
71
- }
72
- const bytes = await readFile(input);
73
- if (bytes.length > 64 * 1024 * 1024) {
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.7",
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.3.7"
44
+ "@penpair/core": "0.4.1"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsc -p tsconfig.json",