{"version":3,"file":"sharp.mjs","names":[],"sources":["../../../../src/batteries/media/engines/sharp.ts"],"sourcesContent":["/**\n * A Node-native image {@link @nhtio/adk/batteries/media/contracts!MediaEngine}\n * backed by sharp (libvips bindings — fast, full-format).\n *\n * @module @nhtio/adk/batteries/media/engines/sharp\n *\n * @remarks\n * The performance/fidelity engine for Node deployments: native speed plus webp/avif output\n * and the full `fit` mode set, plus a generation edge — `EMPTY_MIME` → a blank 1024×1024\n * white canvas in any supported encoding (resize in the same statement). sharp is permanently\n * Node-only (native bindings); for a cross-environment engine compose the jimp implementation\n * instead — the capability declaration is the seam, and BYO instances adapt via\n * {@link fromSharp}.\n *\n * `sharp` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { EMPTY_MIME } from '../contracts'\nimport { isError } from '@nhtio/adk/guards'\nimport { default as SharpDefault } from 'sharp'\nimport { E_INVALID_MEDIA_PIPELINE_CONFIG } from '../exceptions'\nimport type {\n  ImageAnnotation,\n  MediaEngine,\n  MutateCapability,\n  MutateRequest,\n  EngineBytesResult,\n  ConvertCapability,\n  ConvertRequest,\n  ConvertResult,\n} from '../contracts'\n\ntype SharpFn = typeof SharpDefault\n\n/** Options for {@link sharpEngine}. */\nexport interface SharpEngineOptions {\n  /** Override the module resolution (electron/custom builds). Default: `import('sharp')`. */\n  sharp?: () => SharpFn | { default: SharpFn } | Promise<SharpFn | { default: SharpFn }>\n}\n\nconst SUPPORTED_OUTPUT = ['png', 'jpg', 'jpeg', 'webp', 'tiff', 'avif', 'gif'] as const\n\nconst MIME_BY_FORMAT: Record<string, string> = {\n  png: 'image/png',\n  jpg: 'image/jpeg',\n  jpeg: 'image/jpeg',\n  webp: 'image/webp',\n  tiff: 'image/tiff',\n  avif: 'image/avif',\n  gif: 'image/gif',\n}\n\nconst resolveSharp = async (supplied: SharpEngineOptions['sharp']): Promise<SharpFn> => {\n  try {\n    const mod = await (supplied ? supplied() : import('sharp'))\n    const fn = typeof mod === 'function' ? mod : (mod as { default: SharpFn }).default\n    if (typeof fn !== 'function') throw new Error('sharp did not resolve to a function')\n    return fn\n  } catch (err) {\n    const detail = isError(err) ? err.message : String(err)\n    throw new E_INVALID_MEDIA_PIPELINE_CONFIG([\n      `the sharp engine could not load its peer dependency \"sharp\": ${detail} — install it (pnpm add sharp); note sharp is Node-only`,\n    ])\n  }\n}\n\n/** Run a fused mutate request through a sharp instance function. */\nconst runTransform = async (sharp: SharpFn, request: MutateRequest): Promise<EngineBytesResult> => {\n  let img = sharp(request.bytes, { animated: true })\n\n  if (request.annotate) {\n    const metadata = await img.metadata()\n    const pages = metadata.pages ?? 1\n    const width = metadata.width ?? 1\n    const height = metadata.pageHeight ?? metadata.height ?? 1\n    const esc = (value: string): string =>\n      value\n        .replace(/&/g, '&amp;')\n        .replace(/</g, '&lt;')\n        .replace(/>/g, '&gt;')\n        .replace(/\"/g, '&quot;')\n    const stroke = (shape: ImageAnnotation): string => esc(shape.color ?? '#ff0000')\n    const elements = request.annotate\n      .map((shape) => {\n        switch (shape.type) {\n          case 'rect':\n            return `<rect x=\"${shape.x}\" y=\"${shape.y}\" width=\"${shape.width}\" height=\"${shape.height}\" fill=\"${esc(shape.fill ?? 'none')}\" stroke=\"${stroke(shape)}\" stroke-width=\"${shape.strokeWidth ?? 2}\"/>`\n          case 'line':\n            return `<line x1=\"${shape.x1}\" y1=\"${shape.y1}\" x2=\"${shape.x2}\" y2=\"${shape.y2}\" stroke=\"${stroke(shape)}\" stroke-width=\"${shape.strokeWidth ?? 2}\"/>`\n          case 'arrow': {\n            const angle = Math.atan2(shape.y2 - shape.y1, shape.x2 - shape.x1)\n            const size = Math.max(shape.strokeWidth ?? 2, 6)\n            const left = `${shape.x2 - size * Math.cos(angle - Math.PI / 6)},${shape.y2 - size * Math.sin(angle - Math.PI / 6)}`\n            const right = `${shape.x2 - size * Math.cos(angle + Math.PI / 6)},${shape.y2 - size * Math.sin(angle + Math.PI / 6)}`\n            return `<line x1=\"${shape.x1}\" y1=\"${shape.y1}\" x2=\"${shape.x2}\" y2=\"${shape.y2}\" stroke=\"${stroke(shape)}\" stroke-width=\"${shape.strokeWidth ?? 2}\"/><polygon points=\"${shape.x2},${shape.y2} ${left} ${right}\" fill=\"${stroke(shape)}\"/>`\n          }\n          case 'ellipse':\n            return `<ellipse cx=\"${shape.cx}\" cy=\"${shape.cy}\" rx=\"${shape.rx}\" ry=\"${shape.ry}\" fill=\"${esc(shape.fill ?? 'none')}\" stroke=\"${stroke(shape)}\" stroke-width=\"${shape.strokeWidth ?? 2}\"/>`\n          case 'text':\n            return `<text x=\"${shape.x}\" y=\"${shape.y}\" fill=\"${stroke(shape)}\" font-size=\"${shape.size ?? 16}\" font-family=\"${esc(shape.font ?? 'sans-serif')}\">${esc(shape.text)}</text>`\n        }\n      })\n      .join('')\n    const overlay = Buffer.from(\n      `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${width}\" height=\"${height}\"><g>${elements}</g></svg>`\n    )\n    const composites: Array<{\n      input: Buffer\n      blend: 'over'\n      top: number\n      left: number\n    }> = []\n    for (let i = 0; i < pages; i++) {\n      composites.push({\n        input: overlay,\n        blend: 'over',\n        top: i * height,\n        left: 0,\n      })\n    }\n    img = img.composite(composites)\n\n    // Sharp evaluates operations lazily. If we chain resize or rotate after composite, Sharp\n    // applies the resize FIRST, breaking the original-coordinate geometry of the SVG overlay.\n    // Force materialization here to apply annotations in original image space before rotating/resizing.\n    img = sharp(await img.toBuffer(), { animated: true })\n  }\n\n  if (request.rotate) img = img.rotate(request.rotate)\n  if (request.flip?.vertical) img = img.flip()\n  if (request.flip?.horizontal) img = img.flop()\n\n  if (\n    request.resize &&\n    (request.resize.width !== undefined || request.resize.height !== undefined)\n  ) {\n    img = img.resize({\n      width: request.resize.width,\n      height: request.resize.height,\n      fit: request.resize.fit ?? 'cover',\n    })\n  }\n  const target = request.format?.to ?? mimeToFormat(request.mimeType)\n  const mimeType = MIME_BY_FORMAT[target]\n  if (!mimeType) {\n    throw new Error(`sharp cannot encode \"${target}\"; supported: ${SUPPORTED_OUTPUT.join(', ')}`)\n  }\n  const formatKey = (target === 'jpg' ? 'jpeg' : target) as\n    | 'png'\n    | 'jpeg'\n    | 'webp'\n    | 'tiff'\n    | 'avif'\n    | 'gif'\n  img = img.toFormat(\n    formatKey,\n    request.format?.quality !== undefined ? { quality: request.format.quality } : {}\n  )\n  if (request.stripMetadata !== true) {\n    // sharp strips metadata by default; keep it when NOT asked to strip.\n    img = img.keepMetadata()\n  }\n  const buffer = await img.toBuffer()\n  return { bytes: new Uint8Array(buffer), mimeType }\n}\n\n/** Build the single mutate capability over a sharp source. */\nconst capabilityOf = (\n  run: (request: MutateRequest) => Promise<EngineBytesResult>\n): MutateCapability => ({\n  over: ['image/*'],\n  ops: ['resize', 'rotate', 'flip', 'strip_metadata', 'annotate'],\n  encodes: SUPPORTED_OUTPUT,\n  mutate: run,\n})\n\n/**\n * Build the generation capability over a sharp source: `EMPTY_MIME` → a blank 1024×1024\n * white canvas (3 channels, so jpeg never trips on alpha) in any supported encoding.\n */\nconst blankCapability = (getSharp: () => Promise<SharpFn>): ConvertCapability => ({\n  from: [EMPTY_MIME],\n  to: SUPPORTED_OUTPUT,\n  convert: async (request: ConvertRequest): Promise<ConvertResult> => {\n    const mimeType = MIME_BY_FORMAT[request.to]\n    if (!mimeType) {\n      throw new Error(\n        `sharp cannot generate \"${request.to}\"; supported: ${SUPPORTED_OUTPUT.join(', ')}`\n      )\n    }\n    const sharp = await getSharp()\n    const formatKey = (request.to === 'jpg' ? 'jpeg' : request.to) as\n      | 'png'\n      | 'jpeg'\n      | 'webp'\n      | 'tiff'\n      | 'avif'\n      | 'gif'\n    const buffer = await sharp({\n      create: { width: 1024, height: 1024, channels: 3, background: { r: 255, g: 255, b: 255 } },\n    })\n      .toFormat(formatKey)\n      .toBuffer()\n    return { outputs: [{ bytes: new Uint8Array(buffer), mimeType }] }\n  },\n})\n\n/**\n * Construct the sharp-backed image engine.\n *\n * @param options - Optional module resolver override.\n * @returns The engine.\n */\nexport const sharpEngine = (options: SharpEngineOptions = {}): MediaEngine => {\n  let sharpPromise: Promise<SharpFn> | undefined\n  const getSharp = (): Promise<SharpFn> => {\n    sharpPromise ??= resolveSharp(options.sharp)\n    return sharpPromise\n  }\n  return {\n    id: 'sharp',\n    converts: [blankCapability(getSharp)],\n    mutates: [capabilityOf(async (request) => runTransform(await getSharp(), request))],\n  }\n}\n\n/**\n * Adapt an already-configured sharp module (your import, your build flags) to the engine\n * contract — the BYO transformer.\n *\n * @param sharp - The sharp function you imported.\n * @returns The engine.\n */\nexport const fromSharp = (sharp: SharpFn): MediaEngine => ({\n  id: 'sharp',\n  converts: [blankCapability(async () => sharp)],\n  mutates: [capabilityOf(async (request) => runTransform(sharp, request))],\n})\n\nconst mimeToFormat = (mimeType: string): string => {\n  const sub = mimeType.toLowerCase().split(';')[0].trim().split('/')[1] ?? 'png'\n  return sub === 'jpeg' ? 'jpg' : sub\n}\n"],"mappings":"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"}