Local photo tools for developers and assistants
Free batch watermarks, filename labels, and date stamps. Discover the capabilities, then process explicitly selected photos on the device.
Read as Markdown · OpenAPI 3.1 · Recipe schema
Choose an execution surface
The discovery API is public, free, read-only, and requires no API key. It describes tools and settings; it never accepts images. There is no POST /watermark, cloud processing service, or hosted MCP endpoint.
Use the browser SDK for explicitly selected Files on this origin. For an assistant with local filesystem access, connect the repository’s stdio MCP server explicitly and authorize a root directory. A model finding this documentation does not gain access to a device or its files.
| Surface | Purpose |
|---|---|
| GET JSON / Markdown | Discover capabilities, recipes, constraints, and local tool signatures. |
| Browser ESM SDK | Decode and render files locally; requires Canvas, DOM, createImageBitmap, and File/Blob. Not a Node or remote HTTP processing API. |
| Local CLI / stdio MCP | Explicitly connected local automation using installed Google Chrome and repository dependencies. |
Public resources
GET and HEAD return static resources. No authentication, request body, query parameters, or user data. Unknown resource paths return 404. Treat version 1 as the contract version; bookmark canonical paths rather than hashed build chunks. Cross-origin browser access is not promised; use same-origin SDK imports.
- Capability catalog
- Complete default recipes and supported text variables
- Recipe JSON Schema (2020-12)
- Local MCP input schemas
- OpenAPI 3.1 discovery specification
- Optional discovery directory
curl https://watermark.noxxxx.com/api/v1/capabilities.json
curl https://watermark.noxxxx.com/api/v1/recipes/filename.jsonBrowser SDK
Import /sdk/watermark.js on this site’s origin. This is a stable ESM entry URL; implementation chunks can change. The following example expects a multiple file input and runs after the user selects local files. It creates a ZIP of successful copies; callers must inspect failures and cancelled rather than treating partial export as complete.
import { createRecipe, validateRecipe, inspectImage, processBatch, packageResults }
from '/sdk/watermark.js';
// Run on this site's origin in a modern browser, after explicit file selection.
const files = [...document.querySelector('input[type="file"]').files];
const recipe = createRecipe('filename'); // watermark | filename | date | frame
const errors = validateRecipe(recipe);
if (errors.length) throw new Error(errors.join('; '));
const records = [];
try {
for (const file of files) records.push(await inspectImage(file));
const result = await processBatch(records, recipe);
if (result.failures.length || result.cancelled) {
console.warn('Check partial results before downloading', result);
}
if (result.entries.length) {
const zip = await packageResults(result.entries);
const url = URL.createObjectURL(zip);
const link = document.createElement('a');
link.href = url; link.download = 'labeled-photos.zip'; link.click();
setTimeout(() => URL.revokeObjectURL(url), 30000);
}
} finally {
records.forEach(record => URL.revokeObjectURL(record.thumbnail));
}| Function | Input → result |
|---|---|
| createRecipe(mode) | watermark | filename | date | frame → independent v1 recipe. |
| validateRecipe(recipe) | Recipe → string[] errors; empty means configuration is valid. Semantic validation also checks unique IDs and crop bounds. |
| inspectImage(file) | File → Promise<Record> with id, name, file, width, height, thumbnail object URL, overrides, metadata, metadataWarning. Revoke thumbnail when finished.{id,name,file,width,height,thumbnail,overrides,metadata,metadataWarning} |
| renderImage(record, recipe, options?) | → Promise<{blob, geometry}>; preview:true returns {canvas, geometry}. options: assets:Map<assetId,ImageBitmap|Canvas>, index:number, maxDimension:number, canvas:Canvas. missingMetadataText?:string.{blob,geometry} | {canvas,geometry} |
| processBatch(records, recipe, options?) | → Promise<{entries,failures,cancelled}>. Entry: {name,blob,width,height,sourceId}; failure: {id,name,longEdge,message}. options: assets, signal:AbortSignal, onProgress({done,total,failed}), onResult(entry). missingMetadataText?:string.{entries:[{name,blob,width,height,sourceId}],failures:[{id,name,longEdge,message}],cancelled} |
| packageResults(entries) | → Promise<Blob> ZIP; rejects when combined output exceeds 450 MiB. |
| decodeImage(blob) | → Promise<ImageBitmap>; close() when finished. HEIC uses the first decoded image. |
| parseLabelCsv(text) | filename,text CSV → Map<string,string>; exact-name matches, duplicate names reject. Set record.label yourself before processing. |
| imageRecipe(recipe, record) | Merges record.overrides.image and record.overrides.layers[id] without mutating the shared recipe. |
| renderStudioImage / cleanLogo | Low-level Canvas helpers; see repository packages/watermark-core/src/studio.js for signatures. Prefer renderImage for encoded output. |
Recipe rules and errors
Download full recipes from /api/v1/recipes/watermark.json, filename.json, date.json, or frame.json. Validate JSON Schema first, then validateRecipe for semantic checks. Layer IDs must be unique. Crop x + width and y + height must stay inside the image. Sizes, margins, borders, and footers use percentages of the cropped and rotated image’s short edge. Output longEdge:0 keeps size; resizing never upscales. Optional image.crop.aspectRatio is the content width/height after rotation. It centers a crop inside each image’s normalized selection, so mixed-size batches keep the same ratio. Borders and footers are added afterwards. Editing crop percentages clears the ratio rule.
Text variables: {filename} excludes extension; {fullname} includes it; {index} uses sequence.start and padding; {label} uses record.label. Camera variables are {camera}, {lens}, {aperture}, {iso}, {shutter}, {focal}. A CSV does not automatically populate SDK records.
The date preset uses each image’s DateTimeOriginal or CreateDate. No file modification date is substituted. Missing date defaults to an empty date label; missing:error fails that image, missing:custom uses date.custom if parseable. source:custom uses the supplied date directly. Custom dates are not certified timestamps. Invalid dates can yield an empty label or an error according to this policy.
A recipe contains settings, not logo bytes or custom fonts. Supply logos through options.assets keyed by layer.assetId, and close decoded logo bitmaps after use. Custom fonts must be loaded into document.fonts under their LocalFont family. Missing resources fail explicitly.
The SDK throws for invalid recipes before batch execution. Per-image decode, render, metadata, or encode errors appear in failures, while successful entries remain available. Errors currently contain human-readable Chinese messages; consumers should not branch on their text. Cancellation is checked between outputs; the current render is not interrupted. With onResult, entries are delivered to the callback and are not retained in the returned entries array.
Limits and privacy
No paid photo-count quota, account, API key, subscription, or added site branding. Each input is limited to 150 MiB and 120 million pixels; ZIP output is limited to 450 MiB. Maximum 30 layers, 6 additional output sizes, long edge up to 32768. Browser Canvas, memory, decoder, and filesystem limits can be lower; split large batches.
Outputs are JPEG, PNG, or WebP. JPEG and lossy WebP are re-encoded. Metadata defaults to strip; safe/all preservation requires JPEG. safe excludes GPS; all can retain location metadata. Neither mode preserves every possible source segment: ICC, HDR, and signatures are not preserved. Original files are never changed.
Only program resources are optionally cached for offline use in /studio/. The new public pages need a network connection to load initially. User images and filenames are not sent to the public API or stored in presets. Save/share only recipes that contain no private text.
Local MCP
This capability is provided by the repository, not an internet endpoint. Prerequisites: Node.js, pnpm install in the repository, and installed Google Chrome. Run the server from the repository, with an explicit allowed directory. Do not infer a private filesystem path from web search results.
node scripts/watermark-mcp.mjs --root /absolute/authorized/photo-folder| Tool | Input and result |
|---|---|
| validate_recipe | {recipe} → {errors:string[]}. Does not read images. |
| inspect_images | {files:string[]} → {images:[{id,name,width,height,metadata,metadataWarning}],failures:[{id,name,index,stage,message}]}. |
| preview_watermark | {files,recipe,output?,logos?} → {files:[{path,width,height}],failures,cancelled}. Writes only the first image, longest edge ≤1400. |
| batch_watermark | {files,recipe,output?,logos?} → {files:[{path,width,height}],failures,cancelled}. Writes individual images, not a ZIP. |
files are resolved inside --root. Logos map asset IDs to paths within the same root. output defaults to watermark-output inside the root; absolute paths must also remain inside the root. Symbolic-link escapes are rejected. Existing outputs are preserved with new filenames. recipe is required by the preview/batch input schemas; inspect_images accepts only files. Invalid images are reported in failures while valid images continue. Failure index is zero-based in the original files array; stage is inspect or render. Preview processes only files[0] and never substitutes another file. Invalid requests return MCP isError with a text message; per-image failures remain in the result. Calls are request/response and do not expose an abort tool.
MCP returns JSON serialized inside content[0].text, not structuredContent. Inspect and validate are read-only; preview and batch write copies. No tool uploads photos. A client must explicitly configure this local stdio command before using the tools.