@hoardodile/sdk-types 0.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 +18 -0
- package/README.md +59 -0
- package/dist/image-variant.d.ts +90 -0
- package/dist/image-variant.js +115 -0
- package/dist/image-variant.js.map +1 -0
- package/dist/index.d.ts +772 -0
- package/dist/index.js +353 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest-Dk6_xyNy.d.ts +204 -0
- package/dist/media-exts.d.ts +92 -0
- package/dist/media-exts.js +160 -0
- package/dist/media-exts.js.map +1 -0
- package/dist/plugin-asset-limits.d.ts +16 -0
- package/dist/plugin-asset-limits.js +13 -0
- package/dist/plugin-asset-limits.js.map +1 -0
- package/dist/plugin-capabilities.d.ts +64 -0
- package/dist/plugin-capabilities.js +37 -0
- package/dist/plugin-capabilities.js.map +1 -0
- package/dist/plugin.d.ts +49 -0
- package/dist/plugin.js +12 -0
- package/dist/plugin.js.map +1 -0
- package/dist/resource.d.ts +26 -0
- package/dist/resource.js +9 -0
- package/dist/resource.js.map +1 -0
- package/dist/result.d.ts +47 -0
- package/dist/result.js +20 -0
- package/dist/result.js.map +1 -0
- package/dist/schema.d.ts +29 -0
- package/dist/schema.js +124 -0
- package/dist/schema.js.map +1 -0
- package/dist/template.d.ts +67 -0
- package/dist/template.js +137 -0
- package/dist/template.js.map +1 -0
- package/dist/text-limits.d.ts +11 -0
- package/dist/text-limits.js +7 -0
- package/dist/text-limits.js.map +1 -0
- package/package.json +102 -0
- package/src/file-list.ts +14 -0
- package/src/image-variant.test.ts +140 -0
- package/src/image-variant.ts +234 -0
- package/src/index.ts +115 -0
- package/src/manifest.ts +186 -0
- package/src/media-exts.ts +245 -0
- package/src/plugin-asset-limits.ts +23 -0
- package/src/plugin-asset.ts +127 -0
- package/src/plugin-capabilities.ts +91 -0
- package/src/plugin-definition.test.ts +117 -0
- package/src/plugin-definition.ts +902 -0
- package/src/plugin.ts +54 -0
- package/src/read-range.ts +12 -0
- package/src/resource.ts +28 -0
- package/src/result.test.ts +64 -0
- package/src/result.ts +73 -0
- package/src/schema.ts +29 -0
- package/src/template.test.ts +116 -0
- package/src/template.ts +199 -0
- package/src/text-limits.ts +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wooloo <ayan0312000@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @hoardodile/sdk-types
|
|
2
|
+
|
|
3
|
+
The plugin manifest contract and shared message/danmaku/anchor shapes —
|
|
4
|
+
the single source of truth for wire types consumed by every SDK package,
|
|
5
|
+
the host, and the app. Nothing here touches the DOM or node.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @hoardodile/sdk-types
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What's in it
|
|
14
|
+
|
|
15
|
+
The root entry is **zod-free** — plugin bundles that import it never
|
|
16
|
+
pull zod. Runtime validators live behind the
|
|
17
|
+
`@hoardodile/sdk-types/schema` subpath, used only by the host, the
|
|
18
|
+
server, and tooling.
|
|
19
|
+
|
|
20
|
+
- **`PluginSchema`** — the per-plugin schema interface: declare `file`,
|
|
21
|
+
`sourceMeta`, `searchMeta`, `anchor` once and both `definePlugin`
|
|
22
|
+
(`@hoardodile/sdk-server`) and the web API (`@hoardodile/sdk-web` /
|
|
23
|
+
`@hoardodile/sdk-react`) are typed from it
|
|
24
|
+
- **Plugin definition** — `PluginDefinition`/`ResourceAPI`/`definePlugin`/
|
|
25
|
+
fixtures/`HOOK_NAMES`, re-exported by `@hoardodile/sdk-server`
|
|
26
|
+
- **Wire shapes** — `Message`, `Danmaku`, `ResAnchor`, `AnchorData`,
|
|
27
|
+
`DanmakuMode`, `DanmakuListFilter`, `FileStats`, `SearchMeta`,
|
|
28
|
+
`SerializedFileList`, `ReadFileRange`
|
|
29
|
+
- **Template vocabulary** — the corner-template and anchor-chip
|
|
30
|
+
directives the host's template engine renders
|
|
31
|
+
- **`@hoardodile/sdk-types/schema`** — the zod layer: `pluginManifest`
|
|
32
|
+
(validated everywhere via its parse: server install, build CLI,
|
|
33
|
+
workbench), `pluginManifestId`, `anchorData`
|
|
34
|
+
- **Constants** (subpath-only, no root export):
|
|
35
|
+
- `@hoardodile/sdk-types/media-exts` — canonical media extension
|
|
36
|
+
sets: `IMAGE_EXTS`, `VIDEO_EXTS`, `AUDIO_EXTS`, extension ↔ MIME /
|
|
37
|
+
media-kind lookups
|
|
38
|
+
- `@hoardodile/sdk-types/plugin` — plugin runtime limits: read cap
|
|
39
|
+
(`PLUGIN_READ_FILE_MAX_BYTES`), probe/stat fan-out bounds, animation
|
|
40
|
+
scan batch
|
|
41
|
+
- `@hoardodile/sdk-types/resource` — resource caps: `SEARCH_META_VERSION`,
|
|
42
|
+
preview eligibility caps
|
|
43
|
+
- `@hoardodile/sdk-types/image-variant` — derived-image variant
|
|
44
|
+
contract: spec types, query parsing/encoding, canonical cache
|
|
45
|
+
identity
|
|
46
|
+
- `@hoardodile/sdk-types/template` — the host cover/message template
|
|
47
|
+
grammar (tokeniser, parser — shared with the web renderer and the
|
|
48
|
+
CLI's build-time lint)
|
|
49
|
+
- `@hoardodile/sdk-types/text-limits` — plugin input limits:
|
|
50
|
+
`MAX_DANMAKU_TEXT_LENGTH`, `MAX_COMMENT_BODY_LENGTH`
|
|
51
|
+
|
|
52
|
+
## When to import it
|
|
53
|
+
|
|
54
|
+
Usually only for types (`PluginSchema`, `PluginManifest`).
|
|
55
|
+
`@hoardodile/sdk-server` re-exports the plugin definition and the
|
|
56
|
+
message/danmaku shapes; `@hoardodile/sdk-web` re-exports the wire types
|
|
57
|
+
its consumers need — you rarely import this package directly in plugin
|
|
58
|
+
code. Reach for `/schema` only when you need a runtime validator (app
|
|
59
|
+
code, tooling).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
declare const IMAGE_VARIANT_FORMATS: readonly ["avif", "webp"];
|
|
2
|
+
type ImageVariantFormat = (typeof IMAGE_VARIANT_FORMATS)[number];
|
|
3
|
+
/**
|
|
4
|
+
* How the source image is fitted into the variant:
|
|
5
|
+
* - `inside` — downscaled (never upscaled) to fit within `maxArea`;
|
|
6
|
+
* - `exact` — transcode only: output pixels are exactly the source
|
|
7
|
+
* dimensions (needed when downstream code maps coordinates onto the
|
|
8
|
+
* texture, e.g. Live2D models).
|
|
9
|
+
*/
|
|
10
|
+
declare const IMAGE_VARIANT_FITS: readonly ["inside", "exact"];
|
|
11
|
+
type ImageVariantFit = (typeof IMAGE_VARIANT_FITS)[number];
|
|
12
|
+
/** Upper bound for the `area` query param (bounded cache-key space). */
|
|
13
|
+
declare const IMAGE_VARIANT_MAX_AREA = 1000000000;
|
|
14
|
+
/** Encode-quality range for the `q` query param. */
|
|
15
|
+
declare const IMAGE_VARIANT_MIN_QUALITY = 1;
|
|
16
|
+
declare const IMAGE_VARIANT_MAX_QUALITY = 100;
|
|
17
|
+
/**
|
|
18
|
+
* Client-declared variant request. Every field is optional; omitted
|
|
19
|
+
* fields fall back to the defaults (`format` avif, `fit` inside,
|
|
20
|
+
* `maxArea` {@link RESOURCE_PREVIEW_MAX_AREA}, per-format quality).
|
|
21
|
+
* `maxArea` only has an effect when `fit` is `inside`; it is carried
|
|
22
|
+
* through (and cached under) regardless so URL identity stays stable.
|
|
23
|
+
*/
|
|
24
|
+
type ImageVariantSpec = {
|
|
25
|
+
readonly format?: ImageVariantFormat;
|
|
26
|
+
readonly fit?: ImageVariantFit;
|
|
27
|
+
readonly maxArea?: number;
|
|
28
|
+
readonly quality?: number;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* A fully resolved variant: every field concrete, including the
|
|
32
|
+
* per-format encode qualities the pipeline consumes. This is the render
|
|
33
|
+
* boundary shape and the input to {@link imageVariantCanonical}.
|
|
34
|
+
*/
|
|
35
|
+
type ResolvedImageVariant = {
|
|
36
|
+
readonly format: ImageVariantFormat;
|
|
37
|
+
readonly fit: ImageVariantFit;
|
|
38
|
+
readonly maxArea: number;
|
|
39
|
+
readonly webpQuality: number;
|
|
40
|
+
readonly avifQuality: number;
|
|
41
|
+
};
|
|
42
|
+
/** Query parameters accepted by the resource file route. */
|
|
43
|
+
type ImageVariantQuery = {
|
|
44
|
+
readonly size?: string;
|
|
45
|
+
readonly fmt?: string;
|
|
46
|
+
readonly fit?: string;
|
|
47
|
+
readonly area?: string | number;
|
|
48
|
+
readonly q?: string | number;
|
|
49
|
+
};
|
|
50
|
+
type ImageVariantParseResult = {
|
|
51
|
+
readonly kind: "none";
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: "variant";
|
|
54
|
+
readonly spec: ImageVariantSpec;
|
|
55
|
+
} | {
|
|
56
|
+
readonly kind: "invalid";
|
|
57
|
+
readonly reason: string;
|
|
58
|
+
};
|
|
59
|
+
declare function isImageVariantFormat(value: string): value is ImageVariantFormat;
|
|
60
|
+
declare function isImageVariantFit(value: string): value is ImageVariantFit;
|
|
61
|
+
/**
|
|
62
|
+
* Interpret the file route's query. A variant is requested when
|
|
63
|
+
* `size=preview` (the compatibility alias) or any variant parameter is
|
|
64
|
+
* present; otherwise the route serves the original bytes.
|
|
65
|
+
* Returns `invalid` for out-of-range or malformed values.
|
|
66
|
+
*/
|
|
67
|
+
declare function parseImageVariantQuery(query: ImageVariantQuery): ImageVariantParseResult;
|
|
68
|
+
/**
|
|
69
|
+
* Fill the request defaults and clamp out-of-range values. `quality` is
|
|
70
|
+
* the single client knob; it maps onto both per-format pipeline
|
|
71
|
+
* qualities, which default to the caller's constants when omitted.
|
|
72
|
+
*/
|
|
73
|
+
declare function normalizeImageVariantSpec(spec: ImageVariantSpec, qualityDefaults: {
|
|
74
|
+
readonly avifQuality: number;
|
|
75
|
+
readonly webpQuality: number;
|
|
76
|
+
}): ResolvedImageVariant;
|
|
77
|
+
/**
|
|
78
|
+
* Stable string identity of a resolved variant — the cache key input.
|
|
79
|
+
* Two requests produce the same identity iff they render identically.
|
|
80
|
+
*/
|
|
81
|
+
declare function imageVariantCanonical(variant: ResolvedImageVariant): string;
|
|
82
|
+
/**
|
|
83
|
+
* Encode a variant request as the file route's query string. Always
|
|
84
|
+
* carries `size=preview` so an older server that does not know the
|
|
85
|
+
* variant parameters still degrades to its default preview instead of
|
|
86
|
+
* serving the original bytes.
|
|
87
|
+
*/
|
|
88
|
+
declare function imageVariantQuery(spec: ImageVariantSpec): string;
|
|
89
|
+
|
|
90
|
+
export { IMAGE_VARIANT_FITS, IMAGE_VARIANT_FORMATS, IMAGE_VARIANT_MAX_AREA, IMAGE_VARIANT_MAX_QUALITY, IMAGE_VARIANT_MIN_QUALITY, type ImageVariantFit, type ImageVariantFormat, type ImageVariantParseResult, type ImageVariantQuery, type ImageVariantSpec, type ResolvedImageVariant, imageVariantCanonical, imageVariantQuery, isImageVariantFit, isImageVariantFormat, normalizeImageVariantSpec, parseImageVariantQuery };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/resource.ts
|
|
2
|
+
var RESOURCE_PREVIEW_MAX_AREA = 4e6;
|
|
3
|
+
|
|
4
|
+
// src/image-variant.ts
|
|
5
|
+
var IMAGE_VARIANT_FORMATS = ["avif", "webp"];
|
|
6
|
+
var IMAGE_VARIANT_FITS = ["inside", "exact"];
|
|
7
|
+
var IMAGE_VARIANT_MAX_AREA = 1e9;
|
|
8
|
+
var IMAGE_VARIANT_MIN_QUALITY = 1;
|
|
9
|
+
var IMAGE_VARIANT_MAX_QUALITY = 100;
|
|
10
|
+
function isImageVariantFormat(value) {
|
|
11
|
+
return IMAGE_VARIANT_FORMATS.some((format) => format === value);
|
|
12
|
+
}
|
|
13
|
+
function isImageVariantFit(value) {
|
|
14
|
+
return IMAGE_VARIANT_FITS.some((fit) => fit === value);
|
|
15
|
+
}
|
|
16
|
+
function parseImageVariantQuery(query) {
|
|
17
|
+
const requested = query.size === "preview" || query.fmt !== void 0 || query.fit !== void 0 || query.area !== void 0 || query.q !== void 0;
|
|
18
|
+
if (!requested) return { kind: "none" };
|
|
19
|
+
let format;
|
|
20
|
+
let fit;
|
|
21
|
+
let maxArea;
|
|
22
|
+
let quality;
|
|
23
|
+
if (query.fmt !== void 0) {
|
|
24
|
+
if (!isImageVariantFormat(query.fmt)) {
|
|
25
|
+
return invalidFormat(query.fmt);
|
|
26
|
+
}
|
|
27
|
+
format = query.fmt;
|
|
28
|
+
}
|
|
29
|
+
if (query.fit !== void 0) {
|
|
30
|
+
if (!isImageVariantFit(query.fit)) {
|
|
31
|
+
return invalidFit(query.fit);
|
|
32
|
+
}
|
|
33
|
+
fit = query.fit;
|
|
34
|
+
}
|
|
35
|
+
if (query.area !== void 0) {
|
|
36
|
+
const area = parseVariantInteger(query.area, IMAGE_VARIANT_MAX_AREA);
|
|
37
|
+
if (area === void 0) return invalidArea(query.area);
|
|
38
|
+
maxArea = area;
|
|
39
|
+
}
|
|
40
|
+
if (query.q !== void 0) {
|
|
41
|
+
const q = parseVariantInteger(query.q, IMAGE_VARIANT_MAX_QUALITY);
|
|
42
|
+
if (q === void 0) return invalidQuality(query.q);
|
|
43
|
+
quality = q;
|
|
44
|
+
}
|
|
45
|
+
return { kind: "variant", spec: { format, fit, maxArea, quality } };
|
|
46
|
+
}
|
|
47
|
+
function invalidFormat(value) {
|
|
48
|
+
return {
|
|
49
|
+
kind: "invalid",
|
|
50
|
+
reason: `invalid format "${value}" (expected ${IMAGE_VARIANT_FORMATS.join(", ")})`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function invalidFit(value) {
|
|
54
|
+
return {
|
|
55
|
+
kind: "invalid",
|
|
56
|
+
reason: `invalid fit "${value}" (expected ${IMAGE_VARIANT_FITS.join(", ")})`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function invalidArea(value) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "invalid",
|
|
62
|
+
reason: `area must be an integer between 1 and ${IMAGE_VARIANT_MAX_AREA} (got "${String(value)}")`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function invalidQuality(value) {
|
|
66
|
+
return {
|
|
67
|
+
kind: "invalid",
|
|
68
|
+
reason: `quality must be an integer between ${IMAGE_VARIANT_MIN_QUALITY} and ${IMAGE_VARIANT_MAX_QUALITY} (got "${String(value)}")`
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function parseVariantInteger(value, max) {
|
|
72
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
73
|
+
if (!Number.isInteger(num)) return void 0;
|
|
74
|
+
if (num < 1 || num > max) return void 0;
|
|
75
|
+
return num;
|
|
76
|
+
}
|
|
77
|
+
function normalizeImageVariantSpec(spec, qualityDefaults) {
|
|
78
|
+
return {
|
|
79
|
+
format: spec.format ?? "avif",
|
|
80
|
+
fit: spec.fit ?? "inside",
|
|
81
|
+
maxArea: clampArea(spec.maxArea ?? RESOURCE_PREVIEW_MAX_AREA),
|
|
82
|
+
avifQuality: clampQuality(spec.quality ?? qualityDefaults.avifQuality),
|
|
83
|
+
webpQuality: clampQuality(spec.quality ?? qualityDefaults.webpQuality)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function clampArea(value) {
|
|
87
|
+
return Math.min(Math.max(1, Math.round(value)), IMAGE_VARIANT_MAX_AREA);
|
|
88
|
+
}
|
|
89
|
+
function clampQuality(value) {
|
|
90
|
+
return Math.min(
|
|
91
|
+
Math.max(IMAGE_VARIANT_MIN_QUALITY, Math.round(value)),
|
|
92
|
+
IMAGE_VARIANT_MAX_QUALITY
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
function imageVariantCanonical(variant) {
|
|
96
|
+
return [
|
|
97
|
+
variant.format,
|
|
98
|
+
variant.fit,
|
|
99
|
+
variant.maxArea,
|
|
100
|
+
variant.webpQuality,
|
|
101
|
+
variant.avifQuality
|
|
102
|
+
].join(":");
|
|
103
|
+
}
|
|
104
|
+
function imageVariantQuery(spec) {
|
|
105
|
+
const params = ["size=preview"];
|
|
106
|
+
if (spec.format !== void 0) params.push(`fmt=${spec.format}`);
|
|
107
|
+
if (spec.fit !== void 0) params.push(`fit=${spec.fit}`);
|
|
108
|
+
if (spec.maxArea !== void 0) params.push(`area=${spec.maxArea}`);
|
|
109
|
+
if (spec.quality !== void 0) params.push(`q=${spec.quality}`);
|
|
110
|
+
return params.join("&");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { IMAGE_VARIANT_FITS, IMAGE_VARIANT_FORMATS, IMAGE_VARIANT_MAX_AREA, IMAGE_VARIANT_MAX_QUALITY, IMAGE_VARIANT_MIN_QUALITY, imageVariantCanonical, imageVariantQuery, isImageVariantFit, isImageVariantFormat, normalizeImageVariantSpec, parseImageVariantQuery };
|
|
114
|
+
//# sourceMappingURL=image-variant.js.map
|
|
115
|
+
//# sourceMappingURL=image-variant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resource.ts","../src/image-variant.ts"],"names":[],"mappings":"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","file":"image-variant.js","sourcesContent":["/**\n * Plugin-facing resource caps. The preview policy (`exceedsPreviewThresholds`,\n * which consumes the two caps below) lives with its caller in\n * `@hoardodile/sdk-server/helpers`; the cover cap below is consumed by\n * the app's thumb pipeline, the CLI's workbench renders and the media\n * helpers in `@hoardodile/host`. Character image-area caps stay\n * app-internal in `@hoardodile/shared`.\n */\n\n/**\n * Schema version stamped onto every `SearchMeta` payload. Plugins\n * that build search-meta MUST emit this exact value so the host can\n * detect format drift across plugin upgrades.\n */\nexport const SEARCH_META_VERSION = 1\n\n/** Max pixel area for resource covers; larger images are scaled down. */\nexport const RESOURCE_COVER_MAX_AREA = 300_000\n\n/** Max pixel area for preview variants served to resource previews. */\nexport const RESOURCE_PREVIEW_MAX_AREA = 4_000_000\n\n/**\n * Byte-size threshold for preview eligibility. An image whose\n * area is at or below the cap may still qualify for preview when\n * its byte size exceeds this value.\n */\nexport const RESOURCE_PREVIEW_SIZE_THRESHOLD = 1_000_000\n","/**\n * Image variant contract: how a plugin asks the host to derive an image\n * from a resource file, and how that request travels on the wire.\n *\n * One {@link ImageVariantSpec} has three projections, all derived from\n * this module:\n *\n * - the URL query (`imageVariantQuery`) the iframe client emits,\n * - the resolved render spec (`normalizeImageVariantSpec`) the\n * pipelines execute,\n * - the canonical cache identity (`imageVariantCanonical`) that keys\n * the on-disk caches.\n *\n * The per-format encode qualities are pipeline constants, not part of\n * the wire contract — callers fill them in at the render boundary, so\n * this module stays dependency-free and browser-safe.\n */\nimport { RESOURCE_PREVIEW_MAX_AREA } from \"./resource.ts\"\n\nexport const IMAGE_VARIANT_FORMATS = [\"avif\", \"webp\"] as const\nexport type ImageVariantFormat = (typeof IMAGE_VARIANT_FORMATS)[number]\n\n/**\n * How the source image is fitted into the variant:\n * - `inside` — downscaled (never upscaled) to fit within `maxArea`;\n * - `exact` — transcode only: output pixels are exactly the source\n * dimensions (needed when downstream code maps coordinates onto the\n * texture, e.g. Live2D models).\n */\nexport const IMAGE_VARIANT_FITS = [\"inside\", \"exact\"] as const\nexport type ImageVariantFit = (typeof IMAGE_VARIANT_FITS)[number]\n\n/** Upper bound for the `area` query param (bounded cache-key space). */\nexport const IMAGE_VARIANT_MAX_AREA = 1_000_000_000\n\n/** Encode-quality range for the `q` query param. */\nexport const IMAGE_VARIANT_MIN_QUALITY = 1\nexport const IMAGE_VARIANT_MAX_QUALITY = 100\n\n/**\n * Client-declared variant request. Every field is optional; omitted\n * fields fall back to the defaults (`format` avif, `fit` inside,\n * `maxArea` {@link RESOURCE_PREVIEW_MAX_AREA}, per-format quality).\n * `maxArea` only has an effect when `fit` is `inside`; it is carried\n * through (and cached under) regardless so URL identity stays stable.\n */\nexport type ImageVariantSpec = {\n\treadonly format?: ImageVariantFormat\n\treadonly fit?: ImageVariantFit\n\treadonly maxArea?: number\n\treadonly quality?: number\n}\n\n/**\n * A fully resolved variant: every field concrete, including the\n * per-format encode qualities the pipeline consumes. This is the render\n * boundary shape and the input to {@link imageVariantCanonical}.\n */\nexport type ResolvedImageVariant = {\n\treadonly format: ImageVariantFormat\n\treadonly fit: ImageVariantFit\n\treadonly maxArea: number\n\treadonly webpQuality: number\n\treadonly avifQuality: number\n}\n\n/** Query parameters accepted by the resource file route. */\nexport type ImageVariantQuery = {\n\treadonly size?: string\n\treadonly fmt?: string\n\treadonly fit?: string\n\treadonly area?: string | number\n\treadonly q?: string | number\n}\n\nexport type ImageVariantParseResult =\n\t| { readonly kind: \"none\" }\n\t| { readonly kind: \"variant\"; readonly spec: ImageVariantSpec }\n\t| { readonly kind: \"invalid\"; readonly reason: string }\n\nexport function isImageVariantFormat(\n\tvalue: string,\n): value is ImageVariantFormat {\n\treturn IMAGE_VARIANT_FORMATS.some((format) => format === value)\n}\n\nexport function isImageVariantFit(value: string): value is ImageVariantFit {\n\treturn IMAGE_VARIANT_FITS.some((fit) => fit === value)\n}\n\n/**\n * Interpret the file route's query. A variant is requested when\n * `size=preview` (the compatibility alias) or any variant parameter is\n * present; otherwise the route serves the original bytes.\n * Returns `invalid` for out-of-range or malformed values.\n */\nexport function parseImageVariantQuery(\n\tquery: ImageVariantQuery,\n): ImageVariantParseResult {\n\tconst requested =\n\t\tquery.size === \"preview\" ||\n\t\tquery.fmt !== undefined ||\n\t\tquery.fit !== undefined ||\n\t\tquery.area !== undefined ||\n\t\tquery.q !== undefined\n\tif (!requested) return { kind: \"none\" }\n\n\tlet format: ImageVariantFormat | undefined\n\tlet fit: ImageVariantFit | undefined\n\tlet maxArea: number | undefined\n\tlet quality: number | undefined\n\tif (query.fmt !== undefined) {\n\t\tif (!isImageVariantFormat(query.fmt)) {\n\t\t\treturn invalidFormat(query.fmt)\n\t\t}\n\t\tformat = query.fmt\n\t}\n\tif (query.fit !== undefined) {\n\t\tif (!isImageVariantFit(query.fit)) {\n\t\t\treturn invalidFit(query.fit)\n\t\t}\n\t\tfit = query.fit\n\t}\n\tif (query.area !== undefined) {\n\t\tconst area = parseVariantInteger(query.area, IMAGE_VARIANT_MAX_AREA)\n\t\tif (area === undefined) return invalidArea(query.area)\n\t\tmaxArea = area\n\t}\n\tif (query.q !== undefined) {\n\t\tconst q = parseVariantInteger(query.q, IMAGE_VARIANT_MAX_QUALITY)\n\t\tif (q === undefined) return invalidQuality(query.q)\n\t\tquality = q\n\t}\n\treturn { kind: \"variant\", spec: { format, fit, maxArea, quality } }\n}\n\nfunction invalidFormat(value: string): ImageVariantParseResult {\n\treturn {\n\t\tkind: \"invalid\",\n\t\treason: `invalid format \"${value}\" (expected ${IMAGE_VARIANT_FORMATS.join(\", \")})`,\n\t}\n}\n\nfunction invalidFit(value: string): ImageVariantParseResult {\n\treturn {\n\t\tkind: \"invalid\",\n\t\treason: `invalid fit \"${value}\" (expected ${IMAGE_VARIANT_FITS.join(\", \")})`,\n\t}\n}\n\nfunction invalidArea(value: unknown): ImageVariantParseResult {\n\treturn {\n\t\tkind: \"invalid\",\n\t\treason: `area must be an integer between 1 and ${IMAGE_VARIANT_MAX_AREA} (got \"${String(value)}\")`,\n\t}\n}\n\nfunction invalidQuality(value: unknown): ImageVariantParseResult {\n\treturn {\n\t\tkind: \"invalid\",\n\t\treason: `quality must be an integer between ${IMAGE_VARIANT_MIN_QUALITY} and ${IMAGE_VARIANT_MAX_QUALITY} (got \"${String(value)}\")`,\n\t}\n}\n\nfunction parseVariantInteger(\n\tvalue: string | number,\n\tmax: number,\n): number | undefined {\n\tconst num = typeof value === \"number\" ? value : Number(value)\n\tif (!Number.isInteger(num)) return undefined\n\tif (num < 1 || num > max) return undefined\n\treturn num\n}\n\n/**\n * Fill the request defaults and clamp out-of-range values. `quality` is\n * the single client knob; it maps onto both per-format pipeline\n * qualities, which default to the caller's constants when omitted.\n */\nexport function normalizeImageVariantSpec(\n\tspec: ImageVariantSpec,\n\tqualityDefaults: {\n\t\treadonly avifQuality: number\n\t\treadonly webpQuality: number\n\t},\n): ResolvedImageVariant {\n\treturn {\n\t\tformat: spec.format ?? \"avif\",\n\t\tfit: spec.fit ?? \"inside\",\n\t\tmaxArea: clampArea(spec.maxArea ?? RESOURCE_PREVIEW_MAX_AREA),\n\t\tavifQuality: clampQuality(spec.quality ?? qualityDefaults.avifQuality),\n\t\twebpQuality: clampQuality(spec.quality ?? qualityDefaults.webpQuality),\n\t}\n}\n\nfunction clampArea(value: number): number {\n\treturn Math.min(Math.max(1, Math.round(value)), IMAGE_VARIANT_MAX_AREA)\n}\n\nfunction clampQuality(value: number): number {\n\treturn Math.min(\n\t\tMath.max(IMAGE_VARIANT_MIN_QUALITY, Math.round(value)),\n\t\tIMAGE_VARIANT_MAX_QUALITY,\n\t)\n}\n\n/**\n * Stable string identity of a resolved variant — the cache key input.\n * Two requests produce the same identity iff they render identically.\n */\nexport function imageVariantCanonical(variant: ResolvedImageVariant): string {\n\treturn [\n\t\tvariant.format,\n\t\tvariant.fit,\n\t\tvariant.maxArea,\n\t\tvariant.webpQuality,\n\t\tvariant.avifQuality,\n\t].join(\":\")\n}\n\n/**\n * Encode a variant request as the file route's query string. Always\n * carries `size=preview` so an older server that does not know the\n * variant parameters still degrades to its default preview instead of\n * serving the original bytes.\n */\nexport function imageVariantQuery(spec: ImageVariantSpec): string {\n\tconst params: string[] = [\"size=preview\"]\n\tif (spec.format !== undefined) params.push(`fmt=${spec.format}`)\n\tif (spec.fit !== undefined) params.push(`fit=${spec.fit}`)\n\tif (spec.maxArea !== undefined) params.push(`area=${spec.maxArea}`)\n\tif (spec.quality !== undefined) params.push(`q=${spec.quality}`)\n\treturn params.join(\"&\")\n}\n"]}
|