@michaelyagi/kiri 0.1.0-alpha.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kiri.mjs","sources":["../src/filters.ts","../src/stage.ts","../src/gestures.ts","../src/exif.ts","../src/validate.ts","../src/export.ts","../src/upload.ts","../src/kiri.ts","../src/batch.ts"],"sourcesContent":["import type { Filters } from \"./types\";\n\nexport const DEFAULT_FILTERS: Filters = {\n brightness: 1,\n contrast: 1,\n saturation: 1,\n grayscale: false,\n sepia: false,\n};\n\n/** Merges a partial filter update into the current filters, clamping numeric values to >= 0. */\nexport function mergeFilters(current: Filters, partial: Partial<Filters>): Filters {\n return {\n brightness: Math.max(0, partial.brightness ?? current.brightness),\n contrast: Math.max(0, partial.contrast ?? current.contrast),\n saturation: Math.max(0, partial.saturation ?? current.saturation),\n grayscale: partial.grayscale ?? current.grayscale,\n sepia: partial.sepia ?? current.sepia,\n };\n}\n\n/**\n * Builds a CSS `filter` value from the current filter state. Used verbatim as\n * both the live-preview `img.style.filter` and a canvas 2D context's\n * `ctx.filter` before drawing, so preview and export always match exactly —\n * no separately hand-rolled brightness/contrast/saturation pixel math.\n */\nexport function buildFilterString(filters: Filters): string {\n const parts = [\n `brightness(${filters.brightness})`,\n `contrast(${filters.contrast})`,\n `saturate(${filters.saturation})`,\n ];\n if (filters.grayscale) parts.push(\"grayscale(1)\");\n if (filters.sepia) parts.push(\"sepia(1)\");\n return parts.join(\" \");\n}\n","import type { Filters, FrameShape, KiriState, ZoomerPosition } from \"./types\";\nimport { buildFilterString } from \"./filters\";\n\nexport interface StageElements {\n stageEl: HTMLDivElement;\n frameEl: HTMLDivElement;\n imageLayerEl: HTMLDivElement;\n imgEl: HTMLImageElement;\n zoomerEl: HTMLInputElement | null;\n}\n\nexport interface ZoomerConfig {\n show: boolean;\n position: ZoomerPosition;\n min: number;\n max: number;\n value: number;\n}\n\nexport function createStage(\n container: HTMLElement,\n frameShape: FrameShape,\n frameWidth: number,\n frameHeight: number,\n zoomer: ZoomerConfig\n): StageElements {\n container.innerHTML = \"\";\n\n const stageEl = document.createElement(\"div\");\n stageEl.className = \"kiri-stage\";\n\n const imageLayerEl = document.createElement(\"div\");\n imageLayerEl.className = \"kiri-image-layer\";\n\n const imgEl = document.createElement(\"img\");\n imgEl.draggable = false;\n imageLayerEl.appendChild(imgEl);\n\n const frameEl = document.createElement(\"div\");\n frameEl.className =\n frameShape === \"circle\" ? \"kiri-frame kiri-frame--circle\" : \"kiri-frame\";\n setFrameSize(frameEl, frameWidth, frameHeight);\n\n stageEl.appendChild(imageLayerEl);\n stageEl.appendChild(frameEl);\n\n let zoomerEl: HTMLInputElement | null = null;\n\n if (zoomer.show) {\n const rootEl = document.createElement(\"div\");\n rootEl.className = `kiri-root kiri-root--${zoomer.position}`;\n\n zoomerEl = document.createElement(\"input\");\n zoomerEl.type = \"range\";\n zoomerEl.className = \"kiri-zoomer\";\n zoomerEl.min = String(zoomer.min);\n zoomerEl.max = String(zoomer.max);\n zoomerEl.step = \"0.01\";\n zoomerEl.value = String(zoomer.value);\n\n rootEl.appendChild(stageEl);\n rootEl.appendChild(zoomerEl);\n container.appendChild(rootEl);\n } else {\n container.appendChild(stageEl);\n }\n\n return { stageEl, frameEl, imageLayerEl, imgEl, zoomerEl };\n}\n\nexport function setFrameSize(\n frameEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n frameEl.style.width = `${width}px`;\n frameEl.style.height = `${height}px`;\n}\n\n/** Padding added around the frame when the stage auto-sizes itself — small\n * enough to avoid empty space, big enough to fit the resize handle and give\n * a visible drag margin. */\nexport const STAGE_AUTO_SIZE_PADDING = 20;\n\nexport function setStageSize(\n stageEl: HTMLDivElement,\n width: number,\n height: number\n): void {\n stageEl.style.width = `${width}px`;\n stageEl.style.height = `${height}px`;\n}\n\nexport function applyTransform(\n imageLayerEl: HTMLDivElement,\n state: KiriState,\n renderedScale: number\n): void {\n // translate(-50%,-50%) centers the layer's own center at the stage center;\n // the pixel translate shifts it by the (rotation/scale-independent) offset;\n // rotate then scale (with flip folded into scale's sign) apply around the\n // layer's own center (default transform-origin), flip first/innermost so a\n // mirrored image still rotates the way the user expects.\n const scaleX = renderedScale * (state.flip.horizontal ? -1 : 1);\n const scaleY = renderedScale * (state.flip.vertical ? -1 : 1);\n imageLayerEl.style.transform =\n `translate(-50%, -50%) ` +\n `translate(${state.offset.x}px, ${state.offset.y}px) ` +\n `rotate(${state.rotation}deg) ` +\n `scale(${scaleX}, ${scaleY})`;\n}\n\nexport function applyFilters(imgEl: HTMLImageElement, filters: Filters): void {\n imgEl.style.filter = buildFilterString(filters);\n}\n","import type { KiriState, Offset } from \"./types\";\n\nexport interface Size {\n width: number;\n height: number;\n}\n\n/** Rotation-aware natural size: 90/270 degrees swap width and height. */\nexport function effectiveNaturalSize(natural: Size, rotationDeg: number): Size {\n const swapped = ((rotationDeg / 90) % 2 + 2) % 2 === 1;\n return swapped\n ? { width: natural.height, height: natural.width }\n : { width: natural.width, height: natural.height };\n}\n\n/** The smallest scale at which the (rotation-adjusted) image still fully covers the frame. */\nexport function computeCoverScale(\n natural: Size,\n frame: Size,\n rotationDeg: number\n): number {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n if (eff.width <= 0 || eff.height <= 0) return 1;\n return Math.max(frame.width / eff.width, frame.height / eff.height);\n}\n\n/** Rendered size of the image layer given a user zoom multiplier (relative to cover scale). */\nexport function effectiveRenderedSize(\n natural: Size,\n frame: Size,\n rotationDeg: number,\n zoom: number\n): Size {\n const eff = effectiveNaturalSize(natural, rotationDeg);\n const scale = computeCoverScale(natural, frame, rotationDeg) * zoom;\n return { width: eff.width * scale, height: eff.height * scale };\n}\n\nexport function clampZoom(zoom: number, minZoom: number, maxZoom: number): number {\n return Math.min(Math.max(zoom, minZoom), maxZoom);\n}\n\n/**\n * Clamp the image-center offset (from stage/frame center) so the frame stays\n * fully covered by the rendered image on every axis.\n */\nexport function clampOffset(offset: Offset, rendered: Size, frame: Size): Offset {\n const maxX = Math.max(0, (rendered.width - frame.width) / 2);\n const maxY = Math.max(0, (rendered.height - frame.height) / 2);\n // `|| 0` normalizes -0 (e.g. clamping a negative offset to a zero-width\n // range) to +0, keeping state comparisons and serialized CSS values sane.\n return {\n x: Math.min(Math.max(offset.x, -maxX), maxX) || 0,\n y: Math.min(Math.max(offset.y, -maxY), maxY) || 0,\n };\n}\n\nexport function normalizeRotation(deg: number): number {\n return ((deg % 360) + 360) % 360;\n}\n\ninterface GestureCallbacks {\n getNaturalSize: () => Size;\n getFrameSize: () => Size;\n getState: () => KiriState;\n getMinMaxZoom: () => { min: number; max: number };\n setState: (next: KiriState) => void;\n}\n\nexport interface GestureOptions {\n mouseWheelZoom?: boolean | \"ctrl\";\n}\n\nfunction pointerDistance(a: PointerEvent, b: PointerEvent): number {\n return Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);\n}\n\nexport function attachGestures(\n stageEl: HTMLElement,\n callbacks: GestureCallbacks,\n options: GestureOptions\n): { destroy: () => void } {\n const activePointers = new Map<number, PointerEvent>();\n let dragStart: { x: number; y: number; offset: Offset } | null = null;\n let pinchStartDistance = 0;\n let pinchStartZoom = 1;\n\n function applyClampedState(next: KiriState): void {\n const { min, max } = callbacks.getMinMaxZoom();\n const zoom = clampZoom(next.zoom, min, max);\n const rendered = effectiveRenderedSize(\n callbacks.getNaturalSize(),\n callbacks.getFrameSize(),\n next.rotation,\n zoom\n );\n const offset = clampOffset(next.offset, rendered, callbacks.getFrameSize());\n callbacks.setState({\n zoom,\n offset,\n rotation: normalizeRotation(next.rotation),\n flip: next.flip,\n filters: next.filters,\n });\n }\n\n function onPointerDown(e: PointerEvent): void {\n stageEl.setPointerCapture(e.pointerId);\n activePointers.set(e.pointerId, e);\n if (activePointers.size === 1) {\n const state = callbacks.getState();\n dragStart = { x: e.clientX, y: e.clientY, offset: state.offset };\n stageEl.classList.add(\"kiri-dragging\");\n } else if (activePointers.size === 2) {\n dragStart = null;\n const [a, b] = [...activePointers.values()];\n pinchStartDistance = pointerDistance(a, b);\n pinchStartZoom = callbacks.getState().zoom;\n }\n }\n\n function onPointerMove(e: PointerEvent): void {\n if (!activePointers.has(e.pointerId)) return;\n activePointers.set(e.pointerId, e);\n\n if (activePointers.size === 2) {\n const [a, b] = [...activePointers.values()];\n const distance = pointerDistance(a, b);\n if (pinchStartDistance > 0) {\n const ratio = distance / pinchStartDistance;\n const state = callbacks.getState();\n applyClampedState({ ...state, zoom: pinchStartZoom * ratio });\n }\n return;\n }\n\n if (dragStart) {\n const dx = e.clientX - dragStart.x;\n const dy = e.clientY - dragStart.y;\n const state = callbacks.getState();\n applyClampedState({\n ...state,\n offset: { x: dragStart.offset.x + dx, y: dragStart.offset.y + dy },\n });\n }\n }\n\n function onPointerUp(e: PointerEvent): void {\n activePointers.delete(e.pointerId);\n if (activePointers.size < 2) pinchStartDistance = 0;\n if (activePointers.size === 0) {\n dragStart = null;\n stageEl.classList.remove(\"kiri-dragging\");\n }\n }\n\n function onWheel(e: WheelEvent): void {\n if (!options.mouseWheelZoom) return;\n if (options.mouseWheelZoom === \"ctrl\" && !e.ctrlKey) return;\n e.preventDefault();\n const state = callbacks.getState();\n const delta = -e.deltaY * 0.0015;\n applyClampedState({ ...state, zoom: state.zoom * (1 + delta) });\n }\n\n stageEl.addEventListener(\"pointerdown\", onPointerDown);\n stageEl.addEventListener(\"pointermove\", onPointerMove);\n stageEl.addEventListener(\"pointerup\", onPointerUp);\n stageEl.addEventListener(\"pointercancel\", onPointerUp);\n stageEl.addEventListener(\"wheel\", onWheel, { passive: false });\n\n return {\n destroy(): void {\n stageEl.removeEventListener(\"pointerdown\", onPointerDown);\n stageEl.removeEventListener(\"pointermove\", onPointerMove);\n stageEl.removeEventListener(\"pointerup\", onPointerUp);\n stageEl.removeEventListener(\"pointercancel\", onPointerUp);\n stageEl.removeEventListener(\"wheel\", onWheel);\n },\n };\n}\n","/**\n * Reads the EXIF orientation tag (1-8) from a JPEG ArrayBuffer, defaulting to 1\n * (no adjustment) for images with no EXIF data or non-JPEG input.\n */\nexport function readExifOrientation(buffer: ArrayBuffer): number {\n const view = new DataView(buffer);\n if (view.byteLength < 4 || view.getUint16(0, false) !== 0xffd8) return 1;\n\n let offset = 2;\n while (offset + 4 <= view.byteLength) {\n const marker = view.getUint16(offset, false);\n const size = view.getUint16(offset + 2, false);\n\n if (marker === 0xffe1) {\n const exifOffset = offset + 4;\n if (view.getUint32(exifOffset, false) !== 0x45786966) return 1; // \"Exif\"\n return readOrientationFromTiff(view, exifOffset + 6);\n }\n\n if ((marker & 0xff00) !== 0xff00) break;\n offset += 2 + size;\n }\n return 1;\n}\n\nfunction readOrientationFromTiff(view: DataView, tiffStart: number): number {\n const littleEndian = view.getUint16(tiffStart, false) === 0x4949;\n const firstIfdOffset = view.getUint32(tiffStart + 4, littleEndian);\n const ifdStart = tiffStart + firstIfdOffset;\n if (ifdStart + 2 > view.byteLength) return 1;\n\n const entryCount = view.getUint16(ifdStart, littleEndian);\n for (let i = 0; i < entryCount; i++) {\n const entryOffset = ifdStart + 2 + i * 12;\n if (entryOffset + 12 > view.byteLength) break;\n const tag = view.getUint16(entryOffset, littleEndian);\n if (tag === 0x0112) {\n const value = view.getUint16(entryOffset + 8, littleEndian);\n return value >= 1 && value <= 8 ? value : 1;\n }\n }\n return 1;\n}\n\nexport interface OrientationTransform {\n rotation: number;\n flipHorizontal: boolean;\n}\n\n/**\n * Maps an EXIF orientation value to the rotation + horizontal-flip pair that\n * normalizes it. A vertical flip is never needed on its own: orientation 4\n * (mirror vertical) is expressed as rotate(180) + flip horizontal, which is\n * mathematically equivalent and matches Kiri's transform order (flip is\n * applied before rotation, so this composes correctly).\n */\nexport function orientationToTransform(orientation: number): OrientationTransform {\n switch (orientation) {\n case 2:\n return { rotation: 0, flipHorizontal: true };\n case 3:\n return { rotation: 180, flipHorizontal: false };\n case 4:\n return { rotation: 180, flipHorizontal: true };\n case 5:\n return { rotation: 90, flipHorizontal: true };\n case 6:\n return { rotation: 90, flipHorizontal: false };\n case 7:\n return { rotation: 270, flipHorizontal: true };\n case 8:\n return { rotation: 270, flipHorizontal: false };\n default:\n return { rotation: 0, flipHorizontal: false };\n }\n}\n","/**\n * Resolves an optional string-enum option: undefined silently uses the\n * default, a valid value passes through, and an invalid value warns and\n * falls back to the default rather than silently misbehaving (e.g. a\n * typo'd option matching no CSS rule, or an unsupported canvas export type).\n */\nexport function resolveEnumOption<T extends string>(\n value: T | undefined,\n valid: readonly T[],\n fallback: T,\n optionName: string\n): T {\n if (value === undefined) return fallback;\n if ((valid as readonly string[]).includes(value)) return value;\n console.warn(\n `Kiri: invalid ${optionName} \"${value}\" — defaulting to \"${fallback}\". ` +\n `Valid values: ${valid.join(\", \")}.`\n );\n return fallback;\n}\n","import type {\n ExportFormat,\n ExportOptions,\n ExportResult,\n ExportType,\n FrameShape,\n KiriState,\n Offset,\n} from \"./types\";\nimport { computeCoverScale, effectiveRenderedSize, type Size } from \"./gestures\";\nimport { buildFilterString } from \"./filters\";\nimport { resolveEnumOption } from \"./validate\";\n\nconst VALID_EXPORT_TYPES: ExportType[] = [\"base64\", \"blob\", \"canvas\"];\nconst VALID_EXPORT_FORMATS: ExportFormat[] = [\"image/jpeg\", \"image/png\", \"image/webp\"];\n\n/**\n * Top-left corner of the frame, in the local pixel space of the rendered\n * (rotated + scaled) image. Pure so the crop-region math is testable without\n * a real canvas: the frame is centered on the stage, the image's own center\n * sits at `offset` from that same point, so the frame's top-left in the\n * rendered image's local space is the rendered center, shifted back by the\n * offset.\n */\nexport function computeFrameSourceRect(\n rendered: Size,\n offset: Offset,\n frame: Size\n): { left: number; top: number } {\n return {\n left: rendered.width / 2 - offset.x - frame.width / 2,\n top: rendered.height / 2 - offset.y - frame.height / 2,\n };\n}\n\nexport function renderCropToCanvas(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n outputWidth: number,\n outputHeight: number,\n frameShape: FrameShape\n): HTMLCanvasElement {\n const natural: Size = { width: img.naturalWidth, height: img.naturalHeight };\n const scale = computeCoverScale(natural, frame, state.rotation) * state.zoom;\n const rendered = effectiveRenderedSize(natural, frame, state.rotation, state.zoom);\n\n const sourceCanvas = document.createElement(\"canvas\");\n sourceCanvas.width = Math.max(1, Math.round(rendered.width));\n sourceCanvas.height = Math.max(1, Math.round(rendered.height));\n const sctx = sourceCanvas.getContext(\"2d\");\n if (!sctx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n sctx.translate(sourceCanvas.width / 2, sourceCanvas.height / 2);\n sctx.rotate((state.rotation * Math.PI) / 180);\n // Flip is folded into scale's sign, applied before rotate (matches\n // stage.ts's transform order) so rotation happens in \"already mirrored\"\n // space, consistent with what the user sees on screen.\n sctx.scale(scale * (state.flip.horizontal ? -1 : 1), scale * (state.flip.vertical ? -1 : 1));\n // Same CSS filter string as the live preview (see stage.ts's applyFilters),\n // so the browser's own filter implementation guarantees they match exactly.\n sctx.filter = buildFilterString(state.filters);\n sctx.drawImage(img, -natural.width / 2, -natural.height / 2, natural.width, natural.height);\n\n const { left: frameLeft, top: frameTop } = computeFrameSourceRect(\n rendered,\n state.offset,\n frame\n );\n\n const outCanvas = document.createElement(\"canvas\");\n outCanvas.width = outputWidth;\n outCanvas.height = outputHeight;\n const octx = outCanvas.getContext(\"2d\");\n if (!octx) throw new Error(\"Kiri: unable to get 2D canvas context\");\n\n // The circle frame is otherwise just a visual overlay (stage.ts's\n // .kiri-frame--circle border) — without this, the export would always be\n // a plain rectangle regardless of frame.shape. Clipping to an ellipse\n // inscribed in the output canvas matches what's visible on screen even\n // when a custom output width/height changes its aspect ratio.\n if (frameShape === \"circle\") {\n octx.save();\n octx.beginPath();\n octx.ellipse(\n outputWidth / 2,\n outputHeight / 2,\n outputWidth / 2,\n outputHeight / 2,\n 0,\n 0,\n Math.PI * 2\n );\n octx.clip();\n }\n\n octx.drawImage(\n sourceCanvas,\n frameLeft,\n frameTop,\n frame.width,\n frame.height,\n 0,\n 0,\n outputWidth,\n outputHeight\n );\n\n if (frameShape === \"circle\") octx.restore();\n\n return outCanvas;\n}\n\nexport async function exportCrop(\n img: HTMLImageElement,\n state: KiriState,\n frame: Size,\n frameShape: FrameShape,\n options: ExportOptions\n): Promise<ExportResult> {\n const type = resolveEnumOption(options.type, VALID_EXPORT_TYPES, \"base64\", \"export type\");\n const format = resolveEnumOption(options.format, VALID_EXPORT_FORMATS, \"image/png\", \"export format\");\n const quality = options.quality;\n const width = options.width ?? frame.width;\n const height = options.height ?? frame.height;\n\n if (frameShape === \"circle\" && format === \"image/jpeg\") {\n console.warn(\n \"Kiri: exporting a circle-shaped frame as image/jpeg — JPEG has no \" +\n \"alpha channel, so the area outside the circle will render as solid \" +\n \"black instead of transparent. Use image/png or image/webp instead.\"\n );\n }\n\n const canvas = renderCropToCanvas(img, state, frame, width, height, frameShape);\n\n if (type === \"canvas\") return canvas;\n if (type === \"base64\") return canvas.toDataURL(format, quality);\n\n return new Promise<Blob>((resolve, reject) => {\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"Kiri: canvas.toBlob failed\"))),\n format,\n quality\n );\n });\n}\n","import type { UploadOptions } from \"./types\";\n\n/**\n * Default uploader: builds a FormData from the crop blob and POSTs it via\n * fetch. Swappable per-call (`UploadOptions.uploader`) or per-instance\n * (`KiriOptions.uploader`) for a custom protocol (presigned URLs, GraphQL,\n * etc.) while callers keep calling the same `cropper.upload(url, options)`.\n */\nexport async function uploadBlob(\n blob: Blob,\n options: UploadOptions & { url: string }\n): Promise<Response> {\n const fieldName = options.fieldName ?? \"file\";\n const fileName = options.fileName ?? `crop.${extensionFor(options.format ?? \"image/png\")}`;\n\n const formData = new FormData();\n formData.append(fieldName, blob, fileName);\n for (const [key, value] of Object.entries(options.extraFields ?? {})) {\n formData.append(key, value);\n }\n\n return fetch(options.url, {\n ...options.fetchOptions,\n method: \"POST\",\n body: formData,\n });\n}\n\nfunction extensionFor(format: string): string {\n switch (format) {\n case \"image/jpeg\":\n return \"jpg\";\n case \"image/webp\":\n return \"webp\";\n default:\n return \"png\";\n }\n}\n","import type {\n ExportOptions,\n ExportResult,\n Filters,\n Flip,\n FrameShape,\n KiriEventCallback,\n KiriEventName,\n KiriOptions,\n KiriState,\n LoadOptions,\n UploadOptions,\n Uploader,\n ZoomerPosition,\n} from \"./types\";\nimport {\n applyFilters,\n applyTransform,\n createStage,\n setFrameSize as setStageFrameSize,\n setStageSize,\n STAGE_AUTO_SIZE_PADDING,\n type StageElements,\n} from \"./stage\";\nimport {\n attachGestures,\n clampOffset,\n clampZoom,\n computeCoverScale,\n effectiveRenderedSize,\n normalizeRotation,\n type Size,\n} from \"./gestures\";\nimport { orientationToTransform, readExifOrientation } from \"./exif\";\nimport { exportCrop } from \"./export\";\nimport { DEFAULT_FILTERS, mergeFilters } from \"./filters\";\nimport { uploadBlob } from \"./upload\";\nimport { resolveEnumOption } from \"./validate\";\n\nconst DEFAULT_FRAME_SIZE = 200;\nconst MIN_FRAME_SIZE = 20;\nconst VALID_FRAME_SHAPES: FrameShape[] = [\"rectangle\", \"circle\"];\nconst VALID_ZOOMER_POSITIONS: ZoomerPosition[] = [\"top\", \"bottom\", \"left\", \"right\"];\n\nfunction resolveMouseWheelZoom(value: KiriOptions[\"mouseWheelZoom\"]): boolean | \"ctrl\" {\n if (value === undefined) return true;\n if (typeof value === \"boolean\" || value === \"ctrl\") return value;\n console.warn(\n `Kiri: invalid mouseWheelZoom \"${String(value)}\" — defaulting to true. ` +\n `Valid values: true, false, \"ctrl\".`\n );\n return true;\n}\n\ninterface ResolvedOptions {\n frame: { shape: FrameShape; width: number; height: number };\n minZoom: number;\n maxZoom: number;\n rotatable: boolean;\n flippable: boolean;\n resizableFrame: boolean;\n mouseWheelZoom: boolean | \"ctrl\";\n useExifOrientation: boolean;\n uploader: Uploader | undefined;\n autoSizeStage: boolean;\n showZoomer: boolean;\n zoomerPosition: ZoomerPosition;\n}\n\n/**\n * An interactive image cropper attached to a plain DOM element. Drag to pan,\n * zoom via wheel/pinch/an optional built-in slider, rotate in 90° steps,\n * flip, apply filters, then export or upload the crop.\n */\nexport class Kiri {\n private readonly container: HTMLElement;\n private readonly opts: ResolvedOptions;\n private readonly stage: StageElements;\n private readonly gestureHandle: { destroy: () => void };\n private resizeHandle: { destroy: () => void } | null = null;\n private zoomerHandle: { destroy: () => void } | null = null;\n private naturalSize: Size = { width: 0, height: 0 };\n private state: KiriState = {\n zoom: 1,\n offset: { x: 0, y: 0 },\n rotation: 0,\n flip: { horizontal: false, vertical: false },\n filters: DEFAULT_FILTERS,\n };\n private listeners: Record<KiriEventName, KiriEventCallback[]> = { change: [] };\n\n /**\n * @param container An element already present in the DOM. Passing\n * `null`/`undefined`, or an element that isn't in the DOM yet, throws.\n * @param options See the {@link KiriOptions} fields for defaults.\n */\n constructor(container: HTMLElement, options: KiriOptions = {}) {\n if (!container || typeof container.appendChild !== \"function\") {\n throw new Error(\n \"Kiri: container element is null/undefined or not a DOM element. \" +\n \"This usually means the element wasn't in the DOM yet when \" +\n \"`new Kiri(...)` ran — e.g. document.getElementById() was called \" +\n \"before the element existed. Place the <script> after the \" +\n \"element, or construct inside a DOMContentLoaded listener.\"\n );\n }\n this.container = container;\n this.opts = {\n frame: {\n shape: resolveEnumOption(\n options.frame?.shape,\n VALID_FRAME_SHAPES,\n \"rectangle\",\n \"frame.shape\"\n ),\n width: options.frame?.width ?? DEFAULT_FRAME_SIZE,\n height: options.frame?.height ?? DEFAULT_FRAME_SIZE,\n },\n minZoom: options.minZoom ?? 1,\n maxZoom: options.maxZoom ?? 4,\n rotatable: options.rotatable ?? true,\n flippable: options.flippable ?? true,\n resizableFrame: options.resizableFrame ?? false,\n mouseWheelZoom: resolveMouseWheelZoom(options.mouseWheelZoom),\n useExifOrientation: options.useExifOrientation ?? true,\n uploader: options.uploader,\n autoSizeStage: options.autoSizeStage ?? true,\n showZoomer: options.showZoomer ?? false,\n zoomerPosition: resolveEnumOption(\n options.zoomerPosition,\n VALID_ZOOMER_POSITIONS,\n \"bottom\",\n \"zoomerPosition\"\n ),\n };\n this.state.filters = mergeFilters(DEFAULT_FILTERS, options.filters ?? {});\n\n this.stage = createStage(\n this.container,\n this.opts.frame.shape,\n this.opts.frame.width,\n this.opts.frame.height,\n {\n show: this.opts.showZoomer,\n position: this.opts.zoomerPosition,\n min: this.opts.minZoom,\n max: this.opts.maxZoom,\n value: this.state.zoom,\n }\n );\n if (this.opts.autoSizeStage) this.syncStageSize();\n applyFilters(this.stage.imgEl, this.state.filters);\n if (this.stage.zoomerEl) this.enableZoomer(this.stage.zoomerEl);\n\n this.gestureHandle = attachGestures(\n this.stage.stageEl,\n {\n getNaturalSize: () => this.naturalSize,\n getFrameSize: () => this.getFrameSize(),\n getState: () => this.state,\n getMinMaxZoom: () => ({ min: this.opts.minZoom, max: this.opts.maxZoom }),\n setState: (next) => this.commitState(next),\n },\n { mouseWheelZoom: this.opts.mouseWheelZoom }\n );\n\n if (this.opts.resizableFrame) this.enableFrameResize();\n }\n\n /**\n * Loads an image, replacing whatever was loaded before. EXIF orientation\n * (rotation + horizontal flip) is corrected automatically unless\n * `useExifOrientation: false` was passed to the constructor — only for\n * `File`/`Blob` sources, since a plain URL string can't be read for EXIF\n * data without an extra fetch.\n * @param source A `File` (e.g. from a file input), a `Blob`, or a URL string.\n * @param loadOptions Initial `zoom`/`offset`/`rotation`/`flip`.\n */\n async load(source: File | Blob | string, loadOptions: LoadOptions = {}): Promise<void> {\n let rotation = normalizeRotation(loadOptions.rotation ?? 0);\n let flipHorizontal = loadOptions.flip?.horizontal ?? false;\n const flipVertical = loadOptions.flip?.vertical ?? false;\n let objectUrl: string | null = null;\n let url: string;\n\n if (typeof source === \"string\") {\n url = source;\n } else {\n if (this.opts.useExifOrientation) {\n const buffer = await source.arrayBuffer();\n const orientation = readExifOrientation(buffer);\n const exifTransform = orientationToTransform(orientation);\n rotation = normalizeRotation(rotation + exifTransform.rotation);\n // XOR: two horizontal flips (EXIF + a requested one) cancel out.\n flipHorizontal = flipHorizontal !== exifTransform.flipHorizontal;\n }\n objectUrl = URL.createObjectURL(source);\n url = objectUrl;\n }\n\n await new Promise<void>((resolve, reject) => {\n this.stage.imgEl.onload = () => resolve();\n this.stage.imgEl.onerror = () => reject(new Error(\"Kiri: failed to load image\"));\n this.stage.imgEl.src = url;\n });\n\n if (objectUrl) URL.revokeObjectURL(objectUrl);\n\n this.naturalSize = {\n width: this.stage.imgEl.naturalWidth,\n height: this.stage.imgEl.naturalHeight,\n };\n\n const zoom = clampZoom(\n loadOptions.zoom ?? this.opts.minZoom,\n this.opts.minZoom,\n this.opts.maxZoom\n );\n const rendered = effectiveRenderedSize(this.naturalSize, this.getFrameSize(), rotation, zoom);\n const offset = clampOffset(loadOptions.offset ?? { x: 0, y: 0 }, rendered, this.getFrameSize());\n\n this.commitState({\n zoom,\n offset,\n rotation,\n flip: { horizontal: flipHorizontal, vertical: flipVertical },\n filters: this.state.filters,\n });\n }\n\n /** A snapshot of the current state — mutating the returned object has no effect. */\n getState(): KiriState {\n return {\n zoom: this.state.zoom,\n offset: { ...this.state.offset },\n rotation: this.state.rotation,\n flip: { ...this.state.flip },\n filters: { ...this.state.filters },\n };\n }\n\n /** Sets the zoom to an absolute value, clamped to `[minZoom, maxZoom]`. */\n setZoom(zoom: number): void {\n const clamped = clampZoom(zoom, this.opts.minZoom, this.opts.maxZoom);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n clamped\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, zoom: clamped, offset });\n }\n\n /**\n * Rotates relative to the current rotation, snapped to the nearest 90°.\n * No-op if `rotatable: false` was passed to the constructor.\n */\n rotate(deltaDeg: number): void {\n if (!this.opts.rotatable) return;\n const snapped = Math.round(deltaDeg / 90) * 90;\n const rotation = normalizeRotation(this.state.rotation + snapped);\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, rotation, offset });\n }\n\n /** Toggles horizontal flip, independent of rotation. No-op if `flippable: false`. */\n flipHorizontal(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, horizontal: !this.state.flip.horizontal };\n this.commitState({ ...this.state, flip });\n }\n\n /** Toggles vertical flip, independent of rotation. No-op if `flippable: false`. */\n flipVertical(): void {\n if (!this.opts.flippable) return;\n const flip: Flip = { ...this.state.flip, vertical: !this.state.flip.vertical };\n this.commitState({ ...this.state, flip });\n }\n\n /**\n * Resizes the frame. Also resizes the stage to match, if\n * `autoSizeStage: true` (the default). Each axis is clamped to a 20px\n * minimum.\n */\n setFrameSize(width: number, height: number): void {\n this.opts.frame.width = Math.max(MIN_FRAME_SIZE, width);\n this.opts.frame.height = Math.max(MIN_FRAME_SIZE, height);\n setStageFrameSize(this.stage.frameEl, this.opts.frame.width, this.opts.frame.height);\n if (this.opts.autoSizeStage) this.syncStageSize();\n const rendered = effectiveRenderedSize(\n this.naturalSize,\n this.getFrameSize(),\n this.state.rotation,\n this.state.zoom\n );\n const offset = clampOffset(this.state.offset, rendered, this.getFrameSize());\n this.commitState({ ...this.state, offset });\n }\n\n /**\n * Merges a partial update into the current filters (omitted fields are\n * left as they are). Numeric values are clamped to `>= 0`.\n */\n setFilters(filters: Partial<Filters>): void {\n this.commitState({ ...this.state, filters: mergeFilters(this.state.filters, filters) });\n }\n\n /**\n * Renders the current crop. A circle frame is a real clip in the output\n * (transparent corners on PNG/WebP); a circle exported as JPEG warns and\n * renders solid black corners instead, since JPEG has no alpha channel.\n * @returns A data URL string (`type: \"base64\"`, the default), a `Blob`, or an `HTMLCanvasElement`.\n */\n async export(options: ExportOptions = {}): Promise<ExportResult> {\n return exportCrop(\n this.stage.imgEl,\n this.state,\n this.getFrameSize(),\n this.opts.frame.shape,\n options\n );\n }\n\n /**\n * Exports the current crop as a blob, then uploads it — a default\n * FormData/`fetch` POST, or a custom `uploader` (per-call `options.uploader`\n * wins over the constructor's, which wins over the built-in default).\n */\n async upload(url: string, options: UploadOptions = {}): Promise<unknown> {\n const blob = (await this.export({ ...options, type: \"blob\" })) as Blob;\n const uploader = options.uploader ?? this.opts.uploader ?? uploadBlob;\n return uploader(blob, { ...options, url });\n }\n\n /** Subscribes to `\"change\"` — fires on every state update (drag/zoom/rotate/flip/filters), and once after `load()` resolves. */\n on(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event].push(callback);\n }\n\n /** Unsubscribes a callback previously passed to {@link on}. */\n off(event: KiriEventName, callback: KiriEventCallback): void {\n this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);\n }\n\n /**\n * Tears the instance down: removes all pointer/wheel event listeners\n * (drag/zoom gestures), the resize-handle listener (if `resizableFrame`),\n * and the zoom-slider listener (if `showZoomer`); clears the container's\n * `innerHTML`, leaving an empty container element; and clears all\n * `\"change\"` listeners. Call this when you're done with an instance (e.g.\n * unmounting) to avoid leaking listeners.\n */\n destroy(): void {\n this.gestureHandle.destroy();\n this.resizeHandle?.destroy();\n this.zoomerHandle?.destroy();\n this.container.innerHTML = \"\";\n this.listeners.change = [];\n }\n\n private getFrameSize(): Size {\n return { width: this.opts.frame.width, height: this.opts.frame.height };\n }\n\n private syncStageSize(): void {\n setStageSize(\n this.stage.stageEl,\n this.opts.frame.width + STAGE_AUTO_SIZE_PADDING * 2,\n this.opts.frame.height + STAGE_AUTO_SIZE_PADDING * 2\n );\n }\n\n private commitState(next: KiriState): void {\n this.state = next;\n const scale =\n computeCoverScale(this.naturalSize, this.getFrameSize(), next.rotation) * next.zoom;\n applyTransform(this.stage.imageLayerEl, next, scale);\n applyFilters(this.stage.imgEl, next.filters);\n // Keeps the slider in sync regardless of what triggered the zoom change\n // (wheel, pinch, drag-clamping, or setZoom() itself) — setting .value\n // programmatically doesn't re-fire \"input\", so no feedback loop.\n if (this.stage.zoomerEl) this.stage.zoomerEl.value = String(next.zoom);\n for (const cb of this.listeners.change) cb(this.getState());\n }\n\n private enableZoomer(zoomerEl: HTMLInputElement): void {\n const onInput = (): void => this.setZoom(Number(zoomerEl.value));\n zoomerEl.addEventListener(\"input\", onInput);\n this.zoomerHandle = {\n destroy(): void {\n zoomerEl.removeEventListener(\"input\", onInput);\n },\n };\n }\n\n private enableFrameResize(): void {\n const handle = document.createElement(\"div\");\n handle.className = \"kiri-frame-handle\";\n handle.style.right = \"-5px\";\n handle.style.bottom = \"-5px\";\n this.stage.frameEl.appendChild(handle);\n\n let start: { x: number; y: number; width: number; height: number } | null = null;\n\n const onDown = (e: PointerEvent): void => {\n e.stopPropagation();\n handle.setPointerCapture(e.pointerId);\n start = {\n x: e.clientX,\n y: e.clientY,\n width: this.opts.frame.width,\n height: this.opts.frame.height,\n };\n };\n const onMove = (e: PointerEvent): void => {\n if (!start) return;\n const dx = (e.clientX - start.x) * 2;\n const dy = (e.clientY - start.y) * 2;\n this.setFrameSize(start.width + dx, start.height + dy);\n };\n const onUp = (): void => {\n start = null;\n };\n\n handle.addEventListener(\"pointerdown\", onDown);\n handle.addEventListener(\"pointermove\", onMove);\n handle.addEventListener(\"pointerup\", onUp);\n handle.addEventListener(\"pointercancel\", onUp);\n\n this.resizeHandle = {\n destroy(): void {\n handle.removeEventListener(\"pointerdown\", onDown);\n handle.removeEventListener(\"pointermove\", onMove);\n handle.removeEventListener(\"pointerup\", onUp);\n handle.removeEventListener(\"pointercancel\", onUp);\n handle.remove();\n },\n };\n }\n}\n","import { Kiri } from \"./kiri\";\nimport type { ExportOptions, ExportResult, KiriOptions, LoadOptions } from \"./types\";\n\nexport interface KiriBatchItem {\n source: File | Blob | string;\n loadOptions?: LoadOptions;\n}\n\n/**\n * Steps a single shared `Kiri` instance through a queue of images, so the\n * whole drag/zoom/rotate/flip/filters interaction surface is reused as-is —\n * no per-image instance, no duplicated cropping logic.\n */\nexport class KiriBatch {\n /** The shared `Kiri` instance. Use its normal methods (drag/zoom/rotate/filters/etc.) to adjust the currently-loaded item. */\n readonly cropper: Kiri;\n private readonly items: KiriBatchItem[];\n private index = -1;\n private captures: ExportResult[] = [];\n\n /** @param items An initial queue; more can be added later via `add()`. */\n constructor(container: HTMLElement, options: KiriOptions = {}, items: KiriBatchItem[] = []) {\n this.cropper = new Kiri(container, options);\n this.items = [...items];\n }\n\n /** Appends an item to the queue. */\n add(item: KiriBatchItem): void {\n this.items.push(item);\n }\n\n /** Total number of queued items. */\n get length(): number {\n return this.items.length;\n }\n\n /** Loads the next queued image into `cropper`. Returns false once the queue is exhausted. */\n async next(): Promise<boolean> {\n if (this.index + 1 >= this.items.length) return false;\n this.index += 1;\n const item = this.items[this.index];\n await this.cropper.load(item.source, item.loadOptions);\n return true;\n }\n\n /** Metadata for the currently loaded item, or null before the first next() / after exhaustion. */\n current(): KiriBatchItem | null {\n return this.items[this.index] ?? null;\n }\n\n /** Exports the current crop and stores it, indexed by item order. */\n async capture(options?: ExportOptions): Promise<ExportResult> {\n const result = await this.cropper.export(options);\n this.captures[this.index] = result;\n return result;\n }\n\n /** All captures made so far, in item order (sparse where an item hasn't been captured yet). */\n results(): ExportResult[] {\n return [...this.captures];\n }\n\n /** Tears down the shared `Kiri` instance. */\n destroy(): void {\n this.cropper.destroy();\n }\n}\n"],"names":["DEFAULT_FILTERS","mergeFilters","current","partial","buildFilterString","filters","parts","createStage","container","frameShape","frameWidth","frameHeight","zoomer","stageEl","imageLayerEl","imgEl","frameEl","setFrameSize","zoomerEl","rootEl","width","height","STAGE_AUTO_SIZE_PADDING","setStageSize","applyTransform","state","renderedScale","scaleX","scaleY","applyFilters","effectiveNaturalSize","natural","rotationDeg","computeCoverScale","frame","eff","effectiveRenderedSize","zoom","scale","clampZoom","minZoom","maxZoom","clampOffset","offset","rendered","maxX","maxY","normalizeRotation","deg","pointerDistance","a","b","attachGestures","callbacks","options","activePointers","dragStart","pinchStartDistance","pinchStartZoom","applyClampedState","next","min","max","onPointerDown","e","onPointerMove","distance","ratio","dx","dy","onPointerUp","onWheel","delta","readExifOrientation","buffer","view","marker","size","exifOffset","readOrientationFromTiff","tiffStart","littleEndian","firstIfdOffset","ifdStart","entryCount","i","entryOffset","value","orientationToTransform","orientation","resolveEnumOption","valid","fallback","optionName","VALID_EXPORT_TYPES","VALID_EXPORT_FORMATS","computeFrameSourceRect","renderCropToCanvas","img","outputWidth","outputHeight","sourceCanvas","sctx","frameLeft","frameTop","outCanvas","octx","exportCrop","type","format","quality","canvas","resolve","reject","blob","uploadBlob","fieldName","fileName","extensionFor","formData","key","DEFAULT_FRAME_SIZE","MIN_FRAME_SIZE","VALID_FRAME_SHAPES","VALID_ZOOMER_POSITIONS","resolveMouseWheelZoom","Kiri","__publicField","_a","_b","_c","source","loadOptions","rotation","flipHorizontal","flipVertical","objectUrl","url","exifTransform","clamped","deltaDeg","snapped","flip","setStageFrameSize","event","callback","cb","onInput","handle","start","onDown","onMove","onUp","KiriBatch","items","item","result"],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { Filters, FrameShape, KiriState, ZoomerPosition } from './types';
2
+ export interface StageElements {
3
+ stageEl: HTMLDivElement;
4
+ frameEl: HTMLDivElement;
5
+ imageLayerEl: HTMLDivElement;
6
+ imgEl: HTMLImageElement;
7
+ zoomerEl: HTMLInputElement | null;
8
+ }
9
+ export interface ZoomerConfig {
10
+ show: boolean;
11
+ position: ZoomerPosition;
12
+ min: number;
13
+ max: number;
14
+ value: number;
15
+ }
16
+ export declare function createStage(container: HTMLElement, frameShape: FrameShape, frameWidth: number, frameHeight: number, zoomer: ZoomerConfig): StageElements;
17
+ export declare function setFrameSize(frameEl: HTMLDivElement, width: number, height: number): void;
18
+ /** Padding added around the frame when the stage auto-sizes itself — small
19
+ * enough to avoid empty space, big enough to fit the resize handle and give
20
+ * a visible drag margin. */
21
+ export declare const STAGE_AUTO_SIZE_PADDING = 20;
22
+ export declare function setStageSize(stageEl: HTMLDivElement, width: number, height: number): void;
23
+ export declare function applyTransform(imageLayerEl: HTMLDivElement, state: KiriState, renderedScale: number): void;
24
+ export declare function applyFilters(imgEl: HTMLImageElement, filters: Filters): void;
@@ -0,0 +1,117 @@
1
+ export type FrameShape = "rectangle" | "circle";
2
+ export type ZoomerPosition = "top" | "bottom" | "left" | "right";
3
+ export interface FrameSize {
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export interface Offset {
8
+ x: number;
9
+ y: number;
10
+ }
11
+ export interface Flip {
12
+ horizontal: boolean;
13
+ vertical: boolean;
14
+ }
15
+ /** Brightness/contrast/saturation are `>= 0`, where `1` means unchanged. */
16
+ export interface Filters {
17
+ brightness: number;
18
+ contrast: number;
19
+ saturation: number;
20
+ grayscale: boolean;
21
+ sepia: boolean;
22
+ }
23
+ /** A custom upload implementation, for `KiriOptions.uploader` or `UploadOptions.uploader`. */
24
+ export type Uploader = (blob: Blob, options: UploadOptions & {
25
+ url: string;
26
+ }) => Promise<unknown>;
27
+ export interface KiriOptions {
28
+ frame?: {
29
+ /** Default `"rectangle"`. `"circle"` is a real clip on export, not just a visual overlay. */
30
+ shape?: FrameShape;
31
+ /** Pixels. Default `200`. */
32
+ width?: number;
33
+ /** Pixels. Default `200`. */
34
+ height?: number;
35
+ };
36
+ /** Default `1`. */
37
+ minZoom?: number;
38
+ /** Default `4`. */
39
+ maxZoom?: number;
40
+ /** Default `true`. */
41
+ rotatable?: boolean;
42
+ /** Default `true`. */
43
+ flippable?: boolean;
44
+ /** Adds a drag handle at the frame's corner. Default `false`. */
45
+ resizableFrame?: boolean;
46
+ /** `"ctrl"` requires holding Ctrl while scrolling to zoom. Default `true`. */
47
+ mouseWheelZoom?: boolean | "ctrl";
48
+ /** Corrects rotation + horizontal flip from EXIF data on `File`/`Blob` sources. Default `true`. */
49
+ useExifOrientation?: boolean;
50
+ /** Initial filter values; see {@link Filters}. */
51
+ filters?: Partial<Filters>;
52
+ /** A custom upload implementation, used by `upload()` unless overridden per-call. */
53
+ uploader?: Uploader;
54
+ /**
55
+ * When true (default), the stage sizes itself to the frame's dimensions
56
+ * (plus a small margin) so it looks right with zero CSS. Set false to
57
+ * have the stage fill its container instead (100% width/height) — for
58
+ * embedding in a layout where you want to control the stage's size via
59
+ * your own CSS on the container element.
60
+ */
61
+ autoSizeStage?: boolean;
62
+ /** Renders a built-in zoom slider, kept in sync with wheel/pinch/setZoom() in both directions. Default false. */
63
+ showZoomer?: boolean;
64
+ /** Where the zoom slider sits relative to the stage. Purely a placement choice — identical behavior in every position. Default "bottom". */
65
+ zoomerPosition?: ZoomerPosition;
66
+ }
67
+ /** The full mutable state of a `Kiri` instance, as returned by `getState()`. */
68
+ export interface KiriState {
69
+ zoom: number;
70
+ offset: Offset;
71
+ rotation: number;
72
+ flip: Flip;
73
+ filters: Filters;
74
+ }
75
+ /** Options for `load()`. */
76
+ export interface LoadOptions {
77
+ /** Clamped to `[minZoom, maxZoom]`. Default `minZoom`. */
78
+ zoom?: number;
79
+ /** Clamped so the frame stays covered by the image. Default `{ x: 0, y: 0 }`. */
80
+ offset?: Offset;
81
+ /** Degrees, snapped to the nearest 90°. Default `0`. */
82
+ rotation?: number;
83
+ /** Default `{ horizontal: false, vertical: false }`. */
84
+ flip?: Partial<Flip>;
85
+ }
86
+ export type ExportType = "base64" | "blob" | "canvas";
87
+ export type ExportFormat = "image/jpeg" | "image/png" | "image/webp";
88
+ /** Options for `export()`. */
89
+ export interface ExportOptions {
90
+ /** Default `"base64"` (a data URL string). */
91
+ type?: ExportType;
92
+ /** Default `"image/png"`. A circle frame exported as `"image/jpeg"` warns — JPEG has no alpha channel. */
93
+ format?: ExportFormat;
94
+ /** `0`-`1`. Only meaningful for `"image/jpeg"`/`"image/webp"`. Default: browser default. */
95
+ quality?: number;
96
+ /** Output pixel width. Default: the frame's width. */
97
+ width?: number;
98
+ /** Output pixel height. Default: the frame's height. */
99
+ height?: number;
100
+ }
101
+ /** Options for `upload()` — everything `export()` takes, plus these. */
102
+ export interface UploadOptions extends ExportOptions {
103
+ /** The FormData field name. Default `"file"`. */
104
+ fieldName?: string;
105
+ /** Default: `"crop.<ext>"`, extension derived from `format`. */
106
+ fileName?: string;
107
+ /** Extra FormData fields to send alongside the file. Default `{}`. */
108
+ extraFields?: Record<string, string>;
109
+ /** Merged into the underlying `fetch()` call (method/body are always overridden). Default `{}`. */
110
+ fetchOptions?: RequestInit;
111
+ /** Overrides the constructor's `uploader` for this call only. */
112
+ uploader?: Uploader;
113
+ }
114
+ export type ExportResult = string | Blob | HTMLCanvasElement;
115
+ export type KiriEventName = "change";
116
+ /** Receives the same snapshot `getState()` returns. */
117
+ export type KiriEventCallback = (state: KiriState) => void;
@@ -0,0 +1,10 @@
1
+ import { UploadOptions } from './types';
2
+ /**
3
+ * Default uploader: builds a FormData from the crop blob and POSTs it via
4
+ * fetch. Swappable per-call (`UploadOptions.uploader`) or per-instance
5
+ * (`KiriOptions.uploader`) for a custom protocol (presigned URLs, GraphQL,
6
+ * etc.) while callers keep calling the same `cropper.upload(url, options)`.
7
+ */
8
+ export declare function uploadBlob(blob: Blob, options: UploadOptions & {
9
+ url: string;
10
+ }): Promise<Response>;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Resolves an optional string-enum option: undefined silently uses the
3
+ * default, a valid value passes through, and an invalid value warns and
4
+ * falls back to the default rather than silently misbehaving (e.g. a
5
+ * typo'd option matching no CSS rule, or an unsupported canvas export type).
6
+ */
7
+ export declare function resolveEnumOption<T extends string>(value: T | undefined, valid: readonly T[], fallback: T, optionName: string): T;
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@michaelyagi/kiri",
3
+ "version": "0.1.0-alpha.2",
4
+ "description": "A dependency-free TypeScript library for interactive image cropping in the browser.",
5
+ "homepage": "https://michaelyagi.github.io/kiri",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/MichaelYagi/kiri.git",
9
+ "directory": "packages/core"
10
+ },
11
+ "bugs": "https://github.com/MichaelYagi/kiri/issues",
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "main": "./dist/kiri.min.js",
16
+ "module": "./dist/kiri.mjs",
17
+ "unpkg": "./dist/kiri.min.js",
18
+ "jsdelivr": "./dist/kiri.min.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/kiri.mjs",
24
+ "require": "./dist/kiri.min.js"
25
+ },
26
+ "./kiri.js": "./dist/kiri.js",
27
+ "./kiri.min.js": "./dist/kiri.min.js",
28
+ "./kiri.css": "./dist/kiri.css",
29
+ "./kiri.min.css": "./dist/kiri.min.css"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build": "vite build && KIRI_MINIFY=false vite build && node -e \"const fs=require('fs'),css=fs.readFileSync('src/kiri.css','utf8');fs.writeFileSync('dist/kiri.css',css);fs.writeFileSync('dist/kiri.min.css',require('esbuild').transformSync(css,{loader:'css',minify:true}).code)\"",
37
+ "test": "vitest run",
38
+ "docs:api": "typedoc",
39
+ "docs:sync-dist": "node -e \"const fs=require('fs');fs.mkdirSync('../../docs/dist',{recursive:true});for(const f of ['kiri.min.js','kiri.min.css'])fs.copyFileSync('dist/'+f,'../../docs/dist/'+f)\"",
40
+ "docs:build": "npm run build && npm run docs:api && npm run docs:sync-dist"
41
+ },
42
+ "keywords": [
43
+ "image",
44
+ "crop",
45
+ "cropper",
46
+ "typescript"
47
+ ],
48
+ "license": "MIT",
49
+ "devDependencies": {
50
+ "jsdom": "^25.0.0",
51
+ "typedoc": "^0.28.20",
52
+ "typescript": "^5.6.0",
53
+ "vite": "^5.4.0",
54
+ "vite-plugin-dts": "^4.2.0",
55
+ "vitest": "^2.1.0"
56
+ }
57
+ }