@effected/images 0.0.0-stage → 0.1.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.
@@ -0,0 +1,48 @@
1
+ import { Result } from "effect";
2
+
3
+ //#region src/internal/canonical.ts
4
+ const MAX_DEPTH = 256;
5
+ const isPlainObject = (value) => {
6
+ const proto = Object.getPrototypeOf(value);
7
+ return proto === Object.prototype || proto === null;
8
+ };
9
+ const encode = (value, depth) => {
10
+ if (depth > MAX_DEPTH) return Result.fail(`nesting deeper than ${MAX_DEPTH}`);
11
+ if (value === null || typeof value === "boolean" || typeof value === "string") return Result.succeed(JSON.stringify(value));
12
+ if (typeof value === "number") return Number.isFinite(value) ? Result.succeed(JSON.stringify(value)) : Result.fail(`non-finite number ${value}`);
13
+ if (Array.isArray(value)) {
14
+ const parts = [];
15
+ for (const item of value) {
16
+ if (item === void 0) return Result.fail("undefined array element");
17
+ const part = encode(item, depth + 1);
18
+ if (Result.isFailure(part)) return part;
19
+ parts.push(part.success);
20
+ }
21
+ return Result.succeed(`[${parts.join(",")}]`);
22
+ }
23
+ if (typeof value === "object" && isPlainObject(value)) {
24
+ const parts = [];
25
+ for (const key of Object.keys(value).sort()) {
26
+ const member = value[key];
27
+ if (member === void 0) continue;
28
+ const part = encode(member, depth + 1);
29
+ if (Result.isFailure(part)) return part;
30
+ parts.push(`${JSON.stringify(key)}:${part.success}`);
31
+ }
32
+ return Result.succeed(`{${parts.join(",")}}`);
33
+ }
34
+ const kind = typeof value === "object" ? value.constructor?.name ?? "object" : typeof value;
35
+ return Result.fail(`value of type ${kind} is not JSON data`);
36
+ };
37
+ /**
38
+ * Canonical JSON: recursively sorted keys, no whitespace, undefined members omitted; non-JSON values refused.
39
+ *
40
+ * Core has no canonical serializer, and `@effected/schemastore`'s `CanonicalJson` is a file formatter that keeps
41
+ * insertion order, so this one is the package's own.
42
+ *
43
+ * @internal
44
+ */
45
+ const canonicalJson = (value) => encode(value, 0);
46
+
47
+ //#endregion
48
+ export { canonicalJson };
@@ -0,0 +1,31 @@
1
+ import { ascii, matches } from "./bytes.js";
2
+ import { isAvifBrand } from "./avif.js";
3
+
4
+ //#region src/internal/detect.ts
5
+ const PNG = [
6
+ 137,
7
+ 80,
8
+ 78,
9
+ 71,
10
+ 13,
11
+ 10,
12
+ 26,
13
+ 10
14
+ ];
15
+ const JPEG = [
16
+ 255,
17
+ 216,
18
+ 255
19
+ ];
20
+ /** The format whose signature `bytes` starts with, or undefined. A partial signature is no match. */
21
+ const detectFormat = (b) => {
22
+ if (matches(b, 0, PNG)) return "png";
23
+ if (matches(b, 0, JPEG)) return "jpeg";
24
+ const gif = b.length >= 6 ? ascii(b, 0, 6) : "";
25
+ if (gif === "GIF87a" || gif === "GIF89a") return "gif";
26
+ if (b.length >= 12 && ascii(b, 0, 4) === "RIFF" && ascii(b, 8, 4) === "WEBP") return "webp";
27
+ if (isAvifBrand(b)) return "avif";
28
+ };
29
+
30
+ //#endregion
31
+ export { detectFormat };
@@ -0,0 +1,6 @@
1
+ //#region src/internal/digest.ts
2
+ /** A lowercase hex SHA-256 digest: the shape of every image cache key, and the only name a directory backend writes. */
3
+ const DIGEST_PATTERN = /^[0-9a-f]{64}$/u;
4
+
5
+ //#endregion
6
+ export { DIGEST_PATTERN };
@@ -0,0 +1,9 @@
1
+ import { u16le } from "./bytes.js";
2
+ import { dimensions, truncated } from "./result.js";
3
+
4
+ //#region src/internal/gif.ts
5
+ /** The logical screen descriptor follows the 6-byte signature: width(2 LE), height(2 LE). */
6
+ const readGif = (b) => b.length < 10 ? truncated(`need 10 bytes for the screen descriptor, have ${b.length}`) : dimensions(u16le(b, 6), u16le(b, 8));
7
+
8
+ //#endregion
9
+ export { readGif };
@@ -0,0 +1,41 @@
1
+ import { u16be } from "./bytes.js";
2
+ import { dimensions, malformed, truncated } from "./result.js";
3
+
4
+ //#region src/internal/jpeg.ts
5
+ /** Markers plus fill bytes the walk may consume before it gives up. Hostile input cannot loop past it. */
6
+ const JPEG_STEP_BUDGET = 4096;
7
+ /** SOFn frame headers: C0-CF except C4 (DHT), C8 (JPG) and CC (DAC). */
8
+ const isFrameHeader = (marker) => marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
9
+ /** Markers that carry no length field. */
10
+ const isStandalone = (marker) => marker === 1 || marker === 216 || marker >= 208 && marker <= 215;
11
+ const readJpeg = (b) => {
12
+ let offset = 2;
13
+ let steps = 0;
14
+ for (;;) {
15
+ if (++steps > 4096) return malformed("segment walk exceeded its step budget");
16
+ if (offset >= b.length) return truncated("ended before a frame header");
17
+ if (b[offset] !== 255) return malformed(`expected a marker at offset ${offset}`);
18
+ let at = offset + 1;
19
+ while (at < b.length && b[at] === 255) {
20
+ at++;
21
+ if (++steps > 4096) return malformed("segment walk exceeded its step budget");
22
+ }
23
+ if (at >= b.length) return truncated("ended inside a marker");
24
+ const marker = b[at];
25
+ offset = at + 1;
26
+ if (isStandalone(marker)) continue;
27
+ if (marker === 217 || marker === 218) return malformed("reached end of image or a scan before any frame header");
28
+ if (offset + 2 > b.length) return truncated("ended inside a segment length");
29
+ const length = u16be(b, offset);
30
+ if (length < 2) return malformed(`segment length ${length} is below 2`);
31
+ if (isFrameHeader(marker)) {
32
+ if (length < 11) return malformed(`frame header length ${length} is below 11`);
33
+ if (offset + 7 > b.length) return truncated("ended inside the frame header");
34
+ return dimensions(u16be(b, offset + 5), u16be(b, offset + 3));
35
+ }
36
+ offset += length;
37
+ }
38
+ };
39
+
40
+ //#endregion
41
+ export { JPEG_STEP_BUDGET, readJpeg };
@@ -0,0 +1,15 @@
1
+ import { ascii, u32be } from "./bytes.js";
2
+ import { dimensions, malformed, truncated } from "./result.js";
3
+
4
+ //#region src/internal/png.ts
5
+ /** IHDR is the first chunk: length(4)=13, type(4)="IHDR", width(4), height(4) after the 8-byte signature. */
6
+ const readPng = (b) => {
7
+ if (b.length < 24) return truncated(`need 24 bytes for IHDR dimensions, have ${b.length}`);
8
+ if (ascii(b, 12, 4) !== "IHDR") return malformed("first chunk is not IHDR");
9
+ const length = u32be(b, 8);
10
+ if (length !== 13) return malformed(`IHDR length ${length} is not 13`);
11
+ return dimensions(u32be(b, 16), u32be(b, 20));
12
+ };
13
+
14
+ //#endregion
15
+ export { readPng };
@@ -0,0 +1,21 @@
1
+ import { Result } from "effect";
2
+
3
+ //#region src/internal/result.ts
4
+ const MAX_DIMENSION = 2147483647;
5
+ const truncated = (detail) => Result.fail({
6
+ reason: "truncated",
7
+ detail
8
+ });
9
+ const malformed = (detail) => Result.fail({
10
+ reason: "malformed",
11
+ detail
12
+ });
13
+ const legal = (n) => Number.isInteger(n) && n >= 1 && n <= 2147483647;
14
+ /** Succeed with the pair, or fail `malformed` when either side is out of range. */
15
+ const dimensions = (width, height) => legal(width) && legal(height) ? Result.succeed({
16
+ width,
17
+ height
18
+ }) : malformed(`dimensions ${width}x${height} out of range 1..${MAX_DIMENSION}`);
19
+
20
+ //#endregion
21
+ export { MAX_DIMENSION, dimensions, malformed, truncated };
@@ -0,0 +1,33 @@
1
+ import { ascii, u16le, u24le } from "./bytes.js";
2
+ import { dimensions, malformed, truncated } from "./result.js";
3
+
4
+ //#region src/internal/webp.ts
5
+ const PAYLOAD = 20;
6
+ const readWebp = (b) => {
7
+ if (b.length < 16) return truncated("need the first chunk's fourcc");
8
+ const fourcc = ascii(b, 12, 4);
9
+ switch (fourcc) {
10
+ case "VP8 ":
11
+ if (b.length < 30) return truncated("need the VP8 frame header");
12
+ if (b[23] !== 157 || b[24] !== 1 || b[25] !== 42) return malformed("VP8 start code is not 9D 01 2A");
13
+ return dimensions(u16le(b, 26) & 16383, u16le(b, 28) & 16383);
14
+ case "VP8L": {
15
+ if (b.length < 25) return truncated("need the VP8L header");
16
+ if (b[PAYLOAD] !== 47) return malformed("VP8L signature byte is not 0x2F");
17
+ const b1 = b[21];
18
+ const b2 = b[22];
19
+ const b3 = b[23];
20
+ const b4 = b[24];
21
+ const width = 1 + ((b2 & 63) << 8 | b1);
22
+ const height = 1 + ((b4 & 15) << 10 | b3 << 2 | (b2 & 192) >> 6);
23
+ return dimensions(width, height);
24
+ }
25
+ case "VP8X":
26
+ if (b.length < 30) return truncated("need the VP8X canvas size");
27
+ return dimensions(1 + u24le(b, 24), 1 + u24le(b, 27));
28
+ default: return malformed(`unknown first WebP chunk ${JSON.stringify(fourcc)}`);
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ export { readWebp };
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
- "name": "@effected/images",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@effected/images",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "description": "Image facts (format, MIME type, dimensions) from bytes, and a generated-image cache, for Effect.",
6
+ "keywords": [
7
+ "image",
8
+ "dimensions",
9
+ "png",
10
+ "jpeg",
11
+ "webp",
12
+ "avif",
13
+ "cache",
14
+ "effect",
15
+ "effected"
16
+ ],
17
+ "homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/images#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/spencerbeggs/effected/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/spencerbeggs/effected.git",
24
+ "directory": "packages/images"
25
+ },
26
+ "license": "MIT",
27
+ "author": {
28
+ "name": "C. Spencer Beggs",
29
+ "email": "spencer@beggs.codes",
30
+ "url": "https://spencerbeg.gs"
31
+ },
32
+ "sideEffects": false,
33
+ "type": "module",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./index.d.ts",
37
+ "import": "./index.js",
38
+ "default": "./index.js"
39
+ },
40
+ "./cache": {
41
+ "types": "./cache.d.ts",
42
+ "import": "./cache.js",
43
+ "default": "./cache.js"
44
+ },
45
+ "./package.json": "./package.json"
46
+ },
47
+ "peerDependencies": {
48
+ "effect": "^4.0.0"
49
+ },
50
+ "engines": {
51
+ "node": ">=24.11.0"
52
+ }
53
+ }
@@ -0,0 +1,11 @@
1
+ // This file is read by tools that parse documentation comments conforming to the TSDoc standard.
2
+ // It should be published with your NPM package. It should not be tracked by Git.
3
+ {
4
+ "tsdocVersion": "0.12",
5
+ "toolPackages": [
6
+ {
7
+ "packageName": "@microsoft/api-extractor",
8
+ "packageVersion": "7.59.1"
9
+ }
10
+ ]
11
+ }