{"_id":"@coloristic.org/darkroom","_rev":"19-55d616f3b1d694d7f2fe28b3239b1c95","name":"@coloristic.org/darkroom","dist-tags":{"beta":"0.2.0-beta","latest":"0.3.0"},"versions":{"0.2.0-beta":{"name":"@coloristic.org/darkroom","version":"0.2.0-beta","keywords":["photo-editor","photo-editing","color-grading","color-science","image-processing","lut","3d-lut","typescript","browser","web-worker","local-first"],"author":{"url":"https://ysr.design","name":"Yasir Dora"},"license":"MIT","_id":"@coloristic.org/darkroom@0.2.0-beta","maintainers":[{"name":"yasirdora","email":"yasirdora@gmail.com"}],"homepage":"https://darkroom.ysr.design","bugs":{"url":"https://github.com/Yasirdora/darkroom/issues"},"dist":{"shasum":"3d17b9d0c765ebc29bfae9dcaed1f5cf30d6c722","tarball":"https://registry.npmjs.org/@coloristic.org/darkroom/-/darkroom-0.2.0-beta.tgz","fileCount":140,"integrity":"sha512-XOavTxxGMILlmCjb5IzxUFe45/J/F3UCxXpTlHn3rvlTMzVZQyqiqLhlsh653tZrORs5UuH1xfl/6urOuDvxsA==","signatures":[{"sig":"MEYCIQDSy4baz/BZPMKXYg7xMIWZeWCRsvD4rKsnrcvPIYevdwIhANbB2EnEsjyqRYYsjBpa8Fovhp95aNYVcF8g1Pg7HUYF","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coloristic.org%2fdarkroom@0.2.0-beta","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":972476},"type":"module","types":"./dist/index.d.ts","engines":{"node":"^22.12.0 || ^24.0.0 || ^26.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./browser":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js","default":"./dist/browser/index.js"},"./presets":{"types":"./dist/presets/index.d.ts","import":"./dist/presets/index.js","default":"./dist/presets/index.js"},"./package.json":"./package.json"},"gitHead":"3adc3da54976d578efd6099c99263f96ba78e6bc","scripts":{"test":"vitest run --config vitest.config.ts","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.browser.json","check":"npm run typecheck && npm run test:coverage && npm run build && npm run verify && npm run smoke:built","clean":"node ./scripts/clean.mjs","smoke":"node ./scripts/smoke-package.mjs","verify":"node ./scripts/verify-tarball.mjs","prepack":"npm run build","typecheck":"tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.browser.json --noEmit","smoke:built":"node ./scripts/smoke-package.mjs --skip-build","test:coverage":"vitest run --config vitest.config.ts --coverage"},"_npmUser":{"name":"yasirdora","email":"yasirdora@gmail.com"},"repository":{"url":"git+https://github.com/Yasirdora/darkroom.git","type":"git"},"_npmVersion":"11.17.0","description":"A local-first, framework-independent TypeScript photo-grading and 3D LUT engine.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"~6.0.2","@vitest/coverage-v8":"^4.1.10"},"_npmOperationalInternal":{"tmp":"tmp/darkroom_0.2.0-beta_1788302435587_0.5799892654669316","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@coloristic.org/darkroom","version":"0.2.0","keywords":["photo-editor","photo-editing","color-grading","color-science","image-processing","lut","3d-lut","typescript","browser","web-worker","local-first"],"author":{"url":"https://ysr.design","name":"Yasir Dora"},"license":"MIT","_id":"@coloristic.org/darkroom@0.2.0","maintainers":[{"name":"yasirdora","email":"yasirdora@gmail.com"}],"homepage":"https://darkroom.ysr.design","bugs":{"url":"https://github.com/Yasirdora/darkroom/issues"},"dist":{"shasum":"40e9a6dab9a72a970b47831df338ee7ef50841fc","tarball":"https://registry.npmjs.org/@coloristic.org/darkroom/-/darkroom-0.2.0.tgz","fileCount":140,"integrity":"sha512-Xu0giD2E4LK72KApnkEyAyoI81WjmsFineHpT7cdIBGI7aCXfRc0JunJQjrti3ELTHOTllYxszhsG1mOnp/f5A==","signatures":[{"sig":"MEQCIDfcJgmd86xuiCDxeiCbiBUSCD3ILrf0CMU7v4Ubt9KuAiBQRdfVcrIGksxZ+pMi80kq5tEna7TtdTB0IZLqGaFtPA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIA6/0w+lPiDBDazbC6z/7gsZGfaB+RP8OKKfAcFvgkvCAiEA8MPgxVFdZh+raDIoZufgfysWT+LnMEnlrWsP06C7PA4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coloristic.org%2fdarkroom@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":972471},"type":"module","types":"./dist/index.d.ts","engines":{"node":"^22.12.0 || ^24.0.0 || ^26.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./browser":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js","default":"./dist/browser/index.js"},"./presets":{"types":"./dist/presets/index.d.ts","import":"./dist/presets/index.js","default":"./dist/presets/index.js"},"./package.json":"./package.json"},"gitHead":"0856b8b5c76342ed646f9722ce31053b1a7b938d","scripts":{"test":"vitest run --config vitest.config.ts","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.browser.json","check":"npm run typecheck && npm run test:coverage && npm run build && npm run verify && npm run smoke:built","clean":"node ./scripts/clean.mjs","smoke":"node ./scripts/smoke-package.mjs","verify":"node ./scripts/verify-tarball.mjs","prepack":"npm run build","typecheck":"tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.browser.json --noEmit","smoke:built":"node ./scripts/smoke-package.mjs --skip-build","test:coverage":"vitest run --config vitest.config.ts --coverage"},"_npmUser":{"name":"yasirdora","email":"yasirdora@gmail.com"},"repository":{"url":"git+https://github.com/Yasirdora/darkroom.git","type":"git"},"_npmVersion":"11.17.0","description":"A local-first, framework-independent TypeScript photo-grading and 3D LUT engine.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"~6.0.2","@vitest/coverage-v8":"^4.1.10"},"_npmOperationalInternal":{"tmp":"tmp/darkroom_0.2.0_1788303483606_0.4277438363581485","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"_id":"@coloristic.org/darkroom@0.3.0","bugs":{"url":"https://github.com/Yasirdora/darkroom/issues"},"dist":{"shasum":"3c7bdee0a324a06399648f83aac024f302c7ce1a","tarball":"https://registry.npmjs.org/@coloristic.org/darkroom/-/darkroom-0.3.0.tgz","fileCount":140,"integrity":"sha512-LoVZJqXAxvT7rbtrINzpxZJpIvPia+kvGl0nYXQpAzovGdlGy8OTjIOv9sQ1+VO8q7IHRPd89jAZKaVHH1seaA==","signatures":[{"sig":"MEQCIEIeIMI3Uoddo76JEfvV2FuE8ftjYSJZrxZRTBcbHcXkAiAqE1uaLcRqRDUJv4/yRS4z9NBTkw8c+hmyFnhK40v5qw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCgRoc4+wGzFjRipBcUbwlmxbcJRNbx/2HyioaOwaY7hwIhAMCLbXrSrpHOwzPHMO0iaru/oxjgKdnGn0wonU/d4Ayb"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@coloristic.org%2fdarkroom@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":972471},"name":"@coloristic.org/darkroom","type":"module","types":"./dist/index.d.ts","author":{"url":"https://ysr.design","name":"Yasir Dora"},"engines":{"node":"^22.12.0 || ^24.0.0 || ^26.0.0"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./browser":{"types":"./dist/browser/index.d.ts","import":"./dist/browser/index.js","default":"./dist/browser/index.js"},"./presets":{"types":"./dist/presets/index.d.ts","import":"./dist/presets/index.js","default":"./dist/presets/index.js"},"./package.json":"./package.json"},"gitHead":"b8a73361de7ab8a00288b26f998323bab3b418bf","license":"MIT","scripts":{"test":"vitest run --config vitest.config.ts","build":"npm run clean && tsc -p tsconfig.build.json && tsc -p tsconfig.browser.json","check":"npm run typecheck && npm run test:coverage && npm run build && npm run verify && npm run smoke:built","clean":"node ./scripts/clean.mjs","smoke":"node ./scripts/smoke-package.mjs","verify":"node ./scripts/verify-tarball.mjs","prepack":"npm run build","typecheck":"tsc -p tsconfig.build.json --noEmit && tsc -p tsconfig.browser.json --noEmit","smoke:built":"node ./scripts/smoke-package.mjs --skip-build","test:coverage":"vitest run --config vitest.config.ts --coverage"},"version":"0.3.0","_npmUser":{"name":"yasirdora","email":"yasirdora@gmail.com"},"homepage":"https://darkroom.ysr.design","keywords":["photo-editor","photo-editing","color-grading","color-science","image-processing","lut","3d-lut","typescript","browser","web-worker","local-first"],"repository":{"url":"git+https://github.com/Yasirdora/darkroom.git","type":"git"},"_npmVersion":"11.17.0","description":"A local-first, framework-independent TypeScript photo-grading and 3D LUT engine.","directories":{},"maintainers":[{"name":"yasirdora","email":"yasirdora@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.1.10","typescript":"~6.0.2","@vitest/coverage-v8":"^4.1.10"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/darkroom_0.3.0_1788303573075_0.7038893743673715"}}},"time":{"created":"2026-09-01T22:40:35.290Z","modified":"2026-09-01T22:59:33.550Z","0.1.0-beta.0":"2026-08-09T06:46:50.343Z","0.1.1":"2026-09-01T13:18:04.954Z","0.1.2":"2026-09-01T13:18:19.458Z","0.1.3":"2026-09-01T13:25:15.172Z","0.1.4":"2026-09-01T15:23:07.313Z","0.1.5":"2026-09-01T15:28:27.199Z","0.1.0-beta":"2026-09-01T16:44:50.380Z","0.1.6-beta":"2026-09-01T16:51:25.433Z","0.1.6":"2026-09-01T16:52:07.296Z","0.1.7-beta":"2026-09-01T16:52:35.905Z","0.1.8-beta":"2026-09-01T16:56:40.911Z","0.1.8":"2026-09-01T16:57:48.837Z","0.2.0-beta":"2026-09-01T22:40:35.718Z","0.2.0":"2026-09-01T22:58:03.696Z","0.3.0":"2026-09-01T22:59:33.225Z"},"bugs":{"url":"https://github.com/Yasirdora/darkroom/issues"},"author":{"url":"https://ysr.design","name":"Yasir Dora"},"license":"MIT","homepage":"https://darkroom.ysr.design","keywords":["photo-editor","photo-editing","color-grading","color-science","image-processing","lut","3d-lut","typescript","browser","web-worker","local-first"],"repository":{"url":"git+https://github.com/Yasirdora/darkroom.git","type":"git"},"description":"A local-first, framework-independent TypeScript photo-grading and 3D LUT engine.","maintainers":[{"name":"yasirdora","email":"yasirdora@gmail.com"}],"readme":"# Darkroom\n\n[![npm](https://img.shields.io/npm/v/@coloristic.org/darkroom?color=0b7285&label=npm)](https://www.npmjs.com/package/@coloristic.org/darkroom)\n[![CI](https://github.com/Yasirdora/darkroom/actions/workflows/ci.yml/badge.svg)](https://github.com/Yasirdora/darkroom/actions/workflows/ci.yml)\n[![dependencies](https://img.shields.io/badge/runtime%20dependencies-0-0b7285)](#dependency-policy)\n[![license](https://img.shields.io/npm/l/@coloristic.org/darkroom?color=0b7285)](./LICENSE)\n\nA local-first, framework-independent photo-grading and 3D LUT engine written\nin TypeScript. Zero runtime dependencies, DOM-free core, ESM only.\n\n```sh\nnpm install @coloristic.org/darkroom\n```\n\n## Why Darkroom\n\nMost of what a grading engine does can be assembled from existing parts. The\npiece that cannot is the layer underneath: turning log-encoded camera footage\ninto something you can actually grade.\n\nDarkroom decodes **ARRI LogC3, Sony S-Log3, RED Log3G10, Canon C-Log3,\nPanasonic V-Log, Blackmagic Film Gen 4 and 5, and DaVinci Intermediate** to\nscene-linear reflectance, then converts each one's native gamut — AWG3, REDWideGamutRGB,\nS-Gamut3.Cine, Cinema Gamut, V-Gamut, Blackmagic Wide Gamut, DaVinci Wide Gamut —\nto sRGB using published matrices. Those transforms are transcribed from\nmanufacturer specifications rather than fitted, because a wrong coefficient\nproduces an image that looks plausible and is wrong.\n\nEverything else is built on that: a grade model, a deterministic render\npipeline shared by preview and export, and 3D LUTs in both directions so a\ngrade made in a browser opens in Resolve.\n\nThe root module is DOM-free and works with the same RGBA buffer contract in\nNode.js, browser main threads, and module workers.\n\n- Camera log and gamut normalization to scene-linear, with profile detection\n- Deterministic CPU rendering with explicit inputs and outputs\n- Adjustments, curves, qualifiers, effects, transforms, and looks\n- 3D LUT creation, interpolation, resampling, `.cube` parsing, and export\n- Histograms, vectorscopes, scene analysis, automatic corrections, and color matching\n- Bounded ICC, EXIF, JPEG, PNG, WebP, grade, curve, and LUT inputs\n- Browser decoding isolated behind an explicit subpath\n- Lazy, opt-in built-in looks isolated from root-only consumer bundles\n- Zero runtime dependencies and no package-initiated network requests\n\n## Dependency policy\n\nDarkroom implements the domain behavior that defines the project: validated\nRGBA buffers, alpha-correct resizing and blur, color conversions, curves,\ntrilinear 3D LUT sampling, `.cube` parsing, masks, qualifiers, effects, scopes,\nanalysis, and camera-profile normalization. This keeps numerical behavior,\nresource limits, and bundle composition under the package's control.\n\nZero runtime dependencies does not mean rebuilding general infrastructure.\nTypeScript, Vitest, and Vite remain development tools, while compressed image\ndecoding uses native browser APIs behind the `/browser` entry. Consumers do not\ninstall those tools or any transitive runtime package. The rationale and the\ncriteria for any future exception are recorded in\n[ADR 0002](https://github.com/Yasirdora/darkroom/blob/main/docs/decisions/0002-zero-runtime-dependencies.md).\n\n## Installation\n\n```sh\nnpm install @coloristic.org/darkroom\n```\n\nThe package is ESM-only. CommonJS `require()` and package-internal deep imports\nare not supported.\n\n## Entry points\n\n| Import | Environment | Purpose |\n| --- | --- | --- |\n| `@coloristic.org/darkroom` | Node.js, browsers, module workers | DOM-free pixel processing, grades, LUTs, analysis, transforms, and profile helpers |\n| `@coloristic.org/darkroom/browser` | Browsers with Canvas 2D and `createImageBitmap` | `File` decoding and conversions between browser image objects and `PixelBuffer` |\n| `@coloristic.org/darkroom/presets` | Node.js, browsers, module workers | Immutable preset metadata and lazy compilation into generic engine `Look` values |\n\nThe root entry never imports either optional subpath. Importing the root alone\ntherefore excludes browser adapters and preset catalog data from its module graph.\n\n## Core workflow\n\nCreate or obtain an RGBA `PixelBuffer`, describe a partial grade, and render it:\n\n```ts\nimport {\n  createPixelBuffer,\n  render,\n  renderForExport,\n  type GradeInput,\n} from '@coloristic.org/darkroom';\n\nconst input = createPixelBuffer({\n  width: 2,\n  height: 1,\n  data: [32, 48, 64, 255, 180, 150, 120, 255],\n});\n\nconst grade: GradeInput = {\n  adjustments: {\n    exposure: 12,\n    contrast: 8,\n    vibrance: 10,\n  },\n  curves: {\n    master: [\n      { x: 0, y: 0 },\n      { x: 0.5, y: 0.54 },\n      { x: 1, y: 1 },\n    ],\n  },\n  transform: {\n    crop: { x: 0, y: 0, width: 1, height: 1 },\n  },\n};\n\nconst preview = render(input, grade); // crop is an overlay by default\nconst exported = renderForExport(input, grade); // crop is baked in\n```\n\nGrade fields are partial. `normalizeGrade()` fills defaults, clamps supported\nnumeric controls, sorts valid curve points, and rejects unsafe structures.\n`ADJUSTMENT_RANGES`, `DEFAULT_ADJUSTMENTS`, `DEFAULT_QUALIFIER`, and\n`DEFAULT_TRANSFORM` are exported for UI construction and validation.\n\nProcessing functions do not mutate the caller's input pixels. A no-op render\nmay return the original `PixelBuffer` to avoid a full-frame allocation; use\n`clonePixelBuffer()` when a distinct buffer identity is required.\n\n## Browser decoding\n\nThe browser adapter validates the encoded file type, byte size, dimensions, and\npixel count before allocating the decoded buffer:\n\n```ts\nimport { render } from '@coloristic.org/darkroom';\nimport {\n  decodeImageFile,\n  pixelBufferToImageData,\n} from '@coloristic.org/darkroom/browser';\n\nasync function gradeFile(file: File): Promise<ImageData> {\n  const decoded = await decodeImageFile(file, {\n    colorSpaceConversion: 'default',\n  });\n\n  const output = render(decoded.buffer, {\n    adjustments: { exposure: 6, saturation: 8 },\n  });\n\n  return pixelBufferToImageData(output);\n}\n```\n\n`decodeImageFile()` accepts JPEG, PNG, and WebP `File` objects. EXIF orientation\nis baked into the returned pixels. Its result reports the effective\n`colorSpaceConversionApplied` value because browsers may fall back to their\ndefault conversion when the requested `createImageBitmap` option is unavailable.\nThe field identifies the option used by the successful call; it cannot prove\nthat a non-conforming browser honored every option internally.\n\nThe browser entry also exports `pixelBufferFromImageData()`,\n`pixelBufferToImageData()`, and `pixelBufferFromImageBitmap()`. Consumers own\ncanvas presentation, worker lifecycle, object URLs, downloads, and storage.\n\n## Built-in presets\n\nPresets live in the same installed tarball but are opt-in through `/presets`:\n\n```ts\nimport { render } from '@coloristic.org/darkroom';\nimport {\n  compilePreset,\n  getPreset,\n  isPresetId,\n  presets,\n} from '@coloristic.org/darkroom/presets';\n\nconst metadata = getPreset('portra400');\nconst look = compilePreset('portra400', { lutSize: 33 });\nconst output = render(input, { look, lookIntensity: 80 });\n\nconsole.log(metadata?.label, presets.length, isPresetId('portra400'));\n```\n\nCatalog metadata is immutable. Preset LUT compilation is lazy, and every call\nreturns fresh LUT data. Applications that reuse a preset should cache the\ncompiled `Look` by preset ID and LUT size. The renderer accepts generic `Look`\nobjects rather than preset IDs, so custom and imported LUTs use the same stage.\n\nThe preset subpath provides import-graph, initialization, and consumer-bundle\nisolation; it does not provide separate download or licensing isolation.\n\nFilm- and scanner-inspired looks are independently authored creative\napproximations. Their names do not imply affiliation, certification,\nendorsement, or colorimetric equivalence to any manufacturer or product.\n\n## Importing and exporting `.cube` LUTs\n\n```ts\nimport {\n  formatCube,\n  parseCube,\n  render,\n} from '@coloristic.org/darkroom';\n\nconst lut = parseCube(cubeText, {\n  fallbackTitle: 'Imported look',\n  size: 33,\n});\n\nconst output = render(input, {\n  look: { lut },\n  lookIntensity: 100,\n});\n\nconst normalizedCubeText = formatCube(lut, {\n  precision: 6,\n  title: 'Darkroom export',\n});\n```\n\nThe parser supports 3D `.cube` data with optional `TITLE`, `DOMAIN_MIN`, and\n`DOMAIN_MAX` directives. It rejects 1D or combined LUTs, duplicate or unknown\ndirectives, non-finite values, invalid row counts, and inputs beyond the public\nresource limits. Source cubes from size 2 through 65 are resampled to 17, 33,\nor 65 as requested.\n\n## Public API overview\n\nThe package uses named exports. Major groups include:\n\n| Area | Representative exports |\n| --- | --- |\n| Buffers and sizing | `createPixelBuffer`, `assertPixelBuffer`, `clonePixelBuffer`, `resizePixelBuffer`, `resizeToFit` |\n| Grades and rendering | `createDefaultGrade`, `normalizeGrade`, `render`, `renderForExport` |\n| Curves and selective processing | `applyCurves`, `identityCurve`, `makeDefaultSCurve`, `buildQualifierMask`, `applyWithMask` |\n| Looks and effects | `applyLook`, `applyFilmGrain`, `applyEffect` |\n| LUTs and `.cube` | `createLut3D`, `buildLut`, `applyLut`, `resampleLut`, `parseCube`, `formatCube` |\n| Analysis | `computeHistogram`, `computeVectorscope`, `analyzeImage`, `computeColorMatchStats`, `computeAutoEnhance` |\n| Geometry | `applyTransform`, `fullCropRect`, `largestCropForAspect`, `constrainCropToAspect` |\n| Input profiles | `applyInputProfile`, `normalizeInput`, `getInputProfileSpec`, `detectInputProfileFromHeader`, `extractExifOrientation` |\n| Contracts | `DarkroomError`, `DARKROOM_LIMITS`, exported TypeScript interfaces and unions |\n\nThe export map—not files under `dist/`—defines the supported API. Imports such\nas `@coloristic.org/darkroom/dist/index.js` are intentionally blocked.\n\n## Resource limits\n\n`DARKROOM_LIMITS` is frozen, exported from the root entry, and used as a shared\ncontract by the engine, browser adapter, and reference application.\n\n| Limit | Value | Enforcement |\n| --- | ---: | --- |\n| Encoded image file | 10 MiB | `decodeImageFile()` before decoding |\n| Decoded image | 12,000,000 pixels | Buffer constructors, processing boundaries, and browser decoding |\n| Encoded metadata header | 512 KiB | ICC, EXIF, orientation, profile, and dimension scans |\n| Encoded `.cube` file | 16 MiB | Exported boundary for consumers to check before text decoding |\n| Direct `.cube` text | 16,777,216 UTF-16 code units | `parseCube()` before splitting lines |\n| Source 3D LUT dimension | 2 through 65 | `parseCube()` |\n| Parsed `.cube` lines | 278,721 | `parseCube()` |\n| Curve points | 32 per channel | `normalizeGrade()` |\n| Vectorscope dimension | 512 maximum | `computeVectorscope()` |\n| Engine LUT dimensions | 17, 33, or 65 | LUT creation, building, resampling, and preset compilation |\n\nLimits are part of the package's denial-of-service protections. Consumers\nshould reject oversized encoded `.cube` files before converting them to text\nand should avoid disabling or bypassing these boundaries for untrusted input.\n\n## Error handling\n\nPublic validation and browser operations throw `DarkroomError` with a stable,\nmachine-readable `code`:\n\n```ts\nimport {\n  DarkroomError,\n  parseCube,\n} from '@coloristic.org/darkroom';\n\ntry {\n  const lut = parseCube(untrustedText, { size: 33 });\n  // Use the validated LUT.\n} catch (error) {\n  if (error instanceof DarkroomError) {\n    console.error(error.code, error.message);\n  } else {\n    throw error;\n  }\n}\n```\n\n| Code | Meaning |\n| --- | --- |\n| `INVALID_DIMENSIONS` | Dimensions or caller-provided allocation bounds are invalid |\n| `BUFFER_LENGTH_MISMATCH` | RGBA data does not match the declared dimensions |\n| `RESOURCE_LIMIT_EXCEEDED` | A byte, pixel, line, curve, or allocation limit was exceeded |\n| `INVALID_GRADE` | Grade structure contains an unsafe or contradictory value |\n| `INVALID_LUT` | LUT shape, domain, title, or data is invalid |\n| `UNSUPPORTED_LUT_SIZE` | A requested or imported LUT dimension is unsupported |\n| `INVALID_CUBE` | `.cube` syntax or declared content is invalid |\n| `INVALID_PROFILE` | An input profile request is invalid |\n| `UNSUPPORTED_IMAGE` | A browser image file is empty, invalid, or unsupported |\n| `DECODE_FAILED` | Browser image decoding failed |\n| `CANVAS_UNAVAILABLE` | Canvas 2D is not available in the current browser context |\n\nDo not branch on human-readable messages. `asDarkroomError()` is available to\nwrap an unknown exception at application boundaries without double-wrapping an\nexisting `DarkroomError`.\n\n## Compatibility\n\n- **Modules:** ESM only, with an explicit package export map\n- **JavaScript target:** readable ES2022 modules\n- **Node.js:** supported `22.12+`, `24`, and `26` release lines\n- **TypeScript:** bundled declarations and declaration maps; strict NodeNext\n  consumers are covered by packed-tarball smoke tests\n- **Browsers:** ES2022-capable browsers for the root and preset entries\n- **Browser adapter:** requires `File`, `createImageBitmap`, `ImageData`, and\n  Canvas 2D through `OffscreenCanvas` or an HTML document\n- **Workers:** the root and preset entries are module-worker safe; browser APIs\n  still depend on the capabilities exposed by the worker runtime\n\nThe clean-consumer release gate installs the exact packed tarball and checks\nNode ESM execution, root/browser TypeScript graphs, root-versus-preset Vite\nbundle isolation, module-worker bundling, export-map enforcement, source maps,\npackage contents, and gzip budgets.\n\n## Processing scope and non-goals\n\nDarkroom is currently an 8-bit, SDR, display-referred photo engine. Camera-log\ninput transforms and film-inspired presets are creative processing tools, not a\nreplacement for a calibrated, end-to-end color-managed finishing pipeline.\n\nThe package does not currently provide RAW decoding, HDR mastering, GPU\nrendering, UI components, editor state, history, worker orchestration, file\ndownloads, persistence, or server storage. Those responsibilities remain with\nthe consumer or the Coloristic Darkroom reference application.\n\n## Privacy and security\n\nThe package has no runtime dependencies, analytics, telemetry, storage, or\nnetwork code. Browser image decoding remains local to the executing browser.\nConsumer applications control file acquisition, persistence, logging, DOM\ninsertion, uploads, and network behavior.\n\nTreat image metadata, filenames, grade data, and `.cube` text as untrusted.\nReport suspected vulnerabilities through the repository's\n[private security advisory form](https://github.com/Yasirdora/darkroom/security/advisories/new),\nnot a public issue.\n\n## Versioning\n\nDarkroom follows [Semantic Versioning](https://semver.org/). `0.1.1` is the\nfirst stable release; `0.1.0-beta.0` and `0.1.0-beta.1` preceded it and are\nrecorded in [CHANGELOG.md](./CHANGELOG.md).\n\nThe contract that versioning covers:\n\n- **Public API** is the set of names exported from the three declared entry\n  points. Nothing reachable only by deep import is public, and the export map\n  enforces that rather than documenting it.\n- **Numerical behavior** of the render paths is part of the contract. A change\n  that moves pixels for an unchanged grade is breaking, even when no type\n  changes, because the output is what consumers actually depend on.\n- **Preset identifiers** are stable. Renaming one breaks saved grades.\n- Every breaking change is documented in the changelog, and pre-1.0 breaking\n  changes land in a new minor version.\n\nReleases publish from a tagged commit through npm trusted publishing, so the\nregistry verifies the artifact came from this repository's workflow. The\ndistribution tag is derived from the version: a prerelease goes to `beta`, a\nstable release to `latest`.\n\n## License\n\nMIT © Yasir Dora. See [LICENSE](./LICENSE).\n","readmeFilename":"README.md"}