@giveitsmaller/sdk 0.15.0 → 0.17.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/README.md +21 -0
- package/dist/_audit.js +15 -2
- package/dist/client.d.ts +27 -4
- package/dist/client.js +46 -4
- package/dist/ergonomic/image_output_routes.d.ts +89 -0
- package/dist/ergonomic/image_output_routes.js +155 -0
- package/dist/ergonomic/option_types.d.ts +147 -0
- package/dist/ergonomic/option_types.js +57 -0
- package/dist/ergonomic/option_validation.d.ts +58 -0
- package/dist/ergonomic/option_validation.js +143 -0
- package/dist/ergonomic/preset_resolver.d.ts +7 -6
- package/dist/ergonomic/preset_resolver.js +43 -39
- package/dist/ergonomic/presets/document_pdf_compress.d.ts +3 -5
- package/dist/ergonomic/presets/document_pdf_compress.js +10 -12
- package/dist/ergonomic/presets/image_compress.d.ts +4 -11
- package/dist/ergonomic/presets/image_compress.js +12 -25
- package/dist/ergonomic/presets/index.d.ts +21 -1
- package/dist/ergonomic/presets/index.js +4 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +34 -0
- package/dist/file-first.d.ts +241 -49
- package/dist/file-first.js +803 -249
- package/dist/generated/sdk_spec/enums.d.ts +8 -46
- package/dist/generated/sdk_spec/enums.js +12 -33
- package/dist/generated/sdk_spec/presets.js +9 -20
- package/dist/generated/sdk_spec/version.d.ts +3 -3
- package/dist/generated/sdk_spec/version.js +3 -3
- package/dist/gisl.d.ts +8 -1
- package/dist/gisl.js +12 -0
- package/dist/handle.js +9 -1
- package/dist/index.core.d.ts +7 -4
- package/dist/index.core.js +8 -1
- package/dist/types.d.ts +21 -0
- package/package.json +3 -3
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed per-op option interfaces for the ergonomic verbs (card Dhje3Faq).
|
|
3
|
+
* These replace the untyped `Record<string, unknown>` bags so the IDE can offer
|
|
4
|
+
* key completion and `tsc` rejects typos. The KEY SET of each interface is pinned
|
|
5
|
+
* to the contract two ways: (1) the source-level `Equal<...>` assertions below tie
|
|
6
|
+
* each interface to its `*_OPTION_KEYS` tuple at `tsc` time; (2) the wire-key
|
|
7
|
+
* conformance guard ties each tuple (∪ positional-owned) to the generated
|
|
8
|
+
* `OperationMetadata` at test time. Value types are best-effort (per-value/enum
|
|
9
|
+
* sync is out of scope — keys are the contract anchor).
|
|
10
|
+
*
|
|
11
|
+
* Keys a verb owns via a positional argument are EXCLUDED from its interface
|
|
12
|
+
* (`output_format` on convert, `text` on textWatermark) — they are set by the
|
|
13
|
+
* first argument and rejected if supplied in the bag (see `option_validation.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Mirrored by the PHP array-shape docblocks — keep in lockstep.
|
|
16
|
+
*/
|
|
17
|
+
const CONVERT_OPTION_KEYS = [
|
|
18
|
+
'quality', 'background', 'crf', 'trim_start', 'trim_end', 'fps', 'width',
|
|
19
|
+
'max_colors', 'loop', 'dither', 'bitrate', 'pages', 'dpi',
|
|
20
|
+
];
|
|
21
|
+
const THUMBNAIL_OPTION_KEYS = [
|
|
22
|
+
'width', 'height', 'fit', 'format', 'quality', 'timestamp', 'source', 'page',
|
|
23
|
+
];
|
|
24
|
+
const TEXT_WATERMARK_OPTION_KEYS = [
|
|
25
|
+
'font_size', 'color', 'font_family', 'rotation', 'watermark_mode',
|
|
26
|
+
'tile_spacing', 'anchor', 'margin_x', 'margin_y', 'opacity',
|
|
27
|
+
];
|
|
28
|
+
const WATERMARK_OPTION_KEYS = [
|
|
29
|
+
'anchor', 'margin_x', 'margin_y', 'opacity', 'overlay_width',
|
|
30
|
+
];
|
|
31
|
+
const OUTPUT_OPTION_KEYS = [
|
|
32
|
+
'quality', 'width', 'height', 'fit', 'background', 'progressive',
|
|
33
|
+
'optimization_level', 'avif_speed', 'metadata', 'lossless', 'lossy',
|
|
34
|
+
];
|
|
35
|
+
const _convertKeysMatch = true;
|
|
36
|
+
const _thumbnailKeysMatch = true;
|
|
37
|
+
const _textWatermarkKeysMatch = true;
|
|
38
|
+
const _watermarkKeysMatch = true;
|
|
39
|
+
const _outputKeysMatch = true;
|
|
40
|
+
// Reference the assertions so `noUnusedLocals` doesn't strip them.
|
|
41
|
+
void _convertKeysMatch;
|
|
42
|
+
void _thumbnailKeysMatch;
|
|
43
|
+
void _textWatermarkKeysMatch;
|
|
44
|
+
void _watermarkKeysMatch;
|
|
45
|
+
void _outputKeysMatch;
|
|
46
|
+
/**
|
|
47
|
+
* The user-supplyable option keys per verb (excludes positional-owned keys).
|
|
48
|
+
* Exported for the wire-key conformance guard, which asserts each tuple ∪ its
|
|
49
|
+
* positional-owned keys equals the contract `operationOptionKeys(metadata)`.
|
|
50
|
+
*/
|
|
51
|
+
export const VERB_OPTION_KEYS = {
|
|
52
|
+
convert: CONVERT_OPTION_KEYS,
|
|
53
|
+
thumbnail: THUMBNAIL_OPTION_KEYS,
|
|
54
|
+
textWatermark: TEXT_WATERMARK_OPTION_KEYS,
|
|
55
|
+
watermark: WATERMARK_OPTION_KEYS,
|
|
56
|
+
output: OUTPUT_OPTION_KEYS,
|
|
57
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type OperationMetadata } from '@giveitsmaller/contracts/operations';
|
|
2
|
+
/**
|
|
3
|
+
* Eager, synchronous, PRE-UPLOAD option-key validation for the ergonomic verbs
|
|
4
|
+
* (card Dhje3Faq). The file-first builders accept verb options as untyped bags;
|
|
5
|
+
* a user typo (`{ quaity: 80 }`) would otherwise flow to the server and 422.
|
|
6
|
+
* This module rejects unknown keys at the verb call — before any upload —
|
|
7
|
+
* mirroring the existing `compress()` optimize-check and the watermark eager
|
|
8
|
+
* gate. The allowed key set is read from the generated `OperationMetadata`
|
|
9
|
+
* sidecars (the same contract-anchored source the wire-key conformance guard
|
|
10
|
+
* uses), so it can never silently drift from the contract.
|
|
11
|
+
*
|
|
12
|
+
* SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
|
|
13
|
+
* is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
|
|
14
|
+
* (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
|
|
15
|
+
* `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
|
|
16
|
+
* own `unknown_field` validation through the preset resolver.
|
|
17
|
+
*
|
|
18
|
+
* Mirrored by the PHP `OptionValidation` helper — keep the two in lockstep.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* OPERATION-LEVEL contract option keys (the keys valid in `OperationDef.options`):
|
|
22
|
+
* the union of every mime group's `options` plus `direct_options` for
|
|
23
|
+
* media-agnostic ops. Deliberately EXCLUDES `per_input_options` (valid only on a
|
|
24
|
+
* merge input). Promoted from the wire-key conformance guard for runtime reuse.
|
|
25
|
+
*/
|
|
26
|
+
export declare function operationOptionKeys(metadata: OperationMetadata): ReadonlySet<string>;
|
|
27
|
+
/** The ergonomic verbs whose option bags this module key-validates. */
|
|
28
|
+
export type ValidatedVerb = 'convert' | 'thumbnail' | 'textWatermark' | 'watermark' | 'output';
|
|
29
|
+
/** Accessor for the conformance guard (pins these sets to the contract metadata). */
|
|
30
|
+
export declare function allowedKeysFor(verb: ValidatedVerb): ReadonlySet<string>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a USER-supplied options bag for an ergonomic verb. Throws
|
|
33
|
+
* {@link GislConfigError} (reason `unknown_field`) synchronously, BEFORE any
|
|
34
|
+
* upload or wire-key injection. Call this at the TOP of every verb body, before
|
|
35
|
+
* the `format`-drop / `output_format` / `text` injection.
|
|
36
|
+
*
|
|
37
|
+
* @throws {GislConfigError} reason `unknown_field` when the bag carries a key the
|
|
38
|
+
* verb owns via a positional argument, or a key absent from the op's contract
|
|
39
|
+
* option set.
|
|
40
|
+
*/
|
|
41
|
+
export declare function validateVerbOptions(verb: ValidatedVerb, options: object | null | undefined): void;
|
|
42
|
+
/**
|
|
43
|
+
* Assert thumbnail `width` AND `height` are both present and non-nullish (the
|
|
44
|
+
* contract marks both `required` for image/video/document). The typed signature
|
|
45
|
+
* already enforces this at compile time; this RUNTIME guard catches JS callers and
|
|
46
|
+
* an explicit `undefined`/`null` BEFORE upload. Rejecting `null` (not just
|
|
47
|
+
* `undefined`) keeps TS in lockstep with the PHP `assertThumbnailDimensions`, which
|
|
48
|
+
* must reject `null` because PHP drops null values pre-lower — so a `null` dimension
|
|
49
|
+
* is a pre-upload error in BOTH languages, never a wire `null` that 422s. Mirrored
|
|
50
|
+
* in PHP.
|
|
51
|
+
*
|
|
52
|
+
* @throws {GislConfigError} reason `missing_required_field` naming the absent
|
|
53
|
+
* dimension(s) in `conflictingFields`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function assertThumbnailDimensions(options: {
|
|
56
|
+
width?: unknown;
|
|
57
|
+
height?: unknown;
|
|
58
|
+
} | null | undefined): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { convertMetadata, thumbnailMetadata, textWatermarkMetadata, imageWatermarkMetadata, videoWatermarkMetadata, } from '@giveitsmaller/contracts/operations';
|
|
2
|
+
import { GislConfigError } from '../errors.js';
|
|
3
|
+
import { VERB_OPTION_KEYS } from './option_types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Eager, synchronous, PRE-UPLOAD option-key validation for the ergonomic verbs
|
|
6
|
+
* (card Dhje3Faq). The file-first builders accept verb options as untyped bags;
|
|
7
|
+
* a user typo (`{ quaity: 80 }`) would otherwise flow to the server and 422.
|
|
8
|
+
* This module rejects unknown keys at the verb call — before any upload —
|
|
9
|
+
* mirroring the existing `compress()` optimize-check and the watermark eager
|
|
10
|
+
* gate. The allowed key set is read from the generated `OperationMetadata`
|
|
11
|
+
* sidecars (the same contract-anchored source the wire-key conformance guard
|
|
12
|
+
* uses), so it can never silently drift from the contract.
|
|
13
|
+
*
|
|
14
|
+
* SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
|
|
15
|
+
* is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
|
|
16
|
+
* (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
|
|
17
|
+
* `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
|
|
18
|
+
* own `unknown_field` validation through the preset resolver.
|
|
19
|
+
*
|
|
20
|
+
* Mirrored by the PHP `OptionValidation` helper — keep the two in lockstep.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* OPERATION-LEVEL contract option keys (the keys valid in `OperationDef.options`):
|
|
24
|
+
* the union of every mime group's `options` plus `direct_options` for
|
|
25
|
+
* media-agnostic ops. Deliberately EXCLUDES `per_input_options` (valid only on a
|
|
26
|
+
* merge input). Promoted from the wire-key conformance guard for runtime reuse.
|
|
27
|
+
*/
|
|
28
|
+
export function operationOptionKeys(metadata) {
|
|
29
|
+
const keys = new Set();
|
|
30
|
+
for (const group of Object.values(metadata.mime_groups)) {
|
|
31
|
+
for (const k of Object.keys(group.options))
|
|
32
|
+
keys.add(k);
|
|
33
|
+
}
|
|
34
|
+
for (const k of Object.keys(metadata.direct_options ?? {}))
|
|
35
|
+
keys.add(k);
|
|
36
|
+
return keys;
|
|
37
|
+
}
|
|
38
|
+
function union(...sets) {
|
|
39
|
+
const out = new Set();
|
|
40
|
+
for (const set of sets)
|
|
41
|
+
for (const k of set)
|
|
42
|
+
out.add(k);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The contract option-key set per validated verb (the GENERIC allowed set).
|
|
47
|
+
* `watermark` is the UNION of `image_watermark` + `video_watermark` because the
|
|
48
|
+
* base media may be undetectable at the `.watermark()` call; the existing
|
|
49
|
+
* media-routing gate still rejects the wrong route by media.
|
|
50
|
+
*/
|
|
51
|
+
const ALLOWED_KEYS = {
|
|
52
|
+
convert: operationOptionKeys(convertMetadata),
|
|
53
|
+
thumbnail: operationOptionKeys(thumbnailMetadata),
|
|
54
|
+
textWatermark: operationOptionKeys(textWatermarkMetadata),
|
|
55
|
+
watermark: union(operationOptionKeys(imageWatermarkMetadata), operationOptionKeys(videoWatermarkMetadata)),
|
|
56
|
+
// `output` is the image Output facade — its allowed keys are the UNION of every
|
|
57
|
+
// image route's honored+planned options (the image-output-routes projection),
|
|
58
|
+
// INCLUDING `output_format` (in every cell's honored set) which — like `convert`
|
|
59
|
+
// — is in the allowed set but rejected first by the positional-owned guard. This
|
|
60
|
+
// is the COARSE static gate (reject keys no image route ever honors, e.g. a video
|
|
61
|
+
// `crf`); the precise per-route honored/planned narrowing happens in the
|
|
62
|
+
// `output()` lowering (`resolveOutputRoute`). Pinned to the projection union by
|
|
63
|
+
// the output-route conformance test.
|
|
64
|
+
output: new Set([...VERB_OPTION_KEYS.output, 'output_format']),
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Keys a verb OWNS via a positional argument: a user must not also supply them
|
|
68
|
+
* in the options bag (they would be silently overridden by the positional). The
|
|
69
|
+
* guard runs BEFORE the generic check so these get a specific, actionable
|
|
70
|
+
* message rather than the generic "unknown option" one. `format` is an SDK alias
|
|
71
|
+
* for the positional (not a contract key) and is owned too.
|
|
72
|
+
*/
|
|
73
|
+
const POSITIONAL_OWNED = {
|
|
74
|
+
convert: ['output_format', 'format'],
|
|
75
|
+
textWatermark: ['text'],
|
|
76
|
+
// `output(format, …)` sets the target format via its first argument; the wire
|
|
77
|
+
// key `output_format` and the SDK alias `format` must not be supplied in the bag.
|
|
78
|
+
output: ['output_format', 'format'],
|
|
79
|
+
};
|
|
80
|
+
/** Accessor for the conformance guard (pins these sets to the contract metadata). */
|
|
81
|
+
export function allowedKeysFor(verb) {
|
|
82
|
+
return ALLOWED_KEYS[verb];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate a USER-supplied options bag for an ergonomic verb. Throws
|
|
86
|
+
* {@link GislConfigError} (reason `unknown_field`) synchronously, BEFORE any
|
|
87
|
+
* upload or wire-key injection. Call this at the TOP of every verb body, before
|
|
88
|
+
* the `format`-drop / `output_format` / `text` injection.
|
|
89
|
+
*
|
|
90
|
+
* @throws {GislConfigError} reason `unknown_field` when the bag carries a key the
|
|
91
|
+
* verb owns via a positional argument, or a key absent from the op's contract
|
|
92
|
+
* option set.
|
|
93
|
+
*/
|
|
94
|
+
export function validateVerbOptions(verb, options) {
|
|
95
|
+
// The typed signatures require an options object, but an untyped JS caller can
|
|
96
|
+
// still omit it (e.g. `thumbnail()` — which dropped its `= {}` default when
|
|
97
|
+
// width/height became required). A nullish bag has no keys to reject; the
|
|
98
|
+
// separate `assertThumbnailDimensions` then reports the missing dimensions as a
|
|
99
|
+
// clean GislConfigError rather than a raw TypeError.
|
|
100
|
+
if (options === null || options === undefined)
|
|
101
|
+
return;
|
|
102
|
+
const owned = POSITIONAL_OWNED[verb];
|
|
103
|
+
if (owned !== undefined) {
|
|
104
|
+
for (const key of owned) {
|
|
105
|
+
if (Object.prototype.hasOwnProperty.call(options, key)) {
|
|
106
|
+
const arg = verb === 'textWatermark' ? 'text' : 'output format';
|
|
107
|
+
throw new GislConfigError(`${verb}() sets the ${arg} via its first argument; remove '${key}' from the options bag.`, { reason: 'unknown_field', conflictingFields: [key] });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const allowed = ALLOWED_KEYS[verb];
|
|
112
|
+
for (const key of Object.keys(options)) {
|
|
113
|
+
if (!allowed.has(key)) {
|
|
114
|
+
throw new GislConfigError(`${verb}: unknown option '${key}'. Valid options: ${[...allowed].sort().join(', ')}.`, { reason: 'unknown_field', conflictingFields: [key] });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Assert thumbnail `width` AND `height` are both present and non-nullish (the
|
|
120
|
+
* contract marks both `required` for image/video/document). The typed signature
|
|
121
|
+
* already enforces this at compile time; this RUNTIME guard catches JS callers and
|
|
122
|
+
* an explicit `undefined`/`null` BEFORE upload. Rejecting `null` (not just
|
|
123
|
+
* `undefined`) keeps TS in lockstep with the PHP `assertThumbnailDimensions`, which
|
|
124
|
+
* must reject `null` because PHP drops null values pre-lower — so a `null` dimension
|
|
125
|
+
* is a pre-upload error in BOTH languages, never a wire `null` that 422s. Mirrored
|
|
126
|
+
* in PHP.
|
|
127
|
+
*
|
|
128
|
+
* @throws {GislConfigError} reason `missing_required_field` naming the absent
|
|
129
|
+
* dimension(s) in `conflictingFields`.
|
|
130
|
+
*/
|
|
131
|
+
export function assertThumbnailDimensions(options) {
|
|
132
|
+
// Null-safe so an untyped JS `thumbnail()` (no args, no default) reports both
|
|
133
|
+
// dimensions missing as a clean GislConfigError instead of a raw TypeError.
|
|
134
|
+
const o = options ?? {};
|
|
135
|
+
const missing = [];
|
|
136
|
+
if (o.width === undefined || o.width === null)
|
|
137
|
+
missing.push('width');
|
|
138
|
+
if (o.height === undefined || o.height === null)
|
|
139
|
+
missing.push('height');
|
|
140
|
+
if (missing.length > 0) {
|
|
141
|
+
throw new GislConfigError(`thumbnail requires both width and height (the contract marks both required); missing: ${missing.join(', ')}.`, { reason: 'missing_required_field', conflictingFields: missing });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -2,13 +2,14 @@ import type { ResolvedOptions } from '../builder.js';
|
|
|
2
2
|
import type { OptimizeFor } from '../generated/sdk_spec/enums.js';
|
|
3
3
|
import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/index.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* The preset matrix version emitted on every resolve. Re-exported from the
|
|
6
|
+
* GENERATED `sdk_spec/version.ts` (source of truth: contracts
|
|
7
|
+
* `sdk-spec/version.yaml` `presetVersion`) so it can NEVER drift from the
|
|
8
|
+
* generated preset cells — a regen that bumps the cells bumps this by
|
|
9
|
+
* construction. Previously a hand-typed literal that the v2.73.0 regen had to
|
|
10
|
+
* bump manually (yREs0srv).
|
|
10
11
|
*/
|
|
11
|
-
export declare const PRESET_VERSION
|
|
12
|
+
export declare const PRESET_VERSION: "1.4";
|
|
12
13
|
/**
|
|
13
14
|
* Inputs to {@link resolveCompressOptions}. `media` selects which leaf
|
|
14
15
|
* DTO drives sdkDefault + clientDefault lookups + invalid-combo
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// `quality: 100`, `codec: 'h264'`, …).
|
|
22
22
|
//
|
|
23
23
|
// Validation runs AFTER merge so post-merge-only invariants
|
|
24
|
-
// (targetSize + codec
|
|
25
|
-
//
|
|
24
|
+
// (e.g. targetSize + codec on video) catch combinations where
|
|
25
|
+
// the explicit knob and a layered default disagree. Resolver
|
|
26
26
|
// throws `GislConfigError` BEFORE any network round-trip, with a
|
|
27
27
|
// `resolvedSnapshot` showing the would-have-been-sent payload.
|
|
28
28
|
//
|
|
@@ -36,33 +36,33 @@
|
|
|
36
36
|
// to the wire; conversely if `crf` is explicit, `encoding_mode='crf'`.
|
|
37
37
|
import { sha256Hex } from '../sha256.js';
|
|
38
38
|
import { GislConfigError } from '../errors.js';
|
|
39
|
-
import {
|
|
39
|
+
import { PRESET_VERSION as GENERATED_PRESET_VERSION } from '../generated/sdk_spec/version.js';
|
|
40
|
+
import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* The preset matrix version emitted on every resolve. Re-exported from the
|
|
43
|
+
* GENERATED `sdk_spec/version.ts` (source of truth: contracts
|
|
44
|
+
* `sdk-spec/version.yaml` `presetVersion`) so it can NEVER drift from the
|
|
45
|
+
* generated preset cells — a regen that bumps the cells bumps this by
|
|
46
|
+
* construction. Previously a hand-typed literal that the v2.73.0 regen had to
|
|
47
|
+
* bump manually (yREs0srv).
|
|
46
48
|
*/
|
|
47
|
-
export const PRESET_VERSION =
|
|
49
|
+
export const PRESET_VERSION = GENERATED_PRESET_VERSION;
|
|
48
50
|
// ---------------------------------------------------------------------------
|
|
49
51
|
// Wire-field alias map (declarative — NOT generic toSnakeCase).
|
|
50
52
|
// ---------------------------------------------------------------------------
|
|
51
53
|
//
|
|
52
54
|
// Lifted from docs/plans/sdk-ergonomics/plan.md §11a. Maps camelCase
|
|
53
55
|
// ergonomic-DTO field names to their snake_case wire counterparts.
|
|
54
|
-
// Fields whose ergonomic name IS the wire name (`
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// `
|
|
58
|
-
// safe path.
|
|
56
|
+
// Fields whose ergonomic name IS the wire name (`quality`, `codec`,
|
|
57
|
+
// …) are NOT in this map — `applyAlias` returns them unchanged. A
|
|
58
|
+
// generic snake-case regex would mistranslate acronym/camel names
|
|
59
|
+
// (e.g. an `outputFormat` → `output_format` rename or an acronym like
|
|
60
|
+
// the former `iccProfile`); the declarative map is the only safe path.
|
|
59
61
|
const WIRE_ALIASES = Object.freeze({
|
|
60
|
-
iccProfile: 'icc_profile',
|
|
61
62
|
outputFormat: 'output_format',
|
|
62
63
|
sampleRate: 'sample_rate',
|
|
63
64
|
audioCodec: 'audio_codec',
|
|
64
65
|
audioBitrate: 'audio_bitrate',
|
|
65
|
-
flattenForms: 'flatten_forms',
|
|
66
66
|
imageQuality: 'image_quality',
|
|
67
67
|
stripMacros: 'strip_macros',
|
|
68
68
|
stripHiddenData: 'strip_hidden_data',
|
|
@@ -199,10 +199,10 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
|
|
|
199
199
|
if (op !== 'compress')
|
|
200
200
|
return undefined;
|
|
201
201
|
// Each overload returns `<Specific>PresetOptions | undefined`. We
|
|
202
|
-
// erase the per-media type at runtime by
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
202
|
+
// erase the per-media type at runtime by reducing the instance to a
|
|
203
|
+
// plain Record. `cellFor` returns `undefined` when no delta was
|
|
204
|
+
// registered for the tuple — the resolver treats that the same as
|
|
205
|
+
// "this layer did not participate."
|
|
206
206
|
let cell;
|
|
207
207
|
switch (media) {
|
|
208
208
|
case 'image':
|
|
@@ -229,7 +229,16 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
|
|
|
229
229
|
}
|
|
230
230
|
if (cell === undefined)
|
|
231
231
|
return undefined;
|
|
232
|
-
|
|
232
|
+
// Reduce to a SPARSE camelCase record, dropping undefined-valued keys.
|
|
233
|
+
// The leaf DTO's field DECLARATIONS define every field as an
|
|
234
|
+
// own-enumerable `undefined` property under `useDefineForClassFields`
|
|
235
|
+
// (ES2022) even when the ctor skipped the assignment — a naive spread
|
|
236
|
+
// would carry those `undefined`s into the presetConfigHash input,
|
|
237
|
+
// diverging from PHP's sparse `leafToRecord` (SVQcoR1K). `mergeLayer`
|
|
238
|
+
// already ignores `undefined`, so this only affects the hash path.
|
|
239
|
+
// Reuses the same helper `PresetDefaults.merge` uses for this exact
|
|
240
|
+
// `useDefineForClassFields` problem.
|
|
241
|
+
return definedFieldsOf(cell);
|
|
233
242
|
}
|
|
234
243
|
// ---------------------------------------------------------------------------
|
|
235
244
|
// presetOverrides type-mismatch detection
|
|
@@ -241,10 +250,10 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
|
|
|
241
250
|
// for clearly-typed plain objects whose key set only intersects with a
|
|
242
251
|
// non-matching media.
|
|
243
252
|
const MEDIA_FIELDS = Object.freeze({
|
|
244
|
-
image: new Set(['
|
|
253
|
+
image: new Set(['quality', 'metadata', 'outputFormat']),
|
|
245
254
|
audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
|
|
246
255
|
video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
|
|
247
|
-
document_pdf: new Set(['profile', '
|
|
256
|
+
document_pdf: new Set(['profile', 'grayscale']),
|
|
248
257
|
document_office: new Set(['imageQuality', 'stripMacros', 'stripHiddenData', 'stripUnusedFonts']),
|
|
249
258
|
document_odf: new Set(['imageQuality', 'stripMetadata', 'stripUnusedStyles']),
|
|
250
259
|
document_epub: new Set(['imageQuality', 'fontSubsetting', 'stripUnusedCss']),
|
|
@@ -310,10 +319,10 @@ function mergeLayer(acc, layer, source) {
|
|
|
310
319
|
// can pin this hand-maintained allowlist to the generated contract metadata: every
|
|
311
320
|
// field the resolver may emit MUST be a real contract option key for `compress`.
|
|
312
321
|
export const KNOWN_WIRE_FIELDS = Object.freeze({
|
|
313
|
-
image: new Set(['
|
|
322
|
+
image: new Set(['quality', 'metadata', 'output_format']),
|
|
314
323
|
audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
|
|
315
324
|
video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
|
|
316
|
-
document_pdf: new Set(['profile', '
|
|
325
|
+
document_pdf: new Set(['profile', 'grayscale']),
|
|
317
326
|
document_office: new Set(['image_quality', 'strip_macros', 'strip_hidden_data', 'strip_unused_fonts']),
|
|
318
327
|
document_odf: new Set(['image_quality', 'strip_metadata', 'strip_unused_styles']),
|
|
319
328
|
document_epub: new Set(['image_quality', 'font_subsetting', 'strip_unused_css']),
|
|
@@ -335,19 +344,6 @@ function validateMerged(media, merged, explicitKeys, winners) {
|
|
|
335
344
|
});
|
|
336
345
|
}
|
|
337
346
|
}
|
|
338
|
-
// Image: `mode: Lossless` + `quality` set is invalid per the wire
|
|
339
|
-
// contract (`depends_on: { mode: lossy }`). Runs on post-merge so a
|
|
340
|
-
// caller passing explicit `quality` and inheriting `mode=Lossless`
|
|
341
|
-
// from a client preset is caught.
|
|
342
|
-
if (media === 'image' && merged.mode === 'lossless' && merged.quality !== undefined) {
|
|
343
|
-
const snapshot = { ...merged };
|
|
344
|
-
throw new GislConfigError(`Image compress: 'quality' is ignored when 'mode' is Lossless — passing both is a configuration bug.`, {
|
|
345
|
-
reason: 'missing_dependency',
|
|
346
|
-
conflictingFields: ['quality', 'mode'],
|
|
347
|
-
resolvedSnapshot: Object.freeze(snapshot),
|
|
348
|
-
suggestion: "Either drop 'quality' for lossless output, or set 'mode' to Lossy.",
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
347
|
// Video: targetSize-derived encoding_mode='target_size' is only
|
|
352
348
|
// valid for H264 today. Catch the combination post-merge — explicit
|
|
353
349
|
// codec overrides a layered default and either resolution must end
|
|
@@ -406,7 +402,15 @@ function canonicalJson(value) {
|
|
|
406
402
|
return '[' + value.map(canonicalJson).join(',') + ']';
|
|
407
403
|
}
|
|
408
404
|
const record = value;
|
|
409
|
-
|
|
405
|
+
// Skip undefined-valued keys (mirror `JSON.stringify` object semantics,
|
|
406
|
+
// which omit undefined members). Without this, `canonicalJson(undefined)`
|
|
407
|
+
// would serialise the literal token `undefined` into the hash input — a
|
|
408
|
+
// latent foot-gun. The registered-cell records are already sparse (see
|
|
409
|
+
// `presetDefaultsCellRecord`), so this is defense-in-depth; the
|
|
410
|
+
// override-path anchors carry no undefined and are unaffected (SVQcoR1K).
|
|
411
|
+
const keys = Object.keys(record)
|
|
412
|
+
.filter((k) => record[k] !== undefined)
|
|
413
|
+
.sort();
|
|
410
414
|
return ('{' +
|
|
411
415
|
keys
|
|
412
416
|
.map((k) => JSON.stringify(k) + ':' + canonicalJson(record[k]))
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { PdfProfile,
|
|
1
|
+
import { PdfProfile, OptimizeFor } from '../../generated/sdk_spec/enums.js';
|
|
2
2
|
export interface DocumentPdfCompressPresetOptionsInput {
|
|
3
3
|
readonly profile?: PdfProfile;
|
|
4
|
-
readonly
|
|
5
|
-
readonly flattenForms?: boolean;
|
|
4
|
+
readonly grayscale?: boolean;
|
|
6
5
|
}
|
|
7
6
|
export declare class DocumentPdfCompressPresetOptions {
|
|
8
7
|
readonly profile?: PdfProfile;
|
|
9
|
-
readonly
|
|
10
|
-
readonly flattenForms?: boolean;
|
|
8
|
+
readonly grayscale?: boolean;
|
|
11
9
|
private constructor();
|
|
12
10
|
static from(input: DocumentPdfCompressPresetOptionsInput): DocumentPdfCompressPresetOptions;
|
|
13
11
|
static shippedDefaultsFor(level: OptimizeFor): DocumentPdfCompressPresetOptions;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
// T4a — DocumentPdfCompressPresetOptions leaf DTO.
|
|
2
2
|
//
|
|
3
|
-
// Field set
|
|
4
|
-
//
|
|
3
|
+
// Field set (2): profile, grayscale — the worker-honored stable PDF controls
|
|
4
|
+
// (contracts v2.96.0 Acrobat-PDF realignment Lw1LseYr). The earlier
|
|
5
|
+
// {profile, colorspace, flattenForms} set was retired: colorspace + flatten_forms
|
|
6
|
+
// are `planned` (not read by the worker) so presets never emit them, and
|
|
7
|
+
// `image_dpi` + `pages` are per-call knobs, not preset cells.
|
|
5
8
|
import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
|
|
6
9
|
import { translateEnum } from './_translate.js';
|
|
7
10
|
export class DocumentPdfCompressPresetOptions {
|
|
8
11
|
profile;
|
|
9
|
-
|
|
10
|
-
flattenForms;
|
|
12
|
+
grayscale;
|
|
11
13
|
constructor(input) {
|
|
12
14
|
if (input.profile !== undefined)
|
|
13
15
|
this.profile = input.profile;
|
|
14
|
-
if (input.
|
|
15
|
-
this.
|
|
16
|
-
if (input.flattenForms !== undefined)
|
|
17
|
-
this.flattenForms = input.flattenForms;
|
|
16
|
+
if (input.grayscale !== undefined)
|
|
17
|
+
this.grayscale = input.grayscale;
|
|
18
18
|
Object.freeze(this);
|
|
19
19
|
}
|
|
20
20
|
static from(input) {
|
|
@@ -26,10 +26,8 @@ export class DocumentPdfCompressPresetOptions {
|
|
|
26
26
|
const mut = input;
|
|
27
27
|
if ('profile' in cell)
|
|
28
28
|
mut.profile = translateEnum('PdfProfile', cell.profile);
|
|
29
|
-
if ('
|
|
30
|
-
mut.
|
|
31
|
-
if ('flattenForms' in cell)
|
|
32
|
-
mut.flattenForms = cell.flattenForms;
|
|
29
|
+
if ('grayscale' in cell)
|
|
30
|
+
mut.grayscale = cell.grayscale;
|
|
33
31
|
return new DocumentPdfCompressPresetOptions(input);
|
|
34
32
|
}
|
|
35
33
|
}
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ImageMetadataPolicy, ImageFormat, OptimizeFor } from '../../generated/sdk_spec/enums.js';
|
|
2
2
|
export interface ImageCompressPresetOptionsInput {
|
|
3
|
-
readonly mode?: ImageMode;
|
|
4
3
|
readonly quality?: number;
|
|
5
4
|
readonly metadata?: ImageMetadataPolicy;
|
|
6
|
-
readonly iccProfile?: IccProfilePolicy;
|
|
7
|
-
readonly progressive?: boolean;
|
|
8
5
|
readonly outputFormat?: ImageFormat;
|
|
9
6
|
}
|
|
10
7
|
export declare class ImageCompressPresetOptions {
|
|
11
|
-
readonly mode?: ImageMode;
|
|
12
8
|
readonly quality?: number;
|
|
13
9
|
readonly metadata?: ImageMetadataPolicy;
|
|
14
|
-
readonly iccProfile?: IccProfilePolicy;
|
|
15
|
-
readonly progressive?: boolean;
|
|
16
10
|
readonly outputFormat?: ImageFormat;
|
|
17
11
|
private constructor();
|
|
18
12
|
/**
|
|
@@ -26,10 +20,9 @@ export declare class ImageCompressPresetOptions {
|
|
|
26
20
|
* the given level. Reads the F3 PRESETS matrix and translates member
|
|
27
21
|
* names to wire backing values.
|
|
28
22
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `quality`, so shipping a default would mislead callers.
|
|
23
|
+
* Since the v2.80.0 honesty pass the worker is lossy-only, so every
|
|
24
|
+
* level ships a concrete `quality` (Size 65 / Balanced 80 / Quality 92),
|
|
25
|
+
* `metadata: All`, and `outputFormat: Original`.
|
|
33
26
|
*/
|
|
34
27
|
static shippedDefaultsFor(level: OptimizeFor): ImageCompressPresetOptions;
|
|
35
28
|
}
|
|
@@ -7,32 +7,26 @@
|
|
|
7
7
|
// values ARE the wire backing values — so the leaf DTO is wire-compatible
|
|
8
8
|
// once the resolver (T4b) snake_cases the property names.
|
|
9
9
|
//
|
|
10
|
-
// Field set (
|
|
11
|
-
// (
|
|
12
|
-
// `
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// Field set (3) per contracts v2.80.0 compress.image honesty pass (Option B,
|
|
11
|
+
// lossy-only): image = (quality, metadata, outputFormat).
|
|
12
|
+
// `mode` + `iccProfile` were REMOVED — the worker is lossy-only and always
|
|
13
|
+
// strips metadata, so advertising a lossless mode or ICC-profile policy was
|
|
14
|
+
// an over-claim. `progressive` is still a per-JPEG wire option but is no
|
|
15
|
+
// longer carried in the preset cell. `width`/`height`/`fit`/`autoOrient`
|
|
16
|
+
// were removed earlier — the image-compress worker never resized (resize-fit
|
|
17
|
+
// lives on thumbnail/convert; video keeps its own fit). Per-call knobs are
|
|
18
|
+
// deliberately excluded — they belong on the per-call argument shape.
|
|
16
19
|
import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
|
|
17
20
|
import { translateEnum } from './_translate.js';
|
|
18
21
|
export class ImageCompressPresetOptions {
|
|
19
|
-
mode;
|
|
20
22
|
quality;
|
|
21
23
|
metadata;
|
|
22
|
-
iccProfile;
|
|
23
|
-
progressive;
|
|
24
24
|
outputFormat;
|
|
25
25
|
constructor(input) {
|
|
26
|
-
if (input.mode !== undefined)
|
|
27
|
-
this.mode = input.mode;
|
|
28
26
|
if (input.quality !== undefined)
|
|
29
27
|
this.quality = input.quality;
|
|
30
28
|
if (input.metadata !== undefined)
|
|
31
29
|
this.metadata = input.metadata;
|
|
32
|
-
if (input.iccProfile !== undefined)
|
|
33
|
-
this.iccProfile = input.iccProfile;
|
|
34
|
-
if (input.progressive !== undefined)
|
|
35
|
-
this.progressive = input.progressive;
|
|
36
30
|
if (input.outputFormat !== undefined)
|
|
37
31
|
this.outputFormat = input.outputFormat;
|
|
38
32
|
Object.freeze(this);
|
|
@@ -50,25 +44,18 @@ export class ImageCompressPresetOptions {
|
|
|
50
44
|
* the given level. Reads the F3 PRESETS matrix and translates member
|
|
51
45
|
* names to wire backing values.
|
|
52
46
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `quality`, so shipping a default would mislead callers.
|
|
47
|
+
* Since the v2.80.0 honesty pass the worker is lossy-only, so every
|
|
48
|
+
* level ships a concrete `quality` (Size 65 / Balanced 80 / Quality 92),
|
|
49
|
+
* `metadata: All`, and `outputFormat: Original`.
|
|
57
50
|
*/
|
|
58
51
|
static shippedDefaultsFor(level) {
|
|
59
52
|
const cell = f3ShippedDefaultsFor('image_compress', level);
|
|
60
53
|
const input = {};
|
|
61
54
|
const mut = input;
|
|
62
|
-
if ('mode' in cell)
|
|
63
|
-
mut.mode = translateEnum('ImageMode', cell.mode);
|
|
64
55
|
if ('quality' in cell)
|
|
65
56
|
mut.quality = cell.quality;
|
|
66
57
|
if ('metadata' in cell)
|
|
67
58
|
mut.metadata = translateEnum('ImageMetadataPolicy', cell.metadata);
|
|
68
|
-
if ('iccProfile' in cell)
|
|
69
|
-
mut.iccProfile = translateEnum('IccProfilePolicy', cell.iccProfile);
|
|
70
|
-
if ('progressive' in cell)
|
|
71
|
-
mut.progressive = cell.progressive;
|
|
72
59
|
if ('outputFormat' in cell)
|
|
73
60
|
mut.outputFormat = translateEnum('ImageFormat', cell.outputFormat);
|
|
74
61
|
return new ImageCompressPresetOptions(input);
|
|
@@ -13,7 +13,7 @@ export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptions
|
|
|
13
13
|
export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
|
|
14
14
|
export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
|
|
15
15
|
export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
|
|
16
|
-
export { OptimizeFor,
|
|
16
|
+
export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
|
|
17
17
|
/** Supported media×op pairs for preset cells in T4a. Compress-only. */
|
|
18
18
|
export type PresetMedia = 'image' | 'audio' | 'video' | 'document_pdf' | 'document_office' | 'document_odf' | 'document_epub';
|
|
19
19
|
export type PresetOp = 'compress';
|
|
@@ -23,6 +23,26 @@ export type PresetOp = 'compress';
|
|
|
23
23
|
* narrows the return automatically via the overload set below.
|
|
24
24
|
*/
|
|
25
25
|
export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentPdfCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
|
|
26
|
+
/**
|
|
27
|
+
* Per-cell field-merge: parent fields ⊕ child fields where defined.
|
|
28
|
+
* Re-construct the leaf DTO via the matching `<LeafClass>.from(merged)`
|
|
29
|
+
* call so the result is a freshly-frozen `*PresetOptions` instance —
|
|
30
|
+
* NOT a mutated reference into either input. Used by
|
|
31
|
+
* {@link PresetDefaults.merge} when both parent and child registered
|
|
32
|
+
* the same `(cellKey, level)` tuple.
|
|
33
|
+
*
|
|
34
|
+
* `definedFieldsOf` filters undefined values out of each instance
|
|
35
|
+
* BEFORE the merge: with TS `useDefineForClassFields` (the ES2022
|
|
36
|
+
* default), `readonly outputFormat?: ImageFormat` declarations initialise
|
|
37
|
+
* the field as an enumerable own property with value `undefined` BEFORE
|
|
38
|
+
* the ctor body runs. A naive `Object.assign({}, parent, child)`
|
|
39
|
+
* therefore lets child's `undefined` overwrite parent's defined value
|
|
40
|
+
* — caught by CI on PR #125 first run. Filter-then-spread restores
|
|
41
|
+
* the documented merge-not-replace semantics.
|
|
42
|
+
*
|
|
43
|
+
* @internal
|
|
44
|
+
*/
|
|
45
|
+
export declare function definedFieldsOf<T extends object>(opts: T): Partial<Record<string, unknown>>;
|
|
26
46
|
export declare class PresetDefaults {
|
|
27
47
|
private readonly cells;
|
|
28
48
|
private constructor();
|