@yafh/image-map
Advanced tools
| #!/usr/bin/env node | ||
| import process from 'node:process' | ||
| import { main } from '../dist/cli.mjs' | ||
| try { | ||
| // eslint-disable-next-line antfu/no-top-level-await | ||
| await main(process.argv.slice(2)) | ||
| } | ||
| catch (err) { | ||
| const message = err instanceof Error | ||
| ? (err.stack ?? err.message) | ||
| : String(err) | ||
| process.stderr.write(`${message}\n`) | ||
| process.exit(1) | ||
| } |
| //#region src/protocol.d.ts | ||
| type TileFormat = 'png' | 'jpg' | 'jpeg' | 'webp'; | ||
| type Origin = 'topLeft' | 'center'; | ||
| /** Resize filter used for downscaling between zoom levels. */ | ||
| type ResizeFilter = 'lanczos3' | 'catmullRom' | 'mitchell' | 'hamming' | 'bilinear' | 'box' | 'gaussian'; | ||
| /** Options for downscale sharpening. */ | ||
| interface DownscaleSharpenOptions { | ||
| /** Whether downscale sharpening is enabled. */ | ||
| enabled?: boolean; | ||
| /** Gaussian blur sigma for unsharp mask. */ | ||
| sigma?: number; | ||
| /** Unsharp mask amount multiplier. */ | ||
| amount?: number; | ||
| /** Threshold for minimal brightness change that will be sharpened. */ | ||
| threshold?: number; | ||
| } | ||
| interface GenerateOptions { | ||
| /** Resize filter for building lower zoom levels. */ | ||
| resizeFilter: ResizeFilter; | ||
| /** Downscale sharpening configuration. */ | ||
| downscaleSharpen: Required<DownscaleSharpenOptions>; | ||
| /** Tile size in pixels. */ | ||
| tileSize: number; | ||
| /** Output formats. */ | ||
| formats: TileFormat[]; | ||
| /** Origin position. */ | ||
| origin: Origin; | ||
| /** Minimum zoom level. */ | ||
| minZoom: number; | ||
| /** Maximum zoom level. */ | ||
| maxZoom: number; | ||
| } | ||
| interface GenerateResult { | ||
| /** Total number of tiles generated. */ | ||
| tilesGenerated: number; | ||
| /** Output directory path. */ | ||
| outputDir: string; | ||
| } | ||
| /** Resize by percentage of original size. */ | ||
| interface ResizeModePercentage { | ||
| type: 'percentage'; | ||
| /** Percentage value (e.g., 50 means 50% of original size). */ | ||
| value: number; | ||
| } | ||
| /** Resize by specifying the long edge in pixels. */ | ||
| interface ResizeModeLongEdge { | ||
| type: 'longEdge'; | ||
| /** Target long edge size in pixels. */ | ||
| pixels: number; | ||
| } | ||
| /** Resize by specifying both width and height (fit within, keep aspect ratio). */ | ||
| interface ResizeModeFit { | ||
| type: 'fit'; | ||
| /** Maximum width in pixels. */ | ||
| width: number; | ||
| /** Maximum height in pixels. */ | ||
| height: number; | ||
| } | ||
| /** Resize mode specification. */ | ||
| type ResizeMode = ResizeModePercentage | ResizeModeLongEdge | ResizeModeFit; | ||
| /** Options for resizing an image (without tiling). */ | ||
| interface ResizeImageOptions { | ||
| /** The resize mode specifying how to calculate output dimensions. */ | ||
| mode: ResizeMode; | ||
| /** Output format for the resized image. */ | ||
| format: TileFormat; | ||
| /** Resize filter for downscaling. */ | ||
| resizeFilter: ResizeFilter; | ||
| /** Sharpening configuration for downscaling. */ | ||
| sharpen: Required<DownscaleSharpenOptions>; | ||
| } | ||
| /** Result payload for a completed resize request. */ | ||
| interface ResizeResult { | ||
| /** Output file path. */ | ||
| outputPath: string; | ||
| /** Original image width. */ | ||
| originalWidth: number; | ||
| /** Original image height. */ | ||
| originalHeight: number; | ||
| /** Resized image width. */ | ||
| width: number; | ||
| /** Resized image height. */ | ||
| height: number; | ||
| } | ||
| interface GenerateRequestMessage { | ||
| /** Message type. */ | ||
| type: 'generate'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Input image path. */ | ||
| input: string; | ||
| /** Output directory path. */ | ||
| output: string; | ||
| /** Generation options. */ | ||
| options: GenerateOptions; | ||
| } | ||
| interface ResizeRequestMessage { | ||
| /** Message type. */ | ||
| type: 'resize'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Input image path. */ | ||
| input: string; | ||
| /** Output file path. */ | ||
| output: string; | ||
| /** Resize options. */ | ||
| options: ResizeImageOptions; | ||
| } | ||
| type RequestMessage = GenerateRequestMessage | ResizeRequestMessage; | ||
| interface ProgressResponseMessage { | ||
| /** Message type. */ | ||
| type: 'progress'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Current progress value. */ | ||
| current: number; | ||
| /** Total progress value. */ | ||
| total: number; | ||
| /** Human readable message. */ | ||
| message: string; | ||
| } | ||
| interface CompleteResponseMessage { | ||
| /** Message type. */ | ||
| type: 'complete'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Generation result payload. */ | ||
| result: GenerateResult; | ||
| } | ||
| interface ResizeCompleteResponseMessage { | ||
| /** Message type. */ | ||
| type: 'resizeComplete'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Resize result payload. */ | ||
| result: ResizeResult; | ||
| } | ||
| interface ErrorResponseMessage { | ||
| /** Message type. */ | ||
| type: 'error'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Error message. */ | ||
| error: string; | ||
| } | ||
| type ResponseMessage = ProgressResponseMessage | CompleteResponseMessage | ResizeCompleteResponseMessage | ErrorResponseMessage; | ||
| //#endregion | ||
| export { ResizeRequestMessage as _, GenerateRequestMessage as a, TileFormat as b, ProgressResponseMessage as c, ResizeFilter as d, ResizeImageOptions as f, ResizeModePercentage as g, ResizeModeLongEdge as h, GenerateOptions as i, RequestMessage as l, ResizeModeFit as m, DownscaleSharpenOptions as n, GenerateResult as o, ResizeMode as p, ErrorResponseMessage as r, Origin as s, CompleteResponseMessage as t, ResizeCompleteResponseMessage as u, ResizeResult as v, ResponseMessage as y }; |
| import fs from "node:fs"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { MessageChannel } from "node:worker_threads"; | ||
| import Tinypool from "tinypool"; | ||
| //#region src/pool.ts | ||
| /** | ||
| * Create a new image map task pool. | ||
| */ | ||
| function createPool(options = {}) { | ||
| return new Pool(options); | ||
| } | ||
| /** | ||
| * Default implementation backed by Tinypool. | ||
| */ | ||
| var Pool = class { | ||
| concurrency; | ||
| queue = []; | ||
| /** | ||
| * Create a pool with configured concurrency. | ||
| */ | ||
| constructor(options) { | ||
| this.concurrency = Math.max(1, options.concurrency ?? 1); | ||
| } | ||
| /** Add a generate task into the pool queue. */ | ||
| add(task) { | ||
| this.queue.push({ | ||
| type: "generate", | ||
| params: task | ||
| }); | ||
| } | ||
| /** Add a resize task into the pool queue. */ | ||
| addResize(task) { | ||
| this.queue.push({ | ||
| type: "resize", | ||
| params: task | ||
| }); | ||
| } | ||
| /** Execute all queued tasks with a Tinypool worker pool. */ | ||
| async run() { | ||
| const tasks = this.queue.splice(0, this.queue.length); | ||
| if (tasks.length === 0) return []; | ||
| const threadCount = Math.min(this.concurrency, tasks.length); | ||
| const pool = new Tinypool({ | ||
| filename: resolveWorkerUrl(), | ||
| minThreads: threadCount, | ||
| maxThreads: threadCount, | ||
| concurrentTasksPerWorker: 1, | ||
| teardown: "teardown" | ||
| }); | ||
| try { | ||
| const results = Array.from({ length: tasks.length }); | ||
| await Promise.all(tasks.map((task, index) => this.runTask(pool, task, index, results))); | ||
| return results; | ||
| } finally { | ||
| await pool.destroy(); | ||
| } | ||
| } | ||
| /** Execute all queued generate tasks only. */ | ||
| async runGenerate() { | ||
| const generateTasks = this.queue.filter((t) => t.type === "generate"); | ||
| this.queue.length = 0; | ||
| generateTasks.forEach((t) => this.queue.push(t)); | ||
| return await this.run(); | ||
| } | ||
| /** Execute all queued resize tasks only. */ | ||
| async runResize() { | ||
| const resizeTasks = this.queue.filter((t) => t.type === "resize"); | ||
| this.queue.length = 0; | ||
| resizeTasks.forEach((t) => this.queue.push(t)); | ||
| return await this.run(); | ||
| } | ||
| /** | ||
| * Run a single task in Tinypool and store its result. | ||
| */ | ||
| async runTask(pool, task, index, results) { | ||
| const payload = { params: toWorkerParams(task) }; | ||
| const onProgress = task.params.onProgress; | ||
| if (!onProgress) { | ||
| results[index] = await pool.run(payload); | ||
| return; | ||
| } | ||
| const { port1, port2 } = new MessageChannel(); | ||
| payload.port = port1; | ||
| const handleMessage = (message) => { | ||
| if (!isWorkerProgressMessage(message)) return; | ||
| onProgress(message.current, message.total, message.message); | ||
| }; | ||
| port2.on("message", handleMessage); | ||
| try { | ||
| results[index] = await pool.run(payload, { transferList: { transfer: [port1] } }); | ||
| } finally { | ||
| port2.off("message", handleMessage); | ||
| port2.close(); | ||
| } | ||
| } | ||
| }; | ||
| /** | ||
| * Remove non-serializable fields from params before sending to workers. | ||
| */ | ||
| function toWorkerParams(task) { | ||
| if (task.type === "generate") { | ||
| const { onProgress: _onProgress, ...rest } = task.params; | ||
| return { | ||
| type: "generate", | ||
| ...rest | ||
| }; | ||
| } else { | ||
| const { onProgress: _onProgress, ...rest } = task.params; | ||
| return { | ||
| type: "resize", | ||
| ...rest | ||
| }; | ||
| } | ||
| } | ||
| /** | ||
| * Resolve the worker entry url, preferring the built .mjs output. | ||
| */ | ||
| function resolveWorkerUrl() { | ||
| const mjsUrl = new URL("./pool-worker.mjs", import.meta.url); | ||
| if (fs.existsSync(fileURLToPath(mjsUrl))) return mjsUrl.href; | ||
| const tsUrl = new URL("./pool-worker.ts", import.meta.url); | ||
| if (fs.existsSync(fileURLToPath(tsUrl))) return tsUrl.href; | ||
| return mjsUrl.href; | ||
| } | ||
| /** | ||
| * Narrow unknown messages into worker progress messages. | ||
| */ | ||
| function isWorkerProgressMessage(value) { | ||
| if (!value || typeof value !== "object") return false; | ||
| const msg = value; | ||
| if (msg.type !== "progress") return false; | ||
| return typeof msg.current === "number" && typeof msg.total === "number" && typeof msg.message === "string"; | ||
| } | ||
| //#endregion | ||
| //#region src/index.ts | ||
| var ImageMap = class { | ||
| /** | ||
| * Generate tiles for an image. | ||
| */ | ||
| static async generate(params) { | ||
| const pool = createPool({ concurrency: 1 }); | ||
| pool.add(params); | ||
| const [result] = await pool.run(); | ||
| return result; | ||
| } | ||
| /** | ||
| * Resize an image without tiling. | ||
| */ | ||
| static async resize(params) { | ||
| const pool = createPool({ concurrency: 1 }); | ||
| pool.addResize(params); | ||
| const [result] = await pool.run(); | ||
| return result; | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { createPool as n, ImageMap as t }; |
+50
| # @yafh/image-map | ||
| Rust 驱动的图片瓦片生成器,提供 Node.js SDK + CLI,适合批量切图与多级缩放瓦片生成。 | ||
| ## 安装 | ||
| ```bash | ||
| pnpm add @yafh/image-map | ||
| ``` | ||
| ## CLI | ||
| ```bash | ||
| image-map generate --input <file> --output <dir> [options] | ||
| ``` | ||
| 常用选项: | ||
| - `--tile-size <n>`:瓦片尺寸,默认 `256`。 | ||
| - `--format <fmt>`:可重复,`png | jpg | jpeg | webp`,默认 `webp`。 | ||
| - `--origin <o>`:`topLeft | center`,默认 `topLeft`。 | ||
| - `--min-zoom <n>`:最小缩放级别,默认 `0`。 | ||
| - `--max-zoom <n>`:最大缩放级别,默认 `0`。 | ||
| - `-h, --help`:显示帮助。 | ||
| ## SDK | ||
| ```ts | ||
| import { ImageMap } from '@yafh/image-map' | ||
| const result = await ImageMap.generate({ | ||
| input: './input.png', | ||
| output: './tiles', | ||
| tileSize: 256, | ||
| formats: ['webp'], | ||
| origin: 'topLeft', | ||
| minZoom: 0, | ||
| maxZoom: 4, | ||
| }) | ||
| console.log(result) | ||
| ``` | ||
| ## 平台依赖 | ||
| 平台原生二进制由可选依赖提供,安装时会按需拉取对应平台包。 | ||
| ## 相关链接 | ||
| - GitHub:https://github.com/YanAndFish/image-map |
+79
-3
@@ -1,2 +0,2 @@ | ||
| import { t as ImageMap } from "./src-CMNnQvJ2.mjs"; | ||
| import { t as ImageMap } from "./src-CRFV3_1p.mjs"; | ||
| import process from "node:process"; | ||
@@ -11,3 +11,7 @@ | ||
| } | ||
| if (command !== "generate") throw new Error(`Unknown command: ${command}`); | ||
| if (command === "generate") await runGenerate(args); | ||
| else if (command === "resize") await runResize(args); | ||
| else throw new Error(`Unknown command: ${command}`); | ||
| } | ||
| async function runGenerate(args) { | ||
| const input = getStringArg(args, "--input"); | ||
@@ -45,2 +49,46 @@ const output = getStringArg(args, "--output"); | ||
| } | ||
| async function runResize(args) { | ||
| const input = getStringArg(args, "--input"); | ||
| const output = getStringArg(args, "--output"); | ||
| if (!input) throw new Error("Missing required option: --input"); | ||
| if (!output) throw new Error("Missing required option: --output"); | ||
| const mode = parseResizeMode(args); | ||
| const format = getStringArg(args, "--format") ?? "webp"; | ||
| const resizeFilter = getStringArg(args, "--resize-filter"); | ||
| const sharpen = { | ||
| enabled: getBooleanArg(args, "--sharpen", true), | ||
| sigma: getNumberArg(args, "--sharpen-sigma", .5), | ||
| amount: getNumberArg(args, "--sharpen-amount", .35), | ||
| threshold: getNumberArg(args, "--sharpen-threshold", 2) | ||
| }; | ||
| const result = await ImageMap.resize({ | ||
| input, | ||
| output, | ||
| mode, | ||
| format, | ||
| resizeFilter, | ||
| sharpen, | ||
| onProgress: (current, total, message) => { | ||
| process.stderr.write(`${current}/${total} ${message}\n`); | ||
| } | ||
| }); | ||
| process.stdout.write(`${JSON.stringify(result)}\n`); | ||
| } | ||
| function parseResizeMode(args) { | ||
| const modeType = getStringArg(args, "--mode") ?? "percentage"; | ||
| if (modeType === "percentage") return { | ||
| type: "percentage", | ||
| value: getNumberArg(args, "--value", 50) | ||
| }; | ||
| else if (modeType === "longEdge" || modeType === "long-edge") return { | ||
| type: "longEdge", | ||
| pixels: getNumberArg(args, "--pixels", 1200) | ||
| }; | ||
| else if (modeType === "fit") return { | ||
| type: "fit", | ||
| width: getNumberArg(args, "--width", 1200), | ||
| height: getNumberArg(args, "--height", 1200) | ||
| }; | ||
| else throw new Error(`Invalid resize mode: ${modeType}. Must be one of: percentage, longEdge, fit`); | ||
| } | ||
| function parseCommand(argv) { | ||
@@ -56,2 +104,6 @@ if (argv.length === 0) return { | ||
| }; | ||
| if (first === "resize") return { | ||
| command: "resize", | ||
| args: argv.slice(1) | ||
| }; | ||
| if (first === "help" || first === "--help" || first === "-h") return { | ||
@@ -103,4 +155,10 @@ command: "help", | ||
| process.stdout.write([ | ||
| "image-map (Rust-powered image tile generator)", | ||
| "image-map (Rust-powered image tile generator & resizer)", | ||
| "", | ||
| "Commands:", | ||
| " generate Generate ZXY tiles from an image", | ||
| " resize Resize an image without tiling", | ||
| " help Show this help", | ||
| "", | ||
| "=== generate ===", | ||
| "Usage:", | ||
@@ -120,2 +178,20 @@ " image-map generate --input <file> --output <dir> [options]", | ||
| " --downscale-sharpen-threshold <n> Threshold 0-255 (default: 2)", | ||
| "", | ||
| "=== resize ===", | ||
| "Usage:", | ||
| " image-map resize --input <file> --output <file> [options]", | ||
| "", | ||
| "Options:", | ||
| " --mode <m> Resize mode: percentage | longEdge | fit (default: percentage)", | ||
| " --value <n> Percentage value for percentage mode (default: 50)", | ||
| " --pixels <n> Long edge pixels for longEdge mode (default: 1200)", | ||
| " --width <n> Max width for fit mode (default: 1200)", | ||
| " --height <n> Max height for fit mode (default: 1200)", | ||
| " --format <fmt> One of: png | jpg | jpeg | webp (default: webp)", | ||
| " --resize-filter <f> One of: lanczos3 | catmullRom | mitchell | hamming | bilinear | box | gaussian (default: catmullRom)", | ||
| " --sharpen <bool> Enable sharpening (default: true)", | ||
| " --sharpen-sigma <n> Gaussian blur sigma (default: 0.5)", | ||
| " --sharpen-amount <n> Unsharp amount (default: 0.35)", | ||
| " --sharpen-threshold <n> Threshold 0-255 (default: 2)", | ||
| "", | ||
| " -h, --help Show this help", | ||
@@ -122,0 +198,0 @@ "" |
+37
-5
@@ -1,2 +0,2 @@ | ||
| import { a as GenerateRequestMessage, c as ProgressResponseMessage, d as ResponseMessage, f as TileFormat, i as GenerateOptions, l as RequestMessage, n as DownscaleSharpenOptions, o as GenerateResult, r as ErrorResponseMessage, s as Origin, t as CompleteResponseMessage, u as ResizeFilter } from "./protocol-Bn9_T70a.mjs"; | ||
| import { _ as ResizeRequestMessage, a as GenerateRequestMessage, b as TileFormat, c as ProgressResponseMessage, d as ResizeFilter, f as ResizeImageOptions, g as ResizeModePercentage, h as ResizeModeLongEdge, i as GenerateOptions, l as RequestMessage, m as ResizeModeFit, n as DownscaleSharpenOptions, o as GenerateResult, p as ResizeMode, r as ErrorResponseMessage, s as Origin, t as CompleteResponseMessage, u as ResizeCompleteResponseMessage, v as ResizeResult, y as ResponseMessage } from "./protocol-DsP6EbtC.mjs"; | ||
@@ -31,2 +31,21 @@ //#region src/pool.d.ts | ||
| /** | ||
| * Parameters for resizing an image (without tiling). | ||
| */ | ||
| interface ResizeParams { | ||
| /** Input file path. */ | ||
| input: string; | ||
| /** Output file path. */ | ||
| output: string; | ||
| /** Resize mode (percentage, longEdge, or fit). */ | ||
| mode: ResizeMode; | ||
| /** Output format. */ | ||
| format?: TileFormat; | ||
| /** Resize filter for downscaling. */ | ||
| resizeFilter?: ResizeFilter; | ||
| /** Sharpening configuration for downscaling. */ | ||
| sharpen?: DownscaleSharpenOptions; | ||
| /** Progress callback. */ | ||
| onProgress?: (current: number, total: number, message: string) => void; | ||
| } | ||
| /** | ||
| * Pool configuration options. | ||
@@ -42,6 +61,12 @@ */ | ||
| interface ImageMapPool { | ||
| /** Add a task into the queue. */ | ||
| /** Add a generate task into the queue. */ | ||
| add: (task: GenerateParams) => void; | ||
| /** Run queued tasks and return ordered results. */ | ||
| run: () => Promise<GenerateResult[]>; | ||
| /** Add a resize task into the queue. */ | ||
| addResize: (task: ResizeParams) => void; | ||
| /** Run queued tasks and return ordered results (GenerateResult or ResizeResult). */ | ||
| run: () => Promise<(GenerateResult | ResizeResult)[]>; | ||
| /** Run queued generate tasks and return ordered results. */ | ||
| runGenerate: () => Promise<GenerateResult[]>; | ||
| /** Run queued resize tasks and return ordered results. */ | ||
| runResize: () => Promise<ResizeResult[]>; | ||
| } | ||
@@ -55,5 +80,12 @@ /** | ||
| declare class ImageMap { | ||
| /** | ||
| * Generate tiles for an image. | ||
| */ | ||
| static generate(params: GenerateParams): Promise<GenerateResult>; | ||
| /** | ||
| * Resize an image without tiling. | ||
| */ | ||
| static resize(params: ResizeParams): Promise<ResizeResult>; | ||
| } | ||
| //#endregion | ||
| export { CompleteResponseMessage, DownscaleSharpenOptions, ErrorResponseMessage, GenerateOptions, type GenerateParams, GenerateRequestMessage, GenerateResult, ImageMap, type ImageMapPool, Origin, type PoolOptions, ProgressResponseMessage, RequestMessage, ResizeFilter, ResponseMessage, TileFormat, createPool }; | ||
| export { CompleteResponseMessage, DownscaleSharpenOptions, ErrorResponseMessage, GenerateOptions, type GenerateParams, GenerateRequestMessage, GenerateResult, ImageMap, type ImageMapPool, Origin, type PoolOptions, ProgressResponseMessage, RequestMessage, ResizeCompleteResponseMessage, ResizeFilter, ResizeImageOptions, ResizeMode, ResizeModeFit, ResizeModeLongEdge, ResizeModePercentage, type ResizeParams, ResizeRequestMessage, ResizeResult, ResponseMessage, TileFormat, createPool }; |
+1
-1
@@ -1,3 +0,3 @@ | ||
| import { n as createPool, t as ImageMap } from "./src-CMNnQvJ2.mjs"; | ||
| import { n as createPool, t as ImageMap } from "./src-CRFV3_1p.mjs"; | ||
| export { ImageMap, createPool }; |
@@ -1,2 +0,2 @@ | ||
| import { f as TileFormat, n as DownscaleSharpenOptions, o as GenerateResult, s as Origin, u as ResizeFilter } from "./protocol-Bn9_T70a.mjs"; | ||
| import { b as TileFormat, d as ResizeFilter, n as DownscaleSharpenOptions, o as GenerateResult, p as ResizeMode, s as Origin, v as ResizeResult } from "./protocol-DsP6EbtC.mjs"; | ||
| import { MessagePort } from "node:worker_threads"; | ||
@@ -7,5 +7,7 @@ | ||
| /** | ||
| * Serialized task params sent to the Tinypool worker. | ||
| * Serialized generate task params sent to the Tinypool worker. | ||
| */ | ||
| interface WorkerParams { | ||
| interface WorkerGenerateParams { | ||
| /** Task type. */ | ||
| type: 'generate'; | ||
| /** Input file path. */ | ||
@@ -31,2 +33,25 @@ input: string; | ||
| /** | ||
| * Serialized resize task params sent to the Tinypool worker. | ||
| */ | ||
| interface WorkerResizeParams { | ||
| /** Task type. */ | ||
| type: 'resize'; | ||
| /** Input file path. */ | ||
| input: string; | ||
| /** Output file path. */ | ||
| output: string; | ||
| /** Resize mode. */ | ||
| mode: ResizeMode; | ||
| /** Output format. */ | ||
| format?: TileFormat; | ||
| /** Resize filter for downscaling. */ | ||
| resizeFilter?: ResizeFilter; | ||
| /** Sharpening configuration for downscaling. */ | ||
| sharpen?: DownscaleSharpenOptions; | ||
| } | ||
| /** | ||
| * Unified worker params. | ||
| */ | ||
| type WorkerTaskParams = WorkerGenerateParams | WorkerResizeParams; | ||
| /** | ||
| * Payload passed into a Tinypool worker task. | ||
@@ -36,3 +61,3 @@ */ | ||
| /** Serializable task params. */ | ||
| params: WorkerParams; | ||
| params: WorkerTaskParams; | ||
| /** Progress channel for this task. */ | ||
@@ -43,6 +68,8 @@ port?: MessagePort; | ||
| //#region src/pool-worker.d.ts | ||
| /** Union result type for generate/resize tasks. */ | ||
| type TaskResult = GenerateResult | ResizeResult; | ||
| /** | ||
| * Tinypool entry point. Runs a single task inside the worker. | ||
| */ | ||
| declare function run(task: WorkerTask): Promise<GenerateResult>; | ||
| declare function run(task: WorkerTask): Promise<TaskResult>; | ||
| /** | ||
@@ -49,0 +76,0 @@ * Tinypool teardown hook to dispose of the binary process. |
+86
-4
@@ -101,2 +101,10 @@ import { createRequire } from "node:module"; | ||
| /** | ||
| * Enqueue a resize request on the underlying binary. | ||
| */ | ||
| resize(params, port) { | ||
| const task = this.last.then(() => this.resizeImpl(params, port)); | ||
| this.last = task.then(() => void 0, () => void 0); | ||
| return task; | ||
| } | ||
| /** | ||
| * Dispose the child process and clear pending tasks. | ||
@@ -126,6 +134,7 @@ */ | ||
| output: params.output, | ||
| options: normalizeOptions(params) | ||
| options: normalizeGenerateOptions(params) | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { | ||
| taskType: "generate", | ||
| port, | ||
@@ -144,2 +153,30 @@ resolve, | ||
| /** | ||
| * Send a resize request to the Rust binary and await the response. | ||
| */ | ||
| resizeImpl(params, port) { | ||
| if (this.disposed) return Promise.reject(/* @__PURE__ */ new Error("image-map worker is disposed")); | ||
| const id = nanoid(); | ||
| const request = { | ||
| type: "resize", | ||
| id, | ||
| input: params.input, | ||
| output: params.output, | ||
| options: normalizeResizeOptions(params) | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { | ||
| taskType: "resize", | ||
| port, | ||
| resolve, | ||
| reject | ||
| }); | ||
| const line = `${JSON.stringify(request)}\n`; | ||
| this.child.stdin.write(line, (err) => { | ||
| if (!err) return; | ||
| this.pending.delete(id); | ||
| reject(err instanceof Error ? err : new Error(String(err))); | ||
| }); | ||
| }); | ||
| } | ||
| /** | ||
| * Handle one line of output from the Rust binary. | ||
@@ -170,2 +207,6 @@ */ | ||
| } | ||
| if (msg.type === "resizeComplete") { | ||
| pending.resolve(msg.result); | ||
| return; | ||
| } | ||
| if (msg.type === "error") pending.reject(new Error(msg.error)); | ||
@@ -180,3 +221,6 @@ } | ||
| try { | ||
| return await worker.generate(task.params, task.port); | ||
| const params = task.params; | ||
| if ("type" in params && params.type === "resize") return await worker.resize(params, task.port); | ||
| const generateParams = params; | ||
| return await worker.generate(generateParams, task.port); | ||
| } finally { | ||
@@ -193,5 +237,5 @@ task.port?.close(); | ||
| /** | ||
| * Normalize options for the Rust binary. | ||
| * Normalize generate options for the Rust binary. | ||
| */ | ||
| function normalizeOptions(params) { | ||
| function normalizeGenerateOptions(params) { | ||
| const tileSize = params.tileSize ?? 256; | ||
@@ -259,2 +303,40 @@ const formats = params.formats ?? ["webp"]; | ||
| /** | ||
| * Normalize and validate image format inputs. | ||
| */ | ||
| function normalizeFormat(value) { | ||
| const format = value ?? "webp"; | ||
| const allowed = [ | ||
| "png", | ||
| "jpg", | ||
| "jpeg", | ||
| "webp" | ||
| ]; | ||
| if (!allowed.includes(format)) throw new Error(`format must be one of: ${allowed.join(", ")}`); | ||
| return format; | ||
| } | ||
| /** | ||
| * Normalize resize options for the Rust binary. | ||
| */ | ||
| function normalizeResizeOptions(params) { | ||
| const mode = params.mode; | ||
| const format = normalizeFormat(params.format); | ||
| const resizeFilter = normalizeResizeFilter(params.resizeFilter); | ||
| const sharpen = normalizeDownscaleSharpen(params.sharpen); | ||
| if (!mode || typeof mode !== "object" || !("type" in mode)) throw new Error("mode must be a valid resize mode object"); | ||
| if (mode.type === "percentage") { | ||
| if (typeof mode.value !== "number" || !Number.isFinite(mode.value) || mode.value <= 0) throw new Error("mode.value must be a positive number for percentage mode"); | ||
| } else if (mode.type === "longEdge") { | ||
| if (typeof mode.pixels !== "number" || !Number.isInteger(mode.pixels) || mode.pixels <= 0) throw new Error("mode.pixels must be a positive integer for longEdge mode"); | ||
| } else if (mode.type === "fit") { | ||
| if (typeof mode.width !== "number" || !Number.isInteger(mode.width) || mode.width <= 0) throw new Error("mode.width must be a positive integer for fit mode"); | ||
| if (typeof mode.height !== "number" || !Number.isInteger(mode.height) || mode.height <= 0) throw new Error("mode.height must be a positive integer for fit mode"); | ||
| } else throw new Error("mode.type must be one of: percentage, longEdge, fit"); | ||
| return { | ||
| mode, | ||
| format, | ||
| resizeFilter, | ||
| sharpen | ||
| }; | ||
| } | ||
| /** | ||
| * Convert protocol progress message to worker progress message. | ||
@@ -261,0 +343,0 @@ */ |
+16
-7
| { | ||
| "name": "@yafh/image-map", | ||
| "type": "module", | ||
| "version": "1.0.3", | ||
| "version": "1.1.1", | ||
| "description": "Image tile generator (SDK + CLI)", | ||
| "author": "YanAndFish", | ||
| "license": "MIT", | ||
| "homepage": "https://github.com/YanAndFish/image-map#readme", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "https://github.com/YanAndFish/image-map.git", | ||
| "directory": "packages/image-map" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/YanAndFish/image-map/issues" | ||
| }, | ||
| "keywords": [ | ||
@@ -22,3 +31,3 @@ "image-tile", | ||
| "bin": { | ||
| "image-map": "./bin/image-map" | ||
| "image-map": "./bin/image-map.mjs" | ||
| }, | ||
@@ -34,7 +43,7 @@ "files": [ | ||
| "optionalDependencies": { | ||
| "@yafh/image-map-darwin-arm64": "1.0.3", | ||
| "@yafh/image-map-linux-arm64": "1.0.3", | ||
| "@yafh/image-map-linux-x64": "1.0.3", | ||
| "@yafh/image-map-win32-arm64": "1.0.3", | ||
| "@yafh/image-map-win32-x64": "1.0.3" | ||
| "@yafh/image-map-darwin-arm64": "1.1.1", | ||
| "@yafh/image-map-linux-arm64": "1.1.1", | ||
| "@yafh/image-map-win32-arm64": "1.1.1", | ||
| "@yafh/image-map-win32-x64": "1.1.1", | ||
| "@yafh/image-map-linux-x64": "1.1.1" | ||
| }, | ||
@@ -41,0 +50,0 @@ "devDependencies": { |
Sorry, the diff of this file is not supported yet
| //#region src/protocol.d.ts | ||
| type TileFormat = 'png' | 'jpg' | 'jpeg' | 'webp'; | ||
| type Origin = 'topLeft' | 'center'; | ||
| /** Resize filter used for downscaling between zoom levels. */ | ||
| type ResizeFilter = 'lanczos3' | 'catmullRom' | 'mitchell' | 'hamming' | 'bilinear' | 'box' | 'gaussian'; | ||
| /** Options for downscale sharpening. */ | ||
| interface DownscaleSharpenOptions { | ||
| /** Whether downscale sharpening is enabled. */ | ||
| enabled?: boolean; | ||
| /** Gaussian blur sigma for unsharp mask. */ | ||
| sigma?: number; | ||
| /** Unsharp mask amount multiplier. */ | ||
| amount?: number; | ||
| /** Threshold for minimal brightness change that will be sharpened. */ | ||
| threshold?: number; | ||
| } | ||
| interface GenerateOptions { | ||
| /** Resize filter for building lower zoom levels. */ | ||
| resizeFilter: ResizeFilter; | ||
| /** Downscale sharpening configuration. */ | ||
| downscaleSharpen: Required<DownscaleSharpenOptions>; | ||
| /** Tile size in pixels. */ | ||
| tileSize: number; | ||
| /** Output formats. */ | ||
| formats: TileFormat[]; | ||
| /** Origin position. */ | ||
| origin: Origin; | ||
| /** Minimum zoom level. */ | ||
| minZoom: number; | ||
| /** Maximum zoom level. */ | ||
| maxZoom: number; | ||
| } | ||
| interface GenerateResult { | ||
| /** Total number of tiles generated. */ | ||
| tilesGenerated: number; | ||
| /** Output directory path. */ | ||
| outputDir: string; | ||
| } | ||
| interface GenerateRequestMessage { | ||
| /** Message type. */ | ||
| type: 'generate'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Input image path. */ | ||
| input: string; | ||
| /** Output directory path. */ | ||
| output: string; | ||
| /** Generation options. */ | ||
| options: GenerateOptions; | ||
| } | ||
| type RequestMessage = GenerateRequestMessage; | ||
| interface ProgressResponseMessage { | ||
| /** Message type. */ | ||
| type: 'progress'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Current progress value. */ | ||
| current: number; | ||
| /** Total progress value. */ | ||
| total: number; | ||
| /** Human readable message. */ | ||
| message: string; | ||
| } | ||
| interface CompleteResponseMessage { | ||
| /** Message type. */ | ||
| type: 'complete'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Generation result payload. */ | ||
| result: GenerateResult; | ||
| } | ||
| interface ErrorResponseMessage { | ||
| /** Message type. */ | ||
| type: 'error'; | ||
| /** Request id for correlating responses. */ | ||
| id: string; | ||
| /** Error message. */ | ||
| error: string; | ||
| } | ||
| type ResponseMessage = ProgressResponseMessage | CompleteResponseMessage | ErrorResponseMessage; | ||
| //#endregion | ||
| export { GenerateRequestMessage as a, ProgressResponseMessage as c, ResponseMessage as d, TileFormat as f, GenerateOptions as i, RequestMessage as l, DownscaleSharpenOptions as n, GenerateResult as o, ErrorResponseMessage as r, Origin as s, CompleteResponseMessage as t, ResizeFilter as u }; |
| import fs from "node:fs"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { MessageChannel } from "node:worker_threads"; | ||
| import Tinypool from "tinypool"; | ||
| //#region src/pool.ts | ||
| /** | ||
| * Create a new image map task pool. | ||
| */ | ||
| function createPool(options = {}) { | ||
| return new Pool(options); | ||
| } | ||
| /** | ||
| * Default implementation backed by Tinypool. | ||
| */ | ||
| var Pool = class { | ||
| concurrency; | ||
| queue = []; | ||
| /** | ||
| * Create a pool with configured concurrency. | ||
| */ | ||
| constructor(options) { | ||
| this.concurrency = Math.max(1, options.concurrency ?? 1); | ||
| } | ||
| /** Add a task into the pool queue. */ | ||
| add(task) { | ||
| this.queue.push(task); | ||
| } | ||
| /** Execute all queued tasks with a Tinypool worker pool. */ | ||
| async run() { | ||
| const tasks = this.queue.splice(0, this.queue.length); | ||
| if (tasks.length === 0) return []; | ||
| const threadCount = Math.min(this.concurrency, tasks.length); | ||
| const pool = new Tinypool({ | ||
| filename: resolveWorkerUrl(), | ||
| minThreads: threadCount, | ||
| maxThreads: threadCount, | ||
| concurrentTasksPerWorker: 1, | ||
| teardown: "teardown" | ||
| }); | ||
| try { | ||
| const results = Array.from({ length: tasks.length }); | ||
| await Promise.all(tasks.map((task, index) => this.runTask(pool, task, index, results))); | ||
| return results; | ||
| } finally { | ||
| await pool.destroy(); | ||
| } | ||
| } | ||
| /** | ||
| * Run a single task in Tinypool and store its result. | ||
| */ | ||
| async runTask(pool, task, index, results) { | ||
| const payload = { params: toWorkerParams(task) }; | ||
| if (!task.onProgress) { | ||
| results[index] = await pool.run(payload); | ||
| return; | ||
| } | ||
| const { port1, port2 } = new MessageChannel(); | ||
| const onProgress = task.onProgress; | ||
| payload.port = port1; | ||
| const handleMessage = (message) => { | ||
| if (!isWorkerProgressMessage(message)) return; | ||
| onProgress(message.current, message.total, message.message); | ||
| }; | ||
| port2.on("message", handleMessage); | ||
| try { | ||
| results[index] = await pool.run(payload, { transferList: { transfer: [port1] } }); | ||
| } finally { | ||
| port2.off("message", handleMessage); | ||
| port2.close(); | ||
| } | ||
| } | ||
| }; | ||
| /** | ||
| * Remove non-serializable fields from params before sending to workers. | ||
| */ | ||
| function toWorkerParams(params) { | ||
| const { onProgress: _onProgress, ...rest } = params; | ||
| return rest; | ||
| } | ||
| /** | ||
| * Resolve the worker entry url, preferring the built .mjs output. | ||
| */ | ||
| function resolveWorkerUrl() { | ||
| const mjsUrl = new URL("./pool-worker.mjs", import.meta.url); | ||
| if (fs.existsSync(fileURLToPath(mjsUrl))) return mjsUrl.href; | ||
| const tsUrl = new URL("./pool-worker.ts", import.meta.url); | ||
| if (fs.existsSync(fileURLToPath(tsUrl))) return tsUrl.href; | ||
| return mjsUrl.href; | ||
| } | ||
| /** | ||
| * Narrow unknown messages into worker progress messages. | ||
| */ | ||
| function isWorkerProgressMessage(value) { | ||
| if (!value || typeof value !== "object") return false; | ||
| const msg = value; | ||
| if (msg.type !== "progress") return false; | ||
| return typeof msg.current === "number" && typeof msg.total === "number" && typeof msg.message === "string"; | ||
| } | ||
| //#endregion | ||
| //#region src/index.ts | ||
| var ImageMap = class { | ||
| static async generate(params) { | ||
| const pool = createPool({ concurrency: 1 }); | ||
| pool.add(params); | ||
| const [result] = await pool.run(); | ||
| return result; | ||
| } | ||
| }; | ||
| //#endregion | ||
| export { createPool as n, ImageMap as t }; |
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No website
QualityPackage does not have a website.
35898
54.52%11
10%710
44.6%0
-100%0
-100%0
-100%51
Infinity%1
-50%