@image-marker/web 0.1.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.
Files changed (124) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +100 -0
  3. package/api-contract.json +224 -0
  4. package/lib/commonjs/core/batch.d.ts +32 -0
  5. package/lib/commonjs/core/batch.js +85 -0
  6. package/lib/commonjs/core/content-credentials.d.ts +53 -0
  7. package/lib/commonjs/core/content-credentials.js +96 -0
  8. package/lib/commonjs/core/image-info.d.ts +30 -0
  9. package/lib/commonjs/core/image-info.js +348 -0
  10. package/lib/commonjs/core/index.d.ts +1014 -0
  11. package/lib/commonjs/core/index.js +92 -0
  12. package/lib/commonjs/core/invisible-watermark.d.ts +102 -0
  13. package/lib/commonjs/core/invisible-watermark.js +913 -0
  14. package/lib/commonjs/core/job.d.ts +57 -0
  15. package/lib/commonjs/core/job.js +151 -0
  16. package/lib/commonjs/core/layout.d.ts +19 -0
  17. package/lib/commonjs/core/layout.js +75 -0
  18. package/lib/commonjs/core/marker.d.ts +1 -0
  19. package/lib/commonjs/core/marker.js +11 -0
  20. package/lib/commonjs/core/recipe.d.ts +44 -0
  21. package/lib/commonjs/core/recipe.js +175 -0
  22. package/lib/commonjs/core/result.d.ts +34 -0
  23. package/lib/commonjs/core/result.js +41 -0
  24. package/lib/commonjs/core/trace-runtime.d.ts +43 -0
  25. package/lib/commonjs/core/trace-runtime.js +92 -0
  26. package/lib/commonjs/core/validate.d.ts +4 -0
  27. package/lib/commonjs/core/validate.js +192 -0
  28. package/lib/commonjs/core/web/browser.d.ts +105 -0
  29. package/lib/commonjs/core/web/browser.js +434 -0
  30. package/lib/commonjs/core/web/helpers.d.ts +41 -0
  31. package/lib/commonjs/core/web/helpers.js +264 -0
  32. package/lib/commonjs/core/web/index.d.ts +59 -0
  33. package/lib/commonjs/core/web/index.js +724 -0
  34. package/lib/commonjs/core/web/invisible-worker-client.d.ts +13 -0
  35. package/lib/commonjs/core/web/invisible-worker-client.js +140 -0
  36. package/lib/commonjs/core/web/renderer.d.ts +20 -0
  37. package/lib/commonjs/core/web/renderer.js +665 -0
  38. package/lib/commonjs/core/web/runtime.d.ts +106 -0
  39. package/lib/commonjs/core/web/runtime.js +2 -0
  40. package/lib/commonjs/editor/adapter.d.ts +26 -0
  41. package/lib/commonjs/editor/adapter.js +102 -0
  42. package/lib/commonjs/editor/controller.d.ts +106 -0
  43. package/lib/commonjs/editor/controller.js +919 -0
  44. package/lib/commonjs/editor/core-contract.d.ts +2 -0
  45. package/lib/commonjs/editor/core-contract.js +23 -0
  46. package/lib/commonjs/editor/geometry.d.ts +8 -0
  47. package/lib/commonjs/editor/geometry.js +109 -0
  48. package/lib/commonjs/editor/headless.d.ts +5 -0
  49. package/lib/commonjs/editor/headless.js +22 -0
  50. package/lib/commonjs/editor/presets.d.ts +19 -0
  51. package/lib/commonjs/editor/presets.js +64 -0
  52. package/lib/commonjs/editor/projection.d.ts +12 -0
  53. package/lib/commonjs/editor/projection.js +195 -0
  54. package/lib/commonjs/editor/types.d.ts +166 -0
  55. package/lib/commonjs/editor/types.js +2 -0
  56. package/lib/commonjs/editor-adapter.d.ts +7 -0
  57. package/lib/commonjs/editor-adapter.js +17 -0
  58. package/lib/commonjs/headless.d.ts +2 -0
  59. package/lib/commonjs/headless.js +26 -0
  60. package/lib/commonjs/index.d.ts +5 -0
  61. package/lib/commonjs/index.js +27 -0
  62. package/lib/commonjs/package.json +1 -0
  63. package/lib/module/core/batch.d.ts +32 -0
  64. package/lib/module/core/batch.js +82 -0
  65. package/lib/module/core/content-credentials.d.ts +53 -0
  66. package/lib/module/core/content-credentials.js +92 -0
  67. package/lib/module/core/image-info.d.ts +30 -0
  68. package/lib/module/core/image-info.js +343 -0
  69. package/lib/module/core/index.d.ts +1014 -0
  70. package/lib/module/core/index.js +56 -0
  71. package/lib/module/core/invisible-watermark.d.ts +102 -0
  72. package/lib/module/core/invisible-watermark.js +897 -0
  73. package/lib/module/core/job.d.ts +57 -0
  74. package/lib/module/core/job.js +143 -0
  75. package/lib/module/core/layout.d.ts +19 -0
  76. package/lib/module/core/layout.js +70 -0
  77. package/lib/module/core/marker.d.ts +1 -0
  78. package/lib/module/core/marker.js +4 -0
  79. package/lib/module/core/recipe.d.ts +44 -0
  80. package/lib/module/core/recipe.js +169 -0
  81. package/lib/module/core/result.d.ts +34 -0
  82. package/lib/module/core/result.js +38 -0
  83. package/lib/module/core/trace-runtime.d.ts +43 -0
  84. package/lib/module/core/trace-runtime.js +89 -0
  85. package/lib/module/core/validate.d.ts +4 -0
  86. package/lib/module/core/validate.js +187 -0
  87. package/lib/module/core/web/browser.d.ts +105 -0
  88. package/lib/module/core/web/browser.js +427 -0
  89. package/lib/module/core/web/helpers.d.ts +41 -0
  90. package/lib/module/core/web/helpers.js +251 -0
  91. package/lib/module/core/web/index.d.ts +59 -0
  92. package/lib/module/core/web/index.js +709 -0
  93. package/lib/module/core/web/invisible-worker-client.d.ts +13 -0
  94. package/lib/module/core/web/invisible-worker-client.js +137 -0
  95. package/lib/module/core/web/renderer.d.ts +20 -0
  96. package/lib/module/core/web/renderer.js +661 -0
  97. package/lib/module/core/web/runtime.d.ts +106 -0
  98. package/lib/module/core/web/runtime.js +1 -0
  99. package/lib/module/editor/adapter.d.ts +26 -0
  100. package/lib/module/editor/adapter.js +99 -0
  101. package/lib/module/editor/controller.d.ts +106 -0
  102. package/lib/module/editor/controller.js +915 -0
  103. package/lib/module/editor/core-contract.d.ts +2 -0
  104. package/lib/module/editor/core-contract.js +2 -0
  105. package/lib/module/editor/geometry.d.ts +8 -0
  106. package/lib/module/editor/geometry.js +103 -0
  107. package/lib/module/editor/headless.d.ts +5 -0
  108. package/lib/module/editor/headless.js +4 -0
  109. package/lib/module/editor/presets.d.ts +19 -0
  110. package/lib/module/editor/presets.js +57 -0
  111. package/lib/module/editor/projection.d.ts +12 -0
  112. package/lib/module/editor/projection.js +188 -0
  113. package/lib/module/editor/types.d.ts +166 -0
  114. package/lib/module/editor/types.js +1 -0
  115. package/lib/module/editor-adapter.d.ts +7 -0
  116. package/lib/module/editor-adapter.js +10 -0
  117. package/lib/module/headless.d.ts +2 -0
  118. package/lib/module/headless.js +2 -0
  119. package/lib/module/index.d.ts +5 -0
  120. package/lib/module/index.js +3 -0
  121. package/lib/module/package.json +1 -0
  122. package/lib/source-manifest.json +34 -0
  123. package/lib/worker/invisible-watermark.js +1 -0
  124. package/package.json +85 -0
@@ -0,0 +1,57 @@
1
+ import type { MarkerOperation, MarkerResult } from './result.js';
2
+ import { type MarkerResultDescriptor } from './result.js';
3
+ export type ImageMarkerErrorCode = 'ABORTED' | 'TIMEOUT' | 'INVALID_OPTIONS' | 'LINKING_ERROR' | 'NATIVE_ERROR' | 'RENDER_FAILED' | 'ENCODE_FAILED' | 'DECODE_FAILED' | 'UNKNOWN';
4
+ export type MarkerJobPhase = 'queued' | 'validating' | 'rendering' | 'detecting' | 'encoding' | 'completed';
5
+ export interface MarkerJobProgress {
6
+ jobId: string;
7
+ operation: MarkerOperation;
8
+ phase: MarkerJobPhase;
9
+ completed: number;
10
+ total: 1;
11
+ progress: number;
12
+ }
13
+ export interface MarkerJobOptions {
14
+ signal?: AbortSignal;
15
+ /** Reject and cancel the native operation after this many milliseconds. */
16
+ timeoutMs?: number;
17
+ /** Observer failures never interrupt image processing. */
18
+ onProgress?: (progress: MarkerJobProgress) => void;
19
+ }
20
+ export declare class ImageMarkerError extends Error {
21
+ readonly code: ImageMarkerErrorCode;
22
+ readonly jobId?: string;
23
+ readonly operation?: MarkerOperation;
24
+ readonly cause?: unknown;
25
+ constructor(code: ImageMarkerErrorCode, message: string, context?: {
26
+ jobId?: string;
27
+ operation?: MarkerOperation;
28
+ cause?: unknown;
29
+ });
30
+ }
31
+ export declare function createMarkerJobId(operation: MarkerOperation): string;
32
+ export declare function normalizeMarkerError(error: unknown, context: {
33
+ jobId: string;
34
+ operation: MarkerOperation;
35
+ }): ImageMarkerError;
36
+ interface RunControlledMarkerJobOptions<Result> {
37
+ operation: MarkerOperation;
38
+ control?: MarkerJobOptions;
39
+ task: (jobId: string) => Promise<Result>;
40
+ cancel?: (jobId: string) => Promise<unknown> | unknown;
41
+ workPhase?: Extract<MarkerJobPhase, 'rendering' | 'detecting'>;
42
+ finishingPhase?: Extract<MarkerJobPhase, 'encoding'>;
43
+ }
44
+ export interface ControlledMarkerJobResult<Result> {
45
+ jobId: string;
46
+ durationMs: number;
47
+ value: Result;
48
+ }
49
+ export declare function runControlledMarkerJob<Result>({ operation, control, task, cancel, workPhase, finishingPhase, }: RunControlledMarkerJobOptions<Result>): Promise<ControlledMarkerJobResult<Result>>;
50
+ interface RunMarkerJobOptions {
51
+ descriptor: MarkerResultDescriptor;
52
+ control?: MarkerJobOptions;
53
+ task: (jobId: string) => Promise<string>;
54
+ cancel?: (jobId: string) => Promise<unknown> | unknown;
55
+ }
56
+ export declare function runMarkerJob({ descriptor, control, task, cancel, }: RunMarkerJobOptions): Promise<MarkerResult>;
57
+ export {};
@@ -0,0 +1,143 @@
1
+ import { createMarkerResult } from './result.js';
2
+ export class ImageMarkerError extends Error {
3
+ constructor(code, message, context = {}) {
4
+ super(message);
5
+ this.name = 'ImageMarkerError';
6
+ this.code = code;
7
+ this.jobId = context.jobId;
8
+ this.operation = context.operation;
9
+ this.cause = context.cause;
10
+ }
11
+ }
12
+ let jobSequence = 0;
13
+ export function createMarkerJobId(operation) {
14
+ jobSequence = (jobSequence + 1) % Number.MAX_SAFE_INTEGER;
15
+ return `${operation}-${Date.now().toString(36)}-${jobSequence.toString(36)}`;
16
+ }
17
+ function validateControlOptions(options) {
18
+ if (options.timeoutMs !== undefined &&
19
+ (!Number.isFinite(options.timeoutMs) ||
20
+ !Number.isInteger(options.timeoutMs) ||
21
+ options.timeoutMs <= 0)) {
22
+ throw new ImageMarkerError('INVALID_OPTIONS', 'timeoutMs must be a positive finite integer.');
23
+ }
24
+ }
25
+ function report(options, jobId, operation, phase, progress) {
26
+ try {
27
+ options.onProgress?.({
28
+ jobId,
29
+ operation,
30
+ phase,
31
+ completed: phase === 'completed' ? 1 : 0,
32
+ total: 1,
33
+ progress,
34
+ });
35
+ }
36
+ catch {
37
+ // Progress observers cannot change operation results.
38
+ }
39
+ }
40
+ function inferErrorCode(error) {
41
+ const message = error instanceof Error
42
+ ? error.message.toLowerCase()
43
+ : String(error).toLowerCase();
44
+ if (message.includes('link') || message.includes('expo go')) {
45
+ return 'LINKING_ERROR';
46
+ }
47
+ if (message.includes('decode') || message.includes('load image')) {
48
+ return 'DECODE_FAILED';
49
+ }
50
+ if (message.includes('encode') || message.includes('save')) {
51
+ return 'ENCODE_FAILED';
52
+ }
53
+ if (message.includes('required') ||
54
+ message.includes('invalid') ||
55
+ message.includes('must be') ||
56
+ message.includes('please set')) {
57
+ return 'INVALID_OPTIONS';
58
+ }
59
+ if (message.includes('render') || message.includes('watermark')) {
60
+ return 'RENDER_FAILED';
61
+ }
62
+ return 'NATIVE_ERROR';
63
+ }
64
+ export function normalizeMarkerError(error, context) {
65
+ if (error instanceof ImageMarkerError) {
66
+ return error;
67
+ }
68
+ const message = error instanceof Error && error.message
69
+ ? error.message
70
+ : 'Image marker operation failed.';
71
+ return new ImageMarkerError(inferErrorCode(error), message, {
72
+ ...context,
73
+ cause: error,
74
+ });
75
+ }
76
+ export async function runControlledMarkerJob({ operation, control = {}, task, cancel, workPhase = 'rendering', finishingPhase, }) {
77
+ validateControlOptions(control);
78
+ const jobId = createMarkerJobId(operation);
79
+ const startedAt = Date.now();
80
+ let timeout;
81
+ let rejectControl;
82
+ let controlSettled = false;
83
+ const cancelWith = (code, message) => {
84
+ if (controlSettled)
85
+ return;
86
+ controlSettled = true;
87
+ Promise.resolve(cancel?.(jobId)).catch(() => {
88
+ // The original abort/timeout remains authoritative.
89
+ });
90
+ rejectControl?.(new ImageMarkerError(code, message, {
91
+ jobId,
92
+ operation,
93
+ }));
94
+ };
95
+ const onAbort = () => cancelWith('ABORTED', 'Image marker operation was aborted.');
96
+ if (control.signal?.aborted) {
97
+ throw new ImageMarkerError('ABORTED', 'Image marker operation was aborted before it started.', { jobId, operation });
98
+ }
99
+ const controlPromise = new Promise((_resolve, reject) => {
100
+ rejectControl = reject;
101
+ });
102
+ control.signal?.addEventListener('abort', onAbort, { once: true });
103
+ if (control.timeoutMs !== undefined) {
104
+ timeout = setTimeout(() => cancelWith('TIMEOUT', `Image marker operation timed out after ${control.timeoutMs}ms.`), control.timeoutMs);
105
+ }
106
+ report(control, jobId, operation, 'queued', 0);
107
+ report(control, jobId, operation, 'validating', 0.1);
108
+ try {
109
+ report(control, jobId, operation, workPhase, 0.25);
110
+ const value = await Promise.race([task(jobId), controlPromise]);
111
+ controlSettled = true;
112
+ if (finishingPhase) {
113
+ report(control, jobId, operation, finishingPhase, 0.85);
114
+ }
115
+ const result = {
116
+ jobId,
117
+ durationMs: Math.max(0, Date.now() - startedAt),
118
+ value,
119
+ };
120
+ report(control, jobId, operation, 'completed', 1);
121
+ return result;
122
+ }
123
+ catch (error) {
124
+ throw normalizeMarkerError(error, { jobId, operation });
125
+ }
126
+ finally {
127
+ controlSettled = true;
128
+ if (timeout !== undefined)
129
+ clearTimeout(timeout);
130
+ control.signal?.removeEventListener('abort', onAbort);
131
+ }
132
+ }
133
+ export async function runMarkerJob({ descriptor, control = {}, task, cancel, }) {
134
+ const result = await runControlledMarkerJob({
135
+ operation: descriptor.operation,
136
+ control,
137
+ task,
138
+ cancel,
139
+ workPhase: 'rendering',
140
+ finishingPhase: 'encoding',
141
+ });
142
+ return createMarkerResult(result.jobId, result.value, result.durationMs, descriptor);
143
+ }
@@ -0,0 +1,19 @@
1
+ import type { WatermarkLayout } from './index.js';
2
+ export declare const MAX_TILE_COPIES = 4096;
3
+ export interface LayoutSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export interface LayoutPoint {
8
+ x: number;
9
+ y: number;
10
+ }
11
+ export type TileWatermarkLayout = Extract<WatermarkLayout, {
12
+ type: 'tile';
13
+ }>;
14
+ export declare function resolveLayoutValue(value: number | string | undefined, relativeTo: number, label: string): number;
15
+ /**
16
+ * Resolve the top-left visible bounds for every tile. Rows and columns begin
17
+ * one step outside the canvas so partial edge tiles preserve the grid phase.
18
+ */
19
+ export declare function resolveTilePlacements(layout: TileWatermarkLayout, canvas: LayoutSize, item: LayoutSize): LayoutPoint[];
@@ -0,0 +1,70 @@
1
+ export const MAX_TILE_COPIES = 4096;
2
+ function assertSize(size, label) {
3
+ if (!Number.isFinite(size.width) ||
4
+ !Number.isFinite(size.height) ||
5
+ size.width <= 0 ||
6
+ size.height <= 0) {
7
+ throw new Error(`${label} dimensions must be finite numbers greater than 0.`);
8
+ }
9
+ }
10
+ export function resolveLayoutValue(value, relativeTo, label) {
11
+ if (value === undefined) {
12
+ return 0;
13
+ }
14
+ if (typeof value === 'number') {
15
+ if (!Number.isFinite(value)) {
16
+ throw new Error(`${label} must be a finite number or percentage.`);
17
+ }
18
+ return value;
19
+ }
20
+ const normalized = value.trim();
21
+ const match = /^([+-]?(?:\d+(?:\.\d+)?|\.\d+))(%)?$/.exec(normalized);
22
+ if (!match) {
23
+ throw new Error(`${label} must be a finite number or percentage.`);
24
+ }
25
+ const parsed = Number(match[1]);
26
+ return match[2] ? (relativeTo * parsed) / 100 : parsed;
27
+ }
28
+ function normalizedOffset(value, step) {
29
+ return ((value % step) + step) % step;
30
+ }
31
+ /**
32
+ * Resolve the top-left visible bounds for every tile. Rows and columns begin
33
+ * one step outside the canvas so partial edge tiles preserve the grid phase.
34
+ */
35
+ export function resolveTilePlacements(layout, canvas, item) {
36
+ assertSize(canvas, 'canvas');
37
+ assertSize(item, 'watermark');
38
+ const gapX = resolveLayoutValue(layout.gapX, canvas.width, 'layout.gapX');
39
+ const gapY = resolveLayoutValue(layout.gapY, canvas.height, 'layout.gapY');
40
+ if (gapX < 0 || gapY < 0) {
41
+ throw new Error('layout gaps must be non-negative.');
42
+ }
43
+ const offsetX = resolveLayoutValue(layout.offsetX, canvas.width, 'layout.offsetX');
44
+ const offsetY = resolveLayoutValue(layout.offsetY, canvas.height, 'layout.offsetY');
45
+ const stepX = item.width + gapX;
46
+ const stepY = item.height + gapY;
47
+ if (!Number.isFinite(stepX) || !Number.isFinite(stepY)) {
48
+ throw new Error('tile layout step must be finite.');
49
+ }
50
+ const phaseX = normalizedOffset(offsetX, stepX);
51
+ const phaseY = normalizedOffset(offsetY, stepY);
52
+ const placements = [];
53
+ for (let row = -1, y = phaseY - stepY; y < canvas.height; row += 1, y += stepY) {
54
+ if (y + item.height <= 0) {
55
+ continue;
56
+ }
57
+ const staggerOffset = layout.stagger && row % 2 !== 0 ? stepX / 2 : 0;
58
+ const rowPhaseX = normalizedOffset(phaseX + staggerOffset, stepX);
59
+ for (let x = rowPhaseX - stepX; x < canvas.width; x += stepX) {
60
+ if (x + item.width <= 0) {
61
+ continue;
62
+ }
63
+ placements.push({ x, y });
64
+ if (placements.length > MAX_TILE_COPIES) {
65
+ throw new Error(`tile layout exceeds the maximum of ${MAX_TILE_COPIES} copies per layer.`);
66
+ }
67
+ }
68
+ }
69
+ return placements;
70
+ }
@@ -0,0 +1 @@
1
+ export { default } from './web/index.js';
@@ -0,0 +1,4 @@
1
+ // The default implementation is browser-safe so standard web bundlers and
2
+ // server-side rendering can import the package without loading React Native's
3
+ // NativeModules. Metro selects marker.native.ts on iOS and Android.
4
+ export { default } from './web/index.js';
@@ -0,0 +1,44 @@
1
+ import { WATERMARK_RECIPE_SCHEMA_VERSION, type LegacyWatermarkRecipeDefinition as SharedLegacyWatermarkRecipeDefinition, type WatermarkRecipeCondition as SharedWatermarkRecipeCondition, type WatermarkRecipeDefinition as SharedWatermarkRecipeDefinition, type WatermarkRecipeDefinitionLayer as SharedWatermarkRecipeDefinitionLayer, type WatermarkRecipeDocument as SharedWatermarkRecipeDocument, type WatermarkRecipeInput as SharedWatermarkRecipeInput, type WatermarkRecipeLayer as SharedWatermarkRecipeLayer, type WatermarkRecipeOptions as SharedWatermarkRecipeOptions, type WatermarkRecipeOutputOptions as SharedWatermarkRecipeOutputOptions, type WatermarkRecipeSchemaVersion as SharedWatermarkRecipeSchemaVersion, type WatermarkRecipeVariable as SharedWatermarkRecipeVariable } from '@image-marker/recipe';
2
+ import type { MarkOptions } from './index.js';
3
+ import type { MarkerResult } from './result.js';
4
+ import type { MarkerJobOptions } from './job.js';
5
+ import type { WatermarkBatchOptions, WatermarkBatchResult } from './batch.js';
6
+ export type { WatermarkBatchAbortedResult, WatermarkBatchFulfilledResult, WatermarkBatchOptions, WatermarkBatchProgress, WatermarkBatchRejectedResult, WatermarkBatchResult, } from './batch.js';
7
+ export { WATERMARK_RECIPE_SCHEMA_VERSION };
8
+ export type WatermarkRecipeSchemaVersion = SharedWatermarkRecipeSchemaVersion;
9
+ export type WatermarkRecipeVariable = SharedWatermarkRecipeVariable;
10
+ export type WatermarkRecipeCondition = SharedWatermarkRecipeCondition;
11
+ export type WatermarkRecipeLayer = SharedWatermarkRecipeLayer<any>;
12
+ export type WatermarkRecipeDefinitionLayer = SharedWatermarkRecipeDefinitionLayer<any>;
13
+ export type WatermarkRecipeOutputOptions = SharedWatermarkRecipeOutputOptions;
14
+ export type WatermarkRecipeOptions = SharedWatermarkRecipeOptions<any>;
15
+ export type WatermarkRecipeDefinition = SharedWatermarkRecipeDefinition<any>;
16
+ export type LegacyWatermarkRecipeDefinition = SharedLegacyWatermarkRecipeDefinition<any>;
17
+ export type WatermarkRecipeDocument = SharedWatermarkRecipeDocument<any>;
18
+ export type WatermarkRecipeInput = SharedWatermarkRecipeInput<any>;
19
+ /** Return the Core 2 structured result. */
20
+ export interface WatermarkMarkerResultRecipeOptions {
21
+ resultType?: 'result';
22
+ }
23
+ /** Return encoded browser bytes without creating an object URL. Web only. */
24
+ export interface WatermarkBlobRecipeResultOptions {
25
+ resultType: 'blob';
26
+ }
27
+ export type WatermarkRecipeResultOptions = WatermarkMarkerResultRecipeOptions | WatermarkBlobRecipeResultOptions;
28
+ export interface WatermarkRecipe<Result = MarkerResult> {
29
+ /** Version of the serializable recipe definition. */
30
+ readonly schemaVersion: WatermarkRecipeSchemaVersion;
31
+ /** Return a detached definition suitable for JSON persistence. */
32
+ toJSON(): WatermarkRecipeDefinition;
33
+ /** Apply the saved layers and output settings to one source image. */
34
+ apply(input: WatermarkRecipeInput, control?: MarkerJobOptions): Promise<Result>;
35
+ /** Apply the recipe to many images while preserving input result order. */
36
+ applyMany(inputs: readonly WatermarkRecipeInput[], options?: WatermarkBatchOptions<Result>): Promise<Array<WatermarkBatchResult<Result>>>;
37
+ }
38
+ type RecipeRenderer<Result> = (options: MarkOptions, control?: MarkerJobOptions) => Promise<Result>;
39
+ /**
40
+ * Upgrade a serialized Recipe v1 document to the v2 layer/output structure.
41
+ */
42
+ export declare function migrateWatermarkRecipe(document: WatermarkRecipeDocument): WatermarkRecipeDefinition;
43
+ export declare function createWatermarkRecipe<Result>(options: WatermarkRecipeOptions, renderer: RecipeRenderer<Result>, maximumConcurrency?: number): WatermarkRecipe<Result>;
44
+ export declare function importWatermarkRecipe<Result>(document: WatermarkRecipeDocument, renderer: RecipeRenderer<Result>, maximumConcurrency?: number): WatermarkRecipe<Result>;
@@ -0,0 +1,169 @@
1
+ import { cloneRecipeValue, createWatermarkRecipeDefinition, materializeWatermarkRecipe, migrateWatermarkRecipe as migrateSharedRecipe, WATERMARK_RECIPE_SCHEMA_VERSION, } from '@image-marker/recipe';
2
+ import { validateMarkOptions } from './validate.js';
3
+ import { runWatermarkBatch } from './batch.js';
4
+ export { WATERMARK_RECIPE_SCHEMA_VERSION };
5
+ function toRenderLayer(layer) {
6
+ const renderLayer = { ...layer };
7
+ delete renderLayer.id;
8
+ delete renderLayer.name;
9
+ delete renderLayer.visible;
10
+ delete renderLayer.locked;
11
+ delete renderLayer.groupId;
12
+ delete renderLayer.visibleWhen;
13
+ return renderLayer;
14
+ }
15
+ function snapshotRecipeOptions(options) {
16
+ if (!options || !Array.isArray(options.layers)) {
17
+ throw new Error('createRecipe requires an ordered layers array.');
18
+ }
19
+ if (options.schemaVersion !== undefined &&
20
+ options.schemaVersion !== WATERMARK_RECIPE_SCHEMA_VERSION) {
21
+ throw new Error(`Unsupported recipe schemaVersion: ${String(options.schemaVersion)}.`);
22
+ }
23
+ if (options.layers.length === 0) {
24
+ throw new Error('createRecipe requires at least one layer.');
25
+ }
26
+ const ids = new Set();
27
+ options.layers.forEach((layer, index) => {
28
+ if (layer.type === 'image' && !layer.src) {
29
+ throw new Error(`layers[${index}].src is required.`);
30
+ }
31
+ const id = layer.id ?? `layer-${index + 1}`;
32
+ if (ids.has(id))
33
+ throw new Error(`Duplicate layer id "${id}".`);
34
+ ids.add(id);
35
+ });
36
+ const runtimeOptions = options;
37
+ const unsupportedKey = [
38
+ 'backgroundImage',
39
+ 'filename',
40
+ 'watermarkTexts',
41
+ 'watermarkImage',
42
+ 'watermarkPositions',
43
+ 'watermarkImages',
44
+ 'watermarks',
45
+ 'quality',
46
+ 'saveFormat',
47
+ 'matteColor',
48
+ 'rotationCanvasMode',
49
+ 'maxSize',
50
+ ].find((key) => Object.prototype.hasOwnProperty.call(runtimeOptions, key));
51
+ if (unsupportedKey) {
52
+ throw new Error(`createRecipe does not accept "${unsupportedKey}" in Recipe v2; use ordered layers, put encoding options under output, and pass per-image fields to apply().`);
53
+ }
54
+ const definition = createWatermarkRecipeDefinition(options);
55
+ definition.layers.forEach((layer, index) => {
56
+ const original = options.layers[index];
57
+ if (layer.type === 'image' && original?.type === 'image') {
58
+ layer.src = original.src;
59
+ }
60
+ });
61
+ validateMarkOptions({
62
+ backgroundImage: { src: 'recipe-validation-placeholder' },
63
+ watermarks: definition.layers.map(toRenderLayer),
64
+ ...definition.output,
65
+ });
66
+ return definition;
67
+ }
68
+ /**
69
+ * Upgrade a serialized Recipe v1 document to the v2 layer/output structure.
70
+ */
71
+ export function migrateWatermarkRecipe(document) {
72
+ return snapshotRecipeOptions(migrateSharedRecipe(document));
73
+ }
74
+ function snapshotInput(input) {
75
+ return {
76
+ backgroundImage: input?.backgroundImage
77
+ ? { ...input.backgroundImage }
78
+ : input?.backgroundImage,
79
+ filename: input?.filename,
80
+ variables: input?.variables ? { ...input.variables } : input?.variables,
81
+ };
82
+ }
83
+ function createMarkOptions(recipe, input, index) {
84
+ const materialized = materializeWatermarkRecipe(recipe, {
85
+ filename: input.filename,
86
+ index,
87
+ variables: input.variables,
88
+ });
89
+ const imageSources = new Map();
90
+ recipe.layers.forEach((layer) => {
91
+ if (layer.type === 'image') {
92
+ imageSources.set(layer.id, layer.src);
93
+ }
94
+ });
95
+ materialized.layers.forEach((layer) => {
96
+ if (layer.type === 'image' && imageSources.has(layer.id)) {
97
+ layer.src = imageSources.get(layer.id);
98
+ }
99
+ });
100
+ return {
101
+ backgroundImage: { ...input.backgroundImage },
102
+ watermarks: materialized.layers.map(toRenderLayer),
103
+ filename: input.filename,
104
+ ...materialized.output,
105
+ };
106
+ }
107
+ function canonicalOutputFilename(filename, saveFormat) {
108
+ const trimmed = filename.trim();
109
+ if (!trimmed)
110
+ return undefined;
111
+ const knownExtension = ['.jpeg', '.jpg', '.png', '.webp'].find((extension) => trimmed.toLowerCase().endsWith(extension));
112
+ const stem = knownExtension
113
+ ? trimmed.slice(0, -knownExtension.length)
114
+ : trimmed;
115
+ const extension = saveFormat === 'png' ? '.png' : saveFormat === 'webp' ? '.webp' : '.jpg';
116
+ return `${stem}${extension}`.toLowerCase();
117
+ }
118
+ function validateUniqueFilenames(inputs, saveFormat) {
119
+ const seen = new Map();
120
+ inputs.forEach((input, index) => {
121
+ if (input.filename === undefined)
122
+ return;
123
+ if (typeof input.filename !== 'string') {
124
+ throw new Error(`inputs[${index}].filename must be a string.`);
125
+ }
126
+ const canonical = canonicalOutputFilename(input.filename, saveFormat);
127
+ if (!canonical)
128
+ return;
129
+ const previousIndex = seen.get(canonical);
130
+ if (previousIndex !== undefined) {
131
+ throw new Error(`Duplicate output filename "${canonical}" for inputs ${previousIndex} and ${index}.`);
132
+ }
133
+ seen.set(canonical, index);
134
+ });
135
+ }
136
+ export function createWatermarkRecipe(options, renderer, maximumConcurrency = 1) {
137
+ const recipe = snapshotRecipeOptions(options);
138
+ const applySnapshot = (input, index, control) => {
139
+ if (!input?.backgroundImage?.src) {
140
+ return Promise.reject(new Error('please set image!'));
141
+ }
142
+ try {
143
+ return renderer(createMarkOptions(recipe, input, index), control);
144
+ }
145
+ catch (error) {
146
+ return Promise.reject(error);
147
+ }
148
+ };
149
+ return {
150
+ schemaVersion: WATERMARK_RECIPE_SCHEMA_VERSION,
151
+ toJSON() {
152
+ return cloneRecipeValue(recipe);
153
+ },
154
+ apply(input, control) {
155
+ return applySnapshot(snapshotInput(input), 0, control);
156
+ },
157
+ async applyMany(inputs, batchOptions = {}) {
158
+ if (!Array.isArray(inputs)) {
159
+ throw new Error('applyMany inputs must be an array.');
160
+ }
161
+ const queuedInputs = inputs.map(snapshotInput);
162
+ validateUniqueFilenames(queuedInputs, recipe.output.saveFormat);
163
+ return runWatermarkBatch(queuedInputs, (input, index) => applySnapshot(input, index, { signal: batchOptions.signal }), batchOptions, maximumConcurrency, 'applyMany');
164
+ },
165
+ };
166
+ }
167
+ export function importWatermarkRecipe(document, renderer, maximumConcurrency = 1) {
168
+ return createWatermarkRecipe(migrateWatermarkRecipe(document), renderer, maximumConcurrency);
169
+ }
@@ -0,0 +1,34 @@
1
+ export type MarkerOperation = 'mark' | 'markText' | 'markImage' | 'embedInvisible' | 'detectInvisible';
2
+ export type MarkerOutputFormat = 'png' | 'jpeg' | 'webp';
3
+ export interface MarkerMetadataResult {
4
+ /** Source orientation is applied to pixels and output orientation is upright. */
5
+ orientation: 'normalized';
6
+ /**
7
+ * Core v2 strips EXIF/GPS and other source metadata by default so stale or
8
+ * sensitive source claims are not copied onto newly rendered pixels.
9
+ */
10
+ policy: 'strip';
11
+ }
12
+ export interface MarkerResult {
13
+ /** Stable identifier shared by progress, errors, and native cancellation. */
14
+ jobId: string;
15
+ /** Public operation that produced this result. */
16
+ operation: MarkerOperation;
17
+ /** File path/URI on native or data URL in a browser/base64 request. */
18
+ uri: string;
19
+ /** Whether `uri` points at a file or embeds the encoded bytes. */
20
+ output: 'file' | 'data-url';
21
+ format: MarkerOutputFormat;
22
+ mimeType: 'image/png' | 'image/jpeg' | 'image/webp';
23
+ /** Total JavaScript-observed operation duration. */
24
+ durationMs: number;
25
+ metadata: MarkerMetadataResult;
26
+ /** Requested output basename, when supplied. */
27
+ filename?: string;
28
+ }
29
+ export interface MarkerResultDescriptor {
30
+ operation: MarkerOperation;
31
+ saveFormat?: 'png' | 'jpg' | 'webp' | 'base64';
32
+ filename?: string;
33
+ }
34
+ export declare function createMarkerResult(jobId: string, uri: string, durationMs: number, descriptor: MarkerResultDescriptor): MarkerResult;
@@ -0,0 +1,38 @@
1
+ function resolveFormat(saveFormat, uri) {
2
+ if (uri?.startsWith('data:image/png'))
3
+ return 'png';
4
+ if (uri?.startsWith('data:image/jpeg'))
5
+ return 'jpeg';
6
+ if (uri?.startsWith('data:image/webp'))
7
+ return 'webp';
8
+ if (saveFormat === 'webp')
9
+ return 'webp';
10
+ return saveFormat === 'png' || saveFormat === 'base64' ? 'png' : 'jpeg';
11
+ }
12
+ export function createMarkerResult(jobId, uri, durationMs, descriptor) {
13
+ if (typeof uri !== 'string' || !uri) {
14
+ throw new Error('The renderer returned an empty output URI.');
15
+ }
16
+ const format = resolveFormat(descriptor.saveFormat, uri);
17
+ const result = {
18
+ jobId,
19
+ operation: descriptor.operation,
20
+ uri,
21
+ output: uri.startsWith('data:') ? 'data-url' : 'file',
22
+ format,
23
+ mimeType: format === 'png'
24
+ ? 'image/png'
25
+ : format === 'webp'
26
+ ? 'image/webp'
27
+ : 'image/jpeg',
28
+ durationMs: Math.max(0, durationMs),
29
+ metadata: {
30
+ orientation: 'normalized',
31
+ policy: 'strip',
32
+ },
33
+ };
34
+ if (descriptor.filename !== undefined) {
35
+ result.filename = descriptor.filename;
36
+ }
37
+ return result;
38
+ }
@@ -0,0 +1,43 @@
1
+ import type { WatermarkBatchOptions, WatermarkBatchResult } from './batch.js';
2
+ import type { DetectInvisibleWatermarkOptions, EmbedInvisibleWatermarkOptions, InvisibleWatermarkDetectionData, InvisibleWatermarkPixelBuffer } from './invisible-watermark.js';
3
+ /** RGBA pixels decoded in display orientation by a server-side image codec. */
4
+ export interface InvisibleWatermarkDecodedImage extends InvisibleWatermarkPixelBuffer {
5
+ }
6
+ /** Image I/O boundary used by the dependency-free trace runtime. */
7
+ export interface InvisibleWatermarkCodec<Source, Output> {
8
+ decode(source: Source, options: {
9
+ maxSize: number;
10
+ }): Promise<InvisibleWatermarkDecodedImage>;
11
+ encode(image: InvisibleWatermarkDecodedImage, options: {
12
+ format: 'jpeg' | 'png';
13
+ quality: number;
14
+ filename?: string;
15
+ }): Promise<Output>;
16
+ }
17
+ export interface InvisibleWatermarkRuntimeOptions<Source, Output> {
18
+ codec: InvisibleWatermarkCodec<Source, Output>;
19
+ /** Hard upper bound for parallel codec/CPU work. @defaultValue 4 */
20
+ maxConcurrency?: number;
21
+ }
22
+ export type RuntimeEmbedInvisibleWatermarkOptions<Source> = Omit<EmbedInvisibleWatermarkOptions, 'image' | 'saveFormat'> & {
23
+ image: {
24
+ src: Source;
25
+ };
26
+ saveFormat?: 'png' | 'jpg';
27
+ };
28
+ export type RuntimeDetectInvisibleWatermarkOptions<Source> = Omit<DetectInvisibleWatermarkOptions, 'image' | 'worker'> & {
29
+ image: {
30
+ src: Source;
31
+ };
32
+ };
33
+ export interface InvisibleWatermarkRuntime<Source, Output> {
34
+ embedInvisible(options: RuntimeEmbedInvisibleWatermarkOptions<Source>): Promise<Output>;
35
+ detectInvisible(options: RuntimeDetectInvisibleWatermarkOptions<Source>): Promise<InvisibleWatermarkDetectionData>;
36
+ embedInvisibleMany(inputs: readonly RuntimeEmbedInvisibleWatermarkOptions<Source>[], options?: WatermarkBatchOptions<Output>): Promise<Array<WatermarkBatchResult<Output>>>;
37
+ detectInvisibleMany(inputs: readonly RuntimeDetectInvisibleWatermarkOptions<Source>[], options?: WatermarkBatchOptions<InvisibleWatermarkDetectionData>): Promise<Array<WatermarkBatchResult<InvisibleWatermarkDetectionData>>>;
38
+ }
39
+ /**
40
+ * Create a DOM-free invisible watermark runtime around an injected image codec.
41
+ * The core package does not install or choose a server-side image dependency.
42
+ */
43
+ export declare function createInvisibleWatermarkRuntime<Source, Output>(options: InvisibleWatermarkRuntimeOptions<Source, Output>): InvisibleWatermarkRuntime<Source, Output>;