@pixerate/editor 0.92.0 → 0.93.0
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.
- package/dist/{chunk-PDCITYBT.cjs → chunk-4F3YDIWW.cjs} +367 -2
- package/dist/chunk-4F3YDIWW.cjs.map +1 -0
- package/dist/{chunk-YLB2WVNA.js → chunk-ISDCJHTO.js} +367 -2
- package/dist/chunk-ISDCJHTO.js.map +1 -0
- package/dist/image-editor/index.cjs +36 -11
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +1 -1
- package/dist/image-editor/index.d.ts +1 -1
- package/dist/image-editor/index.js +26 -1
- package/dist/image-editor/index.js.map +1 -1
- package/dist/paint/index.cjs +26 -2
- package/dist/paint/index.cjs.map +1 -1
- package/dist/paint/index.d.cts +95 -1
- package/dist/paint/index.d.ts +95 -1
- package/dist/paint/index.js +25 -1
- package/package.json +3 -3
- package/skills/pixerate-image-editor/SKILL.md +8 -0
- package/dist/chunk-PDCITYBT.cjs.map +0 -1
- package/dist/chunk-YLB2WVNA.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","../../src/image-editor/color.ts","../../src/image-editor/geometry.ts","../../src/image-editor/selection/mask.ts","../../src/image-editor/selection/modify.ts","../../src/image-editor/selection/layer-mask.ts","../../src/image-editor/adjustment-layers.ts","../../src/image-editor/filters.ts","../../src/image-editor/histogram.ts","../../src/image-editor/selection/overlay.ts","../../src/image-editor/layers/model.ts","../../src/image-editor/layers/compositor.ts","../../src/image-editor/layers/defaults.ts","../../src/image-editor/layers/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/layers/serialize.ts","../../src/image-editor/layers/ops.ts","../../src/image-editor/psd/binary.ts","../../src/image-editor/psd/descriptor.ts","../../src/image-editor/psd/adjust.ts","../../src/image-editor/psd/compression.ts","../../src/image-editor/psd/file.ts","../../src/image-editor/psd/pixels.ts","../../src/image-editor/psd/import.ts","../../src/image-editor/psd/export.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/psd/ui.ts"],"names":["lu","lv","clamp","x","y","q","v","op","lightness","props","mask","layer","decoded","out","num","moveLayer","WRITE_IDEMPOTENT","LAYER_ID","WRITE","DESTRUCTIVE","READ"],"mappings":"AAAA;AACE;AACF,yDAA8B;AAC9B;AACE;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,yDAA8B;AAC9B;AACA;AC7DO,IAAM,uBAAA,EAAyB,CAAA;AAE/B,IAAM,uBAAA,EAAyB,CAAA;AAQ/B,SAAS,uBAAA,CAAwB,KAAA,EAAqC;AAC3E,EAAA,GAAA,CAAI,OAAO,MAAA,IAAU,SAAA,GAAY,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA,EAAG,OAAO,sBAAA;AAClE,EAAA,OAAO,MAAA,GAAS,uBAAA,EAAyB,uBAAA,EAAyB,sBAAA;AACpE;AAMO,SAAS,YAAA,CAAa,CAAA,EAAmB;AAC9C,EAAA,OAAO,EAAA,GAAK,QAAA,EAAU,EAAA,EAAI,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAA,CAAK,EAAA,EAAI,KAAA,EAAA,EAAS,KAAA,EAAO,GAAG,CAAA;AACrE;AAEO,SAAS,YAAA,CAAa,CAAA,EAAmB;AAC9C,EAAA,OAAO,EAAA,GAAK,SAAA,EAAY,EAAA,EAAI,MAAA,EAAQ,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,EAAA,EAAI,GAAG,EAAA,EAAI,KAAA;AACrE;AAGO,IAAM,SAAA,EAA8C,CAAC,MAAA,EAAQ,MAAA,EAAQ,MAAM,CAAA;AAM3E,IAAM,iBAAA,EAA4B,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAE5D,SAAS,UAAA,CAAW,CAAA,EAAY,CAAA,EAAqB;AAC1D,EAAA,MAAM,IAAA,EAAM,IAAI,KAAA,CAAM,CAAC,CAAA,CAAE,IAAA,CAAK,CAAC,CAAA;AAC/B,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,IAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,CAAA,EAAA,EAAK;AAC1B,MAAA,GAAA,CAAI,EAAA,EAAI,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,CAAC,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,CAAC,CAAA;AAAA,IACrF;AAAA,EACF;AACA,EAAA,OAAO,GAAA;AACT;AAEO,SAAS,YAAA,CAAa,CAAA,EAAY,CAAA,EAAgE;AACvG,EAAA,OAAO;AAAA,IACL,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAA;AAAA,IACtC,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAA;AAAA,IACtC,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC;AAAA,EACxC,CAAA;AACF;AAEO,SAAS,aAAA,CAAc,CAAA,EAA4B;AACxD,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,EAAA,EAAI,CAAA;AACpC,EAAA,MAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AACtB,EAAA,MAAM,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,CAAA;AACxB,EAAA,MAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AACtB,EAAA,MAAM,IAAA,EAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AAChC,EAAA,GAAA,CAAI,CAAC,MAAA,CAAO,QAAA,CAAS,GAAG,EAAA,GAAK,IAAA,CAAK,GAAA,CAAI,GAAG,EAAA,EAAI,KAAA,EAAO,OAAO,IAAA;AAC3D,EAAA,MAAM,EAAA,EAAI,EAAA,EAAI,GAAA;AACd,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,CAAA;AAAA,IAAG,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA;AAAA,IAAA,CAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA;AAAA,IAC/C,EAAA,EAAI,CAAA;AAAA,IAAA,CAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA;AAAA,IAAG,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA;AAAA,IAC/C,EAAA,EAAI,CAAA;AAAA,IAAG,CAAA,CAAE,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA;AAAA,IAAA,CAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAAA,EACjD,CAAA;AACF;AAEA,SAAS,KAAA,CAAM,CAAA,EAAW,CAAA,EAAW,CAAA,EAAoB;AACvD,EAAA,OAAO,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AACnC;AAMO,IAAM,IAAA,EAAoB,EAAE,CAAA,EAAG,MAAA,EAAQ,CAAA,EAAG,MAAM,CAAA;AAEvD,SAAS,OAAA,CAAQ,CAAA,EAA2C;AAC1D,EAAA,OAAO,CAAC,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,CAAA,EAAG,CAAA,EAAA,CAAI,EAAA,EAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,CAAA,EAAA,EAAK,CAAA,CAAE,CAAC,CAAA;AAC7C;AAEA,SAAS,OAAA,CAAQ,CAAA,EAAoD;AACnE,EAAA,MAAM,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAA;AAC3B,EAAA,OAAO,EAAA,GAAK,EAAA,EAAI,EAAE,GAAG,IAAI,EAAA,EAAI,EAAE,CAAA,EAAG,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,EAAG,CAAA,EAAG,CAAA,CAAE,CAAC,EAAA,EAAI,EAAE,CAAA;AAC1D;AAGA,IAAM,YAAA,EAAA,CAAwB,CAAA,EAAA,GAAM;AAClC,EAAA,MAAM,EAAA,EAAI,CAAC,EAAE,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,KAAK,CAAA,EAAG,EAAE,CAAA,EAAG,GAAA,EAAK,CAAA,EAAG,IAAI,CAAA,EAAG,EAAE,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,KAAK,CAAC,CAAA,CAAE,GAAA,CAAI,OAAO,CAAA;AACtF,EAAA,MAAM,EAAA,EAAa,CAAC,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,CAAE,CAAC,CAAC,CAAA;AACnG,EAAA,MAAM,IAAA,EAAM,aAAA,CAAc,CAAC,CAAA;AAC3B,EAAA,MAAM,EAAA,EAAI,IAAA,EAAM,YAAA,CAAa,GAAA,EAAK,OAAA,CAAQ,GAAG,CAAC,EAAA,EAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAC1D,EAAA,OAAO,UAAA,CAAW,CAAA,EAAG,KAAA,CAAM,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,CAAC,CAAC,CAAA;AAC9C,CAAA,CAAA,CAAG,CAAA;AACH,IAAM,YAAA,mBAAuB,aAAA,CAAc,WAAW,CAAA,UAAK,kBAAA;AAG3D,IAAM,SAAA,EAAoB,CAAC,MAAA,EAAQ,MAAA,EAAQ,CAAA,MAAA,EAAS,CAAA,MAAA,EAAS,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAQ,CAAA,MAAA,EAAS,MAAM,CAAA;AACpG,IAAM,aAAA,mBAAwB,aAAA,CAAc,QAAQ,CAAA,UAAK,kBAAA;AAGlD,SAAS,kBAAA,CAAmB,GAAA,EAAmB,GAAA,EAA4B;AAChF,EAAA,GAAA,CAAI,GAAA,CAAI,EAAA,IAAM,GAAA,CAAI,EAAA,GAAK,GAAA,CAAI,EAAA,IAAM,GAAA,CAAI,CAAA,EAAG,OAAO,CAAC,GAAG,gBAAgB,CAAA;AACnE,EAAA,MAAM,EAAA,EAAI,YAAA,CAAa,QAAA,EAAU,OAAA,CAAQ,GAAG,CAAC,CAAA;AAC7C,EAAA,MAAM,EAAA,EAAI,YAAA,CAAa,QAAA,EAAU,OAAA,CAAQ,GAAG,CAAC,CAAA;AAC7C,EAAA,OAAO,UAAA,CAAW,YAAA,EAAc,UAAA,CAAW,KAAA,CAAM,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAA,EAAG,CAAA,CAAE,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,CAAC,CAAA,EAAG,QAAQ,CAAC,CAAA;AACpG;AAMO,SAAS,QAAA,CAAS,GAAA,EAA4B;AACnD,EAAA,OAAO,UAAA,CAAW,WAAA,EAAa,UAAA,CAAW,kBAAA,CAAmB,GAAA,EAAK,GAAG,CAAA,EAAG,WAAW,CAAC,CAAA;AACtF;AAGO,SAAS,cAAA,CAAe,GAAA,EAAsD;AACnF,EAAA,OAAO,OAAA,CAAQ,YAAA,CAAa,WAAA,EAAa,GAAG,CAAC,CAAA;AAC/C;AAOO,IAAM,WAAA,EAAa,GAAA;AACnB,IAAM,WAAA,EAAa,IAAA;AACnB,IAAM,WAAA,EAAa,IAAA;AAE1B,SAAS,KAAA,CAAM,GAAA,EAA2B;AACxC,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,UAAA,EAAY,IAAA,CAAK,GAAA,CAAI,UAAA,EAAY,GAAG,CAAC,CAAA;AACxD,EAAA,MAAM,GAAA,EAAK,EAAA,EAAI,CAAA;AACf,EAAA,MAAM,GAAA,EAAK,GAAA,EAAK,CAAA;AAChB,EAAA,MAAM,EAAA,EACJ,EAAA,GAAK,IAAA,EACD,CAAA,UAAA,EAAe,GAAA,EAAK,SAAA,EAAc,GAAA,EAAK,SAAA,EAAc,EAAA,EAAI,QAAA,EACzD,CAAA,WAAA,EAAe,GAAA,EAAK,YAAA,EAAc,GAAA,EAAK,SAAA,EAAc,EAAA,EAAI,OAAA;AAC/D,EAAA,MAAM,GAAA,EAAK,EAAA,EAAI,CAAA;AACf,EAAA,MAAM,GAAA,EAAK,GAAA,EAAK,CAAA;AAChB,EAAA,MAAM,EAAA,EACJ,EAAA,GAAK,KAAA,EACD,CAAA,UAAA,EAAa,GAAA,EAAK,UAAA,EAAY,GAAA,EAAK,WAAA,EAAa,EAAA,EAAI,WAAA,EACpD,EAAA,GAAK,IAAA,EACH,CAAA,UAAA,EAAa,GAAA,EAAK,WAAA,EAAa,GAAA,EAAK,WAAA,EAAa,EAAA,EAAI,WAAA,EACrD,SAAA,EAAW,GAAA,EAAK,UAAA,EAAY,GAAA,EAAK,WAAA,EAAa,EAAA,EAAI,UAAA;AAC1D,EAAA,OAAO,EAAE,CAAA,EAAG,EAAE,CAAA;AAChB;AAGO,SAAS,MAAA,CAAO,CAAA,EAAmC;AACxD,EAAA,MAAM,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA,CAAE,EAAA,EAAI,GAAA,EAAK,CAAA,CAAE,EAAA,EAAI,CAAA;AAChC,EAAA,OAAO,CAAE,EAAA,EAAI,CAAA,CAAE,EAAA,EAAK,CAAA,EAAI,EAAA,EAAI,CAAA,CAAE,EAAA,EAAK,CAAC,CAAA;AACtC;AAGO,SAAS,MAAA,CAAO,CAAA,EAAW,CAAA,EAAyB;AACzD,EAAA,MAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AAC1B,EAAA,OAAO,EAAE,CAAA,EAAI,EAAA,EAAI,EAAA,EAAK,CAAA,EAAG,CAAA,EAAI,EAAA,EAAI,EAAA,EAAK,EAAE,CAAA;AAC1C;AAGA,SAAS,OAAA,CAAQ,GAAA,EAA+B;AAC9C,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,GAAA,EAAM,GAAG,CAAA;AAC5B,EAAA,GAAA,CAAI,EAAA,GAAK,WAAA,GAAc,EAAA,GAAK,UAAA,EAAY,OAAO,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA;AAC9D,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,EAAA,EAAI,WAAA,EAAa,CAAC,UAAA,EAAY,WAAA,EAAa,EAAE,EAAA,EAAI,CAAC,WAAA,EAAa,GAAA,EAAK,UAAU,CAAA;AAC7F,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA;AAChC,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA;AAChC,EAAA,MAAM,GAAA,EAAK,IAAA,EAAM,CAAA;AACjB,EAAA,MAAM,GAAA,EAAK,IAAA,EAAM,CAAA;AACjB,EAAA,MAAM,EAAA,EAAA,CAAK,IAAA,EAAM,EAAA,EAAI,EAAA,EAAA,EAAA,CAAO,GAAA,EAAK,EAAA,CAAA;AACjC,EAAA,OAAO,CAAC,GAAA,EAAA,CAAM,GAAA,EAAK,EAAA,EAAA,EAAM,CAAA,EAAG,GAAA,EAAA,CAAM,GAAA,EAAK,EAAA,EAAA,EAAM,CAAC,CAAA;AAChD;AAGA,SAAS,WAAA,CAAY,CAAA,EAA6B;AAChD,EAAA,MAAM,EAAA,EAAI,IAAA,EAAM,CAAA;AAChB,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,OAAA,CAAQ,IAAA,EAAA,CAAO,EAAA,EAAI,CAAA,CAAE,CAAA;AACtC,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,OAAA,CAAQ,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,CAAA,EAAG,CAAC,CAAC,CAAA;AACjD,EAAA,MAAM,GAAA,EAAK,GAAA,EAAK,EAAA;AAChB,EAAA,MAAM,GAAA,EAAK,GAAA,EAAK,EAAA;AAChB,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,KAAA,CAAM,EAAA,EAAI,EAAE,CAAA,EAAG,KAAK,CAAA;AAC5C,EAAA,MAAM,GAAA,EAAK,CAAC,GAAA,EAAK,CAAA;AACjB,EAAA,MAAM,GAAA,EAAK,GAAA,EAAK,CAAA;AAChB,EAAA,OAAO,GAAA,EAAK,EAAA,EAAI,CAAC,CAAC,EAAA,EAAI,CAAC,EAAE,EAAA,EAAI,CAAC,EAAA,EAAI,EAAE,CAAA;AACtC;AAMO,SAAS,YAAA,CAAa,MAAA,EAAgB,IAAA,EAA4B;AACvE,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,OAAA,CAAQ,MAAM,CAAA;AAC7B,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,WAAA,CAAY,MAAM,CAAA;AACnC,EAAA,MAAM,IAAA,EAAM,KAAA,EAAO,UAAA;AACnB,EAAA,OAAO,MAAA,CAAO,EAAA,EAAI,GAAA,EAAK,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,GAAG,CAAA;AAC1C;AAGO,SAAS,YAAA,CAAa,CAAA,EAAmD;AAC9E,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,MAAA,CAAO,CAAC,CAAA;AACvB,EAAA,MAAM,KAAA,EAAO,CAAC,CAAA,EAAA,GAAc;AAC1B,IAAA,MAAM,CAACA,GAAAA,EAAIC,GAAE,EAAA,EAAI,OAAA,CAAQ,IAAA,EAAM,CAAC,CAAA;AAChC,IAAA,OAAA,CAAQ,EAAA,EAAID,GAAAA,EAAAA,GAAO,EAAA,EAAA,CAAK,EAAA,EAAIC,GAAAA,EAAAA,GAAO,CAAA;AAAA,EACrC,CAAA;AAEA,EAAA,IAAI,KAAA,EAAO,EAAA;AACX,EAAA,IAAI,GAAA,EAAK,QAAA;AACT,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,EAAA,EAAI,EAAA,GAAK,GAAA,EAAM,EAAA,GAAK,CAAA,EAAG;AAClC,IAAA,MAAM,EAAA,EAAI,IAAA,CAAK,CAAC,CAAA;AAChB,IAAA,GAAA,CAAI,EAAA,EAAI,EAAA,EAAI;AACV,MAAA,GAAA,EAAK,CAAA;AACL,MAAA,KAAA,EAAO,CAAA;AAAA,IACT;AAAA,EACF;AACA,EAAA,IAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,CAAA,EAAG,EAAE,CAAA;AAC7B,EAAA,IAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,CAAA,EAAG,GAAI,CAAA;AAC/B,EAAA,MAAM,EAAA,EAAI,aAAA;AACV,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,EAAK;AAC3B,IAAA,MAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAA,CAAK,EAAA,EAAI,CAAA,CAAA;AACvB,IAAA,MAAM,EAAA,EAAI,EAAA,EAAI,EAAA,EAAA,CAAK,EAAA,EAAI,CAAA,CAAA;AACvB,IAAA,GAAA,CAAI,IAAA,CAAK,CAAC,EAAA,EAAI,IAAA,CAAK,CAAC,CAAA,EAAG,EAAA,EAAI,CAAA;AAAA,IAAA,KACtB,EAAA,EAAI,CAAA;AAAA,EACX;AACA,EAAA,MAAM,OAAA,EAAS,IAAA,EAAA,CAAA,CAAQ,EAAA,EAAI,CAAA,EAAA,EAAK,CAAA,CAAA;AAChC,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,OAAA,CAAQ,MAAM,CAAA;AAC/B,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,WAAA,CAAY,MAAM,CAAA;AACnC,EAAA,MAAM,IAAA,EAAA,CAAO,EAAA,EAAI,EAAA,EAAA,EAAM,GAAA,EAAA,CAAM,EAAA,EAAI,EAAA,EAAA,EAAM,EAAA;AACvC,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAM,WAAW,CAAA;AAC1C;AAOO,IAAM,eAAA,EAAiB,IAAA;AAEvB,IAAM,2BAAA,EAA6B,CAAA;AAC1C,IAAM,cAAA,EAAgB,IAAA,EAAM,cAAA;AAQrB,SAAS,mBAAA,CAAoB,WAAA,EAA6B;AAC/D,EAAA,OAAO,IAAA,EAAA,CAAO,cAAA,EAAgB,YAAA,EAAc,0BAAA,CAAA;AAC9C;AAGO,SAAS,mBAAA,CAAoB,MAAA,EAAwB;AAC1D,EAAA,OAAA,CAAQ,cAAA,EAAgB,IAAA,EAAM,MAAA,EAAA,EAAU,0BAAA;AAC1C;AAEA,IAAM,eAAA,mBAA0B,aAAA,CAAc,QAAA,CAAS,YAAA,CAAa,cAAA,EAAgB,CAAC,CAAC,CAAC,CAAA,UAAK,kBAAA;AAOrF,SAAS,kBAAA,CAAmB,WAAA,EAAqB,KAAA,EAAO,CAAA,EAAY;AACzE,EAAA,GAAA,CAAI,CAAC,YAAA,GAAe,CAAC,IAAA,EAAM,OAAO,CAAC,GAAG,gBAAgB,CAAA;AACtD,EAAA,MAAM,IAAA,EAAM,QAAA,CAAS,YAAA,CAAa,mBAAA,CAAoB,WAAW,CAAA,EAAG,IAAI,CAAC,CAAA;AACzE,EAAA,MAAM,EAAA,EAAI,UAAA,CAAW,GAAA,EAAK,cAAc,CAAA;AACxC,EAAA,MAAM,EAAA,EAAI,YAAA,CAAa,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAC,CAAA;AACnC,EAAA,MAAM,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,QAAA,CAAS,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,QAAA,CAAS,CAAC,EAAA,EAAI,CAAA,CAAE,CAAC,EAAA,EAAI,QAAA,CAAS,CAAC,CAAA;AACrE,EAAA,OAAO,UAAA,CAAW,CAAA,EAAG,KAAA,CAAM,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAC,CAAC,CAAA;AACjD;AAOO,SAAS,sBAAA,CACd,GAAA,EACuC;AACvC,EAAA,MAAM,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,EAAA,EAAI,KAAA,CAAM,OAAA,CAAQ,GAAG,EAAA,EAAI,IAAA,EAAM,CAAE,GAAA,CAAY,CAAA,EAAI,GAAA,CAAY,CAAA,EAAI,GAAA,CAAY,CAAC,CAAA;AAC5F,EAAA,MAAM,IAAA,EAAgC,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,EAAA,GAAM,YAAA,CAAa,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAC,EAAA,EAAI,GAAG,CAAC,CAAA;AAK5G,EAAA,GAAA,CAAI,GAAA,CAAI,CAAC,EAAA,EAAI,GAAA,CAAI,CAAC,EAAA,EAAI,GAAA,CAAI,CAAC,EAAA,GAAK,IAAA,EAAM,OAAO,EAAE,WAAA,EAAa,CAAA,EAAG,IAAA,EAAM,EAAE,CAAA;AAGvE,EAAA,MAAM,IAAA,EAAM,cAAA,CAAe,YAAA,CAAa,cAAA,EAAgB,GAAG,CAAC,CAAA;AAC5D,EAAA,MAAM,EAAE,MAAA,EAAQ,KAAK,EAAA,EAAI,YAAA,CAAa,GAAG,CAAA;AACzC,EAAA,MAAMC,OAAAA,EAAQ,CAAC,CAAA,EAAA,GAAc,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,EAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,CAAC,CAAC,CAAA;AAC5D,EAAA,OAAO,EAAE,WAAA,EAAaA,MAAAA,CAAM,mBAAA,CAAoB,MAAM,CAAC,CAAA,EAAG,IAAA,EAAMA,MAAAA,CAAM,IAAI,EAAE,CAAA;AAC9E;AAOO,IAAM,cAAA,EAAgB,IAAA;AAC7B,IAAM,KAAA,EAAO,IAAA;AACb,IAAM,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,EAAA,EAAI,GAAG,CAAA;AAErC,IAAM,cAAA,EAAgB,GAAA;AAaf,SAAS,SAAA,CAAU,CAAA,EAAW,QAAA,EAAkB,UAAA,EAAoB,SAAA,EAAW,CAAA,EAAW;AAC/F,EAAA,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAA,EAAI,OAAO,CAAA;AACrB,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAA;AAC9B,EAAA,IAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAA;AACrB,EAAA,GAAA,CAAI,EAAA,EAAI,EAAA,GAAK,EAAA,EAAI,aAAA,EAAe;AAC9B,IAAA,MAAM,KAAA,EAAO,EAAA,EAAI,aAAA;AACjB,IAAA,MAAM,EAAA,EAAA,CAAK,EAAA,EAAI,aAAA,EAAA,EAAiB,IAAA;AAChC,IAAA,MAAM,EAAA,EAAA,CAAK,EAAA,EAAI,aAAA,EAAA,EAAiB,IAAA;AAChC,IAAA,EAAA,EAAI,cAAA,EAAgB,KAAA,EAAA,CAAS,EAAA,EAAA,CAAK,EAAA,EAAI,EAAA,EAAA,CAAK,EAAA,EAAI,CAAA,CAAA,EAAA,EAAA,CAAQ,EAAA,EAAI,CAAA,CAAA,CAAA;AAAA,EAC7D;AACA,EAAA,GAAA,CAAI,SAAA,IAAa,EAAA,GAAK,WAAA,IAAe,CAAA,EAAG,OAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAA;AAC5D,EAAA,IAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAA,EAAG,EAAA,EAAI,GAAG,CAAA;AACxC,EAAA,MAAM,EAAA,EAAI,SAAA,EAAW,GAAA;AACrB,EAAA,GAAA,CAAI,EAAA,IAAM,CAAA,EAAG;AACX,IAAA,EAAA,GAAK,EAAA,EAAI,IAAA,EAAA,CAAO,EAAA,EAAI,MAAA,EAAA,EAAA,CAAW,EAAA,EAAA,CAAK,EAAA,EAAI,EAAA,EAAI,CAAA,EAAA,GAAM,CAAA,CAAA;AAAA,EACpD;AACA,EAAA,MAAM,EAAA,EAAI,WAAA,EAAa,GAAA;AACvB,EAAA,GAAA,CAAI,EAAA,IAAM,CAAA,EAAG;AAEX,IAAA,EAAA,GAAK,EAAA,EAAI,IAAA,EAAM,EAAA,EAAA,CAAK,EAAA,EAAI,CAAA,CAAA;AAAA,EAC1B;AACA,EAAA,OAAO,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,CAAC,CAAC,CAAA,EAAG,GAAG,CAAA;AAClD;AAGO,SAAS,YAAA,CAAa,QAAA,EAAkB,UAAA,EAAoB,SAAA,EAAW,CAAA,EAAiB;AAC7F,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAA;AAC9B,EAAA,MAAM,IAAA,EAAM,IAAI,YAAA,CAAa,cAAA,EAAgB,CAAC,CAAA;AAC9C,EAAA,IAAA,CAAA,IAAS,EAAA,EAAI,CAAA,EAAG,EAAA,GAAK,aAAA,EAAe,CAAA,EAAA,EAAK;AACvC,IAAA,GAAA,CAAI,CAAC,EAAA,EAAI,SAAA,CAAW,EAAA,EAAI,EAAA,EAAK,aAAA,EAAe,QAAA,EAAU,UAAA,EAAY,CAAC,CAAA;AAAA,EACrE;AACA,EAAA,OAAO,GAAA;AACT;AAGO,SAAS,aAAA,CAAc,GAAA,EAAmB,QAAA,EAAkB,CAAA,EAAmB;AACpF,EAAA,GAAA,CAAI,CAAA,CAAE,EAAA,EAAI,CAAA,CAAA,EAAI,OAAO,GAAA,CAAI,CAAC,CAAA;AAC1B,EAAA,MAAM,EAAA,EAAI,GAAA,CAAI,OAAA,EAAS,CAAA;AACvB,EAAA,MAAM,EAAA,EAAK,EAAA,EAAI,EAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAA;AACxC,EAAA,GAAA,CAAI,EAAA,GAAK,CAAA,EAAG,OAAO,GAAA,CAAI,CAAC,CAAA;AACxB,EAAA,MAAM,EAAA,EAAI,EAAA,EAAI,CAAA;AACd,EAAA,OAAO,GAAA,CAAI,CAAC,EAAA,EAAA,CAAK,GAAA,CAAI,EAAA,EAAI,CAAC,EAAA,EAAI,GAAA,CAAI,CAAC,CAAA,EAAA,EAAA,CAAM,EAAA,EAAI,CAAA,CAAA;AAC/C;ADpEA;AACA;AEzQO,SAAS,cAAA,CAAe,OAAA,EAAyB;AACtD,EAAA,MAAM,EAAA,EAAA,CAAM,QAAA,EAAU,IAAA,EAAO,GAAA,EAAA,EAAO,GAAA;AACpC,EAAA,OAAO,EAAA,IAAM,IAAA,EAAM,EAAA,EAAI,CAAA;AACzB;AAEA,SAAS,MAAA,CAAO,KAAA,EAAiC;AAE/C,EAAA,OAAA,CAAQ,KAAA,EAAO;AAAA,IACb,KAAK,CAAA;AACH,MAAA,OAAO,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,IACd,KAAK,EAAA;AACH,MAAA,OAAO,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,IACd,KAAK,GAAA;AACH,MAAA,OAAO,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA;AAAA,IACf,KAAK,GAAA;AACH,MAAA,OAAO,CAAC,CAAA,EAAG,CAAA,CAAE,CAAA;AAAA,IACf,OAAA,EAAS;AACP,MAAA,MAAM,IAAA,EAAO,MAAA,EAAQ,IAAA,CAAK,GAAA,EAAM,GAAA;AAChC,MAAA,OAAO,CAAC,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAG,CAAC,CAAA;AAAA,IACtC;AAAA,EACF;AACF;AAGO,SAAS,oBAAA,CAAqB,SAAA,EAAoD;AACvF,EAAA,MAAM,MAAA,EAAQ,cAAA,CAAe,SAAA,CAAU,MAAM,CAAA;AAC7C,EAAA,OAAO,MAAA,IAAU,GAAA,GAAM,MAAA,IAAU,GAAA;AACnC;AAGO,SAAS,cAAA,CACd,KAAA,EACA,MAAA,EACA,SAAA,EACmC;AACnC,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,MAAA,CAAO,cAAA,CAAe,SAAA,CAAU,MAAM,CAAC,CAAA;AACtD,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,CAAC,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,CAAC,CAAA;AAAA,IAChD,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,CAAC,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,CAAC;AAAA,EACnD,CAAA;AACF;AAGO,SAAS,iBAAA,CAAkB,EAAA,EAAgB,CAAA,EAAgC;AAChF,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,MAAA,CAAO,CAAA,CAAE,KAAK,CAAA;AAC7B,EAAA,IAAI,EAAA,EAAI,EAAA,CAAG,EAAA,EAAA,CAAK,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,MAAA,EAAQ,CAAA,CAAA;AAC9C,EAAA,IAAI,EAAA,EAAI,EAAA,CAAG,EAAA,EAAA,CAAK,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,OAAA,EAAS,CAAA,CAAA;AAC/C,EAAA,GAAA,CAAI,CAAA,CAAE,KAAA,EAAO,EAAA,EAAI,CAAC,CAAA;AAClB,EAAA,GAAA,CAAI,CAAA,CAAE,KAAA,EAAO,EAAA,EAAI,CAAC,CAAA;AAClB,EAAA,MAAM,GAAA,EAAK,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AACvB,EAAA,MAAM,GAAA,EAAK,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA;AACvB,EAAA,OAAO,EAAE,CAAA,EAAG,CAAA,CAAE,QAAA,EAAU,GAAA,EAAK,CAAA,CAAE,KAAA,EAAO,CAAA,EAAG,CAAA,CAAE,QAAA,EAAU,GAAA,EAAK,CAAA,CAAE,MAAM,CAAA;AACpE;AAGO,SAAS,oBAAA,CAAqB,EAAA,EAAgB,CAAA,EAAgC;AACnF,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,MAAA,CAAO,CAAA,CAAE,KAAK,CAAA;AAC7B,EAAA,MAAM,GAAA,EAAA,CAAM,EAAA,CAAG,EAAA,EAAI,CAAA,CAAE,OAAA,EAAA,EAAW,CAAA,CAAE,KAAA;AAClC,EAAA,MAAM,GAAA,EAAA,CAAM,EAAA,CAAG,EAAA,EAAI,CAAA,CAAE,OAAA,EAAA,EAAW,CAAA,CAAE,KAAA;AAClC,EAAA,IAAI,EAAA,EAAI,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,CAAA;AACtB,EAAA,IAAI,EAAA,EAAI,CAAC,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,CAAA;AACvB,EAAA,GAAA,CAAI,CAAA,CAAE,KAAA,EAAO,EAAA,EAAI,CAAC,CAAA;AAClB,EAAA,GAAA,CAAI,CAAA,CAAE,KAAA,EAAO,EAAA,EAAI,CAAC,CAAA;AAClB,EAAA,OAAO,EAAE,CAAA,EAAG,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,MAAA,EAAQ,CAAA,EAAG,CAAA,EAAG,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,OAAA,EAAS,EAAE,CAAA;AAC3F;AAMO,SAAS,6BAAA,CACd,GAAA,EACA,CAAA,EACM;AACN,EAAA,GAAA,CAAI,SAAA,CAAU,CAAA,CAAE,OAAA,EAAS,CAAA,CAAE,OAAO,CAAA;AAClC,EAAA,GAAA,CAAI,KAAA,CAAM,CAAA,CAAE,KAAA,EAAO,CAAA,CAAE,KAAK,CAAA;AAC1B,EAAA,GAAA,CAAI,CAAA,CAAE,MAAA,IAAU,CAAA,EAAG,GAAA,CAAI,MAAA,CAAQ,CAAA,CAAE,MAAA,EAAQ,IAAA,CAAK,GAAA,EAAM,GAAG,CAAA;AACvD,EAAA,GAAA,CAAI,CAAA,CAAE,MAAA,GAAS,CAAA,CAAE,KAAA,EAAO,GAAA,CAAI,KAAA,CAAM,CAAA,CAAE,MAAA,EAAQ,CAAA,EAAA,EAAK,CAAA,EAAG,CAAA,CAAE,MAAA,EAAQ,CAAA,EAAA,EAAK,CAAC,CAAA;AACpE,EAAA,GAAA,CAAI,SAAA,CAAU,CAAA,CAAE,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,MAAA,EAAQ,CAAA,CAAA,EAAI,CAAA,CAAE,CAAA,CAAE,MAAA,CAAO,EAAA,EAAI,CAAA,CAAE,MAAA,CAAO,OAAA,EAAS,CAAA,CAAE,CAAA;AACvF;AAGO,SAAS,gBAAA,CACd,IAAA,EACA,CAAA,EACyD;AACzD,EAAA,MAAM,QAAA,EAAU;AAAA,IACd,iBAAA,CAAkB,EAAE,CAAA,EAAG,IAAA,CAAK,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,EAAE,CAAA,EAAG,CAAC,CAAA;AAAA,IAC7C,iBAAA,CAAkB,EAAE,CAAA,EAAG,IAAA,CAAK,EAAA,EAAI,IAAA,CAAK,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,EAAE,CAAA,EAAG,CAAC,CAAA;AAAA,IAC1D,iBAAA,CAAkB,EAAE,CAAA,EAAG,IAAA,CAAK,CAAA,EAAG,CAAA,EAAG,IAAA,CAAK,EAAA,EAAI,IAAA,CAAK,OAAO,CAAA,EAAG,CAAC,CAAA;AAAA,IAC3D,iBAAA,CAAkB,EAAE,CAAA,EAAG,IAAA,CAAK,EAAA,EAAI,IAAA,CAAK,KAAA,EAAO,CAAA,EAAG,IAAA,CAAK,EAAA,EAAI,IAAA,CAAK,OAAO,CAAA,EAAG,CAAC;AAAA,EAC1E,CAAA;AACA,EAAA,MAAM,GAAA,EAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,CAAC,CAAA;AACjC,EAAA,MAAM,GAAA,EAAK,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,CAAC,CAAA;AACjC,EAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAG,EAAE,CAAA;AAC3B,EAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAG,EAAE,CAAA;AAC3B,EAAA,OAAO,EAAE,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAG,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,GAAG,EAAE,EAAA,EAAI,KAAK,CAAA;AAC3F;AAGO,SAAS,gBAAA,CAAiB,IAAA,EAAe,KAAA,EAAe,MAAA,EAAyB;AACtF,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAG,IAAA,CAAK,CAAC,CAAC,CAAA;AACjD,EAAA,MAAM,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,CAAA,EAAG,IAAA,CAAK,CAAC,CAAC,CAAA;AAClD,EAAA,OAAO;AAAA,IACL,CAAA;AAAA,IACA,CAAA;AAAA,IACA,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,CAAA,EAAG,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,IAClD,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,CAAA,EAAG,IAAA,CAAK,MAAM,CAAC;AAAA,EACvD,CAAA;AACF;AAEA,SAAS,eAAA,CACP,KAAA,EACA,OAAA,EACA,QAAA,EACA,kBAAA,EACA,YAAA,EACA;AACA,EAAA,MAAM,EAAE,eAAA,EAAiB,IAAA,EAAM,SAAA,EAAW,SAAA,EAAW,IAAA,EAAM,GAAA,EAAK,IAAA,EAAM,SAAS,EAAA,EAAI,KAAA;AACnF,EAAA,MAAM,aAAA,EAAe,eAAA,CAAgB,MAAA,GAAS,GAAA;AAC9C,EAAA,MAAM,cAAA,EAAgB,eAAA,CAAgB,OAAA,GAAU,GAAA;AAChD,EAAA,MAAM,KAAA,EAAgB,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,KAAA,EAAO,YAAA,EAAc,MAAA,EAAQ,cAAc,CAAA;AAC/E,EAAA,MAAM,WAAA,EACJ,aAAA,IAAiB,KAAA,EAAA,GAAa,aAAA,IAAiB,KAAA,EAC3C,aAAA,mBACC,SAAA,UAAA,CAAc,mBAAA,EAAqB,KAAA,EAAO,IAAA,GAAA;AAEjD,EAAA,MAAM,OAAA,EACJ,CAAC,mBAAA,GAAsB,WAAA,EAAa,gBAAA,CAAiB,UAAA,EAAY,YAAA,EAAc,aAAa,EAAA,EAAI,IAAA;AAClG,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,MAAM,EAAA,EAAI,cAAA,CAAe,MAAA,CAAO,KAAA,EAAO,MAAA,CAAO,MAAA,EAAQ,SAAS,CAAA;AAE7F,EAAA,MAAM,SAAA,EACJ,IAAA,IAAQ,UAAA,EACJ,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,KAAA,EAAO,SAAA,EAAW,KAAK,EAAA,EAC1C,IAAA,CAAK,GAAA,CAAK,QAAA,EAAU,KAAA,EAAQ,KAAA,EAAQ,SAAA,EAAW,KAAA,EAAQ,KAAA,EAAO,CAAC,CAAA;AACrE,EAAA,MAAM,QAAA,EAAU,QAAA,EAAU,EAAA,EAAI,GAAA,CAAI,CAAA;AAClC,EAAA,MAAM,QAAA,EAAU,SAAA,EAAW,EAAA,EAAI,GAAA,CAAI,CAAA;AAEnC,EAAA,MAAM,WAAA,EAA8B;AAAA,IAClC,MAAA;AAAA,IACA,KAAA,EAAO,cAAA,CAAe,SAAA,CAAU,MAAM,CAAA;AAAA,IACtC,KAAA,EAAO,SAAA,CAAU,KAAA;AAAA,IACjB,KAAA,EAAO,SAAA,CAAU,KAAA;AAAA,IACjB,KAAA,EAAO,KAAA,EAAO,QAAA;AAAA,IACd,OAAA,EAAS,OAAA;AAAA,IACT,OAAA,EAAS;AAAA,EACX,CAAA;AAEA,EAAA,OAAO,EAAE,UAAA,EAAY,KAAA,EAAO,KAAA,EAAO,QAAA,EAAU,OAAA,EAAS,OAAA,EAAS,WAAW,CAAA;AAC5E;AAKO,SAAS,qBAAA,CACd,KAAA,EACA,OAAA,EACA,QAAA,EACA,mBAAA,EAAqB,KAAA,EACrB,YAAA,EACiB;AACjB,EAAA,OAAO,eAAA,CAAgB,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,kBAAA,EAAoB,YAAY,CAAA,CAAE,UAAA;AACrF;AAMO,SAAS,kBAAA,CACd,KAAA,EACA,OAAA,EACA,QAAA,EACA,mBAAA,EAAqB,KAAA,EACrB,YAAA,EACiB;AACjB,EAAA,MAAM,EAAE,KAAK,EAAA,EAAI,KAAA;AACjB,EAAA,MAAM,EAAE,UAAA,EAAY,KAAA,EAAO,KAAA,EAAO,QAAA,EAAU,OAAA,EAAS,OAAA,EAAS,WAAW,EAAA,EAAI,eAAA;AAAA,IAC3E,KAAA;AAAA,IACA,OAAA;AAAA,IACA,QAAA;AAAA,IACA,kBAAA;AAAA,IACA;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,QAAA,EAAU,MAAA,EAAQ,QAAA;AACxB,EAAA,MAAM,QAAA,EAAU,MAAA,EAAQ,QAAA;AAExB,EAAA,MAAM,UAAA,EAAY;AAAA,IAChB,CAAA,EAAG,QAAA,EAAW,QAAA,EAAU,KAAA,EAAQ,CAAA;AAAA,IAChC,CAAA,EAAG,QAAA,EAAW,QAAA,EAAU,KAAA,EAAQ,CAAA;AAAA,IAChC,KAAA,EAAO,QAAA,EAAU,IAAA;AAAA,IACjB,MAAA,EAAQ,QAAA,EAAU;AAAA,EACpB,CAAA;AAEA,EAAA,MAAM,SAAA,EAAW,WAAA,EAAa,gBAAA,CAAiB,UAAA,EAAY,UAAU,EAAA,EAAI,IAAA;AAEzE,EAAA,OAAO;AAAA,IACL,OAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAA;AAAA,IACA,UAAA,EAAY,KAAA,EAAO,QAAA;AAAA,IACnB,SAAA;AAAA,IACA;AAAA,EACF,CAAA;AACF;AAMO,SAAS,kBAAA,CACd,QAAA,EACA,KAAA,EACA,OAAA,EACA,QAAA,EACA,mBAAA,EAAqB,KAAA,EACT;AACZ,EAAA,OAAO,oBAAA,CAAqB,QAAA,EAAU,qBAAA,CAAsB,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,kBAAkB,CAAC,CAAA;AAC3G;AAMO,SAAS,kBAAA,CACd,KAAA,EACA,KAAA,EACA,OAAA,EACA,QAAA,EACA,mBAAA,EAAqB,KAAA,EACT;AACZ,EAAA,OAAO,iBAAA,CAAkB,KAAA,EAAO,qBAAA,CAAsB,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,kBAAkB,CAAC,CAAA;AACrG;AAEA,IAAM,eAAA,EAAiF;AAAA,EACrF,EAAA,EAAI,CAAC,CAAA,CAAA,EAAI,CAAA,CAAE,CAAA;AAAA,EACX,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,CAAE,CAAA;AAAA,EACT,EAAA,EAAI,CAAC,CAAA,EAAG,CAAA,CAAE,CAAA;AAAA,EACV,CAAA,EAAG,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACR,EAAA,EAAI,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACT,CAAA,EAAG,CAAC,CAAA,EAAG,CAAC,CAAA;AAAA,EACR,EAAA,EAAI,CAAC,CAAA,CAAA,EAAI,CAAC,CAAA;AAAA,EACV,CAAA,EAAG,CAAC,CAAA,CAAA,EAAI,CAAC;AACX,CAAA;AAEA,SAAS,cAAA,CAAe,CAAA,EAAW,CAAA,EAAuB;AACxD,EAAA,MAAM,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAC,EAAA,EAAI,KAAA,EAAO,EAAA,EAAI,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AAC/C,EAAA,MAAM,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,CAAC,EAAA,EAAI,KAAA,EAAO,EAAA,EAAI,IAAA,CAAK,IAAA,CAAK,CAAC,CAAA;AAC/C,EAAA,IAAA,CAAA,MAAW,CAAC,IAAA,EAAM,CAAC,EAAA,EAAI,EAAE,CAAC,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,cAAc,CAAA,EAAG;AAC7D,IAAA,GAAA,CAAI,GAAA,IAAO,GAAA,GAAM,GAAA,IAAO,EAAA,EAAI,OAAO,IAAA;AAAA,EACrC;AACA,EAAA,OAAO,IAAA;AACT;AAMO,SAAS,aAAA,CAAc,MAAA,EAAoB,SAAA,EAA2B,SAAA,EAAW,KAAA,EAAmB;AACzG,EAAA,GAAA,CAAI,OAAA,IAAW,KAAA,GAAQ,OAAA,IAAW,QAAA,EAAU,OAAO,MAAA;AACnD,EAAA,MAAM,CAAC,EAAA,EAAI,EAAE,EAAA,EAAI,cAAA,CAAe,MAAM,CAAA;AACtC,EAAA,MAAM,CAAC,CAAA,EAAG,CAAC,EAAA,EAAI,MAAA,CAAO,cAAA,CAAe,SAAA,CAAU,MAAM,CAAC,CAAA;AACtD,EAAA,MAAM,GAAA,EAAK,SAAA,CAAU,MAAA,EAAQ,CAAA,EAAA,EAAK,CAAA;AAClC,EAAA,MAAM,GAAA,EAAK,SAAA,CAAU,MAAA,EAAQ,CAAA,EAAA,EAAK,CAAA;AAClC,EAAA,GAAA,CAAI,QAAA,EAAU;AACZ,IAAA,MAAMC,GAAAA,EAAI,GAAA,EAAK,EAAA;AACf,IAAA,MAAMC,GAAAA,EAAI,GAAA,EAAK,EAAA;AACf,IAAA,OAAO,cAAA,CAAeD,GAAAA,EAAI,EAAA,EAAIC,GAAAA,EAAI,CAAA,EAAGD,GAAAA,EAAI,EAAA,EAAIC,GAAAA,EAAI,CAAC,CAAA;AAAA,EACpD;AACA,EAAA,MAAM,EAAA,EAAI,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,CAAA;AACxB,EAAA,MAAM,EAAA,EAAI,CAAC,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,CAAA;AACzB,EAAA,OAAO,cAAA,CAAe,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,EAAE,CAAA;AACtC;AAQO,SAAS,WAAA,CACd,QAAA,EACA,KAAA,EACA,OAAA,EACA,QAAA,EACA,gBAAA,EAAkB,EAAA,EAClB,YAAA,EACY;AACZ,EAAA,MAAM,QAAA,EAAU,kBAAA,CAAmB,KAAA,EAAO,OAAA,EAAS,QAAA,EAAU,IAAA,EAAM,YAAY,CAAA;AAC/E,EAAA,GAAA,CAAI,CAAC,OAAA,CAAQ,QAAA,EAAU,OAAO,IAAA;AAE9B,EAAA,MAAM,aAAA,EAAe,eAAA,CAAgB,QAAA,EAAU,OAAA,CAAQ,QAAA,EAAU,eAAe,CAAA;AAChF,EAAA,OAAO,aAAA,CAAc,YAAA,EAAc,KAAA,CAAM,SAAS,CAAA;AACpD;AAEA,SAAS,eAAA,CACP,QAAA,EACA,IAAA,EACA,eAAA,EACY;AACZ,EAAA,MAAM,EAAE,CAAA,EAAG,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,EAAE,EAAA,EAAI,IAAA;AACtC,EAAA,MAAM,GAAA,EAAK,QAAA,CAAS,CAAA;AACpB,EAAA,MAAM,GAAA,EAAK,QAAA,CAAS,CAAA;AAGpB,EAAA,MAAM,KAAA,EAAO,CAAC,EAAA,EAAY,EAAA,EAAY,EAAA,EAAY,EAAA,EAAA,GAChD,IAAA,CAAK,KAAA,CAAM,GAAA,EAAK,EAAA,EAAI,GAAA,EAAK,EAAE,CAAA;AAE7B,EAAA,GAAA,CAAI,IAAA,CAAK,EAAA,EAAI,EAAA,EAAI,CAAA,EAAG,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,IAAA;AAClD,EAAA,GAAA,CAAI,IAAA,CAAK,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAG,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,IAAA;AACtD,EAAA,GAAA,CAAI,IAAA,CAAK,EAAA,EAAI,EAAA,EAAI,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,IAAA;AAC1D,EAAA,GAAA,CAAI,IAAA,CAAK,EAAA,EAAI,EAAA,EAAI,CAAA,EAAG,EAAA,EAAI,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,IAAA;AAGtD,EAAA,MAAM,MAAA,EAAQ,GAAA,GAAM,EAAA,EAAI,gBAAA,GAAmB,GAAA,GAAM,EAAA,EAAI,EAAA,EAAI,eAAA;AACzD,EAAA,MAAM,MAAA,EAAQ,GAAA,GAAM,EAAA,EAAI,gBAAA,GAAmB,GAAA,GAAM,EAAA,EAAI,EAAA,EAAI,eAAA;AAEzD,EAAA,GAAA,CAAI,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,GAAA;AACzD,EAAA,GAAA,CAAI,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAA,CAAM,EAAA,EAAI,CAAA,CAAE,EAAA,GAAK,eAAA,EAAiB,OAAO,GAAA;AAC/D,EAAA,GAAA,CAAI,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,CAAC,EAAA,GAAK,eAAA,EAAiB,OAAO,GAAA;AACzD,EAAA,GAAA,CAAI,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,GAAA,EAAA,CAAM,EAAA,EAAI,CAAA,CAAE,EAAA,GAAK,eAAA,EAAiB,OAAO,GAAA;AAG/D,EAAA,GAAA,CAAI,GAAA,GAAM,EAAA,GAAK,GAAA,GAAM,EAAA,EAAI,EAAA,GAAK,GAAA,GAAM,EAAA,GAAK,GAAA,GAAM,EAAA,EAAI,CAAA,EAAG;AACpD,IAAA,OAAO,QAAA;AAAA,EACT;AAEA,EAAA,OAAO,IAAA;AACT;AAOO,SAAS,aAAA,CAAc,MAAA,EAAoB,SAAA,EAAoC;AACpF,EAAA,MAAM,aAAA,EAAe,UAAA,EAAY,aAAA,CAAc,MAAA,EAAQ,SAAA,EAAW,IAAI,EAAA,EAAI,MAAA;AAC1E,EAAA,OAAA,CAAQ,YAAA,EAAc;AAAA,IACpB,KAAK,IAAA;AAAA,IACL,KAAK,IAAA;AACH,MAAA,OAAO,aAAA;AAAA,IACT,KAAK,IAAA;AAAA,IACL,KAAK,IAAA;AACH,MAAA,OAAO,aAAA;AAAA,IACT,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACH,MAAA,OAAO,WAAA;AAAA,IACT,KAAK,GAAA;AAAA,IACL,KAAK,GAAA;AACH,MAAA,OAAO,WAAA;AAAA,IACT,KAAK,QAAA;AACH,MAAA,OAAO,MAAA;AAAA,IACT,OAAA;AACE,MAAA,OAAO,WAAA;AAAA,EACX;AACF;AAKO,SAAS,iBAAA,CACd,SAAA,EACA,MAAA,EACA,UAAA,EACA,YAAA,EACA,QAAA,EACA,SAAA,EACA,WAAA,EACA,SAAA,EACS;AAET,EAAA,GAAA,CAAI,YAAA,GAAe,UAAA,GAAa,oBAAA,CAAqB,SAAS,CAAA,EAAG;AAC/D,IAAA,YAAA,EAAc,EAAA,EAAI,WAAA;AAAA,EACpB;AACA,EAAA,MAAM,GAAA,EAAK,YAAA,CAAa,EAAA,EAAI,UAAA,CAAW,CAAA;AACvC,EAAA,MAAM,GAAA,EAAK,YAAA,CAAa,EAAA,EAAI,UAAA,CAAW,CAAA;AAGvC,EAAA,GAAA,CAAI,OAAA,IAAW,QAAA,EAAU;AACvB,IAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,SAAA,EAAW,SAAA,CAAU,KAAK,CAAA;AACnD,IAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,UAAA,EAAY,SAAA,CAAU,MAAM,CAAA;AACrD,IAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,SAAA,CAAU,EAAA,EAAI,EAAE,CAAC,CAAA;AACzD,IAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,SAAA,CAAU,EAAA,EAAI,EAAE,CAAC,CAAA;AACzD,IAAA,OAAO;AAAA,MACL,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAAA,MAClB,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAAA,MAClB,KAAA,EAAO,SAAA,CAAU,KAAA;AAAA,MACjB,MAAA,EAAQ,SAAA,CAAU;AAAA,IACpB,CAAA;AAAA,EACF;AAGA,EAAA,IAAI,KAAA,EAAO,SAAA,CAAU,CAAA;AACrB,EAAA,IAAI,IAAA,EAAM,SAAA,CAAU,CAAA;AACpB,EAAA,IAAI,MAAA,EAAQ,SAAA,CAAU,EAAA,EAAI,SAAA,CAAU,KAAA;AACpC,EAAA,IAAI,OAAA,EAAS,SAAA,CAAU,EAAA,EAAI,SAAA,CAAU,MAAA;AAErC,EAAA,MAAM,QAAA,EAAU,EAAA;AAEhB,EAAA,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,KAAA,GAAQ,OAAA,IAAW,IAAA,EAAM;AACxD,IAAA,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,IAAA,CAAK,GAAA,CAAI,KAAA,EAAO,OAAA,EAAS,SAAA,CAAU,EAAA,EAAI,SAAA,CAAU,MAAA,EAAQ,EAAE,CAAC,CAAA;AAAA,EACzF;AACA,EAAA,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,KAAA,GAAQ,OAAA,IAAW,IAAA,EAAM;AACxD,IAAA,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,MAAA,EAAQ,OAAA,EAAS,SAAA,CAAU,EAAA,EAAI,EAAE,CAAC,CAAA;AAAA,EAChE;AACA,EAAA,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,KAAA,GAAQ,OAAA,IAAW,IAAA,EAAM;AACxD,IAAA,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,OAAA,EAAS,SAAA,CAAU,EAAA,EAAI,SAAA,CAAU,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,EAC3F;AACA,EAAA,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,KAAA,GAAQ,OAAA,IAAW,IAAA,EAAM;AACxD,IAAA,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,SAAA,CAAU,EAAA,EAAI,EAAE,CAAC,CAAA;AAAA,EAChE;AAGA,EAAA,GAAA,CAAI,YAAA,GAAe,YAAA,EAAc,CAAA,EAAG;AAClC,IAAA,IAAI,SAAA,EAAW,MAAA,EAAQ,IAAA;AACvB,IAAA,IAAI,SAAA,EAAW,OAAA,EAAS,GAAA;AAExB,IAAA,GAAA,CAAI,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,GAAA,EAAK;AACpC,MAAA,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,SAAA,EAAW,WAAW,CAAA;AACrD,MAAA,OAAA,EAAS,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,IAAA,EAAM,QAAQ,CAAA;AAAA,IAC7C,EAAA,KAAA,GAAA,CAAW,OAAA,IAAW,IAAA,GAAO,OAAA,IAAW,GAAA,EAAK;AAC3C,MAAA,SAAA,EAAW,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,SAAA,EAAW,WAAW,CAAA;AACpD,MAAA,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,KAAA,EAAO,QAAQ,CAAA;AAAA,IAC5C,EAAA,KAAO;AAEL,MAAA,MAAM,QAAA,EAAU,SAAA,EAAW,WAAA;AAC3B,MAAA,GAAA,CAAI,IAAA,EAAM,QAAA,GAAW,SAAA,EAAW;AAC9B,QAAA,OAAA,EAAS,IAAA,EAAM,OAAA;AAAA,MACjB,EAAA,KAAO;AACL,QAAA,MAAM,QAAA,EAAU,SAAA,EAAW,WAAA;AAC3B,QAAA,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,QAAA,EAAU,KAAA,EAAO,OAAO,CAAA;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AAAA,IAClB,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA;AAAA,IACjB,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,IACjD,MAAA,EAAQ,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,GAAG,CAAC;AAAA,EACpD,CAAA;AACF;AAKO,SAAS,mBAAA,CAAoB,GAAA,EAA0E;AAC5G,EAAA,OAAA,CAAQ,GAAA,CAAI,IAAA,EAAM;AAAA,IAChB,KAAK,MAAA;AAAA,IACL,KAAK,OAAA;AACH,MAAA,OAAO;AAAA,QACL,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,EAAA,EAAI,GAAA,CAAI,KAAK,CAAA;AAAA,QACpC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,EAAA,EAAI,GAAA,CAAI,MAAM,CAAA;AAAA,QACrC,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,KAAK,CAAA;AAAA,QACzB,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,MAAM;AAAA,MAC7B,CAAA;AAAA,IACF,KAAK,QAAA;AACH,MAAA,OAAO;AAAA,QACL,CAAA,EAAG,GAAA,CAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AAAA,QAC/B,CAAA,EAAG,GAAA,CAAI,EAAA,EAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,OAAO,CAAA;AAAA,QAC/B,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,OAAO,EAAA,EAAI,CAAA;AAAA,QAC/B,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,OAAO,EAAA,EAAI;AAAA,MAClC,CAAA;AAAA,IACF,KAAK,OAAA;AAAA,IACL,KAAK,MAAA,EAAQ;AACX,MAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA;AACrC,MAAA,MAAM,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,CAAA,EAAG,GAAA,CAAI,IAAI,CAAA;AACrC,MAAA,OAAO;AAAA,QACL,CAAA,EAAG,IAAA;AAAA,QACH,CAAA,EAAG,IAAA;AAAA,QACH,KAAA,EAAO,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,KAAA,EAAO,GAAA,CAAI,CAAC,CAAA,EAAG,EAAE,CAAA;AAAA,QAC9C,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,KAAA,EAAO,GAAA,CAAI,CAAC,CAAA,EAAG,EAAE;AAAA,MACjD,CAAA;AAAA,IACF;AAAA,IACA,KAAK,KAAA,EAAO;AACV,MAAA,GAAA,CAAI,CAAC,GAAA,CAAI,MAAA,CAAO,MAAA,EAAQ,OAAO,EAAE,CAAA,EAAG,GAAA,CAAI,CAAA,EAAG,CAAA,EAAG,GAAA,CAAI,CAAA,EAAG,KAAA,EAAO,CAAA,EAAG,MAAA,EAAQ,EAAE,CAAA;AACzE,MAAA,IAAI,KAAA,EAAO,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,KAAA,EAAO,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,KAAA,EAAO,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,CAAE,CAAA;AACzB,MAAA,IAAI,KAAA,EAAO,GAAA,CAAI,MAAA,CAAO,CAAC,CAAA,CAAE,CAAA;AACzB,MAAA,IAAA,CAAA,MAAW,GAAA,GAAM,GAAA,CAAI,MAAA,EAAQ;AAC3B,QAAA,GAAA,CAAI,EAAA,CAAG,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,EAAA,CAAG,CAAA;AAC3B,QAAA,GAAA,CAAI,EAAA,CAAG,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,EAAA,CAAG,CAAA;AAC3B,QAAA,GAAA,CAAI,EAAA,CAAG,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,EAAA,CAAG,CAAA;AAC3B,QAAA,GAAA,CAAI,EAAA,CAAG,EAAA,EAAI,IAAA,EAAM,KAAA,EAAO,EAAA,CAAG,CAAA;AAAA,MAC7B;AACA,MAAA,OAAO,EAAE,CAAA,EAAG,IAAA,EAAM,CAAA,EAAG,IAAA,EAAM,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,MAAA,EAAQ,KAAA,EAAO,KAAK,CAAA;AAAA,IACrE;AAAA,IACA,KAAK,MAAA;AACH,MAAA,OAAO;AAAA,QACL,CAAA,EAAG,GAAA,CAAI,CAAA;AAAA,QACP,CAAA,EAAG,GAAA,CAAI,CAAA;AAAA,QACP,KAAA,EAAO,GAAA,CAAI,IAAA,CAAK,OAAA,EAAS,GAAA,CAAI,SAAA,EAAW,GAAA;AAAA,QACxC,MAAA,EAAQ,GAAA,CAAI,SAAA,EAAW;AAAA,MACzB,CAAA;AAAA,EACJ;AACF;AAKO,SAAS,iBAAA,CAAkB,KAAA,EAAmB,GAAA,EAAiB,QAAA,EAAU,EAAA,EAAa;AAC3F,EAAA,MAAM,EAAA,EAAI,mBAAA,CAAoB,GAAG,CAAA;AACjC,EAAA,OACE,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,EAAA,EAAI,QAAA,GACjB,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,MAAA,EAAQ,QAAA,GAC3B,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,EAAA,EAAI,QAAA,GACjB,KAAA,CAAM,EAAA,GAAK,CAAA,CAAE,EAAA,EAAI,CAAA,CAAE,OAAA,EAAS,OAAA;AAEhC;AF4FA;AACA;AG/nBO,IAAM,gBAAA,EAA4C,CAAC,SAAA,EAAW,KAAA,EAAO,UAAA,EAAY,WAAW,CAAA;AAE5F,SAAS,eAAA,CAAgB,KAAA,EAAwC;AACtE,EAAA,OAAO,OAAO,MAAA,IAAU,SAAA,GAAa,eAAA,CAAsC,QAAA,CAAS,KAAK,CAAA;AAC3F;AAGO,SAAS,kBAAA,CAAmB,KAAA,EAA+B;AAChE,EAAA,OAAO,eAAA,CAAgB,KAAK,EAAA,EAAI,MAAA,EAAQ,SAAA;AAC1C;AAiBA,IAAM,SAAA,EAAW,EAAA,GAAK,EAAA;AAEtB,SAAS,SAAA,CAAU,KAAA,EAAe,MAAA,EAAsB;AACtD,EAAA,GAAA,CAAI,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,EAAA,GAAK,CAAC,MAAA,CAAO,SAAA,CAAU,MAAM,EAAA,GAAK,MAAA,EAAQ,EAAA,GAAK,OAAA,EAAS,EAAA,GAAK,MAAA,EAAQ,SAAA,GAAY,OAAA,EAAS,QAAA,EAAU;AAC7H,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,uBAAA,EAA0B,KAAK,CAAA,IAAA,EAAI,MAAM,CAAA,CAAA;AAC3D,EAAA;AACF;AAG+G;AACpG,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACwB,EAAA;AACf,IAAA;AACJ,IAAA;AACoB,IAAA;AACL,MAAA;AACf,QAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACe,IAAA;AACJ,IAAA;AAC6B,IAAA;AACb,MAAA;AAChB,QAAA;AACP,QAAA;AACF,MAAA;AACF,IAAA;AACiB,IAAA;AACZ,IAAA;AACgB,IAAA;AACD,IAAA;AACtB,EAAA;AACmB,EAAA;AAC4C,EAAA;AACjE;AAGqG;AAC5E,EAAA;AAC4B,EAAA;AACE,EAAA;AACD,EAAA;AACtD;AAG4E;AACnD,EAAA;AACsC,EAAA;AAC/D;AAEkF;AAChD,EAAA;AAClC;AAGoF;AAC3D,EAAA;AACA,EAAA;AACwC,EAAA;AACtB,EAAA;AAC3C;AAGkD;AAC3B,EAAA;AACF,EAAA;AACY,EAAA;AACjC;AAGiH;AAC7F,EAAA;AACU,EAAA;AACmC,EAAA;AAClB,EAAA;AAC/C;AAGuE;AACxB,EAAA;AACgB,EAAA;AACtD,EAAA;AACT;AAG8E;AAC1C,EAAA;AACxB,EAAA;AAC2B,EAAA;AACG,EAAA;AAChB,IAAA;AACkC,IAAA;AAC1D,EAAA;AACa,EAAA;AACf;AAWwB;AAClB,EAAA;AACiD,EAAA;AAC1C,IAAA;AACO,EAAA;AACT,IAAA;AACF,EAAA;AACyD,IAAA;AAC/C,IAAA;AACA,IAAA;AACoB,IAAA;AACyB,IAAA;AACA,IAAA;AACD,IAAA;AACF,IAAA;AAC3D,EAAA;AACgC,EAAA;AAClC;AAGoE;AACvB,EAAA;AACoB,EAAA;AACR,EAAA;AACzD;AAGmG;AACtC,EAAA;AACpC,EAAA;AACkB,EAAA;AACjB,EAAA;AACC,EAAA;AACR,EAAA;AACD,EAAA;AACiB,EAAA;AAC8B,IAAA;AACrC,IAAA;AACmB,IAAA;AAC3B,IAAA;AACgB,IAAA;AAC2B,MAAA;AACjC,MAAA;AACU,MAAA;AAClB,MAAA;AAC2C,MAAA;AACA,MAAA;AACF,MAAA;AAC3D,IAAA;AACF,EAAA;AAC6C,EAAA;AAC/C;AAYiB;AACiB,EAAA;AACT,EAAA;AAC+B,EAAA;AACnB,EAAA;AACM,EAAA;AACf,EAAA;AAC0B,EAAA;AAC9C,IAAA;AACa,IAAA;AACV,MAAA;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACO,QAAA;AACV,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACG,MAAA;AACW,QAAA;AACd,QAAA;AACF,MAAA;AACyD,QAAA;AAC3D,IAAA;AAC8B,IAAA;AAC0B,IAAA;AAC/C,IAAA;AACX,EAAA;AAC6C,EAAA;AAC/C;AAGkJ;AAC1F,EAAA;AACI,EAAA;AACnC,IAAA;AACF,IAAA;AACkB,MAAA;AACtB,MAAA;AACR,IAAA;AAC8B,MAAA;AACtB,MAAA;AACf,IAAA;AACF,EAAA;AAC2D,EAAA;AAC7D;AAMY;AACa;AACc,EAAA;AACuB,EAAA;AACrD,EAAA;AACN;AAE0D;AAClC,EAAA;AACb,EAAA;AACwB,EAAA;AACjB,IAAA;AACyB,IAAA;AACA,IAAA;AACU,IAAA;AACQ,IAAA;AACpB,IAAA;AACd,IAAA;AACR,MAAA;AACR,MAAA;AACV,IAAA;AACF,EAAA;AACgB,EAAA;AACI,EAAA;AACtB;AAE8C;AACD,EAAA;AACjC,EAAA;AACqB,EAAA;AACI,EAAA;AACmB,EAAA;AAC9C,EAAA;AACwC,EAAA;AACd,IAAA;AACgC,IAAA;AACzB,IAAA;AACgB,IAAA;AAChD,IAAA;AACT,EAAA;AACyC,EAAA;AAC0B,IAAA;AACtB,IAAA;AACD,IAAA;AACP,IAAA;AACrC,EAAA;AACO,EAAA;AACT;AAG6D;AAChC,EAAA;AACjB,EAAA;AACkB,EAAA;AACF,IAAA;AACoB,MAAA;AAC9B,MAAA;AACN,MAAA;AACR,IAAA;AACa,IAAA;AACf,EAAA;AACQ,EAAA;AACgB,EAAA;AACN,IAAA;AACJ,IAAA;AAC6B,IAAA;AACnC,IAAA;AACQ,IAAA;AACK,IAAA;AACK,MAAA;AACI,MAAA;AAC5B,IAAA;AACQ,IAAA;AACJ,IAAA;AACN,EAAA;AACgC,EAAA;AAClC;AAG4E;AACb,EAAA;AACxB,EAAA;AACJ,EAAA;AACzB,EAAA;AACA,EAAA;AACiB,EAAA;AACJ,IAAA;AACT,IAAA;AACE,IAAA;AACH,IAAA;AACgC,MAAA;AACpB,MAAA;AACA,MAAA;AACN,MAAA;AACJ,MAAA;AAC4B,MAAA;AACvC,IAAA;AACmD,IAAA;AAChB,IAAA;AAC9B,IAAA;AACP,EAAA;AACkC,EAAA;AAC3B,EAAA;AACT;AAE6E;AACrB,EAAA;AACxD;AAGoE;AACT,EAAA;AAC/C,EAAA;AACM,EAAA;AACC,EAAA;AACM,EAAA;AACwC,EAAA;AACjE;AAGsE;AAC5D,EAAA;AACyB,EAAA;AACU,IAAA;AACd,MAAA;AACE,MAAA;AAC7B,IAAA;AACK,EAAA;AACoC,IAAA;AACzB,MAAA;AACa,MAAA;AAC7B,IAAA;AACF,EAAA;AACkD,EAAA;AACpD;AHijBiE;AACA;AIj8BnC;AAOsF;AAChG,EAAA;AACY,EAAA;AACpB,EAAA;AACA,EAAA;AACkB,EAAA;AACL,IAAA;AACV,IAAA;AACb,EAAA;AACwC,EAAA;AACxB,EAAA;AACH,EAAA;AAEgC,EAAA;AACb,EAAA;AAChB,IAAA;AACmB,IAAA;AACT,MAAA;AACG,MAAA;AACW,MAAA;AACtC,IAAA;AACU,IAAA;AAC4B,IAAA;AACd,MAAA;AACW,MAAA;AACqB,MAAA;AACxD,IAAA;AACF,EAAA;AACkC,EAAA;AACG,EAAA;AACJ,EAAA;AACf,IAAA;AACJ,IAAA;AACoB,IAAA;AACH,MAAA;AACV,MAAA;AACD,MAAA;AACJ,MAAA;AACM,MAAA;AACS,QAAA;AACE,QAAA;AACwB,QAAA;AACpB,QAAA;AAC/B,QAAA;AACF,MAAA;AACyB,MAAA;AACVC,MAAAA;AACC,MAAA;AAChB,MAAA;AACF,IAAA;AACiB,IAAA;AAC0B,MAAA;AACzC,MAAA;AACF,IAAA;AACQ,IAAA;AACwB,IAAA;AACa,MAAA;AACvB,MAAA;AACL,MAAA;AACe,MAAA;AACY,MAAA;AAC5C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAE4E;AACvC,EAAA;AACiB,EAAA;AAC7C,EAAA;AACT;AAGoG;AAC9C,EAAA;AACf,EAAA;AACwB,EAAA;AACtD,EAAA;AACT;AAGsG;AAC/D,EAAA;AACgB,EAAA;AAChB,EAAA;AACY,EAAA;AAC1C,EAAA;AACT;AAMoG;AAClE,EAAA;AACY,EAAA;AACiB,EAAA;AACxB,EAAA;AAChB,EAAA;AAC0B,EAAA;AACE,EAAA;AACZ,EAAA;AACY,IAAA;AACE,IAAA;AACnD,EAAA;AACO,EAAA;AACT;AAGwH;AAC/C,EAAA;AAC7D,EAAA;AAC+B,EAAA;AACvB,EAAA;AACY,EAAA;AACb,IAAA;AACgB,IAAA;AACjC,EAAA;AACF;AAMoG;AACrC,EAAA;AAC9B,EAAA;AACQ,EAAA;AACK,EAAA;AAChC,EAAA;AACkB,IAAA;AACiB,MAAA;AACT,MAAA;AACpC,IAAA;AAC4B,IAAA;AACa,MAAA;AACY,MAAA;AACrD,IAAA;AACF,EAAA;AAC8D,EAAA;AACvD,EAAA;AACT;AAI0G;AAC5C,EAAA;AACA,EAAA;AACrD,EAAA;AACT;AAMiJ;AAC7H,EAAA;AACgF,EAAA;AACnE,EAAA;AAER,EAAA;AAC8C,EAAA;AACvB,EAAA;AACR,IAAA;AACjB,IAAA;AACyC,IAAA;AACpC,IAAA;AACoC,IAAA;AAC9D,EAAA;AAC8B,EAAA;AAEd,IAAA;AACO,IAAA;AACH,IAAA;AACpB,EAAA;AACF;AAGqH;AAClF,EAAA;AACd,EAAA;AACI,EAAA;AACa,EAAA;AACC,EAAA;AACN,EAAA;AACH,EAAA;AAC2B,IAAA;AACnB,IAAA;AACpC,EAAA;AAC4B,EAAA;AACuB,IAAA;AACE,IAAA;AACrD,EAAA;AACO,EAAA;AACT;AAM0G;AAC7E,EAAA;AACkC,EAAA;AACvC,EAAA;AACM,EAAA;AACT,EAAA;AAC0C,EAAA;AACX,EAAA;AACE,EAAA;AACW,EAAA;AACxD,EAAA;AACT;AAQgG;AAEtB;AAC9B,EAAA;AACmB,EAAA;AACN,EAAA;AACzD;AAG2G;AAC/C,EAAA;AACnB,EAAA;AACL,EAAA;AACF,EAAA;AACpB,EAAA;AACL,IAAA;AACyC,MAAA;AACzC,IAAA;AAC2C,MAAA;AAC3C,IAAA;AAC0C,MAAA;AAC1C,IAAA;AACyC,MAAA;AACzC,IAAA;AACyC,MAAA;AAC9C,IAAA;AACoE,MAAA;AACtE,EAAA;AACF;AJg5BiE;AACA;AKhoC3C;AAOuD;AAC/B,EAAA;AAClC,EAAA;AACqB,EAAA;AAI7B,EAAA;AASO,IAAA;AACT,EAAA;AACsD,EAAA;AACQ,EAAA;AAChB,IAAA;AACjB,IAAA;AAC7B,EAAA;AAC8D,EAAA;AACb,IAAA;AACpB,IAAA;AAC7B,EAAA;AACuC,EAAA;AAChC,EAAA;AACT;AAGkH;AACpD,EAAA;AAC9D;AAG4G;AAChF,EAAA;AACrB,IAAA;AAC6D,IAAA;AACA,IAAA;AACA,IAAA;AACjE,EAAA;AACH;AAGqB;AACiC;AACK;AAEW;AAChD,EAAA;AACK,IAAA;AACP,IAAA;AACA,IAAA;AACTC,IAAAA;AACT,EAAA;AACe,EAAA;AACC,EAAA;AAC8C,EAAA;AACvD,EAAA;AACT;AAGiF;AAC3B,EAAA;AACrB,EAAA;AACzB,IAAA;AACkD,MAAA;AAC9C,IAAA;AACC,MAAA;AACT,IAAA;AACD,EAAA;AACH;AAG4E;AACxD,EAAA;AAC6C,EAAA;AACjE;AAOkF;AAC7C,EAAA;AACiB,EAAA;AAClB,IAAA;AACf,IAAA;AACqC,IAAA;AACE,IAAA;AACxB,IAAA;AACsB,IAAA;AACtB,IAAA;AAC6B,IAAA;AACtD,IAAA;AACR,EAAA;AACH;AAWE;AAMuD,EAAA;AACM,EAAA;AACC,EAAA;AACA,EAAA;AACvC,EAAA;AACC,EAAA;AAEE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACJ,EAAA;AACA,EAAA;AACE,EAAA;AACC,EAAA;AACc,EAAA;AACpB,IAAA;AACA,IAAA;AACD,IAAA;AACwC,IAAA;AACf,MAAA;AACA,MAAA;AACvB,MAAA;AACA,MAAA;AACe,MAAA;AACA,MAAA;AACf,MAAA;AACA,MAAA;AACqC,MAAA;AACA,MAAA;AACnB,MAAA;AACpC,IAAA;AACF,EAAA;AACO,EAAA;AACT;AASE;AAK8D,EAAA;AACjB,EAAA;AACjB,EAAA;AACT,IAAA;AAC4B,IAAA;AAC9B,IAAA;AACR,IAAA;AACC,MAAA;AACqD,MAAA;AAC7D,IAAA;AACF,EAAA;AAC8B,EAAA;AAChC;ALokCiE;AACA;AMpvC9B;AAEuB;AACwB;AACkB;AACxC;AAGW;AAC5C,EAAA;AACF,EAAA;AACD,EAAA;AACK,EAAA;AACb,EAAA;AACyB,EAAA;AACzC;AAW+D;AAC3B,EAAA;AACA,EAAA;AACe,EAAA;AACX,EAAA;AACS,EAAA;AACF,EAAA;AACA,EAAA;AACX,EAAA;AACM,EAAA;AACA,EAAA;AAC1C;AAEmE;AAEO;AACG,EAAA;AAC7E;AAEkE;AAChC,EAAA;AAClC;AAEuE;AAGlB;AACpC,EAAA;AACgD,EAAA;AACjE;AAEiD;AACzB,EAAA;AACI,EAAA;AAC5B;AAEuF;AAGY;AACvB,EAAA;AACX,IAAA;AACb,IAAA;AAC1B,IAAA;AACf,MAAA;AACO,MAAA;AACD,MAAA;AACD,MAAA;AACoC,MAAA;AAChD,IAAA;AAC8C,IAAA;AACe,IAAA;AACL,IAAA;AACxC,IAAA;AACc,IAAA;AACC,IAAA;AACsB,IAAA;AACvD,EAAA;AACsB,EAAA;AACxB;AAEmB;AAcK;AACV,EAAA;AAC6C,IAAA;AACvD,IAAA;AACc,IAAA;AAC0C,IAAA;AAC5B,IAAA;AACA,IAAA;AACJ,IAAA;AACV,IAAA;AAChB,EAAA;AACmC,EAAA;AACrC;AAMY;AAEsC;AACC,EAAA;AAG9C,EAAA;AAE0C,EAAA;AAC/C;AAEgD;AACE,EAAA;AACzC,EAAA;AACgC,IAAA;AACE,IAAA;AACH,IAAA;AACG,IAAA;AACE,IAAA;AAC3C,EAAA;AACF;AAEgD;AACZ,EAAA;AAC6B,EAAA;AACjE;AAE6F;AACxC,EAAA;AACb,EAAA;AACxB,EAAA;AACP,IAAA;AACuD,MAAA;AACvD,IAAA;AACsD,MAAA;AACrC,IAAA;AACqC,MAAA;AACH,QAAA;AACzB,QAAA;AAC8B,QAAA;AAClD,QAAA;AAC2B,UAAA;AACc,UAAA;AACF,UAAA;AACb,UAAA;AACjC,QAAA;AACD,MAAA;AAC+B,MAAA;AACzB,MAAA;AACiD,QAAA;AACR,QAAA;AACF,QAAA;AAC5C,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACK,IAAA;AACmD,MAAA;AACnD,IAAA;AACI,MAAA;AACsB,QAAA;AACE,QAAA;AACI,QAAA;AACY,QAAA;AAC/C,MAAA;AACiB,IAAA;AACJ,MAAA;AAC0C,MAAA;AACC,MAAA;AACjD,MAAA;AACT,IAAA;AACK,IAAA;AACI,MAAA;AACiD,QAAA;AACT,QAAA;AACA,QAAA;AAC/C,MAAA;AACG,IAAA;AACuD,MAAA;AACvD,IAAA;AACuD,MAAA;AACvD,IAAA;AACK,MAAA;AACZ,EAAA;AACF;AAM+E;AACjC,EAAA;AAClC,EAAA;AAC4B,EAAA;AACC,EAAA;AACqB,IAAA;AAClD,IAAA;AACe,IAAA;AACY,IAAA;AACF,IAAA;AACE,IAAA;AACrC,EAAA;AACyD,EAAA;AAEnB,EAAA;AACf,EAAA;AAChB,EAAA;AACT;AAG2E;AACxC,EAAA;AACJ,EAAA;AACG,EAAA;AACR,EAAA;AACqB,IAAA;AAC/B,IAAA;AAC4C,IAAA;AACvC,IAAA;AACH,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAW6H;AAE9E,EAAA;AAGV,EAAA;AACd,EAAA;AAC0C,IAAA;AACjD,IAAA;AACd,EAAA;AACiC,EAAA;AACjB,EAAA;AACL,EAAA;AAC2C,IAAA;AAC7C,IAAA;AACT,EAAA;AAC+B,EAAA;AACA,EAAA;AAEF,EAAA;AAClB,EAAA;AACmB,IAAA;AACA,IAAA;AACI,IAAA;AACL,MAAA;AACA,MAAA;AACV,MAAA;AACO,MAAA;AACN,MAAA;AACuC,MAAA;AAC1B,MAAA;AACjB,MAAA;AACgB,MAAA;AAC9B,IAAA;AAC0D,IAAA;AAC5D,EAAA;AACU,EAAA;AACqB,EAAA;AACN,IAAA;AACP,IAAA;AACO,MAAA;AACrB,MAAA;AACF,IAAA;AACoB,IAAA;AACO,MAAA;AACzB,MAAA;AACF,IAAA;AACuC,IAAA;AACN,IAAA;AAClB,IAAA;AACO,IAAA;AACA,IAAA;AACqC,IAAA;AACzC,IAAA;AACpB,EAAA;AACO,EAAA;AACT;AAGgF;AACnB,EAAA;AAC7D;AAewD;AACjC,EAAA;AACA,EAAA;AACZ,EAAA;AACc,EAAA;AACA,EAAA;AACzB;AAOiF;AAC3B,EAAA;AAChD,EAAA;AACqD,EAAA;AAC7C,IAAA;AACqBD,IAAAA;AACyB,IAAA;AACnD,EAAA;AAC+B,IAAA;AACtC,EAAA;AACiC,EAAA;AAC7B,EAAA;AACO,EAAA;AACsB,IAAA;AACM,IAAA;AACsB,IAAA;AACtD,IAAA;AACO,MAAA;AAC6C,MAAA;AACzD,IAAA;AACK,EAAA;AACgB,IAAA;AACvB,EAAA;AAC2C,EAAA;AAC7C;AAOyE;AAClC,EAAA;AACA,EAAA;AACb,EAAA;AACuB,EAAA;AAC/B,EAAA;AACqC,EAAA;AACjD,EAAA;AACgD,EAAA;AACL,EAAA;AAClB,EAAA;AACV,EAAA;AACrB;AAE+D;AAClC,EAAA;AACmB,EAAA;AAC5BA,EAAAA;AACO,EAAA;AACL,IAAA;AACsB,IAAA;AACpBA,IAAAA;AAC8B,IAAA;AAC3C,IAAA;AACT,EAAA;AACwC,EAAA;AAC1C;AAGqF;AACrD,EAAA;AACyC,EAAA;AACvD,EAAA;AACa,EAAA;AACA,EAAA;AACZ,EAAA;AACmB,EAAA;AACjB,EAAA;AAC4B,EAAA;AACxC,EAAA;AACT;AAE6E;AAG2B;AAC5F,EAAA;AAC2F,EAAA;AAC/C,EAAA;AAC1B,EAAA;AACE,IAAA;AACJ,IAAA;AACS,MAAA;AACd,MAAA;AACe,MAAA;AACD,MAAA;AACjC,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAO+G;AAC1C,EAAA;AACrD,EAAA;AACY,IAAA;AACoC,IAAA;AAC9D,EAAA;AAC+D,EAAA;AACxB,EAAA;AACuB,EAAA;AACpC,EAAA;AAC5B;AAGqE;AAC9B,EAAA;AAC0B,EAAA;AAC3C,EAAA;AACyC,EAAA;AAC/D;AAOqE;AACJ;AAExB;AACnB,EAAA;AACU,EAAA;AACV,EAAA;AACD,EAAA;AACrB;AAEsE;AACrD,EAAA;AACX,EAAA;AAC+C,EAAA;AACL,EAAA;AACjB,EAAA;AAClB,EAAA;AACb;AAGuD;AACjB,EAAA;AACA,EAAA;AACG,EAAA;AACpB,EAAA;AAC4C,EAAA;AAC/C,EAAA;AACL,EAAA;AACE,IAAA;AACyC,IAAA;AACtC,IAAA;AACT,EAAA;AAC8B,IAAA;AACK,IAAA;AACU,IAAA;AAEF,IAAA;AAChB,IAAA;AACZ,IAAA;AACtB,EAAA;AACqB,EAAA;AACO,EAAA;AACiC,EAAA;AAC/D;AAG+E;AACZ,EAAA;AAC7B,EAAA;AACvB,EAAA;AAC+C,IAAA;AACE,IAAA;AAC9D,EAAA;AAC4C,EAAA;AAC9C;AAOuB;AAWb;AACgC,EAAA;AAC6B,EAAA;AACZ,EAAA;AACtC,EAAA;AACW,EAAA;AACd,IAAA;AACH,IAAA;AACC,IAAA;AACF,EAAA;AAC8C,IAAA;AAChC,IAAA;AACD,MAAA;AACe,MAAA;AACA,MAAA;AACoB,MAAA;AACF,MAAA;AACD,MAAA;AACrD,MAAA;AACF,IAAA;AACc,IAAA;AACD,IAAA;AACsC,IAAA;AACK,IAAA;AACI,IAAA;AAChB,IAAA;AAC9C,EAAA;AACO,EAAA;AACT;AAWyF;AACrE,EAAA;AACU,EAAA;AACA,EAAA;AACE,EAAA;AACsB,EAAA;AACE,EAAA;AACY,EAAA;AACpE;AAGyC;AAEjB;AACM,EAAA;AACC,EAAA;AACA,EAAA;AAC/B;AAE+B;AACE,EAAA;AACD,EAAA;AACA,EAAA;AAChC;AAM6G;AACnD,EAAA;AACO,EAAA;AACD,EAAA;AACJ,EAAA;AAC3B,EAAA;AAChB,IAAA;AACS,IAAA;AACxB,EAAA;AACiB,EAAA;AACqC,IAAA;AACtD,EAAA;AACF;AAGkE;AACT,EAAA;AAClB,EAAA;AACgB,EAAA;AACV,EAAA;AAC7C;AAG4F;AAGjD;AACN,EAAA;AACA,EAAA;AAC2B,EAAA;AAChE;AAS0E;AACnD,EAAA;AACP,EAAA;AACa,IAAA;AACA,IAAA;AACA,IAAA;AAC3B,EAAA;AACF;AAG6B;AAGM;AAGZ;AAM6E;AACxF,EAAA;AACmB,EAAA;AACqB,IAAA;AACW,IAAA;AACxB,MAAA;AACqB,MAAA;AAC/C,MAAA;AACR,IAAA;AACH,EAAA;AAC6B,EAAA;AACe,IAAA;AACiB,IAAA;AAC/B,MAAA;AACI,MAAA;AACsB,MAAA;AAC7C,MAAA;AACR,IAAA;AACH,EAAA;AACO,EAAA;AACT;AAM+E;AACzD,EAAA;AACH,IAAA;AACG,MAAA;AAC6B,MAAA;AAChB,MAAA;AAC/B,IAAA;AACe,IAAA;AACG,MAAA;AAC6B,MAAA;AAChB,MAAA;AAC/B,IAAA;AACsB,IAAA;AACJ,MAAA;AAC0C,MAAA;AACL,MAAA;AACI,MAAA;AAC9B,MAAA;AAC8B,MAAA;AAC3C,MAAA;AACa,QAAA;AACb,QAAA;AACc,UAAA;AACI,UAAA;AACA,UAAA;AACA,UAAA;AAC9B,QAAA;AACwD,QAAA;AAC5C,QAAA;AACA,QAAA;AACA,QAAA;AACd,MAAA;AACF,IAAA;AACiB,IAAA;AACmB,MAAA;AACE,MAAA;AACL,MAAA;AACjB,MAAA;AACqB,QAAA;AACrB,QAAA;AACA,QAAA;AACA,QAAA;AACd,MAAA;AACF,IAAA;AACqB,IAAA;AACH,MAAA;AACkC,MAAA;AACI,MAAA;AAC5C,MAAA;AACyB,MAAA;AACH,QAAA;AAC2B,QAAA;AAClD,QAAA;AACR,MAAA;AACgB,MAAA;AACnB,IAAA;AACmB,IAAA;AACD,MAAA;AACyC,MAAA;AAChB,MAAA;AAC3B,MAAA;AACuB,QAAA;AACzB,QAAA;AACgB,UAAA;AACL,UAAA;AACA,UAAA;AACA,UAAA;AACd,QAAA;AACgB,UAAA;AACvB,QAAA;AACF,MAAA;AACF,IAAA;AACoB,IAAA;AACF,MAAA;AACW,MAAA;AACgC,MAAA;AACxC,MAAA;AACL,MAAA;AAC6C,QAAA;AACK,QAAA;AAChD,QAAA;AACwC,UAAA;AAChC,UAAA;AACtB,QAAA;AACmB,QAAA;AACA,QAAA;AACA,QAAA;AACrB,MAAA;AACF,IAAA;AACK,IAAA;AACW,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AAChB,MAAA;AACgB,IAAA;AACY,MAAA;AACd,MAAA;AACsB,QAAA;AACA,QAAA;AACA,QAAA;AACpC,MAAA;AACF,IAAA;AACkB,IAAA;AACuB,MAAA;AACzB,MAAA;AACwC,QAAA;AAC/B,QAAA;AACvB,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG4B;AACyB;AAEQ;AACF,EAAA;AAC9B,EAAA;AACM,IAAA;AAER,IAAA;AACC,IAAA;AACjBE,IAAAA;AACT,EAAA;AACuC,EAAA;AACf,EAAA;AACkC,EAAA;AACnD,EAAA;AACT;AAG0G;AAC3E,EAAA;AACyB,EAAA;AACxD;AAO4G;AAC9D,EAAA;AACiB,EAAA;AACtD,EAAA;AACT;AAE4F;AACtD,EAAA;AACyB,EAAA;AAC7C,EAAA;AACa,EAAA;AACS,EAAA;AAChC,IAAA;AACJ,IAAA;AACF,EAAA;AACuC,EAAA;AACnC,EAAA;AAC0D,EAAA;AACxB,EAAA;AACA,EAAA;AACA,EAAA;AACxC;AAeQ;AACsC,EAAA;AACxB,EAAA;AACwB,EAAA;AACK,EAAA;AAC1B,EAAA;AACN,EAAA;AACM,EAAA;AACU,EAAA;AACR,IAAA;AACN,IAAA;AACI,IAAA;AACA,IAAA;AACmB,IAAA;AAChB,MAAA;AACX,MAAA;AACe,QAAA;AACiC,QAAA;AACpD,MAAA;AAC0B,QAAA;AACjC,MAAA;AACF,IAAA;AACwC,IAAA;AACI,IAAA;AACA,IAAA;AAC9C,EAAA;AACF;AAG4F;AAC9D,EAAA;AAChB,EAAA;AACQ,IAAA;AACqD,MAAA;AAC5B,MAAA;AAClC,MAAA;AACR,IAAA;AACH,EAAA;AACF;AN0hCiE;AACA;AO17DP;AACf,EAAA;AAC3C;AAcQ;AAC+C,EAAA;AACJ,IAAA;AAC1C,EAAA;AACwC,IAAA;AAC/C,EAAA;AAI6D,EAAA;AAGnC,EAAA;AACoC,IAAA;AAC9D,EAAA;AACF;AAMoH;AAC9D,EAAA;AAC7B,EAAA;AACN,EAAA;AAEoB,EAAA;AACJ,EAAA;AACI,EAAA;AACJ,EAAA;AACM,EAAA;AACV,EAAA;AAIG,EAAA;AAIqB,EAAA;AAGQ,EAAA;AAGL,EAAA;AAEH,EAAA;AAClB,IAAA;AACjB,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AAGD,MAAA;AACiB,QAAA;AACA,QAAA;AACA,QAAA;AAClC,MAAA;AAGmB,MAAA;AACY,QAAA;AACR,QAAA;AACA,QAAA;AACA,QAAA;AACvB,MAAA;AAGiB,MAAA;AAC2B,QAAA;AACA,QAAA;AACA,QAAA;AAC5C,MAAA;AAGmB,MAAA;AACkC,QAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACvD,MAAA;AAGoB,MAAA;AACM,QAAA;AACA,QAAA;AAC1B,MAAA;AAGgB,MAAA;AACO,QAAA;AACvB,MAAA;AAEU,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AAChB,IAAA;AACF,EAAA;AACF;AAGwC;AACN,EAAA;AAC2B,EAAA;AACpD,EAAA;AACN;AAGqB;AACW;AACC;AAClB,EAAA;AACgC,IAAA;AACH,IAAA;AACiB,MAAA;AAC5D,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAasC;AAOgC;AAC3B,EAAA;AACM,EAAA;AACd,EAAA;AACQ,EAAA;AACI,EAAA;AACU,EAAA;AACO,EAAA;AACb,EAAA;AAEX,EAAA;AACS,EAAA;AACnB,EAAA;AACe,EAAA;AAC7B,EAAA;AACQ,EAAA;AACM,EAAA;AAEd,IAAA;AACiD,IAAA;AAC3B,IAAA;AACmB,IAAA;AAC1B,IAAA;AAC7B,EAAA;AAC+C,EAAA;AACxC,EAAA;AACT;AASsH;AAC7F,EAAA;AACN,EAAA;AACsB,EAAA;AACV,EAAA;AAGb,EAAA;AAMD,EAAA;AACG,IAAA;AAC6C,MAAA;AAC7D,IAAA;AACA,IAAA;AACF,EAAA;AAEwC,EAAA;AAC5B,EAAA;AACa,EAAA;AACc,EAAA;AACsB,EAAA;AACV,EAAA;AACH,EAAA;AACtC,EAAA;AAC2B,EAAA;AACoB,EAAA;AAExB,EAAA;AACT,IAAA;AACI,IAAA;AACA,IAAA;AAGO,IAAA;AACA,IAAA;AACA,IAAA;AAG7B,IAAA;AACA,IAAA;AACkB,IAAA;AACgB,IAAA;AACjC,IAAA;AAAU,MAAA;AAAgD,MAAA;AAAI,IAAA;AAC7C,IAAA;AACgB,IAAA;AACjC,IAAA;AAAU,MAAA;AAAgD,MAAA;AAAI,IAAA;AAC7C,IAAA;AACgB,IAAA;AACjC,IAAA;AAAU,MAAA;AAAgD,MAAA;AAAI,IAAA;AAGhD,IAAA;AACgB,MAAA;AACf,MAAA;AACA,MAAA;AACA,MAAA;AACpB,IAAA;AAG2D,IAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AAC3D,EAAA;AACF;AAM0H;AACtF,EAAA;AACpC;AAEuH;AACrG,EAAA;AACI,EAAA;AACA,EAAA;AACE,EAAA;AACuB,EAAA;AAGjB,EAAA;AACE,IAAA;AACG,MAAA;AACjB,MAAA;AAC4B,MAAA;AACO,QAAA;AAClB,QAAA;AACf,QAAA;AACI,QAAA;AACA,QAAA;AACA,QAAA;AAChB,QAAA;AACF,MAAA;AAC6B,MAAA;AACM,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACzC,IAAA;AACF,EAAA;AAG4B,EAAA;AACE,IAAA;AACG,MAAA;AACjB,MAAA;AAC4B,MAAA;AACO,QAAA;AAClB,QAAA;AACd,QAAA;AACI,QAAA;AACA,QAAA;AACA,QAAA;AACjB,QAAA;AACF,MAAA;AAC6B,MAAA;AACK,MAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACxC,IAAA;AACF,EAAA;AACF;AAUQ;AACyC,EAAA;AAChB,EAAA;AACA,EAAA;AACsB,EAAA;AACD,EAAA;AACvB,EAAA;AACA,EAAA;AAEI,EAAA;AACL,IAAA;AACd,IAAA;AACQ,MAAA;AACpB,IAAA;AAEI,IAAA;AAEsB,IAAA;AACuB,MAAA;AAChC,QAAA;AACkB,MAAA;AAC0B,QAAA;AACzB,MAAA;AACnB,QAAA;AACsC,MAAA;AACtC,QAAA;AACR,MAAA;AAC6C,QAAA;AACpD,MAAA;AACK,IAAA;AAEkC,MAAA;AACzC,IAAA;AAG4D,IAAA;AAC9D,EAAA;AACF;AAMwD;AAC/B,EAAA;AACN,EAAA;AACgB,EAAA;AACrB,IAAA;AACZ,EAAA;AACF;APs0DiE;AACA;AQ5sEnC;AAgC2C;AAC7B,EAAA;AACE,EAAA;AACD,EAAA;AACK,EAAA;AACE,EAAA;AACG,EAAA;AACvC,EAAA;AACI,EAAA;AACK,EAAA;AACkB,EAAA;AACR,EAAA;AACP,IAAA;AACtB,MAAA;AACA,MAAA;AACF,IAAA;AACA,IAAA;AAC2B,IAAA;AACI,IAAA;AACA,IAAA;AAC1B,IAAA;AACE,IAAA;AACD,IAAA;AACoD,IAAA;AACpC,IAAA;AACA,IAAA;AACA,IAAA;AACK,IAAA;AACA,IAAA;AACA,IAAA;AAC7B,EAAA;AACU,EAAA;AACwC,EAAA;AACY,IAAA;AAC9D,EAAA;AAC4D,EAAA;AAC9D;AAEsC;AACa,EAAA;AACnD;AAaE;AAE8B,EAAA;AACa,EAAA;AACK,EAAA;AACD,EAAA;AACV,EAAA;AACjB,EAAA;AACqB,EAAA;AACC,IAAA;AACH,IAAA;AACvC,EAAA;AACuC,EAAA;AACzC;AAEoD;ARgqEa;AACA;ASjwEoB;AAC7C,EAAA;AACd,EAAA;AACmB,EAAA;AAC5B,EAAA;AACgE,EAAA;AAEnC,EAAA;AAC9B,IAAA;AAC+B,IAAA;AACc,MAAA;AACxB,MAAA;AACD,MAAA;AACJ,QAAA;AAChB,QAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AAE2C,EAAA;AAC7B,IAAA;AACgC,IAAA;AACc,MAAA;AACzB,MAAA;AACD,MAAA;AACJ,QAAA;AAChB,QAAA;AACV,MAAA;AACF,IAAA;AACF,EAAA;AAC6B,EAAA;AAC/B;AAaoG;AACvD,EAAA;AACY,EAAA;AACD,EAAA;AACI,EAAA;AAC/C,IAAA;AACI,IAAA;AACA,IAAA;AACmC,IAAA;AAClD,EAAA;AACO,EAAA;AACT;ATqvEiE;AACA;AUvsEjD;AAGqC;AACU,EAAA;AAC/D;AAWyE;AACO,EAAA;AAChF;AAG4E;AACA,EAAA;AAC5E;AAEsE;AACA,EAAA;AACtE;AAGwD;AAC/B,EAAA;AACwC,EAAA;AACA,EAAA;AACjE;AAGyD;AACrC,EAAA;AACpB;AAG2G;AACvF,EAAA;AACH,EAAA;AAC4C,EAAA;AAC7D;AAG6D;AACR,EAAA;AACrD;AVkrEiE;AACA;AW1yES;AAGQ;AAC7C,EAAA;AACxB,EAAA;AACF,IAAA;AACI,MAAA;AACE,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACC,MAAA;AACf,IAAA;AAC8B,IAAA;AAChC,EAAA;AACO,EAAA;AACT;AAG4F;AACnF,EAAA;AACS,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACG,IAAA;AACC,IAAA;AACV,IAAA;AACG,IAAA;AACE,IAAA;AACb,EAAA;AACF;AAG2F;AAChE,EAAA;AACyB,IAAA;AAChB,IAAA;AACa,IAAA;AACJ,IAAA;AAC3C,EAAA;AACF;AAGiF;AAChC,EAAA;AAChB,EAAA;AACQ,EAAA;AACtB,EAAA;AACS,EAAA;AACG,IAAA;AACiC,IAAA;AACxC,IAAA;AACM,IAAA;AACwB,IAAA;AACpD,EAAA;AACK,EAAA;AAC8B,EAAA;AACrC;AAEoE;AAGA;AAC/C,EAAA;AACO,EAAA;AACoC,EAAA;AACpB,EAAA;AACP,EAAA;AACf,EAAA;AAC0B,IAAA;AACrC,IAAA;AACsB,MAAA;AAC3B,MAAA;AACgC,MAAA;AACL,QAAA;AAC8B,UAAA;AACH,UAAA;AACpD,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAC8B,EAAA;AACvB,EAAA;AACT;AAEwE;AACrC,EAAA;AACW,EAAA;AAC+B,IAAA;AACxB,IAAA;AACnD,EAAA;AAC4D,EAAA;AAA0D;AAEzD,EAAA;AAEhB,EAAA;AACmB,EAAA;AACpB,EAAA;AAC7B,IAAA;AAC6B,IAAA;AACa,IAAA;AACvB,IAAA;AACyB,IAAA;AAClD,IAAA;AACT,EAAA;AACF;AAGqF;AACtC,EAAA;AACf,IAAA;AAC9B,EAAA;AACF;AX6xEiE;AACA;AYr7EL;AAC9C,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACG,EAAA;AACP,EAAA;AACA,EAAA;AACG,EAAA;AACA,EAAA;AACX;AAEwD;AAC9C,EAAA;AACD,EAAA;AACA,EAAA;AACT;AAE2D;AAChD,EAAA;AACG,EAAA;AACN,EAAA;AACa,EAAA;AACT,EAAA;AACF,EAAA;AACV;AZq7EiE;AACA;Aa56E9B;AACE;AACJ;AACH;AA0BkE;AAGkB;AAChF,EAAA;AACf,EAAA;AAC4C,EAAA;AACE,EAAA;AACD,EAAA;AACX,EAAA;AACO,EAAA;AACrB,EAAA;AACK,EAAA;AACnC,EAAA;AACT;AAM8G;AACpF,EAAA;AACkB,IAAA;AACM,IAAA;AACgB,IAAA;AAChE,EAAA;AACO,EAAA;AACT;AAGmG;AACvD,EAAA;AAC/B,EAAA;AACkD,IAAA;AACP,IAAA;AACf,IAAA;AACvC,EAAA;AAC4D,EAAA;AAC9D;AAMmD;AACJ,EAAA;AAC/C;AAGgF;AAC5D,EAAA;AACuC,EAAA;AAC3D;AAEqF;AAC5E,EAAA;AACD,IAAA;AACE,IAAA;AACG,IAAA;AAC6B,IAAA;AACJ,IAAA;AACI,IAAA;AACJ,IAAA;AACM,IAAA;AACd,IAAA;AAC0B,IAAA;AACtD,EAAA;AACF;AAGoE;AACzC,EAAA;AAClB,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACqD,MAAA;AACrD,IAAA;AACI,MAAA;AACX,EAAA;AACF;AAE4E;AACnE,EAAA;AACC,IAAA;AACS,IAAA;AACqB,IAAA;AAC3B,IAAA;AACA,IAAA;AACF,IAAA;AACP,IAAA;AACF,EAAA;AACF;AAEqH;AACtG,EAAA;AACL,IAAA;AACI,IAAA;AACU,IAAA;AACO,IAAA;AACD,IAAA;AACJ,IAAA;AACV,IAAA;AACmC,IAAA;AACjD,EAAA;AACgB,EAAA;AAC6C,IAAA;AACrC,IAAA;AACxB,EAAA;AACO,EAAA;AACT;AAG4F;AAChE,EAAA;AAClB,IAAA;AACF,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACgB,IAAA;AACF,IAAA;AACb,IAAA;AACR,IAAA;AACF,EAAA;AACuB,EAAA;AAChB,EAAA;AACT;AAGsE;AAC5C,EAAA;AACM,IAAA;AACd,IAAA;AAC8D,IAAA;AAC9E,EAAA;AACmC,EAAA;AAC5B,EAAA;AACT;AAGyG;AAElF,EAAA;AAEuB,EAAA;AAEkB,EAAA;AACK,EAAA;AACd,EAAA;AACjD,EAAA;AACU,EAAA;AAC6B,IAAA;AACa,IAAA;AACxD,EAAA;AACoB,EAAA;AAC+B,EAAA;AACS,EAAA;AACrB,EAAA;AACc,EAAA;AACe,EAAA;AACT,EAAA;AACpD,EAAA;AACT;AAOiH;AAC1E,EAAA;AAC1B,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACC,IAAA;AACf,EAAA;AAC+B,EAAA;AAC6B,EAAA;AACrD,EAAA;AACT;AAG8G;AAC9E,EAAA;AACgB,EAAA;AACtB,IAAA;AACiB,MAAA;AACO,MAAA;AAC9C,IAAA;AACF,EAAA;AACW,EAAA;AACQ,EAAA;AACuC,IAAA;AACrC,IAAA;AACrB,EAAA;AAC6D,EAAA;AAC/D;AAY8C;AAGsF;AAC/G,EAAA;AAC+B,EAAA;AACD,EAAA;AACW,EAAA;AACtB,EAAA;AACjC,IAAA;AAEC,IAAA;AACsB,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACC,MAAA;AAEvB,IAAA;AACL,IAAA;AACe,IAAA;AAC2C,IAAA;AAC5D,EAAA;AACyC,EAAA;AACN,EAAA;AACW,EAAA;AACtB,IAAA;AACwB,MAAA;AACS,MAAA;AACT,MAAA;AAC9C,IAAA;AACF,EAAA;AACW,EAAA;AACmD,EAAA;AAC5B,EAAA;AAEC,EAAA;AAEkB,EAAA;AACvD;AAG0D;AACrC,EAAA;AACA,EAAA;AACyC,EAAA;AACX,EAAA;AACG,EAAA;AACO,EAAA;AAC5B,EAAA;AACsB,EAAA;AAC7B,EAAA;AACsC,EAAA;AACH,EAAA;AAC9C,EAAA;AACE,IAAA;AAC6C,IAAA;AACH,IAAA;AACzD,EAAA;AACQ,EAAA;AACU,EAAA;AAC2C,EAAA;AACzC,IAAA;AACiC,IAAA;AACnD,IAAA;AACF,EAAA;AAC+B,EAAA;AACX,IAAA;AACuC,IAAA;AAC3D,EAAA;AACgC,EAAA;AAC+B,EAAA;AACxD,EAAA;AACT;Ab41EiE;AACA;ActrFV;AACd,EAAA;AACzC;AAE4C;AACe,EAAA;AAC3D;AAG0H;AACjH,EAAA;AACL,IAAA;AACsC,IAAA;AACrB,IAAA;AACA,IAAA;AACV,IAAA;AACU,IAAA;AACA,IAAA;AACnB,EAAA;AACF;AAY8D;AACA,EAAA;AACb,EAAA;AACC,EAAA;AACL,EAAA;AACK,EAAA;AACI,EAAA;AACC,EAAA;AACA,EAAA;AACrB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACA,EAAA;AAClB,EAAA;AACA,EAAA;AACS,EAAA;AACT,IAAA;AACA,IAAA;AAC6B,IAAA;AACF,MAAA;AACjB,MAAA;AACD,MAAA;AACV,MAAA;AACQ,MAAA;AACA,MAAA;AACA,MAAA;AACxB,IAAA;AACF,EAAA;AACwC,EAAA;AAC1C;AAGoJ;AACjI,EAAA;AACD,IAAA;AACsC,IAAA;AACtD,EAAA;AAC2D,EAAA;AAC7D;AA2B4D;AACb,EAAA;AACX,EAAA;AACT,EAAA;AACuC,EAAA;AACL,EAAA;AACvB,EAAA;AACV,EAAA;AACmB,EAAA;AACa,IAAA;AACX,IAAA;AACM,IAAA;AACrD,EAAA;AAC8D,EAAA;AACA,EAAA;AACvD,EAAA;AACT;AAG+B;AAOiG;AAC/G,EAAA;AACD,EAAA;AACU,IAAA;AACwB,IAAA;AACzC,IAAA;AACP,EAAA;AACO,EAAA;AACuB,IAAA;AAC2C,IAAA;AACzE,EAAA;AACF;AAGuD;AACxB,EAAA;AACA,EAAA;AACiC,EAAA;AAChE;AAaE;AAE6D,EAAA;AAC3C,IAAA;AAClB,EAAA;AAC6D,EAAA;AACH,EAAA;AACT,EAAA;AAC1C,EAAA;AACT;AAOiD;AACxC,EAAA;AAC8B,IAAA;AACE,IAAA;AACD,IAAA;AACK,IAAA;AAChC,IAAA;AACI,IAAA;AACI,IAAA;AACG,IAAA;AACf,IAAA;AACP,EAAA;AACF;AAGgH;AAC1D,EAAA;AAClB,IAAA;AACa,IAAA;AACnB,MAAA;AACpB,QAAA;AACF,QAAA;AACF,MAAA;AACE,MAAA;AACc,MAAA;AACI,MAAA;AACA,MAAA;AACb,MAAA;AACE,MAAA;AACD,MAAA;AACmD,MAAA;AACnC,MAAA;AACA,MAAA;AACA,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AAC/B,IAAA;AACF,EAAA;AACF;AAG+E;AAClD,EAAA;AACJ,EAAA;AACoB,IAAA;AAClB,MAAA;AACI,MAAA;AACF,MAAA;AACU,MAAA;AACnC,IAAA;AACiB,IAAA;AACI,IAAA;AACO,IAAA;AACG,MAAA;AACM,MAAA;AACrC,IAAA;AACF,EAAA;AACU,EAAA;AACmD,EAAA;AACnD,EAAA;AACH,EAAA;AACT;Ad6mFiE;AACA;AevxFhD;AACmC,iBAAA;AAEO,EAAA;AACpB,IAAA;AACA,IAAA;AAC1B,MAAA;AACT,IAAA;AAEwC,IAAA;AACgB,MAAA;AACnC,QAAA;AACjB,QAAA;AACF,MAAA;AACsB,MAAA;AACJ,MAAA;AACC,MAAA;AACM,QAAA;AACZ,QAAA;AACb,MAAA;AACsC,MAAA;AAC5B,MAAA;AAG4C,MAAA;AACnC,QAAA;AACX,UAAA;AACkD,YAAA;AACC,YAAA;AACL,YAAA;AAC1C,UAAA;AAER,UAAA;AACuB,UAAA;AACZ,UAAA;AACR,QAAA;AACP,MAAA;AACD,IAAA;AACH,EAAA;AAEqD,EAAA;AAC5B,IAAA;AACzB,EAAA;AAAA;AAGuD,EAAA;AACvB,IAAA;AACK,IAAA;AACrC,EAAA;AAEqB,EAAA;AACF,IAAA;AACnB,EAAA;AACF;AAE+C;AAMd;AACX,kBAAA;AACiC,kBAAA;AAC7B,kBAAA;AAAA;AAAA;AAAA;AAAA;AAKyB,kBAAA;AAAA;AAE1C,EAAA;AACyC,kBAAA;AACzB,kBAAA;AAAA;AAAA;AAAA;AAAA;AAKiB,kBAAA;AAC6B,kBAAA;AAAa;AAE3E,EAAA;AAAA;AAAA;AAAA;AAAA;AAK0C,mBAAA;AAAA;AAEE,mBAAA;AACZ,mBAAA;AACyD,mBAAA;AACzE,mBAAA;AACqB,mBAAA;AACS,mBAAA;AAC7B,mBAAA;AAC6B,mBAAA;AAAA;AAAA;AAAA;AAKiB,EAAA;AACpB,IAAA;AACL,MAAA;AACF,MAAA;AACE,MAAA;AAClC,MAAA;AACT,IAAA;AAEO,IAAA;AAC+B,MAAA;AACE,MAAA;AACpB,MAAA;AACD,MAAA;AACH,MAAA;AAAC,MAAA;AACjB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AASE,EAAA;AAEwC,IAAA;AAC9B,IAAA;AAEN,IAAA;AACiC,IAAA;AACQ,MAAA;AACtC,IAAA;AACC,MAAA;AACR,IAAA;AAEuD,IAAA;AACf,IAAA;AAGL,IAAA;AAC9B,MAAA;AACH,MAAA;AAEkC,MAAA;AAGpC,IAAA;AAEiD,IAAA;AAGK,IAAA;AACE,IAAA;AAEnC,IAAA;AACC,IAAA;AAEe,IAAA;AAGb,IAAA;AACuB,MAAA;AAC7C,MAAA;AACF,IAAA;AAG2C,IAAA;AACiB,IAAA;AACzC,IAAA;AACyC,IAAA;AAEA,IAAA;AAChC,IAAA;AACJ,IAAA;AAEN,MAAA;AACM,QAAA;AACd,QAAA;AAC0B,QAAA;AACwB,QAAA;AACC,QAAA;AACJ,QAAA;AACf,QAAA;AACU,QAAA;AACxC,MAAA;AAEY,MAAA;AAC6B,MAAA;AACV,QAAA;AACM,QAAA;AACR,UAAA;AACpC,QAAA;AACuB,QAAA;AACH,QAAA;AACG,QAAA;AACW,wBAAA;AACrC,MAAA;AAGyC,MAAA;AACC,MAAA;AACgB,MAAA;AACrD,IAAA;AAE6C,MAAA;AACH,MAAA;AACA,MAAA;AACY,MAAA;AACR,MAAA;AACzC,MAAA;AAEiC,QAAA;AACC,QAAA;AACc,QAAA;AACA,QAAA;AAC1D,MAAA;AACF,IAAA;AAImD,IAAA;AACxC,MAAA;AACoC,MAAA;AAC/B,MAAA;AAC0C,MAAA;AAC/C,MAAA;AAEkB,MAAA;AACS,QAAA;AACa,QAAA;AACR,UAAA;AACvC,QAAA;AACF,MAAA;AAEoC,MAAA;AACW,QAAA;AACC,QAAA;AAChD,MAAA;AAG6B,MAAA;AAC2B,QAAA;AACxD,MAAA;AAEY,MAAA;AACd,IAAA;AAGmD,IAAA;AACY,IAAA;AACpD,MAAA;AACL,MAAA;AAC2C,QAAA;AAC/B,QAAA;AAC0C,QAAA;AAC/C,QAAA;AAC+C,QAAA;AACA,QAAA;AACX,QAAA;AACvC,MAAA;AAER,MAAA;AACY,MAAA;AACd,IAAA;AAGyB,IAAA;AACL,MAAA;AACT,MAAA;AACL,MAAA;AAC2C,QAAA;AACT,QAAA;AACtB,QAAA;AAC0C,QAAA;AACpC,QAAA;AACF,QAAA;AACP,QAAA;AACK,QAAA;AACE,QAAA;AACP,QAAA;AACL,MAAA;AAER,MAAA;AACY,MAAA;AACd,IAAA;AAGwB,IAAA;AAEuB,MAAA;AAGU,MAAA;AACzD,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAUE,EAAA;AAKmC,IAAA;AACyB,IAAA;AACT,IAAA;AACC,IAAA;AACH,IAAA;AACP,IAAA;AACrB,IAAA;AAEmC,IAAA;AAE3C,IAAA;AACoC,IAAA;AACzC,IAAA;AACN,MAAA;AACO,MAAA;AAAU,MAAA;AAAU,MAAA;AAAc,MAAA;AAClC,MAAA;AAAU,MAAA;AAAU,MAAA;AAAc,MAAA;AAC3C,IAAA;AACgB,IAAA;AAEyC,IAAA;AAC1C,IAAA;AAEuC,MAAA;AACI,MAAA;AAET,MAAA;AACzC,QAAA;AACkD,UAAA;AACE,UAAA;AAC1B,UAAA;AACC,UAAA;AACoB,UAAA;AACP,UAAA;AAC7B,UAAA;AAGE,YAAA;AACoC,YAAA;AACzC,YAAA;AACN,cAAA;AACW,cAAA;AAAe,cAAA;AAAmB,cAAA;AAAoB,cAAA;AAC1D,cAAA;AAAU,cAAA;AAAU,cAAA;AAAc,cAAA;AAC3C,YAAA;AACgB,YAAA;AACmC,YAAA;AACrC,YAAA;AACiC,cAAA;AAC/C,YAAA;AACF,UAAA;AACM,QAAA;AAER,QAAA;AACF,MAAA;AAEoB,sBAAA;AACgB,MAAA;AACtC,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAKgI,EAAA;AAC1H,IAAA;AAC+B,IAAA;AACW,IAAA;AACzC,IAAA;AACwD,MAAA;AACb,MAAA;AAG5B,MAAA;AACd,QAAA;AACA,QAAA;AACqD,QAAA;AACpC,QAAA;AACoC,QAAA;AAGnD,MAAA;AACR,IAAA;AACqB,IAAA;AACd,IAAA;AACT,EAAA;AAAA;AAG2E,EAAA;AACvD,IAAA;AACX,IAAA;AACL,MAAA;AACA,MAAA;AACyD,MAAA;AACnC,MAAA;AAC8B,QAAA;AACK,QAAA;AACzD,MAAA;AACgD,MAAA;AACM,MAAA;AACxD,IAAA;AACF,EAAA;AAAA;AAAA;AAI6G,EAAA;AACpE,IAAA;AACd,MAAA;AACmB,MAAA;AACW,QAAA;AACtB,QAAA;AACzB,QAAA;AAC+C,UAAA;AACH,UAAA;AACX,UAAA;AACgB,UAAA;AACzC,UAAA;AAC4C,YAAA;AAC/C,YAAA;AACP,UAAA;AACM,QAAA;AAER,QAAA;AACF,MAAA;AACqD,MAAA;AAC/C,QAAA;AACwC,UAAA;AACrC,UAAA;AACC,QAAA;AAER,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAG+B,EAAA;AACsB,IAAA;AACS,IAAA;AAC9D,EAAA;AAAA;AAG6F,EAAA;AAClC,IAAA;AACrC,IAAA;AACsC,IAAA;AACpC,MAAA;AACC,MAAA;AACvB,IAAA;AACkC,IAAA;AACyB,IAAA;AACvD,IAAA;AAC+B,MAAA;AACU,MAAA;AACE,MAAA;AACa,MAAA;AACb,MAAA;AACZ,MAAA;AACwB,MAAA;AACnD,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AAEqH,EAAA;AACvF,IAAA;AACA,IAAA;AACF,IAAA;AACA,IAAA;AACe,IAAA;AACC,IAAA;AAChB,IAAA;AACgB,IAAA;AACzB,IAAA;AACqC,IAAA;AACxD,EAAA;AAAA;AAAA;AAAA;AAAA;AAY0B,EAAA;AACQ,IAAA;AAChB,IAAA;AACJ,IAAA;AACgD,IAAA;AACF,MAAA;AACL,MAAA;AAC3C,MAAA;AACV,IAAA;AACuC,IAAA;AACgB,IAAA;AACzC,IAAA;AACoC,IAAA;AACtB,MAAA;AACS,MAAA;AACV,MAAA;AACE,MAAA;AACS,MAAA;AAC1B,MAAA;AAC8C,IAAA;AACxC,IAAA;AACJ,IAAA;AACP,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAY0E,EAAA;AACtD,IAAA;AACqB,IAAA;AACA,IAAA;AACqB,IAAA;AACT,IAAA;AACC,IAAA;AACG,IAAA;AACE,IAAA;AAEzB,IAAA;AAGE,IAAA;AACjB,IAAA;AACe,IAAA;AACvB,IAAA;AACiD,IAAA;AAC5C,IAAA;AAC0C,IAAA;AAC/C,IAAA;AAC0C,IAAA;AACvC,IAAA;AAC6C,IAAA;AACjC,IAAA;AACH,IAAA;AACM,IAAA;AACA,IAAA;AAC7B,EAAA;AAAA;AAGuH,EAAA;AAC9E,IAAA;AACA,IAAA;AACG,IAAA;AACA,IAAA;AACxB,IAAA;AACkB,IAAA;AACT,IAAA;AACC,IAAA;AAC9B,EAAA;AAEiE,EAAA;AACtB,IAAA;AACjB,IAAA;AACW,oBAAA;AACrC,EAAA;AAEuE,mBAAA;AACP,mBAAA;AAAA;AAGqB,EAAA;AACrC,IAAA;AACjC,IAAA;AAC2C,MAAA;AACnB,MAAA;AACsB,MAAA;AACP,MAAA;AACX,MAAA;AACZ,MAAA;AACc,MAAA;AAC3C,IAAA;AAC6D,IAAA;AAC/D,EAAA;AAAA;AAG+G,EAAA;AACnE,IAAA;AAC/B,IAAA;AACwB,MAAA;AACI,MAAA;AACvC,IAAA;AACkB,IAAA;AACO,IAAA;AACX,IAAA;AAC2B,IAAA;AACR,MAAA;AACI,MAAA;AACrC,IAAA;AACgB,IAAA;AACE,IAAA;AACP,IAAA;AACgC,IAAA;AACT,MAAA;AACD,MAAA;AACjC,IAAA;AACkB,IAAA;AACP,IAAA;AACkD,IAAA;AAC/D,EAAA;AAEgG,EAAA;AACrE,IAAA;AACX,IAAA;AAC6C,IAAA;AAC7C,IAAA;AACE,IAAA;AACE,IAAA;AACP,IAAA;AACiD,IAAA;AAC1C,IAAA;AACP,IAAA;AACkD,IAAA;AAC/D,EAAA;AAEoD,EAAA;AACtC,IAAA;AACmB,IAAA;AACT,IAAA;AACR,MAAA;AACc,MAAA;AAC5B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAK2F,EAAA;AAC/C,IAAA;AAClB,IAAA;AACmB,MAAA;AACzC,MAAA;AACF,IAAA;AACyC,IAAA;AACjC,IAAA;AACG,MAAA;AACsB,MAAA;AACU,MAAA;AAC7B,MAAA;AACZ,MAAA;AACF,IAAA;AACmD,IAAA;AAER,MAAA;AAC3C,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQsG,EAAA;AACjF,IAAA;AACsC,IAAA;AACxC,IAAA;AACA,IAAA;AACmB,IAAA;AACF,IAAA;AAChB,IAAA;AACd,IAAA;AACkC,MAAA;AACb,MAAA;AACmB,MAAA;AACE,MAAA;AACD,MAAA;AACV,MAAA;AACuB,MAAA;AACtB,MAAA;AACF,MAAA;AAC1B,IAAA;AACC,MAAA;AACT,IAAA;AACS,IAAA;AACwB,IAAA;AACf,IAAA;AACa,IAAA;AACN,IAAA;AACb,IAAA;AACL,IAAA;AACT,EAAA;AAEmG,EAAA;AAC7D,IAAA;AACV,IAAA;AACpB,MAAA;AACqC,QAAA;AACjC,MAAA;AAER,MAAA;AACF,IAAA;AACsC,IAAA;AACxC,EAAA;AAAA;AAGgH,EAAA;AACrG,IAAA;AACsB,IAAA;AACP,MAAA;AACxB,IAAA;AAEkB,IAAA;AACU,MAAA;AACe,MAAA;AACb,MAAA;AAC9B,IAAA;AAEkB,IAAA;AACJ,MAAA;AACE,QAAA;AACe,QAAA;AACX,UAAA;AACQ,UAAA;AACF,UAAA;AACN,UAAA;AACC,UAAA;AAC4B,UAAA;AACC,UAAA;AACC,YAAA;AAC7C,UAAA;AACW,UAAA;AACb,QAAA;AACA,QAAA;AACF,MAAA;AAEa,MAAA;AACE,QAAA;AACU,QAAA;AACF,QAAA;AACD,QAAA;AACG,UAAA;AAC+B,UAAA;AACtD,QAAA;AAC0B,QAAA;AAC8B,UAAA;AACxD,QAAA;AACA,QAAA;AACF,MAAA;AAEe,MAAA;AACE,QAAA;AACD,QAAA;AACW,QAAA;AACF,QAAA;AACgC,QAAA;AACjC,QAAA;AACG,UAAA;AACd,UAAA;AACX,QAAA;AAC4B,QAAA;AACf,UAAA;AACb,QAAA;AACA,QAAA;AACF,MAAA;AAEc,MAAA;AACE,QAAA;AACU,QAAA;AACF,QAAA;AACA,QAAA;AAGR,QAAA;AACa,QAAA;AACM,QAAA;AACtB,QAAA;AAG0C,QAAA;AACH,QAAA;AACpC,QAAA;AACmB,QAAA;AAC7B,QAAA;AACiD,UAAA;AACA,UAAA;AACrD,QAAA;AACI,QAAA;AACiD,UAAA;AACA,UAAA;AACrD,QAAA;AACc,QAAA;AACL,QAAA;AACT,QAAA;AACF,MAAA;AAEa,MAAA;AACE,QAAA;AACC,QAAA;AACS,QAAA;AACF,QAAA;AACI,QAAA;AACM,QAAA;AACpB,QAAA;AACX,QAAA;AACF,MAAA;AAEa,MAAA;AACK,QAAA;AACwC,QAAA;AAChC,QAAA;AACL,QAAA;AAC4B,QAAA;AAC/C,QAAA;AACF,MAAA;AAEc,MAAA;AACG,QAAA;AACwC,QAAA;AACvD,QAAA;AACF,MAAA;AACF,IAAA;AAEY,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAKsF,EAAA;AAC3E,IAAA;AACS,IAAA;AACF,IAAA;AACM,IAAA;AAEgB,IAAA;AAC1B,IAAA;AAC0C,IAAA;AAGtC,IAAA;AACE,IAAA;AACA,IAAA;AACC,IAAA;AACH,IAAA;AACyB,MAAA;AACe,MAAA;AACC,MAAA;AACE,MAAA;AAC3D,IAAA;AACyB,IAAA;AACkC,MAAA;AACE,MAAA;AAC7D,IAAA;AAEY,IAAA;AACd,EAAA;AAAA;AAGqG,EAAA;AACrE,IAAA;AAChC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMuF,EAAA;AACrD,IAAA;AACjB,IAAA;AAEQ,IAAA;AACA,IAAA;AACc,IAAA;AACA,IAAA;AACb,IAAA;AACA,IAAA;AACE,IAAA;AACA,IAAA;AAEjB,IAAA;AAGO,IAAA;AAEsC,IAAA;AAEM,IAAA;AAEL,IAAA;AAEK,IAAA;AAG1C,IAAA;AACF,IAAA;AACiC,IAAA;AAG/B,IAAA;AACF,IAAA;AACF,IAAA;AAE0B,IAAA;AACW,IAAA;AACL,IAAA;AACW,IAAA;AAEjB,IAAA;AACW,IAAA;AACL,IAAA;AACW,IAAA;AAC9C,IAAA;AAGK,IAAA;AACE,IAAA;AACF,IAAA;AACG,IAAA;AACH,IAAA;AAAA;AAES,MAAA;AACW,MAAA;AACA,MAAA;AACW,MAAA;AAAA;AAEP,MAAA;AACW,MAAA;AACX,MAAA;AACW,MAAA;AACnD,IAAA;AACyB,IAAA;AACkC,MAAA;AACE,MAAA;AAC7D,IAAA;AAEY,IAAA;AACd,EAAA;AAWE,EAAA;AAES,IAAA;AACqC,IAAA;AAC1B,IAAA;AAGO,IAAA;AAEJ,IAAA;AACA,IAAA;AACiC,IAAA;AAC5C,IAAA;AACd,EAAA;AAAA;AAAA;AAAA;AAS8B,EAAA;AACxB,IAAA;AACiC,IAAA;AACQ,MAAA;AACtC,IAAA;AACC,MAAA;AACR,IAAA;AAEuD,IAAA;AACd,IAAA;AAGA,IAAA;AAEmB,IAAA;AACT,IAAA;AACC,IAAA;AAEnB,IAAA;AACC,IAAA;AAEqB,IAAA;AACf,IAAA;AACvB,IAAA;AAEW,IAAA;AACxB,IAAA;AAEqD,IAAA;AACI,MAAA;AACtD,IAAA;AACiD,MAAA;AACV,MAAA;AACG,MAAA;AACA,MAAA;AACf,MAAA;AACsB,MAAA;AACrB,MAAA;AACuB,MAAA;AACA,QAAA;AACf,UAAA;AACtC,QAAA;AACH,MAAA;AAC2C,MAAA;AACJ,MAAA;AAC7B,MAAA;AACE,QAAA;AACyC,QAAA;AACpC,QAAA;AAC0C,QAAA;AAC/C,QAAA;AACyB,QAAA;AACtB,QAAA;AAC2B,QAAA;AAC1C,MAAA;AACF,IAAA;AAG6D,IAAA;AAGA,IAAA;AAClD,MAAA;AACiC,MAAA;AACW,MAAA;AAC1B,MAAA;AACS,QAAA;AACpC,MAAA;AACY,MAAA;AACd,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAU8B,EAAA;AACsB,IAAA;AACC,IAAA;AACA,IAAA;AACb,IAAA;AACrB,IAAA;AAGD,IAAA;AAC8B,IAAA;AAEV,IAAA;AACc,IAAA;AAGvB,IAAA;AACT,MAAA;AAC2C,MAAA;AAC7D,IAAA;AAGoD,IAAA;AACb,MAAA;AACH,QAAA;AAClC,MAAA;AACF,IAAA;AAGyD,IAAA;AAIjD,IAAA;AAKO,IAAA;AACO,MAAA;AACkB,MAAA;AACsB,QAAA;AACnD,QAAA;AACI,QAAA;AACA,QAAA;AACA,QAAA;AACb,MAAA;AACgC,MAAA;AAClC,IAAA;AAEO,IAAA;AACT,EAAA;AAEiF,EAAA;AACtE,IAAA;AACS,IAAA;AACF,IAAA;AACE,IAAA;AACJ,IAAA;AACC,IAAA;AACG,IAAA;AACU,MAAA;AACe,MAAA;AACb,MAAA;AAC9B,IAAA;AAEkB,IAAA;AACJ,MAAA;AACiB,QAAA;AACX,UAAA;AACkC,UAAA;AACL,UAAA;AACC,UAAA;AACC,YAAA;AAC7C,UAAA;AACW,UAAA;AACb,QAAA;AACA,QAAA;AACF,MAAA;AACe,MAAA;AACC,QAAA;AACwC,QAAA;AAC7C,QAAA;AACT,QAAA;AACF,MAAA;AACK,MAAA;AACQ,MAAA;AACG,QAAA;AACkC,QAAA;AACzB,QAAA;AACM,QAAA;AAClB,QAAA;AACX,QAAA;AACF,MAAA;AACK,MAAA;AACA,MAAA;AACQ,MAAA;AACsB,QAAA;AACO,QAAA;AACxC,QAAA;AACF,MAAA;AACF,IAAA;AACY,IAAA;AACd,EAAA;AACF;AAEmB;AACA;AAEiG;AACnG,EAAA;AACR,EAAA;AACqD,IAAA;AACG,IAAA;AAC/D,EAAA;AACF;AAEsH;AAC7G,EAAA;AACL,IAAA;AACsC,IAAA;AACrB,IAAA;AACA,IAAA;AACV,IAAA;AACU,IAAA;AACA,IAAA;AACnB,EAAA;AACF;AAUsB;AAKoD,EAAA;AAAnD,IAAA;AAAoC,IAAA;AACzC,IAAA;AAChB,EAAA;AAFqB,EAAA;AAAoC,EAAA;AAJJ,mBAAA;AACE,mBAAA;AAC7C,mBAAA;AACV,EAAA;AAAA;AAMmC,EAAA;AACJ,IAAA;AACK,IAAA;AACD,IAAA;AACK,IAAA;AACL,MAAA;AACvB,MAAA;AACqB,QAAA;AACU,QAAA;AACH,QAAA;AACA,QAAA;AACiB,QAAA;AACC,QAAA;AACjC,QAAA;AACqC,QAAA;AAC9B,QAAA;AAC5B,MAAA;AACY,MAAA;AACd,IAAA;AAC0B,IAAA;AAC5B,EAAA;AACF;AAE6E;AAChD,EAAA;AACH,EAAA;AACO,IAAA;AACmB,IAAA;AAClD,EAAA;AACO,EAAA;AACT;AfmkFiE;AACA;AgBj1HnD;AAE6B,EAAA;AACkB,EAAA;AAC/B,EAAA;AACL,EAAA;AACiB,EAAA;AACpB,IAAA;AACsB,IAAA;AACU,IAAA;AACU,IAAA;AACA,IAAA;AAC9D,EAAA;AACkC,EAAA;AACvB,EAAA;AACW,EAAA;AACA,EAAA;AACc,EAAA;AACjB,IAAA;AACa,IAAA;AACD,MAAA;AACgC,MAAA;AACR,MAAA;AAC/B,MAAA;AACR,MAAA;AACY,MAAA;AACiC,QAAA;AACzD,MAAA;AACuB,MAAA;AACoB,MAAA;AACb,QAAA;AACQ,QAAA;AACtB,QAAA;AACU,QAAA;AACgB,QAAA;AACP,QAAA;AAC3B,UAAA;AACiD,UAAA;AAChD,UAAA;AACY,YAAA;AACe,YAAA;AAChC,UAAA;AACkC,UAAA;AACpC,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACwD,EAAA;AACjD,EAAA;AACT;AAEsF;AAC1C,EAAA;AACmB,EAAA;AAChB,EAAA;AAC/C;AAQiB;AAC6C,EAAA;AAC9D;AAGsD;AACE,EAAA;AACE,EAAA;AACK,EAAA;AAC/D;AAGgI;AAClG,EAAA;AAChB,EAAA;AACO,IAAA;AACU,IAAA;AACW,IAAA;AACV,IAAA;AAC9B,EAAA;AACmD,EAAA;AACrD;AAG2D;AAC7B,EAAA;AACP,EAAA;AACC,EAAA;AACL,EAAA;AACA,EAAA;AACgB,EAAA;AAEA,EAAA;AAC+B,EAAA;AACrC,EAAA;AACC,EAAA;AACG,IAAA;AAC+B,IAAA;AAC9D,EAAA;AACO,EAAA;AACT;AAGmI;AACrE,EAAA;AAC9D;AhBi0HiE;AACA;AiBr7HpB;AACrB,EAAA;AACsC,EAAA;AACf,IAAA;AAC7C,EAAA;AAC2C,EAAA;AACpC,EAAA;AACT;AAE8B;AACE,EAAA;AACA,EAAA;AACd,IAAA;AAC2C,IAAA;AAC3D,EAAA;AACO,EAAA;AACN;AAEyE;AAGU;AACnD,EAAA;AACA,EAAA;AACA,EAAA;AAC8B,EAAA;AACR,EAAA;AACO,EAAA;AAC7C,EAAA;AACA,EAAA;AACA,EAAA;AACsC,EAAA;AACzD;AAG2H;AACxG,EAAA;AACI,EAAA;AACI,EAAA;AACA,EAAA;AACA,EAAA;AAC2B,EAAA;AACrB,EAAA;AACe,IAAA;AACA,IAAA;AACtB,IAAA;AACR,IAAA;AACE,MAAA;AACG,MAAA;AAGe,MAAA;AACG,MAAA;AACoB,MAAA;AACA,MAAA;AACzD,IAAA;AACF,EAAA;AACc,EAAA;AACE,IAAA;AACG,IAAA;AACe,IAAA;AAGR,IAAA;AAI1B,EAAA;AACF;AAO6H;AACnG,EAAA;AACM,EAAA;AACE,IAAA;AACV,MAAA;AACD,MAAA;AACP,MAAA;AACC,MAAA;AACsB,MAAA;AAChB,QAAA;AACkB,QAAA;AAChB,UAAA;AACwC,UAAA;AACnC,UAAA;AACb,UAAA;AACT,QAAA;AACF,MAAA;AACU,MAAA;AACe,QAAA;AACgC,QAAA;AACzD,MAAA;AACF,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAOgH;AAClF,EAAA;AACI,EAAA;AACJ,EAAA;AACA,EAAA;AAC0B,EAAA;AACE,EAAA;AAC1B,EAAA;AACpB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACsB,EAAA;AAEM,IAAA;AACf,IAAA;AACG,MAAA;AACN,MAAA;AACM,MAAA;AACN,MAAA;AACF,MAAA;AACN,MAAA;AACkD,MAAA;AACD,MAAA;AACrB,MAAA;AACjB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACc,MAAA;AACR,QAAA;AACL,QAAA;AACN,QAAA;AACoB,QAAA;AACmB,UAAA;AACX,UAAA;AAC9B,UAAA;AACV,QAAA;AACF,MAAA;AACF,IAAA;AACK,EAAA;AACuB,IAAA;AACV,MAAA;AACY,MAAA;AACJ,QAAA;AACH,UAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACrB,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAC4C,EAAA;AACiB,EAAA;AACtD,EAAA;AACL,IAAA;AACA,IAAA;AAC2D,IAAA;AAC7D,EAAA;AACF;AAkB6E;AAChD,EAAA;AACmB,IAAA;AACG,IAAA;AACpB,IAAA;AACqB,IAAA;AAClD,EAAA;AACwB,EAAA;AAC1B;AAOsI;AACxG,EAAA;AACU,EAAA;AACW,EAAA;AACa,EAAA;AACD,EAAA;AACjC,EAAA;AAC4B,EAAA;AAC5B,EAAA;AACZ,IAAA;AACQ,IAAA;AACR,IAAA;AACM,IAAA;AACT,IAAA;AACa,IAAA;AACkC,MAAA;AACd,MAAA;AACC,MAAA;AACK,QAAA;AACC,QAAA;AACjD,MAAA;AACqB,MAAA;AACvB,IAAA;AAC4B,IAAA;AAC9B,EAAA;AACoC,EAAA;AACtC;AAK6E;AAC5D,EAAA;AACI,EAAA;AACE,EAAA;AACvB;AAUiB;AACa,EAAA;AACiB,EAAA;AACV,EAAA;AACP,EAAA;AACX,EAAA;AACW,EAAA;AACZ,IAAA;AACK,IAAA;AAC0C,IAAA;AACT,IAAA;AACV,IAAA;AAC5C,EAAA;AAC2D,EAAA;AAC7D;AAM6E;AAC/C,EAAA;AACA,EAAA;AACX,EAAA;AACW,EAAA;AACZ,IAAA;AACK,IAAA;AACF,IAAA;AACI,IAAA;AACA,IAAA;AACM,IAAA;AACA,IAAA;AACP,IAAA;AACH,IAAA;AACb,IAAA;AAC+C,IAAA;AACP,IAAA;AACd,IAAA;AACvB,IAAA;AACmC,IAAA;AAChB,IAAA;AACkC,IAAA;AAC9D,EAAA;AAC2D,EAAA;AAC7D;AjBo3HiE;AACA;AkBjpIK;AAC5D,EAAA;AACK,EAAA;AACL,EAAA;AACC,EAAA;AACC,EAAA;AACC,EAAA;AACb;AAGiC;AAEiD;AAGc;AACvF,EAAA;AAC8C,IAAA;AACpB,IAAA;AACoB,IAAA;AACG,IAAA;AACC,IAAA;AACK,IAAA;AAC9D,EAAA;AACF;AAE+F;AACrB;AACN;AACR;AACL,EAAA;AACU,EAAA;AACjE;AAGsE;AAC1B,EAAA;AACtB,EAAA;AAC0C,EAAA;AAI9C,EAAA;AAIlB;AAG+F;AAC3D,EAAA;AACa,EAAA;AACnB,EAAA;AACV,IAAA;AAC6C,IAAA;AACjC,IAAA;AACE,MAAA;AACI,MAAA;AACW,MAAA;AAC7C,IAAA;AACF,EAAA;AACkC,EAAA;AACN,EAAA;AACgC,IAAA;AAC9B,IAAA;AACE,MAAA;AACI,MAAA;AACa,MAAA;AAC/C,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAaQ;AAEQ,EAAA;AACO,EAAA;AACwC,EAAA;AAC/B,IAAA;AACiB,IAAA;AACtC,IAAA;AACT,EAAA;AAC8B,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACY,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACZ,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACA,EAAA;AACyB,IAAA;AACrB,IAAA;AACL,IAAA;AACA,IAAA;AACK,IAAA;AACL,IAAA;AACK,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AAEC,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACY,IAAA;AACd,IAAA;AACG,IAAA;AACc,MAAA;AACA,MAAA;AACA,MAAA;AAC1C,IAAA;AACQ,IAAA;AACA,IAAA;AACA,IAAA;AAC6B,IAAA;AACvC,EAAA;AAC+B,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACiC,EAAA;AACxD,EAAA;AACT;AAMqI;AACrH,EAAA;AACc,EAAA;AAC6B,EAAA;AAC7B,EAAA;AACG,EAAA;AACE,IAAA;AAC4B,MAAA;AACA,MAAA;AACb,MAAA;AAC9C,IAAA;AACF,EAAA;AAC0C,EAAA;AACA,EAAA;AACC,EAAA;AACb,EAAA;AAC8B,EAAA;AACrD,EAAA;AACT;AAagB;AAC4B,EAAA;AACD,EAAA;AACd,EAAA;AACX,IAAA;AACmB,IAAA;AACvB,IAAA;AACkB,IAAA;AACE,MAAA;AACR,QAAA;AACA,QAAA;AAGP,QAAA;AAGD,QAAA;AACM,UAAA;AACR,UAAA;AACR,QAAA;AACF,MAAA;AACF,IAAA;AACS,IAAA;AACsC,MAAA;AACzB,MAAA;AACyB,MAAA;AACc,MAAA;AAC/B,MAAA;AAC8B,QAAA;AACD,QAAA;AAC1C,QAAA;AAC6B,UAAA;AACZ,UAAA;AAC9B,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACwB,EAAA;AACT,EAAA;AACwB,IAAA;AACpB,IAAA;AAC0C,IAAA;AAC7D,EAAA;AACkD,EAAA;AAC9B,EAAA;AAC0B,IAAA;AACe,IAAA;AAC7D,EAAA;AAC4B,EAAA;AACmC,EAAA;AACxD,EAAA;AACT;AAG4G;AAC5C,EAAA;AACpC,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACX,EAAA;AACoB,EAAA;AACC,IAAA;AACR,MAAA;AAC2B,MAAA;AACvC,MAAA;AACI,MAAA;AACA,MAAA;AACpB,IAAA;AACF,EAAA;AACe,EAAA;AACjB;AAO+H;AACnF,EAAA;AACW,EAAA;AACnC,IAAA;AAClB,EAAA;AACyB,EAAA;AACI,EAAA;AAEc,EAAA;AACA,EAAA;AACiB,EAAA;AACC,EAAA;AAC7C,EAAA;AACA,EAAA;AACkB,EAAA;AACL,EAAA;AACS,IAAA;AACyB,IAAA;AAC/D,EAAA;AAC6D,EAAA;AACX,EAAA;AACC,EAAA;AACtB,EAAA;AACS,IAAA;AACuB,IAAA;AAC7D,EAAA;AACuD,EAAA;AACzD;AlBulIiE;AACA;AmBv3I3B;AAuFgC;AAGM;AACZ,EAAA;AAC/B,EAAA;AACE,EAAA;AAC1B,EAAA;AAC6B,IAAA;AACuB,IAAA;AAC3D,EAAA;AAC6B,EAAA;AACtB,EAAA;AACT;AAOmF;AAChD,EAAA;AACrB,IAAA;AACE,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AACf,EAAA;AACiC,EAAA;AAC6B,EAAA;AACD,EAAA;AACtD,EAAA;AACT;AAEoG;AAC9B,EAAA;AAClD,EAAA;AACsB,IAAA;AAC7B,IAAA;AAC+B,MAAA;AAC1B,MAAA;AACA,MAAA;AACd,IAAA;AACK,EAAA;AACsD,IAAA;AAC7D,EAAA;AAC0D,EAAA;AACrB,EAAA;AACK,EAAA;AACnC,EAAA;AACT;AAE0F;AACtD,EAAA;AACd,EAAA;AACJ,IAAA;AACmE,MAAA;AACjC,MAAA;AACzC,MAAA;AACoC,QAAA;AAC9B,QAAA;AACmC,UAAA;AAC9B,UAAA;AACA,UAAA;AACd,QAAA;AACF,MAAA;AAC6D,MAAA;AACJ,MAAA;AAClD,MAAA;AACT,IAAA;AACK,IAAA;AACqD,MAAA;AAC5C,IAAA;AACgE,MAAA;AACvC,MAAA;AAC9B,MAAA;AACT,IAAA;AACK,IAAA;AACoD,MAAA;AAC3D,EAAA;AACF;AAG8H;AACpF,EAAA;AACV,EAAA;AACR,IAAA;AACT,MAAA;AACgD,MAAA;AAC5C,MAAA;AACN,MAAA;AACT,IAAA;AACF,EAAA;AAC4D,EAAA;AAC9D;AAe6E;AASH;AACnC,EAAA;AACJ,EAAA;AACtB,EAAA;AACM,IAAA;AACa,IAAA;AAC9B,EAAA;AACwC,EAAA;AACe,EAAA;AAC/B,IAAA;AACZ,IAAA;AACuB,MAAA;AACS,MAAA;AAC2B,MAAA;AACd,MAAA;AACpC,MAAA;AACnB,IAAA;AACa,IAAA;AACf,EAAA;AACuC,EAAA;AACzC;AAG+E;AACN,EAAA;AAC9B,EAAA;AAC3C;AAUmC;AACI,EAAA;AACW,EAAA;AACjB,IAAA;AACjB,MAAA;AACV,MAAA;AACF,IAAA;AACY,IAAA;AACqC,MAAA;AAC/C,MAAA;AACF,IAAA;AACI,IAAA;AAC8B,MAAA;AACqB,MAAA;AACE,MAAA;AAC3C,IAAA;AACuB,MAAA;AACrC,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAe2H;AAC1F,EAAA;AACwB,EAAA;AACQ,EAAA;AACzB,EAAA;AAClB,EAAA;AAEsC,IAAA;AACJ,IAAA;AACM,MAAA;AAC1D,IAAA;AACyD,IAAA;AAC3D,EAAA;AAC4D,EAAA;AACX,EAAA;AACU,IAAA;AACxC,IAAA;AACnB,EAAA;AACyD,EAAA;AAC3D;AAG2B;AAEgG;AAChE,EAAA;AAC/B,EAAA;AACG,EAAA;AAC2B,IAAA;AACZ,IAAA;AAC5C,EAAA;AACI,EAAA;AACoD,IAAA;AACG,IAAA;AAC7C,EAAA;AAC4B,IAAA;AACE,IAAA;AAC5C,EAAA;AACF;AAE+F;AACjD,EAAA;AAClC,EAAA;AACmD,EAAA;AACpC,EAAA;AACkC,IAAA;AAC1B,IAAA;AACjC,EAAA;AAC2D,EAAA;AACF,EAAA;AACjB,EAAA;AACM,EAAA;AACyC,IAAA;AACb,IAAA;AAC1E,EAAA;AACO,EAAA;AACT;AAE8D;AAC5B,EAAA;AACJ,EAAA;AACP,EAAA;AACc,IAAA;AACvB,IAAA;AAC2C,IAAA;AACvB,IAAA;AAC+B,IAAA;AAC9B,IAAA;AACpB,MAAA;AACP,QAAA;AACM,QAAA;AACN,QAAA;AAC0C,QAAA;AACJ,QAAA;AACI,QAAA;AACJ,QAAA;AACM,QAAA;AACd,QAAA;AACY,QAAA;AAC3C,MAAA;AACH,IAAA;AACF,EAAA;AAC0B,EAAA;AAC5B;AAE2F;AAC7C,EAAA;AAClC,EAAA;AAC2C,EAAA;AAC7B,EAAA;AACtB,IAAA;AAC4C,IAAA;AACrB,IAAA;AACQ,IAAA;AACE,IAAA;AACnC,EAAA;AACuC,EAAA;AACwB,EAAA;AACzC,EAAA;AACsB,EAAA;AACf,IAAA;AACiC,IAAA;AAChB,IAAA;AAC9C,EAAA;AACgB,EAAA;AACA,IAAA;AACc,MAAA;AACrB,QAAA;AACG,QAAA;AAC+C,QAAA;AACI,QAAA;AAC3D,MAAA;AAC4C,MAAA;AACe,MAAA;AACtB,MAAA;AACR,MAAA;AACtB,MAAA;AACT,IAAA;AACmB,IAAA;AACyC,MAAA;AAC/C,MAAA;AACkC,QAAA;AACpC,QAAA;AACT,MAAA;AACuD,MAAA;AACzD,IAAA;AACc,IAAA;AAC4C,MAAA;AACb,MAAA;AACmD,MAAA;AAClD,MAAA;AACrC,MAAA;AACT,IAAA;AACmB,IAAA;AACL,MAAA;AACyC,MAAA;AACR,QAAA;AACpC,QAAA;AACT,MAAA;AACuD,MAAA;AACzD,IAAA;AACA,IAAA;AAC6C,MAAA;AACpC,MAAA;AACX,EAAA;AACF;AAGuF;AACpD,EAAA;AACJ,EAAA;AAEX,EAAA;AACH,IAAA;AACyC,IAAA;AACpC,IAAA;AACuC,IAAA;AAChD,IAAA;AACR,EAAA;AAC2D,EAAA;AAChE;AnB8tIiE;AACA;AoBtpJlB;AAChB,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AAEyC;AACF,EAAA;AACvC;AAewE;AACtC,EAAA;AACtB,EAAA;AACyC,IAAA;AACU,IAAA;AAC7D,EAAA;AACO,EAAA;AACT;AAGiG;AACjC,EAAA;AACP,EAAA;AACzD;AAGkJ;AACjF,EAAA;AAC3C,EAAA;AACwB,EAAA;AACa,IAAA;AACG,IAAA;AAC5D,EAAA;AACwB,EAAA;AAC6B,IAAA;AACM,IAAA;AACC,IAAA;AAC5D,EAAA;AAC2D,EAAA;AAC7D;AAGqF;AAChD,EAAA;AACU,EAAA;AACP,EAAA;AACxC;AAG8G;AACzE,EAAA;AACQ,EAAA;AACc,EAAA;AACjC,EAAA;AACsC,EAAA;AACF,IAAA;AAC5D,EAAA;AACuC,EAAA;AACY,EAAA;AACJ,EAAA;AACA,EAAA;AACjD;AAG8D;AACL,EAAA;AACsC,EAAA;AACjC,EAAA;AACrD,EAAA;AACT;AAG+H;AAC1F,EAAA;AACF,EAAA;AACe,EAAA;AACpC,EAAA;AACiD,EAAA;AAC/D;AAkBoH;AAC/E,EAAA;AACjB,EAAA;AAC0C,EAAA;AACV,EAAA;AAC1B,EAAA;AACM,EAAA;AAC8B,IAAA;AAC9B,IAAA;AAC9B,EAAA;AAC8D,EAAA;AAC7B,EAAA;AAC6B,IAAA;AACvC,IAAA;AACvB,EAAA;AACqC,EAAA;AACF,IAAA;AACoB,IAAA;AAC3C,IAAA;AAC4B,IAAA;AACX,IAAA;AAC7B,EAAA;AAC+B,EAAA;AACO,IAAA;AACD,MAAA;AACQ,IAAA;AACO,IAAA;AACpD,EAAA;AACiC,EAAA;AACY,IAAA;AACT,IAAA;AACjB,IAAA;AACnB,EAAA;AAC+B,EAAA;AACF,IAAA;AACiC,IAAA;AAChB,IAAA;AAC3B,IAAA;AACnB,EAAA;AACmC,EAAA;AACA,IAAA;AACvB,IAAA;AACyB,IAAA;AACrB,IAAA;AAChB,EAAA;AACsC,EAAA;AACxC;AAMgI;AAClE,EAAA;AACP,EAAA;AAChB,EAAA;AACyB,EAAA;AACR,EAAA;AACQ,EAAA;AACtB,EAAA;AACW,EAAA;AACzB,EAAA;AAClB,IAAA;AACgB,IAAA;AACtB,IAAA;AACS,IAAA;AACA,IAAA;AACF,IAAA;AACsB,IAAA;AACJ,MAAA;AAChB,MAAA;AACR,IAAA;AACH,EAAA;AAEgC,EAAA;AACG,IAAA;AACxB,IAAA;AACW,IAAA;AACtB,EAAA;AACU,EAAA;AACqD,EAAA;AACA,EAAA;AACjE;AAGiH;AAC5E,EAAA;AACc,EAAA;AACH,EAAA;AAC5B,EAAA;AACuB,EAAA;AACR,EAAA;AACuB,IAAA;AACvD,EAAA;AAC0D,EAAA;AAC7D;AAGuH;AAClF,EAAA;AACE,EAAA;AACW,EAAA;AACc,EAAA;AAC5C,EAAA;AAC0C,EAAA;AAC9D;AAG+G;AAC1E,EAAA;AACgB,EAAA;AAChB,EAAA;AACb,IAAA;AACE,IAAA;AACL,IAAA;AACV,IAAA;AACR,EAAA;AACH;AAMgE;AAGgE;AAC9F,EAAA;AACP,EAAA;AACiB,IAAA;AACoB,IAAA;AACH,IAAA;AAC3D,EAAA;AACmF,EAAA;AACxB,EAAA;AAC3B,EAAA;AACc,IAAA;AACvB,IAAA;AACuC,IAAA;AACnD,IAAA;AACX,EAAA;AACyC,EAAA;AACpB,EAAA;AACmC,IAAA;AACxC,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAEkF;AAC7C,EAAA;AACE,EAAA;AACiB,EAAA;AAC3C,EAAA;AACb;AAG6G;AACjE,EAAA;AACmB,EAAA;AACA,EAAA;AAC/D;AAG+I;AACnG,EAAA;AACgB,EAAA;AACC,EAAA;AAC3B,EAAA;AAC6B,EAAA;AACE,EAAA;AAC7B,EAAA;AACrB,EAAA;AAC4C,EAAA;AAC3D;AAE6G;AACjE,EAAA;AACiB,EAAA;AACG,EAAA;AACtC,EAAA;AACW,EAAA;AAClB,EAAA;AACqB,EAAA;AACxC;AAG6G;AAC3E,EAAA;AACyB,EAAA;AACG,EAAA;AACrD,EAAA;AACT;AAGgF;AAC/D,EAAA;AACA,EAAA;AACY,EAAA;AACA,EAAA;AACoC,EAAA;AACjE;ApB+kJiE;AACA;AqB95J7B;AACL,EAAA;AACd,IAAA;AACD,IAAA;AACd,EAAA;AACF;AAGuB;AAOnB,EAAA;AAHS,IAAA;AACQ,IAAA;AACR,IAAA;AAEE,IAAA;AACb,EAAA;AALW,EAAA;AACQ,EAAA;AACR,EAAA;AALX,EAAA;AAUwB,EAAA;AACC,IAAA;AACzB,EAAA;AAEqB,EAAA;AACI,IAAA;AACzB,EAAA;AAE4C,EAAA;AACe,IAAA;AAC3D,EAAA;AAEa,EAAA;AACU,IAAA;AACM,IAAA;AAC7B,EAAA;AAEa,EAAA;AACO,IAAA;AACS,IAAA;AAC7B,EAAA;AAEc,EAAA;AACiB,IAAA;AACgC,IAAA;AACjD,IAAA;AACL,IAAA;AACT,EAAA;AAEc,EAAA;AACO,IAAA;AACY,IAAA;AACjC,EAAA;AAEc,EAAA;AACiB,IAAA;AACd,IAAA;AACA,IAAA;AACH,IAAA;AACuD,IAAA;AACrE,EAAA;AAEc,EAAA;AACQ,IAAA;AACtB,EAAA;AAEc,EAAA;AACQ,IAAA;AACA,IAAA;AACK,IAAA;AACwB,IAAA;AAC1C,IAAA;AACT,EAAA;AAEc,EAAA;AACQ,IAAA;AACA,IAAA;AACE,IAAA;AACxB,EAAA;AAEc,EAAA;AACU,IAAA;AAC6B,IAAA;AACvC,IAAA;AACL,IAAA;AACT,EAAA;AAEc,EAAA;AACW,IAAA;AAC4B,IAAA;AACvC,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG2B,EAAA;AACW,IAAA;AACtC,EAAA;AAE6B,EAAA;AACF,IAAA;AAC4B,IAAA;AACzC,IAAA;AACL,IAAA;AACT,EAAA;AAEyB,EAAA;AACoB,IAAA;AAC7C,EAAA;AAEsB,EAAA;AACK,IAAA;AACb,IAAA;AACd,EAAA;AAAA;AAG0B,EAAA;AACU,IAAA;AACuB,IAAA;AAC7C,IAAA;AACL,IAAA;AACT,EAAA;AAE4B,EAAA;AACkC,IAAA;AAC9D,EAAA;AAEmB,EAAA;AACgC,IAAA;AACjC,IAAA;AACT,IAAA;AACT,EAAA;AAAA;AAGwD,EAAA;AACG,IAAA;AAC3D,EAAA;AAAA;AAGkC,EAAA;AACd,IAAA;AACI,IAAA;AACoB,IAAA;AACH,IAAA;AAChC,IAAA;AACT,EAAA;AAAA;AAGkB,EAAA;AACG,IAAA;AACmB,IAAA;AAC9B,IAAA;AACkD,IAAA;AAC3B,IAAA;AACjC,EAAA;AACF;AAGuB;AACe,mBAAA;AAC3B,mBAAA;AAEuB,EAAA;AACU,IAAA;AACX,IAAA;AACU,IAAA;AACP,IAAA;AACU,IAAA;AAC/B,IAAA;AACb,EAAA;AAEoB,EAAA;AACL,IAAA;AACiB,IAAA;AAChC,EAAA;AAEqB,EAAA;AACN,IAAA;AACyB,IAAA;AACR,IAAA;AAChC,EAAA;AAEqB,EAAA;AACW,IAAA;AAChC,EAAA;AAEqB,EAAA;AACN,IAAA;AAC0B,IAAA;AACA,IAAA;AACD,IAAA;AACR,IAAA;AAChC,EAAA;AAEqB,EAAA;AACH,IAAA;AAClB,EAAA;AAEqB,EAAA;AACa,IAAA;AAChB,IAAA;AAClB,EAAA;AAEqB,EAAA;AACN,IAAA;AAC+C,IAAA;AAC7C,IAAA;AACjB,EAAA;AAEkC,EAAA;AACZ,IAAA;AACqB,IAAA;AACxB,IAAA;AACnB,EAAA;AAEuB,EAAA;AACqC,IAAA;AAC5D,EAAA;AAEuB,EAAA;AACR,IAAA;AACgC,IAAA;AAC9B,IAAA;AACjB,EAAA;AAAA;AAGuD,EAAA;AACrC,IAAA;AACI,IAAA;AACL,IAAA;AACG,IAAA;AACpB,EAAA;AAEoD,EAAA;AACC,IAAA;AACjC,IAAA;AACC,IAAA;AACM,IAAA;AACV,IAAA;AACD,IAAA;AACP,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AACT,IAAA;AACvB,IAAA;AACkB,IAAA;AACmB,IAAA;AAChD,EAAA;AAEsC,EAAA;AACJ,IAAA;AACtB,IAAA;AACiD,IAAA;AACxC,IAAA;AACrB,EAAA;AAEqB,EAAA;AACiB,IAAA;AACtC,EAAA;AACF;AAGwD;AAClD,EAAA;AACuD,IAAA;AACnD,EAAA;AACyB,IAAA;AACjC,EAAA;AACF;AAGwD;AAC/B,EAAA;AACH,EAAA;AACQ,IAAA;AACU,IAAA;AACb,IAAA;AACzB,EAAA;AAC0B,EAAA;AAC5B;ArBk3JiE;AACA;AsBroK/C;AAEiC;AAsC6C;AAC5D,EAAA;AACpC;AAE2E;AAC9B,EAAA;AAC7C;AAG2E;AACjD,EAAA;AACT,EAAA;AAC0C,EAAA;AAClD,EAAA;AACT;AAE0E;AAChD,EAAA;AACc,EAAA;AACxC;AAE+E;AACrD,EAAA;AACiC,EAAA;AAC3D;AAEsC;AACpB,EAAA;AACuB,EAAA;AACzC;AAEiD;AACH,EAAA;AACnC,IAAA;AACG,IAAA;AACL,EAAA;AACU,IAAA;AACL,IAAA;AACZ,EAAA;AACF;AAE4C;AACK,EAAA;AACjD;AAEsD;AACpC,EAAA;AACI,EAAA;AACtB;AAEwD;AAChC,EAAA;AACR,EAAA;AACP,IAAA;AACgD,MAAA;AAChD,IAAA;AACgC,MAAA;AAChC,IAAA;AACoD,MAAA;AACpD,IAAA;AACiD,MAAA;AACjD,IAAA;AAC6C,MAAA;AAC7C,IAAA;AACgD,MAAA;AAChD,IAAA;AACmD,MAAA;AACxD,IAAA;AACqB,MAAA;AACvB,EAAA;AACF;AAEmE;AAClD,EAAA;AACO,EAAA;AACL,EAAA;AACV,IAAA;AACc,MAAA;AACjB,MAAA;AACG,IAAA;AACiB,MAAA;AACD,MAAA;AACnB,MAAA;AACG,IAAA;AACY,MAAA;AACf,MAAA;AACG,IAAA;AACQ,MAAA;AACX,MAAA;AACG,IAAA;AACW,MAAA;AACd,MAAA;AACG,IAAA;AACc,MAAA;AACjB,MAAA;AACJ,EAAA;AACF;AAE2D;AACf,EAAA;AACpB,EAAA;AACR,EAAA;AACC,IAAA;AACK,MAAA;AACyB,MAAA;AACT,MAAA;AAC2B,MAAA;AACtC,MAAA;AACvB,IAAA;AACK,IAAA;AACA,IAAA;AACoD,MAAA;AAC5C,IAAA;AACK,MAAA;AACoB,MAAA;AACR,MAAA;AACwB,MAAA;AAC/B,MAAA;AACvB,IAAA;AACK,IAAA;AAC2B,MAAA;AAC3B,IAAA;AAC6C,MAAA;AACrC,IAAA;AACW,MAAA;AACN,MAAA;AACoB,MAAA;AACV,MAAA;AACqB,MAAA;AACnB,MAAA;AAC9B,IAAA;AACK,IAAA;AAC+B,MAAA;AAC/B,IAAA;AACgD,MAAA;AAChD,IAAA;AAC2B,MAAA;AAC3B,IAAA;AAC2B,MAAA;AAC3B,IAAA;AACgC,MAAA;AAChC,IAAA;AACA,IAAA;AACgC,MAAA;AAChC,IAAA;AACA,IAAA;AACA,IAAA;AACmC,MAAA;AACnC,IAAA;AACsD,MAAA;AAC3D,IAAA;AAC2D,MAAA;AAC7D,EAAA;AACF;AAEsD;AACtC,EAAA;AACE,EAAA;AACT,IAAA;AACiB,MAAA;AAC8B,MAAA;AAClD,MAAA;AACG,IAAA;AACA,IAAA;AAC2B,MAAA;AAC9B,MAAA;AACG,IAAA;AACiB,MAAA;AACsB,MAAA;AAC1C,MAAA;AACG,IAAA;AACU,MAAA;AACb,MAAA;AACG,IAAA;AACW,MAAA;AACD,MAAA;AACb,MAAA;AACG,IAAA;AACW,MAAA;AACO,MAAA;AACY,MAAA;AACjC,MAAA;AACG,IAAA;AACoB,MAAA;AACvB,MAAA;AACG,IAAA;AACgB,MAAA;AACD,MAAA;AAClB,MAAA;AACG,IAAA;AACU,MAAA;AACb,MAAA;AACG,IAAA;AACsC,MAAA;AACtB,MAAA;AACnB,MAAA;AACG,IAAA;AACiB,MAAA;AACpB,MAAA;AACG,IAAA;AACA,IAAA;AACkB,MAAA;AACrB,MAAA;AACG,IAAA;AACA,IAAA;AACA,IAAA;AACgB,MAAA;AACL,MAAA;AACd,MAAA;AACG,IAAA;AACW,MAAA;AACe,MAAA;AAC7B,MAAA;AACJ,EAAA;AACF;AAEqE;AACzB,EAAA;AACnB,EAAA;AACC,EAAA;AACR,EAAA;AACsB,EAAA;AACK,EAAA;AACf,EAAA;AACN,IAAA;AACqB,IAAA;AAC3C,EAAA;AAC8B,EAAA;AAChC;AAEgE;AACxC,EAAA;AACF,EAAA;AACA,EAAA;AACU,EAAA;AAChB,IAAA;AACG,IAAA;AACjB,EAAA;AACF;AAGkE;AAC1C,EAAA;AACuC,EAAA;AAC/B,EAAA;AAChC;AAE4E;AAClE,EAAA;AACgB,EAAA;AAC1B;AAGwE;AACnB,EAAA;AACrD;AAEoE;AAC1C,EAAA;AACK,EAAA;AACb,EAAA;AAClB;AtB4kKiE;AACA;AuBp2KO;AAGC;AACjE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACR;AAEuC;AAC/B,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACR;AAEqD;AAC7B,EAAA;AACxB;AAMiF;AAEtC;AAG6C;AACxB,EAAA;AACf,EAAA;AACZ,EAAA;AAC4B,EAAA;AAC5B,EAAA;AACG,EAAA;AACP,EAAA;AACjC;AAEmD;AAC3B,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACC,EAAA;AACjB,EAAA;AACyB,IAAA;AACA,IAAA;AACI,IAAA;AACF,IAAA;AACA,IAAA;AAClC,EAAA;AACF;AAGqD;AACxB,EAAA;AAC6B,EAAA;AACjD,EAAA;AACT;AAE6D;AAClC,EAAA;AACC,EAAA;AACJ,EAAA;AACqB,EAAA;AACxB,EAAA;AAC4B,EAAA;AAC2B,EAAA;AACvC,EAAA;AACP,IAAA;AACtB,IAAA;AACQ,IAAA;AACqB,MAAA;AAC1B,IAAA;AACW,MAAA;AACiB,MAAA;AAC1B,MAAA;AACqB,MAAA;AACL,QAAA;AACD,QAAA;AACkC,QAAA;AACxD,MAAA;AACoC,MAAA;AACtC,IAAA;AACqD,IAAA;AACvD,EAAA;AACwD,EAAA;AAC1D;AAE6D;AAClC,EAAA;AACnB,EAAA;AACsB,EAAA;AACvB,EAAA;AAC0C,EAAA;AAC5B,EAAA;AACG,EAAA;AACM,EAAA;AACA,EAAA;AACJ,IAAA;AACU,MAAA;AAC9B,MAAA;AACF,IAAA;AAC6C,IAAA;AACD,IAAA;AACbC,IAAAA;AACjC,EAAA;AACsD,EAAA;AACK,EAAA;AAC7D;AAEuE;AACtD,EAAA;AACG,EAAA;AACD,EAAA;AACD,EAAA;AACA,EAAA;AACG,EAAA;AACrB;AACgD;AAEa;AAC5C,EAAA;AAC8B,EAAA;AACmB,IAAA;AAChE,EAAA;AAC8B,EAAA;AACA,EAAA;AACA,EAAA;AACoC,EAAA;AAClB,EAAA;AAClD;AAE0D;AACjB,EAAA;AACY,EAAA;AACf,EAAA;AACJ,IAAA;AACyB,IAAA;AACzD,EAAA;AACwD,EAAA;AACpB,EAAA;AACtC;AAE6D;AACrC,EAAA;AACF,EAAA;AACA,EAAA;AACiD,EAAA;AACV,EAAA;AACC,EAAA;AAC9D;AAGqF;AACtD,EAAA;AACd,EAAA;AACR,IAAA;AAC8B,MAAA;AAC3B,IAAA;AACc,MAAA;AACJ,MAAA;AACA,MAAA;AACwB,MAAA;AACwB,MAAA;AACxC,MAAA;AAC1B,IAAA;AACQ,IAAA;AAEuB,MAAA;AACD,MAAA;AAC9B,IAAA;AACQ,IAAA;AACqC,MAAA;AACiB,MAAA;AAC9D,IAAA;AACQ,IAAA;AACgC,MAAA;AACvB,MAAA;AACjB,IAAA;AACA,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAEkE;AACvC,EAAA;AACH,EAAA;AAClB,EAAA;AACe,EAAA;AACG,IAAA;AACyB,IAAA;AACe,IAAA;AACpC,EAAA;AACoC,IAAA;AACzB,IAAA;AACkB,IAAA;AACM,IAAA;AACtD,EAAA;AACE,IAAA;AACT,EAAA;AACiB,EAAA;AACoB,EAAA;AACC,EAAA;AACqB,EAAA;AAC7D;AAM6F;AACvF,EAAA;AAC0B,IAAA;AACf,IAAA;AACN,MAAA;AAC0D,QAAA;AAC1D,MAAA;AACkD,QAAA;AAClD,MAAA;AACmD,QAAA;AAC3C,MAAA;AACL,QAAA;AACuB,QAAA;AACH,QAAA;AACE,QAAA;AACD,QAAA;AAC4B,QAAA;AACzD,MAAA;AACK,MAAA;AACoB,QAAA;AACpB,MAAA;AAC2B,QAAA;AACnB,MAAA;AAC+B,QAAA;AACiB,QAAA;AACR,QAAA;AACrD,MAAA;AACa,MAAA;AACyC,QAAA;AAC/B,QAAA;AACC,QAAA;AACE,QAAA;AAC0B,QAAA;AACpD,MAAA;AACK,MAAA;AACwB,QAAA;AACxB,MAAA;AACyB,QAAA;AAC9B,MAAA;AACS,QAAA;AACX,IAAA;AACM,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAEoD;AACS;AAEF;AACvC,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AAC2B,EAAA;AAChD;AAE8E;AAG6D;AACjH,EAAA;AACV,EAAA;AACP,IAAA;AAC2C,MAAA;AAC9B,IAAA;AACN,MAAA;AACmB,MAAA;AACtB,MAAA;AACgC,MAAA;AACzC,IAAA;AACkB,IAAA;AACN,MAAA;AAC+B,MAAA;AAClC,MAAA;AACgC,MAAA;AACzC,IAAA;AACe,IAAA;AACH,MAAA;AACH,MAAA;AAC6C,MAAA;AACe,MAAA;AAClB,MAAA;AACV,MAAA;AACzC,IAAA;AACe,IAAA;AACH,MAAA;AACJ,MAAA;AACC,MAAA;AACK,MAAA;AACwC,MAAA;AACzB,QAAA;AACT,QAAA;AACM,QAAA;AACF,UAAA;AACD,UAAA;AACnB,QAAA;AACF,MAAA;AACuC,MAAA;AACzC,IAAA;AACsB,IAAA;AACV,MAAA;AACH,MAAA;AACgB,MAAA;AACjB,MAAA;AACgD,MAAA;AAC1B,MAAA;AACgB,QAAA;AACc,QAAA;AACF,QAAA;AACxD,MAAA;AACuC,MAAA;AACzC,IAAA;AACiB,IAAA;AACL,MAAA;AACmB,MAAA;AACoB,QAAA;AACF,QAAA;AAC9C,MAAA;AACuD,MAAA;AAC1D,IAAA;AACqB,IAAA;AACT,MAAA;AACyC,MAAA;AAClB,MAAA;AAC3B,MAAA;AACiC,MAAA;AACzC,IAAA;AACmB,IAAA;AACP,MAAA;AAC+B,MAAA;AACkC,MAAA;AACQN,MAAAA;AAC7E,MAAA;AACgD,QAAA;AACK,QAAA;AACd,QAAA;AACY,QAAA;AACzD,MAAA;AACqD,MAAA;AACvD,IAAA;AACoB,IAAA;AACR,MAAA;AACkB,MAAA;AACrB,MAAA;AACA,MAAA;AACgD,MAAA;AAChD,MAAA;AACmC,MAAA;AACT,MAAA;AACxB,MAAA;AAC8B,MAAA;AACzC,IAAA;AACF,EAAA;AACoD,EAAA;AACtD;AvBm0KiE;AACA;AwB1tL3B;AAEyB;AACA,EAAA;AAC/D;AAW+C;AACgB,EAAA;AACX,EAAA;AAC3C,EAAA;AACT;AAGiD;AACnB,EAAA;AAC9B;AAGqF;AACrE,EAAA;AACN,EAAA;AACM,EAAA;AACF,IAAA;AACkD,IAAA;AAC9C,IAAA;AACc,MAAA;AACT,MAAA;AACZ,MAAA;AACL,MAAA;AACF,IAAA;AACc,IAAA;AACJ,IAAA;AACiB,IAAA;AACgC,MAAA;AACzD,MAAA;AACA,MAAA;AACF,IAAA;AACkB,IAAA;AAC0B,IAAA;AACtC,IAAA;AACR,EAAA;AACO,EAAA;AACT;AAG2G;AACrF,EAAA;AACZ,EAAA;AACY,EAAA;AACsB,IAAA;AACvB,IAAA;AACJ,IAAA;AACK,MAAA;AAC6B,MAAA;AACH,MAAA;AACN,MAAA;AAC9B,MAAA;AACD,MAAA;AACe,IAAA;AACF,MAAA;AACsB,MAAA;AACvB,MAAA;AACyB,MAAA;AAClB,MAAA;AAClB,MAAA;AACR,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAEwE;AAClE,EAAA;AACA,EAAA;AACqD,IAAA;AAC7C,EAAA;AAC2C,IAAA;AACvD,EAAA;AAC8C,EAAA;AACiB,EAAA;AACjE;AAE0D;AACpB,EAAA;AACtB,EAAA;AACF,EAAA;AAC0C,EAAA;AACP,EAAA;AACT,IAAA;AAC0B,MAAA;AACnC,IAAA;AACG,MAAA;AACyB,QAAA;AACI,QAAA;AACnC,QAAA;AACE,QAAA;AACG,QAAA;AAC3B,MAAA;AACyB,IAAA;AACmC,MAAA;AACxB,MAAA;AACoB,MAAA;AACnD,IAAA;AACc,MAAA;AACrB,IAAA;AACF,EAAA;AACF;AAE4E;AAChD,EAAA;AACU,EAAA;AACc,EAAA;AACb,EAAA;AAC0B,EAAA;AACb,EAAA;AAClB,EAAA;AACvB,EAAA;AACmD,EAAA;AACrD,EAAA;AACT;AAGgG;AACjE,EAAA;AACR,EAAA;AACd,IAAA;AACyC,MAAA;AAClB,MAAA;AACvB,IAAA;AAC2C,MAAA;AAC3C,IAAA;AACsC,MAAA;AACnC,IAAA;AACsC,MAAA;AAC9B,MAAA;AACP,MAAA;AACT,IAAA;AACA,IAAA;AACuD,MAAA;AACzD,EAAA;AACF;AAG2E;AAC/C,EAAA;AACU,EAAA;AACb,EAAA;AAC+B,EAAA;AAC7C,EAAA;AAC8B,EAAA;AACF,EAAA;AACrB,IAAA;AAC6C,IAAA;AAC5C,IAAA;AACD,IAAA;AACuB,MAAA;AACZ,MAAA;AACG,MAAA;AACrB,IAAA;AACmB,MAAA;AACW,MAAA;AACD,MAAA;AACR,MAAA;AAC5B,IAAA;AACF,EAAA;AAC6C,EAAA;AAC/B,EAAA;AAC+B,EAAA;AACtC,EAAA;AACT;AxBosLiE;AACA;AyBt3LhC;AACA;AACZ;AACM;AAEwB;AAC9C,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACL;AAG8B;AAC5B,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAChG,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AAAQ,EAAA;AACzD;AAE4B;AACI;AACK;AAEW;AAChB;AACC;AACE;AAGP;AACD;AACE;AACC;AA+EU;AACuB,EAAA;AAChE;AAE2E;AACzC,EAAA;AACE,EAAA;AACc,EAAA;AACa,EAAA;AACtC,EAAA;AACzB;AAE6C;AACG,EAAA;AACxB,EAAA;AACiC,EAAA;AAC/C,EAAA;AACgE,EAAA;AACb,EAAA;AACP,EAAA;AACZ,EAAA;AACgB,EAAA;AACE,EAAA;AACnD,EAAA;AACT;AAEoD;AAC3B,EAAA;AACK,EAAA;AACF,EAAA;AACf,IAAA;AACQ,IAAA;AACwB,IAAA;AACvB,IAAA;AACM,IAAA;AACY,IAAA;AACT,IAAA;AAC7B,EAAA;AACO,EAAA;AACT;AAEwC;AACc,EAAA;AAC3B,EAAA;AAC2B,EAAA;AACvB,EAAA;AAC/B;AAGqF;AACpD,EAAA;AACO,EAAA;AAC5B,IAAA;AACa,IAAA;AAC0B,IAAA;AACvB,IAAA;AAEd,IAAA;AACmB,IAAA;AACgC,MAAA;AACnD,QAAA;AACN,QAAA;AACF,MAAA;AACF,IAAA;AACa,IAAA;AACmC,MAAA;AACY,MAAA;AAC5D,IAAA;AACU,IAAA;AACY,IAAA;AACxB,EAAA;AACO,EAAA;AACT;AAEgD;AACa,EAAA;AAC7D;AAGsD;AACvB,EAAA;AACA,IAAA;AACkB,IAAA;AACD,IAAA;AAC9C,EAAA;AACqB,EAAA;AACC,EAAA;AACwB,EAAA;AACwB,EAAA;AAC7B,EAAA;AACG,EAAA;AACc,EAAA;AACrC,EAAA;AACvB;AAE4D;AACtD,EAAA;AACuB,IAAA;AACiD,IAAA;AACb,IAAA;AACT,IAAA;AACF,IAAA;AACjB,IAAA;AACrB,IAAA;AACM,MAAA;AACiB,MAAA;AACC,MAAA;AACC,MAAA;AACC,MAAA;AACtC,IAAA;AACkC,IAAA;AAC3B,IAAA;AACD,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAEoG;AAC3E,EAAA;AACP,EAAA;AAC+C,EAAA;AAC/B,EAAA;AACL,EAAA;AACC,EAAA;AACgC,IAAA;AACnC,IAAA;AACzB,EAAA;AAC8C,EAAA;AACpB,EAAA;AACL,EAAA;AACC,EAAA;AACH,EAAA;AACd,EAAA;AACkB,EAAA;AACU,EAAA;AACsB,EAAA;AACzC,EAAA;AACiC,EAAA;AACZ,EAAA;AACmB,EAAA;AACxD;AAE2G;AAC7E,EAAA;AACR,EAAA;AACI,EAAA;AACW,EAAA;AACrB,EAAA;AAC+C,EAAA;AACrB,EAAA;AACH,IAAA;AACZ,MAAA;AACN,MAAA;AACmB,MAAA;AACX,MAAA;AACE,MAAA;AAC1B,IAAA;AACH,EAAA;AACyD,EAAA;AAC3D;AAGoD;AACrB,EAAA;AACF,EAAA;AACX,EAAA;AACqB,EAAA;AACJ,EAAA;AACP,EAAA;AACM,EAAA;AACd,EAAA;AACoB,EAAA;AACrB,EAAA;AACQ,IAAA;AACmC,IAAA;AACpC,IAAA;AACN,MAAA;AACqB,MAAA;AAEe,MAAA;AACM,MAAA;AAC7C,MAAA;AAC+C,QAAA;AACH,QAAA;AACvD,MAAA;AACF,IAAA;AACF,EAAA;AACsD,EAAA;AACH,EAAA;AACrD;AAMqF;AACrC,EAAA;AAChD;AAEuD;AAClB,EAAA;AACzB,EAAA;AACJ,IAAA;AACiC,MAAA;AAC7B,IAAA;AAER,IAAA;AACF,EAAA;AACW,EAAA;AACb;AAOgF;AACnB,EAAA;AAC5B,EAAA;AACN,EAAA;AACN,EAAA;AAC2C,EAAA;AAChD,EAAA;AAChB;AAE+E;AAChD,EAAA;AACwB,EAAA;AACvD;AAE8E;AAC/C,EAAA;AACM,EAAA;AACrC;AASwD;AACpB,EAAA;AACE,EAAA;AACQ,IAAA;AACZ,IAAA;AACf,MAAA;AAC8C,IAAA;AACP,MAAA;AACJ,MAAA;AAC3C,IAAA;AACiC,MAAA;AACxC,IAAA;AACD,EAAA;AACwB,EAAA;AACI,IAAA;AACa,IAAA;AAC1C,EAAA;AACc,EAAA;AAChB;AAEsE;AACV,EAAA;AACK,EAAA;AACpD,EAAA;AACb;AAG2G;AAC1D,EAAA;AAC/B,EAAA;AACuC,EAAA;AAC1B,EAAA;AACsB,IAAA;AACW,IAAA;AAC9D,EAAA;AAC0D,EAAA;AAC5D;AAG6D;AACN,EAAA;AACQ,EAAA;AAC/D;AAE4E;AAC1B,EAAA;AAClD;AAG8D;AAC/B,EAAA;AACE,EAAA;AACC,EAAA;AAClC;AAQmD;AACtC,EAAA;AACC,EAAA;AACE,EAAA;AACD,EAAA;AACf;AAEoG;AAC1E,EAAA;AACqB,IAAA;AACb,IAAA;AACjB,IAAA;AAE2C,IAAA;AACrB,IAAA;AACL,IAAA;AAChB,IAAA;AACH,IAAA;AACb,EAAA;AACF;AAEwD;AACnC,EAAA;AACA,EAAA;AACmC,EAAA;AACA,EAAA;AAC9C,EAAA;AACC,IAAA;AAC4C,IAAA;AACC,IAAA;AACtD,EAAA;AAEoD,EAAA;AACe,EAAA;AACrE;AAEwG;AACjF,EAAA;AACI,EAAA;AACK,EAAA;AAClB,IAAA;AAC0C,IAAA;AAClC,IAAA;AACJ,IAAA;AAChB,EAAA;AACkD,EAAA;AACpC,EAAA;AACM,EAAA;AACJ,EAAA;AACC,EAAA;AACH,EAAA;AACR,EAAA;AACwB,EAAA;AACD,EAAA;AACM,EAAA;AACtB,EAAA;AAEL,EAAA;AACmD,EAAA;AACtC,EAAA;AACY,EAAA;AACnB,EAAA;AAChB;AAG2F;AACtE,EAAA;AACH,EAAA;AACQ,EAAA;AACV,EAAA;AACG,EAAA;AACR,EAAA;AACY,EAAA;AACF,EAAA;AACD,EAAA;AACA,EAAA;AACI,EAAA;AACS,EAAA;AACL,EAAA;AACE,EAAA;AACI,EAAA;AAChB,IAAA;AACJ,IAAA;AACoB,IAAA;AACX,IAAA;AACL,IAAA;AACe,IAAA;AAC/B,EAAA;AACY,EAAA;AACa,EAAA;AAC2B,EAAA;AACzB,IAAA;AACD,IAAA;AACN,MAAA;AAC2C,MAAA;AACH,MAAA;AACzB,MAAA;AACC,QAAA;AACA,UAAA;AACT,UAAA;AACL,UAAA;AAChB,QAAA;AACF,MAAA;AACkC,MAAA;AACpC,IAAA;AACW,IAAA;AACJ,IAAA;AACiC,IAAA;AAC1C,EAAA;AACW,EAAA;AACgB,EAAA;AACL,EAAA;AACN,EAAA;AAClB;AzBwvLiE;AACA;A0B5wMrC;AACJ;AACC;AACQ;AAET;AAGwD;AAChE,EAAA;AACP,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AAC8B,MAAA;AACnC,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAEwD;AAG4E;AACzG,EAAA;AACV,EAAA;AACR,IAAA;AACsB,MAAA;AACtB,IAAA;AACsD,MAAA;AAClD,IAAA;AACmD,MAAA;AAC5C,MAAA;AACwC,QAAA;AAClB,QAAA;AACpC,MAAA;AACF,IAAA;AACQ,IAAA;AACwB,MAAA;AAEhB,MAAA;AACE,QAAA;AACM,QAAA;AACyC,QAAA;AAC/D,MAAA;AACF,IAAA;AACA,IAAA;AACsD,MAAA;AACxD,EAAA;AACF;AAE8E;AACtD,EAAA;AAC+C,EAAA;AACpC,EAAA;AACV,EAAA;AACU,EAAA;AACvB,EAAA;AACH,EAAA;AACuD,IAAA;AACA,IAAA;AACA,IAAA;AAC9D,EAAA;AACF;AAgB4D;AACnB,EAAA;AACrB,EAAA;AACqB,EAAA;AACd,EAAA;AACF,EAAA;AACD,EAAA;AAEuC,EAAA;AACN,EAAA;AACK,EAAA;AACjD,EAAA;AACa,IAAA;AACR,IAAA;AAC6B,IAAA;AAC7B,MAAA;AACI,MAAA;AACA,MAAA;AACQ,MAAA;AAC1B,IAAA;AACO,IAAA;AACT,EAAA;AACoB,EAAA;AACuB,EAAA;AACrC,IAAA;AACA,IAAA;AACA,IAAA;AACU,IAAA;AACP,MAAA;AACA,MAAA;AACY,QAAA;AACgB,QAAA;AACA,QAAA;AAC/B,QAAA;AACc,MAAA;AACuB,QAAA;AAClB,QAAA;AACA,QAAA;AACA,QAAA;AACnB,QAAA;AACF,MAAA;AACK,MAAA;AACsD,QAAA;AACzD,QAAA;AACiB,MAAA;AACwB,QAAA;AACH,QAAA;AAChB,UAAA;AACU,UAAA;AACA,UAAA;AACQ,UAAA;AACtC,UAAA;AACF,QAAA;AACkB,QAAA;AAClB,QAAA;AACF,MAAA;AACA,MAAA;AACyB,QAAA;AAC3B,IAAA;AAC2B,IAAA;AACI,IAAA;AACA,IAAA;AACO,IAAA;AACxC,EAAA;AACO,EAAA;AACT;AAG4G;AAC9C,EAAA;AACb,EAAA;AACC,EAAA;AACY,EAAA;AACrD,EAAA;AACT;AAGmH;AACL,EAAA;AACjE,EAAA;AACpB,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AAC3B,EAAA;AACO,EAAA;AACT;AAM4D;AACjB,EAAA;AACnB,IAAA;AACM,IAAA;AACV,IAAA;AAC4C,IAAA;AAC9D,EAAA;AACF;AAEuE;AAC9B,EAAA;AACC,EAAA;AACnB,IAAA;AACJ,IAAA;AAC8C,IAAA;AAC/D,EAAA;AACO,EAAA;AACT;A1B4uMiE;AACA;A2B54MH;AACtD,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACF,EAAA;AACE,EAAA;AACV;AAEmE;AACZ,EAAA;AACvD;AAEyC;AACR;AACS;AACP;AACQ;AAwCkB;AAE9C;AAC8B,mBAAA;AACf,mBAAA;AAEc,EAAA;AACE,IAAA;AACA,IAAA;AACb,IAAA;AAC/B,EAAA;AAEiB,EAAA;AACW,IAAA;AACmB,IAAA;AACH,MAAA;AACmB,MAAA;AACH,MAAA;AAC1D,IAAA;AACO,IAAA;AACT,EAAA;AACF;AAE8C;AACkB,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AAClC,EAAA;AACV,EAAA;AAC6C,EAAA;AACvB,EAAA;AAChC,EAAA;AACT;AAE8D;AAC9B,EAAA;AACf,EAAA;AACY,EAAA;AACK,EAAA;AACA,EAAA;AACN,EAAA;AACE,EAAA;AACe,EAAA;AAC7C;AAamG;AAC7F,EAAA;AACyC,IAAA;AACjC,EAAA;AACmD,IAAA;AACtD,IAAA;AACT,EAAA;AACF;AAG4F;AACtF,EAAA;AACkB,IAAA;AACV,EAAA;AACkD,IAAA;AAC/B,IAAA;AAC/B,EAAA;AACF;AAE0F;AAClC,EAAA;AACQ,EAAA;AACH,EAAA;AACA,EAAA;AAEF,EAAA;AACM,EAAA;AAC1C,EAAA;AACnB,IAAA;AACA,IAAA;AACuB,IAAA;AACD,IAAA;AACe,IAAA;AACT,IAAA;AACf,IAAA;AACd,EAAA;AAC6D,EAAA;AAChE;AAEyF;AACzE,EAAA;AACC,EAAA;AACmC,EAAA;AACJ,EAAA;AACK,EAAA;AACX,EAAA;AACgC,EAAA;AAC9B,EAAA;AACT,EAAA;AACiB,EAAA;AACV,EAAA;AACG,IAAA;AACe,IAAA;AAC1D,EAAA;AACsD,EAAA;AACO,EAAA;AACF,EAAA;AACZ,EAAA;AACxC,EAAA;AACT;AAEiF;AACxC,EAAA;AACV,EAAA;AACiC,IAAA;AACzB,IAAA;AAClB,EAAA;AACuC,IAAA;AACP,IAAA;AACnD,EAAA;AACgD,EAAA;AAClC,EAAA;AACZ,IAAA;AAC6C,IAAA;AACjB,IAAA;AAC5B,IAAA;AACF,EAAA;AACkC,EAAA;AACT,EAAA;AACA,EAAA;AACgC,EAAA;AACG,IAAA;AACf,IAAA;AAC7C,EAAA;AACyD,EAAA;AAC1C,IAAA;AAC4B,IAAA;AAC3C,EAAA;AAC2D,EAAA;AAC5C,IAAA;AACmB,IAAA;AAClC,EAAA;AACO,EAAA;AACT;AAEmF;AAC3C,EAAA;AACZ,EAAA;AACD,EAAA;AACD,EAAA;AACsD,EAAA;AACxD,EAAA;AAEH,EAAA;AAC4B,IAAA;AACX,IAAA;AACG,IAAA;AACiC,IAAA;AACtC,IAAA;AACG,IAAA;AACa,MAAA;AAChB,MAAA;AAChC,IAAA;AACmEO,IAAAA;AACX,IAAA;AAClB,IAAA;AACfC,IAAAA;AACV,IAAA;AACI,IAAA;AAC8B,IAAA;AACxC,IAAA;AACT,EAAA;AAEwD,EAAA;AAC1C,EAAA;AAC+C,IAAA;AAC9C,IAAA;AAC+B,MAAA;AACrB,MAAA;AACiB,MAAA;AACvB,MAAA;AACR,MAAA;AACT,IAAA;AAC6C,IAAA;AACpB,IAAA;AACwB,MAAA;AACjB,MAAA;AAChC,IAAA;AAC4DD,IAAAA;AACtB,IAAA;AACfC,IAAAA;AACV,IAAA;AACI,IAAA;AAC8B,IAAA;AACxCC,IAAAA;AACT,EAAA;AAE6C,EAAA;AACvB,EAAA;AACqC,IAAA;AACrC,IAAA;AACyB,IAAA;AAC/C,EAAA;AAC0E,EAAA;AAC1C,EAAA;AACkC,EAAA;AAC5B,EAAA;AACf,EAAA;AACV,EAAA;AACI,EAAA;AAC8B,EAAA;AACxC,EAAA;AACT;AAEkD;AACzB,EAAA;AACG,EAAA;AACtB,EAAA;AACA,EAAA;AACiC,IAAA;AACzB,EAAA;AACU,IAAA;AACb,IAAA;AACT,EAAA;AAC6C,EAAA;AAC2B,EAAA;AACzD,EAAA;AAC2C,EAAA;AACrC,EAAA;AACnB,IAAA;AACA,IAAA;AACc,IAAA;AACD,IAAA;AACuC,IAAA;AACtB,IAAA;AACjB,IAAA;AACd,EAAA;AAC8B,EAAA;AACxB,EAAA;AACC,IAAA;AACoB,IAAA;AACpB,IAAA;AACG,IAAA;AACA,IAAA;AACF,IAAA;AAC8C,IAAA;AAChC,IAAA;AACvB,EAAA;AACF;AAG4F;AACvE,EAAA;AACO,EAAA;AAClB,EAAA;AACS,EAAA;AACf,IAAA;AACA,IAAA;AACyC,IAAA;AACP,IAAA;AACxB,IAAA;AAC6C,IAAA;AACL,IAAA;AACpD,EAAA;AAC6D,EAAA;AACnB,EAAA;AACe,IAAA;AACzD,EAAA;AAC0C,EAAA;AAEd,EAAA;AACJ,EAAA;AACK,IAAA;AACE,IAAA;AACJ,IAAA;AACc,MAAA;AACV,MAAA;AAC7B,IAAA;AACS,IAAA;AAC8B,IAAA;AAEX,MAAA;AAClB,MAAA;AACM,QAAA;AACI,QAAA;AAClB,MAAA;AACF,IAAA;AAC0B,EAAA;AACA,IAAA;AACN,IAAA;AACtB,EAAA;AACO,EAAA;AACS,IAAA;AACC,IAAA;AACf,IAAA;AACoB,IAAA;AACP,IAAA;AACc,IAAA;AAC8B,IAAA;AAC3D,EAAA;AACF;AAG8F;AAClD,EAAA;AACA,EAAA;AAC5C;A3Bm0MiE;AACA;A4B3rN9B;AACgC;AAEtB;AACnB,EAAA;AACb,EAAA;AACK,EAAA;AAClB;AAEyC;AACf,EAAA;AACjB,EAAA;AACS,EAAA;AAClB;AAEmE;AACzC,EAAA;AACd,EAAA;AACI,EAAA;AACE,IAAA;AACE,IAAA;AAClB,EAAA;AACgB,EAAA;AAClB;AAEgF;AAE4B;AAC5C,EAAA;AAChE;AAEkF;AAEwC;AACpF,EAAA;AACK,EAAA;AACxB,EAAA;AAC0D,EAAA;AACd,EAAA;AACX,EAAA;AACW,EAAA;AAChB,EAAA;AACgB,EAAA;AACE,EAAA;AACjE;AAE6C;AAC3B,EAAA;AACD,EAAA;AAC+C,EAAA;AAChE;AAGkE;AACtC,EAAA;AACsB,EAAA;AACE,EAAA;AACf,EAAA;AACtB,EAAA;AAEyI,EAAA;AAC1F,IAAA;AACqB,IAAA;AACnD,IAAA;AACyB,IAAA;AAC9C,IAAA;AACqD,MAAA;AAC5B,MAAA;AAC0B,MAAA;AAC5C,MAAA;AACZ,MAAA;AACF,IAAA;AACF,EAAA;AAE2C,EAAA;AACrB,IAAA;AACJ,MAAA;AACC,QAAA;AACL,UAAA;AAC6C,UAAA;AACzC,UAAA;AACD,UAAA;AACC,UAAA;AAC2C,UAAA;AACzC,UAAA;AACJ,UAAA;AAC8C,YAAA;AACD,YAAA;AACrD,UAAA;AACD,QAAA;AAC8C,QAAA;AACD,QAAA;AACG,QAAA;AACrB,QAAA;AACvB,UAAA;AACG,UAAA;AAC6C,UAAA;AACnD,UAAA;AACoB,UAAA;AACtB,QAAA;AACgB,QAAA;AAC6B,UAAA;AAChB,UAAA;AACd,UAAA;AACf,QAAA;AACgB,QAAA;AAChB,QAAA;AACF,MAAA;AACmB,MAAA;AACoC,QAAA;AACzB,QAAA;AAC6B,UAAA;AACjD,UAAA;AAC6C,UAAA;AACZ,UAAA;AACzC,QAAA;AACgB,QAAA;AAC6B,UAAA;AAChB,UAAA;AACd,UAAA;AACf,QAAA;AACgB,QAAA;AAChB,QAAA;AACF,MAAA;AACc,MAAA;AACwC,QAAA;AACD,QAAA;AAClB,QAAA;AAC0B,QAAA;AACH,UAAA;AACxD,QAAA;AACyE,QAAA;AAC/B,QAAA;AAC9B,QAAA;AACsB,UAAA;AACmB,UAAA;AACA,UAAA;AACpC,UAAA;AACqC,YAAA;AACD,YAAA;AACA,YAAA;AACA,YAAA;AACnD,UAAA;AACK,QAAA;AACuC,UAAA;AAC9C,QAAA;AACgB,QAAA;AACoC,UAAA;AACvB,UAAA;AACd,UAAA;AACf,QAAA;AACgB,QAAA;AAChB,QAAA;AACF,MAAA;AACK,MAAA;AAC+C,QAAA;AACtD,IAAA;AACF,EAAA;AAC6C,EAAA;AACE,EAAA;AAG7B,EAAA;AACkC,EAAA;AAES,EAAA;AAClB,EAAA;AACS,EAAA;AACL,EAAA;AACF,EAAA;AAEC,EAAA;AACrB,EAAA;AACG,EAAA;AACI,IAAA;AACtB,IAAA;AACA,IAAA;AACV,EAAA;AAC8B,EAAA;AACjB,EAAA;AACD,EAAA;AACqB,EAAA;AACA,EAAA;AACpB,EAAA;AACoB,EAAA;AACS,IAAA;AACK,IAAA;AAC/C,EAAA;AAEc,EAAA;AACZ,IAAA;AACyD,MAAA;AACxB,MAAA;AAC/B,MAAA;AACQ,MAAA;AACqC,MAAA;AAC9B,MAAA;AACG,MAAA;AACoC,MAAA;AACxD,IAAA;AACA,IAAA;AACF,EAAA;AAC6C,EAAA;AAC/C;A5B4qNiE;AACA;A6BtxNtB;AAoH+E;AACxF,EAAA;AACF,EAAA;AACP,EAAA;AACuC,IAAA;AAC5B,MAAA;AACX,MAAA;AACP,MAAA;AAC6C,MAAA;AACN,MAAA;AACD,MAAA;AACrB,MAAA;AACR,MAAA;AACvB,IAAA;AACF,EAAA;AACoE,EAAA;AACtE;AAEyD;AACC,EAAA;AACf,EAAA;AAClC,EAAA;AACT;AAMgC;AAa5B,EAAA;AAJiB,IAAA;AACR,IAAA;AACQ,IAAA;AACA,IAAA;AAE0B,IAAA;AACxB,IAAA;AACgB,IAAA;AACrC,EAAA;AARmB,EAAA;AACR,EAAA;AACQ,EAAA;AACA,EAAA;AAXF,EAAA;AACA,EAAA;AACA,EAAA;AAC2B,mBAAA;AACT,mBAAA;AAClB,mBAAA;AAaK,EAAA;AACD,IAAA;AACrB,EAAA;AAAA;AAG2C,EAAA;AACN,IAAA;AACC,IAAA;AACA,IAAA;AACtC,EAAA;AAEuC,EAAA;AAClB,IAAA;AACE,IAAA;AAEoD,IAAA;AAClD,IAAA;AACoC,MAAA;AACzB,QAAA;AACH,QAAA;AACqB,QAAA;AAClD,MAAA;AACF,IAAA;AAC4C,IAAA;AACgB,MAAA;AACD,MAAA;AACrC,MAAA;AACsC,MAAA;AAChB,MAAA;AAC5C,IAAA;AAC0D,IAAA;AAC5D,EAAA;AAAA;AAGyB,EAAA;AACwB,IAAA;AACO,IAAA;AACxC,IAAA;AACO,IAAA;AACgB,IAAA;AACvB,IAAA;AACkB,IAAA;AACA,MAAA;AACmB,MAAA;AACb,MAAA;AACP,MAAA;AACR,MAAA;AACgB,MAAA;AACvC,IAAA;AACgE,IAAA;AACL,IAAA;AAC5B,IAAA;AACwB,IAAA;AACzD,EAAA;AAAA;AAGe,EAAA;AACI,IAAA;AACH,IAAA;AAC6C,IAAA;AAC7D,EAAA;AACF;AAEgE;AAClC,EAAA;AACR,EAAA;AACmC,EAAA;AACK,EAAA;AACrD,EAAA;AACT;AA+BqD;AAM8D;AACpE,EAAA;AACf,EAAA;AACzB,IAAA;AACA,IAAA;AAC4C,IAAA;AACjD,EAAA;AAEsD,EAAA;AACtB,EAAA;AAChB,EAAA;AACT,EAAA;AACT;AAGmC;AAWL;AACP,EAAA;AAC4B,IAAA;AAC1C,MAAA;AACkC,MAAA;AACgB,MAAA;AACG,MAAA;AACvB,MAAA;AACuB,MAAA;AAC1D,IAAA;AACO,IAAA;AACT,EAAA;AACe,EAAA;AACuC,EAAA;AAC/C,EAAA;AACqC,IAAA;AACD,IAAA;AACc,IAAA;AACV,IAAA;AAClC,IAAA;AACN,MAAA;AACO,MAAA;AACuC,MAAA;AACnD,IAAA;AAC6D,IAAA;AACD,IAAA;AAC9D,EAAA;AACF;AAWqC;AACiB,EAAA;AACvB,EAAA;AACiC,EAAA;AACP,EAAA;AACR,EAAA;AACpC,IAAA;AACmB,IAAA;AACE,IAAA;AAC9B,IAAA;AACA,IAAA;AACe,IAAA;AACH,IAAA;AACK,IAAA;AACF,IAAA;AACjB,EAAA;AACiE,EAAA;AACtB,EAAA;AACpC,EAAA;AACT;AAEiD;AAEG;AAEU;AAM3B;AACzB,EAAA;AACA,EAAA;AACyC,mBAAA;AACc,mBAAA;AACkB,mBAAA;AACzE,EAAA;AAAA;AAG6B,mBAAA;AACA,mBAAA;AAC7B,EAAA;AACA,EAAA;AACoB,mBAAA;AACpB,EAAA;AAAA;AAEmC,mBAAA;AACZ,mBAAA;AAAA;AAE4B,mBAAA;AAC7B,mBAAA;AAAA;AAEW,mBAAA;AAEK,EAAA;AACM,IAAA;AACM,IAAA;AAC3B,IAAA;AACW,IAAA;AACU,IAAA;AACG,MAAA;AACrD,IAAA;AAC6B,IAAA;AACe,MAAA;AACA,MAAA;AACR,MAAA;AACpC,IAAA;AAEa,IAAA;AAC4C,MAAA;AACZ,MAAA;AACrC,MAAA;AAC4B,MAAA;AACI,MAAA;AACH,MAAA;AACrB,MAAA;AACQ,MAAA;AACV,MAAA;AACN,MAAA;AACY,MAAA;AACE,MAAA;AACT,MAAA;AACA,MAAA;AACF,MAAA;AACM,MAAA;AACD,MAAA;AACC,MAAA;AACmC,MAAA;AAC9B,MAAA;AACtB,IAAA;AAE0D,IAAA;AAC5B,MAAA;AACyB,MAAA;AACxB,MAAA;AAC0C,QAAA;AACE,QAAA;AACzE,MAAA;AACF,IAAA;AAE2C,IAAA;AACN,IAAA;AAC/B,MAAA;AACwC,QAAA;AACpC,MAAA;AAER,MAAA;AACF,IAAA;AAEgD,IAAA;AAET,IAAA;AACT,MAAA;AAC9B,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASsE,EAAA;AAEhB,IAAA;AACT,IAAA;AACnB,IAAA;AACG,IAAA;AACG,IAAA;AACM,IAAA;AACgB,IAAA;AACD,IAAA;AAEQ,MAAA;AAC3D,IAAA;AAC6B,IAAA;AAEmB,IAAA;AACpC,IAAA;AACd,EAAA;AAEqF,EAAA;AACtD,IAAA;AACvB,MAAA;AACgD,QAAA;AAC3C,QAAA;AACM,UAAA;AACF,UAAA;AACG,UAAA;AAC8B,YAAA;AACG,YAAA;AAC7C,UAAA;AACF,QAAA;AACM,MAAA;AAEmD,QAAA;AAC3D,MAAA;AACF,IAAA;AACO,IAAA;AAC0B,MAAA;AACtB,MAAA;AACG,MAAA;AAC8B,QAAA;AACG,QAAA;AAC7C,MAAA;AACF,IAAA;AACF,EAAA;AAEmD,EAAA;AACrC,IAAA;AACd,EAAA;AAAA;AAIoC,EAAA;AACnB,IAAA;AACH,IAAA;AACd,EAAA;AAE0E,EAAA;AACzD,IAAA;AACY,IAAA;AACR,IAAA;AACN,IAAA;AACmB,MAAA;AAChC,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAM0C,mBAAA;AAEI,EAAA;AACS,IAAA;AACV,IAAA;AAC5B,IAAA;AACsB,IAAA;AACJ,oBAAA;AACY,IAAA;AAC/C,EAAA;AAAA;AAG0B,EAAA;AACT,IAAA;AAC6C,IAAA;AACJ,IAAA;AACzC,IAAA;AACgB,IAAA;AACwB,IAAA;AAClC,IAAA;AACF,IAAA;AACE,IAAA;AACvB,EAAA;AAE4B,EAAA;AAC+C,IAAA;AACtB,IAAA;AAC5C,IAAA;AACT,EAAA;AAAA;AAI4C,EAAA;AAC3B,IAAA;AACa,IAAA;AAChB,MAAA;AACO,MAAA;AACD,MAAA;AACe,MAAA;AACH,MAAA;AACX,MAAA;AACS,MAAA;AAClB,MAAA;AACO,QAAA;AACC,QAAA;AACqB,QAAA;AACrC,MAAA;AACF,IAAA;AAC+D,IAAA;AACxD,IAAA;AACT,EAAA;AAEuD,EAAA;AACtC,IAAA;AACE,IAAA;AACO,IAAA;AACD,IAAA;AACmB,IAAA;AACP,IAAA;AACX,IAAA;AACuC,IAAA;AACjD,IAAA;AACM,IAAA;AACoC,IAAA;AACvB,IAAA;AACN,IAAA;AACsB,IAAA;AAClC,IAAA;AAC4C,IAAA;AAChC,MAAA;AAC3B,IAAA;AACF,EAAA;AAE8B,EAAA;AACA,IAAA;AACkB,IAAA;AACI,IAAA;AAC3B,MAAA;AACvB,IAAA;AACkB,IAAA;AACI,IAAA;AACxB,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkC,EAAA;AACL,IAAA;AACc,IAAA;AACd,IAAA;AAC6B,IAAA;AAChD,IAAA;AAC4C,IAAA;AAC7C,IAAA;AACT,EAAA;AAEiC,EAAA;AAC6B,IAAA;AACC,IAAA;AAC/D,EAAA;AAE8B,EAAA;AACG,IAAA;AACjC,EAAA;AAE8B,EAAA;AACG,IAAA;AACjC,EAAA;AAEuB,EAAA;AACK,IAAA;AACD,IAAA;AACqB,IAAA;AAEd,IAAA;AACH,IAAA;AACJ,IAAA;AACb,IAAA;AACL,IAAA;AACT,EAAA;AAEuB,EAAA;AACK,IAAA;AACD,IAAA;AACqB,IAAA;AAEd,IAAA;AACH,IAAA;AACJ,IAAA;AACb,IAAA;AACL,IAAA;AACT,EAAA;AAEqB,EAAA;AACA,IAAA;AAC0B,IAAA;AAC3B,IAAA;AACY,IAAA;AACN,IAAA;AACU,IAAA;AACtB,IAAA;AACd,EAAA;AAAA;AAI4C,EAAA;AACN,IAAA;AACZ,IAAA;AACD,IAAA;AACa,MAAA;AACpC,IAAA;AAEyC,IAAA;AACrB,MAAA;AACb,QAAA;AACA,QAAA;AAC+B,QAAA;AACC,QAAA;AACrC,MAAA;AACF,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAIgE,EAAA;AAC5C,IAAA;AACG,MAAA;AACrB,IAAA;AACkB,IAAA;AACN,IAAA;AACd,EAAA;AAEiD,EAAA;AAC5B,IAAA;AACkB,IAAA;AAEoB,IAAA;AAC3C,MAAA;AACd,IAAA;AACY,IAAA;AAEoC,MAAA;AACzC,IAAA;AACS,MAAA;AACU,MAAA;AAEF,MAAA;AACV,QAAA;AACU,QAAA;AACtB,MAAA;AAEuD,MAAA;AACC,MAAA;AACtC,MAAA;AAChB,QAAA;AACA,QAAA;AACyB,QAAA;AACC,QAAA;AAC5B,MAAA;AACF,IAAA;AACY,IAAA;AACd,EAAA;AAEyB,EAAA;AACJ,IAAA;AACD,IAAA;AACN,IAAA;AACd,EAAA;AAAA;AAIqC,EAAA;AAChB,IAAA;AACkB,IAAA;AACsB,IAAA;AAC/C,IAAA;AACd,EAAA;AAE0C,EAAA;AACrB,IAAA;AACuC,IAAA;AAC9C,IAAA;AACd,EAAA;AAE8B,EAAA;AACT,IAAA;AACuC,IAAA;AAC9C,IAAA;AACd,EAAA;AAE4B,EAAA;AACP,IAAA;AACuC,IAAA;AAC9C,IAAA;AACd,EAAA;AAAA;AAAA;AAKiC,EAAA;AACb,IAAA;AACpB,EAAA;AAEoD,EAAA;AACT,IAAA;AAC3C,EAAA;AAAA;AAGgD,EAAA;AACa,IAAA;AAC7D,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkF,EAAA;AAClC,IAAA;AACP,IAAA;AAC4B,IAAA;AACjB,IAAA;AACvB,IAAA;AACD,IAAA;AACd,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMmF,EAAA;AACnD,IAAA;AACe,IAAA;AACT,IAAA;AACL,IAAA;AAChB,IAAA;AAC4C,IAAA;AAC7D,EAAA;AAE0D,mBAAA;AAAA;AAAA;AAAA;AAAA;AAMR,EAAA;AAClB,IAAA;AACe,IAAA;AACT,IAAA;AACA,IAAA;AACf,IAAA;AACM,MAAA;AACM,MAAA;AACjC,IAAA;AAC4D,IAAA;AAC9D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO2F,EAAA;AAC1D,IAAA;AACe,IAAA;AACtB,IAAA;AACF,IAAA;AACiB,MAAA;AACqB,MAAA;AAC5D,IAAA;AACuD,IAAA;AACP,IAAA;AAClD,EAAA;AAE+C,EAAA;AACd,IAAA;AAC8B,IAAA;AAC/D,EAAA;AAEgD,mBAAA;AACzB,mBAAA;AAAA;AAGS,EAAA;AAC+B,IAAA;AAC5C,IAAA;AACc,IAAA;AACtB,IAAA;AACG,MAAA;AACgB,MAAA;AAC5B,IAAA;AACgB,IAAA;AAClB,EAAA;AAAA;AAGmD,EAAA;AACH,IAAA;AACT,IAAA;AACvC,EAAA;AAAA;AAAA;AAAA;AAAA;AAM6C,EAAA;AACZ,IAAA;AACS,IAAA;AACM,MAAA;AACpC,MAAA;AACqB,QAAA;AACY,QAAA;AACzC,MAAA;AACO,MAAA;AACT,IAAA;AACgB,IAAA;AACoC,IAAA;AACQ,IAAA;AAC5C,IAAA;AACR,IAAA;AACmC,MAAA;AACtB,MAAA;AACe,MAAA;AACrB,MAAA;AACyCC,QAAAA;AACd,QAAA;AACT,QAAA;AACe,QAAA;AACrC,QAAA;AACT,MAAA;AACF,IAAA;AACgC,IAAA;AAClC,EAAA;AAEmD,mBAAA;AAAA;AAGjB,EAAA;AACH,IAAA;AACd,IAAA;AACe,IAAA;AACT,IAAA;AACY,MAAA;AACI,MAAA;AACV,MAAA;AAC3B,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG2C,EAAA;AACzB,IAAA;AACA,IAAA;AACe,IAAA;AAC3B,IAAA;AACqD,MAAA;AACrB,MAAA;AACyB,MAAA;AACtB,MAAA;AACY,MAAA;AACS,MAAA;AAClB,MAAA;AAClC,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAK2F,EAAA;AACjD,IAAA;AACpB,IAAA;AACZ,oBAAA;AACoD,IAAA;AAC/B,MAAA;AACD,MAAA;AAC5B,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAGqE,EAAA;AACX,IAAA;AACC,IAAA;AACT,IAAA;AAClD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOwD,EAAA;AACvB,IAAA;AACH,IAAA;AACX,IAAA;AACC,MAAA;AACyC,MAAA;AACzB,QAAA;AAChC,MAAA;AACiC,MAAA;AACE,IAAA;AACJ,MAAA;AAC0B,MAAA;AAC3D,IAAA;AACmC,IAAA;AAC2B,IAAA;AACJ,IAAA;AAChC,IAAA;AAClB,MAAA;AACkD,MAAA;AACV,MAAA;AACrB,MAAA;AACA,MAAA;AACT,MAAA;AAChB,MAAA;AACyD,MAAA;AAC3D,IAAA;AAC8D,IAAA;AACP,IAAA;AACzB,IAAA;AACK,MAAA;AACP,MAAA;AAC3B,IAAA;AACY,IAAA;AACf,EAAA;AAAA;AAAA;AAAA;AAAA;AAMyH,EAAA;AAC5E,IAAA;AACE,IAAA;AAC/C,EAAA;AAAA;AAG8D,EAAA;AACjC,IAAA;AACE,IAAA;AAC6B,IAAA;AACF,IAAA;AAClD,MAAA;AACiC,QAAA;AACX,QAAA;AAClB,MAAA;AACA,QAAA;AACR,MAAA;AACF,IAAA;AACS,IAAA;AACH,MAAA;AACqB,QAAA;AACiC,QAAA;AAClD,MAAA;AAER,MAAA;AACF,IAAA;AAC6D,IAAA;AACzD,IAAA;AACA,IAAA;AAC0C,MAAA;AACtC,IAAA;AACwB,MAAA;AAChC,IAAA;AACkC,IAAA;AACC,IAAA;AACyB,IAAA;AACF,IAAA;AAC7B,IAAA;AACW,IAAA;AACM,IAAA;AAChD,EAAA;AAEgC,EAAA;AAC8B,IAAA;AACpD,IAAA;AACwB,IAAA;AACf,IAAA;AACnB,EAAA;AAAA;AAGyD,EAAA;AACC,IAAA;AACpB,IAAA;AACH,MAAA;AAChC,IAAA;AACU,IAAA;AACb,EAAA;AAAA;AAGqC,EAAA;AACa,IAAA;AAClB,IAAA;AAC8B,MAAA;AACtB,QAAA;AACpC,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAGoG,EAAA;AACzC,IAAA;AAC3B,IAAA;AACyB,IAAA;AACjC,IAAA;AACsC,IAAA;AAC9D,EAAA;AAAA;AAAA;AAAA;AAAA;AAMkG,EAAA;AAClC,IAAA;AACZ,IAAA;AACtB,IAAA;AAC9B,EAAA;AAAA;AAG+F,EAAA;AAClC,IAAA;AACjC,IAAA;AAClB,MAAA;AACgB,MAAA;AACK,MAAA;AAClB,MAAA;AACA,MAAA;AACT,MAAA;AACW,MAAA;AACb,IAAA;AACuD,IAAA;AACzB,IAAA;AACK,MAAA;AACP,MAAA;AAC3B,IAAA;AACY,IAAA;AACf,EAAA;AAAA;AAGyD,EAAA;AACD,IAAA;AAClB,IAAA;AACH,MAAA;AAChC,IAAA;AACU,IAAA;AACb,EAAA;AAAA;AAG0C,EAAA;AACQ,IAAA;AACpB,IAAA;AACjB,IAAA;AACb,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiL,EAAA;AAC7H,IAAA;AACW,IAAA;AACjB,IAAA;AACA,IAAA;AACc,IAAA;AACR,IAAA;AACO,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOsI,EAAA;AAClF,IAAA;AAC9C,IAAA;AACsB,IAAA;AACD,MAAA;AACyB,MAAA;AACR,MAAA;AACe,MAAA;AAEJ,QAAA;AACnD,MAAA;AACK,IAAA;AACmC,MAAA;AACY,MAAA;AACtD,IAAA;AACuD,IAAA;AACzB,IAAA;AACoB,MAAA;AACjD,IAAA;AACM,IAAA;AACT,EAAA;AAAA;AAGyC,EAAA;AACW,IAAA;AACO,IAAA;AACM,IAAA;AACb,MAAA;AACjD,IAAA;AACH,EAAA;AAAA;AAGgG,EAAA;AAC5C,IAAA;AACO,IAAA;AACb,IAAA;AACmB,IAAA;AACxD,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMuD,EAAA;AACP,IAAA;AAC5B,IAAA;AACwC,IAAA;AACD,IAAA;AACR,IAAA;AAClB,IAAA;AAC8B,IAAA;AACA,IAAA;AACzB,IAAA;AAC1B,IAAA;AACQ,IAAA;AACX,IAAA;AACiC,MAAA;AACN,MAAA;AACI,QAAA;AACvB,QAAA;AAC4C,QAAA;AACJ,QAAA;AACzC,QAAA;AACqB,QAAA;AACwB,QAAA;AACzD,MAAA;AACmB,MAAA;AACrB,IAAA;AACkD,IAAA;AACtC,IAAA;AACA,IAAA;AACgD,IAAA;AACV,MAAA;AACjD,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAAA;AAM8F,EAAA;AACnD,IAAA;AACF,IAAA;AACO,IAAA;AAC5B,IAAA;AACwC,IAAA;AACnC,IAAA;AACyB,IAAA;AACE,IAAA;AACd,IAAA;AACrB,IAAA;AACyC,IAAA;AACtB,IAAA;AACqB,IAAA;AACnB,IAAA;AACC,MAAA;AACmB,QAAA;AACjB,QAAA;AACmB,QAAA;AAClB,QAAA;AACtC,MAAA;AACF,IAAA;AACyB,IAAA;AACjB,MAAA;AACS,MAAA;AAC2C,MAAA;AACjD,MAAA;AACA,MAAA;AACF,MAAA;AACoD,MAAA;AACjC,MAAA;AAC5B,IAAA;AAC4D,IAAA;AAClD,IAAA;AAC+D,MAAA;AACvD,MAAA;AACwB,MAAA;AAC1C,IAAA;AAC8B,IAAA;AACI,MAAA;AACN,MAAA;AAC3B,IAAA;AACW,IAAA;AACd,EAAA;AAAA;AAG+G,EAAA;AAC7E,IAAA;AACsB,IAAA;AAChC,IAAA;AACwC,IAAA;AAChE,EAAA;AAAA;AAG2G,EAAA;AACjD,IAAA;AACN,IAAA;AACY,IAAA;AAChE,EAAA;AAAA;AAGsE,EAAA;AACZ,IAAA;AAClC,IAAA;AACwC,IAAA;AAChE,EAAA;AAAA;AAG6F,EAAA;AAC5D,IAAA;AACkB,IAAA;AACjB,MAAA;AAChC,IAAA;AAC4D,IAAA;AACD,IAAA;AACA,IAAA;AAC7D,EAAA;AAAA;AAGgD,EAAA;AACf,IAAA;AACY,IAAA;AACkB,IAAA;AAC5B,IAAA;AACe,MAAA;AACc,MAAA;AACD,MAAA;AAC7D,IAAA;AAC4B,IAAA;AACF,MAAA;AAC8B,MAAA;AAC1C,MAAA;AACd,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMoD,EAAA;AACJ,IAAA;AAC5B,IAAA;AACuC,IAAA;AACrB,IAAA;AACsB,IAAA;AACA,IAAA;AACE,IAAA;AACpB,IAAA;AACQ,IAAA;AACM,IAAA;AACW,IAAA;AACrD,IAAA;AACiD,IAAA;AACU,IAAA;AACzD,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACuC,IAAA;AACT,IAAA;AACd,IAAA;AACM,MAAA;AACR,MAAA;AAC3B,IAAA;AACa,IAAA;AAChB,EAAA;AAAA;AAAA;AAAA;AAAA;AAM6C,EAAA;AACc,IAAA;AACN,IAAA;AACpB,IAAA;AAC6B,IAAA;AACjC,IAAA;AACnB,MAAA;AACS,MAAA;AACT,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACP,MAAA;AACqB,MAAA;AACvB,IAAA;AAC+C,IAAA;AACQ,IAAA;AACK,IAAA;AAC9B,IAAA;AACM,MAAA;AACR,MAAA;AACQ,MAAA;AACnC,IAAA;AACa,IAAA;AAChB,EAAA;AAAA;AAG6C,EAAA;AACZ,IAAA;AAC0B,IAAA;AAClC,IAAA;AACf,MAAA;AACF,MAAA;AACE,MAAA;AACG,MAAA;AACA,MAAA;AACF,MAAA;AACP,MAAA;AACqB,MAAA;AACvB,IAAA;AAC8B,IAAA;AACE,MAAA;AACJ,MAAA;AACQ,MAAA;AACnC,IAAA;AACS,IAAA;AACZ,EAAA;AAEmE,mBAAA;AAAA;AAAA;AAAA;AAAA;AAM0C,EAAA;AAC7E,IAAA;AACX,IAAA;AACY,IAAA;AAC2B,IAAA;AACN,IAAA;AACR,IAAA;AACC,IAAA;AACa,IAAA;AACrC,IAAA;AACgC,IAAA;AACA,IAAA;AAClB,IAAA;AACI,IAAA;AACb,IAAA;AACV,IAAA;AAC6B,IAAA;AAChB,IAAA;AAC6B,MAAA;AAC3B,MAAA;AAC4B,QAAA;AACpB,QAAA;AACV,QAAA;AACG,QAAA;AACY,UAAA;AACzB,UAAA;AACI,QAAA;AACrB,MAAA;AACF,IAAA;AACiD,IAAA;AACrC,IAAA;AACM,MAAA;AACkB,MAAA;AACpC,IAAA;AACoB,IAAA;AACb,IAAA;AACT,EAAA;AAAA;AAAA;AAKyH,EAAA;AAChG,IAAA;AACI,IAAA;AACgC,MAAA;AACJ,MAAA;AACA,MAAA;AACvD,IAAA;AACmB,IAAA;AACP,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGgC,EAAA;AAC4B,IAAA;AAC5D,EAAA;AAAA;AAG+B,EAAA;AACiB,IAAA;AACa,IAAA;AAC7D,EAAA;AAAA;AAGmG,EAAA;AAChE,IAAA;AACM,IAAA;AACW,IAAA;AACoB,IAAA;AACnD,IAAA;AACwC,MAAA;AACJ,MAAA;AACjB,MAAA;AAC/B,IAAA;AACqD,MAAA;AACnB,MAAA;AACzC,IAAA;AAC4D,IAAA;AACA,IAAA;AACD,IAAA;AACD,IAAA;AACR,IAAA;AACpD,EAAA;AAAA;AAGgG,EAAA;AAClC,IAAA;AACX,IAAA;AACrC,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ8D,EAAA;AAClC,IAAA;AACkC,IAAA;AACE,IAAA;AACN,IAAA;AACI,IAAA;AACA,IAAA;AACC,MAAA;AACvC,MAAA;AACtB,IAAA;AAC2B,IAAA;AACW,MAAA;AACmB,MAAA;AACJ,MAAA;AACpC,MAAA;AACqC,MAAA;AAC3B,MAAA;AAC4B,MAAA;AACtC,MAAA;AACjB,IAAA;AACgD,IAAA;AAClD,EAAA;AAAA;AAG2G,EAAA;AAC/C,IAAA;AACzC,IAAA;AACnB,EAAA;AAAA;AAG4H,EAAA;AAC7D,IAAA;AACA,IAAA;AACrB,IAAA;AACe,MAAA;AACE,MAAA;AACrC,MAAA;AACI,MAAA;AACxB,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAG+C,EAAA;AACb,IAAA;AAClC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOoF,EAAA;AAC1B,IAAA;AAC3B,IAAA;AAC2B,MAAA;AACxD,IAAA;AACqC,IAAA;AACb,IAAA;AACL,IAAA;AACrB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOgE,EAAA;AACtB,IAAA;AAC1C,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO0F,EAAA;AAChD,IAAA;AAC1C,EAAA;AAEsF,EAAA;AAC3B,IAAA;AAC1B,IAAA;AACI,IAAA;AACY,IAAA;AACuB,IAAA;AACb,IAAA;AACC,IAAA;AACzB,IAAA;AACyB,IAAA;AACjD,IAAA;AACiC,MAAA;AACiB,QAAA;AACzD,MAAA;AACgB,IAAA;AACiB,IAAA;AACwB,IAAA;AACF,IAAA;AACH,IAAA;AACO,MAAA;AACvC,MAAA;AACtB,IAAA;AAC2B,IAAA;AACzB,MAAA;AACiD,MAAA;AACU,MAAA;AAC3D,MAAA;AAC8C,MAAA;AACrB,MAAA;AACd,MAAA;AACI,MAAA;AACjB,IAAA;AAC4D,IAAA;AACD,IAAA;AACrC,IAAA;AACgC,IAAA;AACxD,EAAA;AAAA;AAAA;AAK0C,EAAA;AACW,IAAA;AACrD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQwF,EAAA;AACjE,IAAA;AACA,MAAA;AACrB,IAAA;AACiC,IAAA;AACT,IAAA;AACiC,IAAA;AACkB,MAAA;AAC3E,IAAA;AAC2D,IAAA;AACpB,MAAA;AACqB,MAAA;AACH,MAAA;AACzD,IAAA;AACuD,IAAA;AACM,MAAA;AAC7D,IAAA;AACyB,IAAA;AACsB,MAAA;AAC/C,IAAA;AACoB,IAAA;AACR,IAAA;AACd,EAAA;AAAA;AAGwC,EAAA;AACvB,IAAA;AAC8C,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAMiC,EAAA;AACK,IAAA;AACmB,IAAA;AAChD,IAAA;AACT,EAAA;AAAA;AAGgC,EAAA;AACX,IAAA;AACc,IAAA;AACrB,IAAA;AACU,MAAA;AAAoB,QAAA;AAAe,QAAA;AACW,QAAA;AAClE,MAAA;AACF,IAAA;AACkB,IAAA;AACS,IAAA;AACf,IAAA;AACd,EAAA;AAAA;AAAA;AAKgD,EAAA;AAC/B,IAAA;AAC0B,IAAA;AAC3C,EAAA;AAEmE,EAAA;AACR,IAAA;AAC3D,EAAA;AAEkG,EAAA;AAChB,IAAA;AAClF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsF,EAAA;AACrE,IAAA;AACsC,IAAA;AAC9C,IAAA;AACT,EAAA;AAAA;AAOgB,EAAA;AACK,IAAA;AAC0B,MAAA;AACW,MAAA;AACxD,IAAA;AACoB,IAAA;AAC0B,MAAA;AACc,MAAA;AAC5D,IAAA;AACyC,IAAA;AACE,MAAA;AACgB,MAAA;AAClC,MAAA;AACqB,QAAA;AACe,QAAA;AAC3D,MAAA;AACkB,MAAA;AACwC,QAAA;AACC,QAAA;AAC3D,MAAA;AACF,IAAA;AAC4C,IAAA;AAC9C,EAAA;AAAA;AAG4F,EAAA;AAC3D,IAAA;AACgB,IAAA;AACrC,IAAA;AACuC,IAAA;AAChB,MAAA;AACc,MAAA;AAC/C,IAAA;AACwD,IAAA;AAClC,IAAA;AACI,MAAA;AAC+B,MAAA;AACzD,IAAA;AAEkC,IAAA;AACmB,IAAA;AAC9C,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ8G,EAAA;AAC3B,IAAA;AACvB,IAAA;AACX,IAAA;AACA,MAAA;AAClC,MAAA;AACE,MAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACO,MAAA;AACpB,IAAA;AACoD,IAAA;AACtB,IAAA;AACZ,IAAA;AACuC,IAAA;AACF,IAAA;AACK,IAAA;AAC7C,MAAA;AACC,MAAA;AAC6C,MAAA;AAC7D,IAAA;AACW,IAAA;AACC,IAAA;AACf,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOyG,EAAA;AAC5D,IAAA;AACS,IAAA;AACrB,IAAA;AACd,IAAA;AACuB,IAAA;AAC7B,MAAA;AACE,MAAA;AACqB,MAAA;AACA,MAAA;AACJ,MAAA;AACF,MAAA;AACyB,MAAA;AACpD,IAAA;AACmB,IAAA;AACoB,IAAA;AAC3B,IAAA;AACR,MAAA;AACa,MAAA;AACA,MAAA;AACF,MAAA;AACO,MAAA;AACN,MAAA;AACjB,IAAA;AAC+B,IAAA;AAC0B,MAAA;AACjC,MAAA;AACL,MAAA;AACnB,IAAA;AAC0D,IAAA;AAC9C,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGkD,EAAA;AACL,IAAA;AACS,IAAA;AACjC,IAAA;AACuC,IAAA;AACV,IAAA;AACpC,IAAA;AACL,IAAA;AACT,EAAA;AAE4F,EAAA;AACjD,IAAA;AACM,IAAA;AACvB,IAAA;AACqC,IAAA;AACrC,IAAA;AACD,IAAA;AACA,IAAA;AACwB,IAAA;AACP,IAAA;AACkB,IAAA;AAC5D,EAAA;AAAA;AAGiE,EAAA;AACH,IAAA;AACrB,IAAA;AACpB,IAAA;AACC,IAAA;AACR,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGqE,EAAA;AAC3B,IAAA;AACrB,IAAA;AACY,IAAA;AACI,IAAA;AACoB,IAAA;AACzD,EAAA;AAAA;AAGqC,EAAA;AACA,IAAA;AAChB,IAAA;AACA,IAAA;AACK,IAAA;AACqC,IAAA;AACzC,IAAA;AACsC,IAAA;AAC9C,IAAA;AACd,EAAA;AAAA;AAAA;AAK4C,EAAA;AACX,IAAA;AACjC,EAAA;AAEmC,EAAA;AACU,IAAA;AAC7C,EAAA;AAE6D,EAAA;AACpD,IAAA;AACoD,MAAA;AACC,MAAA;AAC5D,IAAA;AACF,EAAA;AAEwC,EAAA;AACf,IAAA;AACb,IAAA;AACqB,IAAA;AAC8B,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO0F,EAAA;AACzD,IAAA;AACE,IAAA;AACvB,IAAA;AAC8C,MAAA;AACI,MAAA;AAC7B,IAAA;AACtB,MAAA;AACT,IAAA;AAC4D,IAAA;AACE,IAAA;AAC3C,IAAA;AACI,IAAA;AACX,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAG4B,EAAA;AACK,IAAA;AACsB,IAAA;AACvD,EAAA;AAAA;AAG2B,EAAA;AACI,IAAA;AAC/B,EAAA;AAAA;AAGkC,EAAA;AACD,IAAA;AAC6B,IAAA;AACP,IAAA;AACvD,EAAA;AAEqF,EAAA;AAC3B,IAAA;AACE,IAAA;AAC5D,EAAA;AAAA;AAGyG,EAAA;AACxE,IAAA;AAC6B,IAAA;AAC9D,EAAA;AAAA;AAG4G,EAAA;AAC3E,IAAA;AAC0B,IAAA;AAC3D,EAAA;AAAA;AAGmH,EAAA;AAClF,IAAA;AAC0B,IAAA;AAC3D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOyD,EAAA;AACnC,IAAA;AACmB,IAAA;AACzC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOuF,EAAA;AACtD,IAAA;AACG,IAAA;AACvB,IAAA;AACL,MAAA;AACqC,QAAA;AACL,QAAA;AACkB,QAAA;AACC,QAAA;AACX,UAAA;AACpB,UAAA;AACQ,UAAA;AAC9B,QAAA;AACM,MAAA;AACC,QAAA;AACT,MAAA;AACF,IAAA;AACkB,IAAA;AACkC,IAAA;AACM,MAAA;AACjD,MAAA;AACT,IAAA;AAC6D,IAAA;AACtB,IAAA;AACH,MAAA;AACI,MAAA;AACN,QAAA;AAChC,MAAA;AACuD,MAAA;AACC,MAAA;AAC1D,IAAA;AAC4C,IAAA;AAC7B,IAAA;AACgB,IAAA;AAE0B,MAAA;AACwB,MAAA;AACxE,MAAA;AACT,IAAA;AACuB,IAAA;AACY,IAAA;AACjC,MAAA;AACA,MAAA;AACuD,MAAA;AACP,MAAA;AAClD,IAAA;AAEkD,IAAA;AACO,IAAA;AACI,IAAA;AAChC,IAAA;AAC/B,EAAA;AAE0E,EAAA;AACvB,IAAA;AACpB,IAAA;AACtB,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AASiC,EAAA;AACgB,IAAA;AACa,IAAA;AACnC,IAAA;AAC3B,EAAA;AAAA;AAMiC,EAAA;AACgB,IAAA;AACS,IAAA;AAC/B,IAAA;AAC3B,EAAA;AAAA;AAGuE,EAAA;AAC9C,IAAA;AACE,IAAA;AACoC,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAMsG,EAAA;AAC7E,IAAA;AACE,IAAA;AAC4B,IAAA;AACY,IAAA;AACnE,EAAA;AAAA;AAAA;AAAA;AAAA;AASiC,EAAA;AAC3B,IAAA;AAC4B,IAAA;AACuB,MAAA;AACnB,MAAA;AACC,MAAA;AACW,MAAA;AACZ,MAAA;AACI,MAAA;AACK,MAAA;AAChB,MAAA;AACqB,MAAA;AAC3C,IAAA;AACI,MAAA;AACX,IAAA;AACwD,IAAA;AAC/B,IAAA;AAC3B,EAAA;AAAA;AAGuC,EAAA;AACK,IAAA;AACnB,IAAA;AACX,IAAA;AACd,EAAA;AAAA;AAIsF,EAAA;AACrD,IAAA;AAC8B,IAAA;AACU,IAAA;AACvC,IAAA;AAClC,EAAA;AAAA;AAAA;AAAA;AAAA;AAMuF,EAAA;AAC9D,IAAA;AACgC,IAAA;AACE,IAAA;AAC3D,EAAA;AAEgG,EAAA;AAC9C,IAAA;AAC/B,IAAA;AACuB,IAAA;AACkB,IAAA;AACpC,MAAA;AACE,MAAA;AACL,MAAA;AACV,MAAA;AACR,IAAA;AACuD,IAAA;AAC5C,IAAA;AACL,IAAA;AACT,EAAA;AAAA;AAGgH,EAAA;AAChE,IAAA;AACA,IAAA;AAChD,EAAA;AAAA;AAG6F,EAAA;AACxD,IAAA;AACuB,IAAA;AACE,IAAA;AAC9D,EAAA;AAAA;AAGyF,EAAA;AACpD,IAAA;AACW,IAAA;AACf,IAAA;AACsB,IAAA;AACA,IAAA;AACK,IAAA;AACP,IAAA;AACjB,IAAA;AACpC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ6C,EAAA;AACtB,IAAA;AACvB,EAAA;AAAA;AAG4F,EAAA;AACtD,IAAA;AACG,IAAA;AAC1B,IAAA;AAC4B,MAAA;AACzC,IAAA;AACF,EAAA;AAAA;AAAA;AAK8D,EAAA;AACzC,IAAA;AACJ,IAAA;AACkB,IAAA;AACrB,IAAA;AAC6D,MAAA;AAC9B,MAAA;AACkB,MAAA;AACrC,QAAA;AACY,QAAA;AACK,UAAA;AACI,QAAA;AAClC,QAAA;AACR,MAAA;AACH,IAAA;AACY,IAAA;AACd,EAAA;AAE8B,EAAA;AACT,IAAA;AACc,IAAA;AACP,IAAA;AACmC,MAAA;AACrC,QAAA;AACR,QAAA;AACL,QAAA;AACR,MAAA;AACH,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAIiE,EAAA;AACvB,IAAA;AACsC,MAAA;AAC9E,IAAA;AACF,EAAA;AAAA;AAGuG,EAAA;AAClF,IAAA;AACwC,IAAA;AAChB,IAAA;AACI,IAAA;AACc,IAAA;AAE1B,IAAA;AAED,IAAA;AACtB,IAAA;AACL,IAAA;AACT,EAAA;AAEgG,EAAA;AACzE,IAAA;AACA,MAAA;AACrB,IAAA;AACqC,IAAA;AAC1B,IAAA;AACgD,MAAA;AACH,MAAA;AACI,MAAA;AAC5D,IAAA;AACY,IAAA;AACd,EAAA;AAE0C,EAAA;AACrB,IAAA;AACkB,IAAA;AACsB,IAAA;AACf,IAAA;AACR,MAAA;AACpC,IAAA;AACY,IAAA;AACd,EAAA;AAEgC,EAAA;AACX,IAAA;AACK,IAAA;AAC8B,IAAA;AAClC,IAAA;AACc,IAAA;AACtB,IAAA;AACd,EAAA;AAEiD,EAAA;AACb,IAAA;AACtB,IAAA;AACd,EAAA;AAAA;AAImC,EAAA;AACiB,IAAA;AACtC,IAAA;AACd,EAAA;AAEkC,EAAA;AACK,IAAA;AACvC,EAAA;AAEmC,EAAA;AACI,IAAA;AACvC,EAAA;AAEyB,EAAA;AACL,IAAA;AACY,IAAA;AAClB,IAAA;AACd,EAAA;AAEqC,EAAA;AAClB,IAAA;AACL,IAAA;AACd,EAAA;AAE8C,EAAA;AACK,IAAA;AACjB,IAAA;AACpB,IAAA;AACd,EAAA;AAEyC,EAAA;AACX,IAAA;AACX,IAAA;AACL,IAAA;AACd,EAAA;AAAA;AAIgD,mBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOQ,EAAA;AACP,IAAA;AAC5B,IAAA;AACc,IAAA;AACF,IAAA;AAC8B,IAAA;AACA,MAAA;AAC7D,IAAA;AACY,IAAA;AACd,EAAA;AAE6F,EAAA;AAC5D,IAAA;AAClB,IAAA;AACE,IAAA;AACI,IAAA;AACE,IAAA;AACjB,IAAA;AACuD,MAAA;AACpD,QAAA;AACkC,QAAA;AACP,QAAA;AAC/B,MAAA;AACK,IAAA;AAER,IAAA;AACF,EAAA;AAE+E,EAAA;AAC9C,IAAA;AAClB,IAAA;AAC2B,MAAA;AACxC,IAAA;AACe,IAAA;AACQ,IAAA;AACgB,MAAA;AACmB,MAAA;AACG,MAAA;AAC7D,IAAA;AAC6D,IAAA;AAC/D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQiI,EAAA;AAChG,IAAA;AACX,IAAA;AACL,IAAA;AACsB,IAAA;AACgB,IAAA;AAChC,IAAA;AACnB,MAAA;AACqD,MAAA;AACrD,MAAA;AAC0C,QAAA;AACE,QAAA;AACd,QAAA;AAC9B,MAAA;AACF,IAAA;AACF,EAAA;AAEqE,EAAA;AACrB,IAAA;AACb,IAAA;AACE,IAAA;AACI,IAAA;AACzC,EAAA;AAEgE,EAAA;AAChB,IAAA;AACb,IAAA;AACE,IAAA;AAEK,IAAA;AACnB,MAAA;AACO,QAAA;AACA,UAAA;AACA,UAAA;AACN,QAAA;AACb,MAAA;AAED,QAAA;AAC8C,UAAA;AACjB,UAAA;AACF,UAAA;AACY,UAAA;AACb,UAAA;AACa,UAAA;AACX,YAAA;AAC9B,UAAA;AACwC,UAAA;AAC5B,QAAA;AACF,UAAA;AACZ,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAIuI,EAAA;AACtH,IAAA;AACoC,IAAA;AACrD,EAAA;AAE+H,EAAA;AACjF,IAAA;AACT,IAAA;AACrC,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOwD,EAAA;AACjB,IAAA;AACkB,IAAA;AACR,IAAA;AACb,IAAA;AACN,IAAA;AACiC,IAAA;AACvB,IAAA;AAC/B,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO8E,EAAA;AACtB,IAAA;AACA,IAAA;AACT,IAAA;AACQ,IAAA;AACvD,EAAA;AAEuG,EAAA;AAC9D,IAAA;AACgB,IAAA;AACzD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOmD,EAAA;AACC,IAAA;AACpD,EAAA;AAAA;AAGgC,EAAA;AAC+B,IAAA;AACN,IAAA;AACzD,EAAA;AAEmC,EAAA;AACuB,IAAA;AAC1D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAS4G,EAAA;AACvD,IAAA;AACR,MAAA;AACzC,MAAA;AACF,IAAA;AAC+B,IAAA;AACW,IAAA;AACW,IAAA;AACU,IAAA;AACT,IAAA;AACG,IAAA;AACF,IAAA;AACzD,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASoH,EAAA;AAC/D,IAAA;AACP,MAAA;AAC1C,MAAA;AACF,IAAA;AACiC,IAAA;AACU,IAAA;AACH,IAAA;AACT,IAAA;AACW,IAAA;AACW,IAAA;AACH,IAAA;AAC+B,IAAA;AACpB,IAAA;AACX,IAAA;AACa,IAAA;AACjD,IAAA;AAChB,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOyD,EAAA;AACL,IAAA;AACN,IAAA;AACb,IAAA;AACY,IAAA;AACvC,IAAA;AAC6B,IAAA;AAC4B,MAAA;AACJ,MAAA;AACxC,MAAA;AACA,MAAA;AAC0C,MAAA;AACD,MAAA;AACtB,MAAA;AACA,MAAA;AACE,MAAA;AACqB,MAAA;AACxB,IAAA;AACyB,MAAA;AACX,MAAA;AACU,MAAA;AACU,MAAA;AACpD,MAAA;AACA,MAAA;AACA,MAAA;AAC4C,MAAA;AACF,MAAA;AACN,MAAA;AACvC,MAAA;AACA,MAAA;AAC0C,MAAA;AAEG,QAAA;AACzD,MAAA;AACK,IAAA;AACyB,MAAA;AAChC,IAAA;AAC6D,IAAA;AACtD,IAAA;AACT,EAAA;AAAA;AAAA;AAKQ,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQ0I,EAAA;AACnF,IAAA;AAC1C,IAAA;AACJ,IAAA;AACK,IAAA;AACN,IAAA;AAC4C,IAAA;AACN,IAAA;AAClC,IAAA;AACT,IAAA;AACmC,IAAA;AAC1B,IAAA;AACT,IAAA;AACK,IAAA;AACW,IAAA;AACmC,IAAA;AACA,IAAA;AAC3C,IAAA;AACW,IAAA;AACG,IAAA;AACnB,IAAA;AACL,IAAA;AACiD,MAAA;AACrC,MAAA;AACH,MAAA;AACC,MAAA;AACC,MAAA;AAClB,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAQsH,EAAA;AACrF,IAAA;AACY,IAAA;AACf,IAAA;AACW,IAAA;AACmC,IAAA;AAC1B,IAAA;AACU,IAAA;AACG,IAAA;AACtB,IAAA;AAEyB,IAAA;AAC3C,MAAA;AACO,MAAA;AACtB,MAAA;AACmD,MAAA;AACF,MAAA;AACA,MAAA;AACG,MAAA;AACN,MAAA;AACS,MAAA;AACxC,MAAA;AAC6C,MAAA;AACN,MAAA;AACnB,MAAA;AACK,MAAA;AACrC,MAAA;AACT,IAAA;AAEyE,IAAA;AAC3C,MAAA;AACM,QAAA;AACA,QAAA;AACL,UAAA;AACH,UAAA;AACxB,QAAA;AACmD,QAAA;AACrD,MAAA;AAC0D,MAAA;AACzB,MAAA;AACZ,QAAA;AACb,UAAA;AACG,UAAA;AACT,QAAA;AACsD,QAAA;AACN,QAAA;AACO,QAAA;AACxC,QAAA;AACA,QAAA;AACsC,QAAA;AAC7B,QAAA;AAChB,UAAA;AACI,UAAA;AACE,UAAA;AACG,UAAA;AACA,UAAA;AACf,UAAA;AACqD,UAAA;AAChC,UAAA;AACvB,QAAA;AACiC,QAAA;AACE,QAAA;AACR,QAAA;AACNF,QAAAA;AACdG,QAAAA;AACT,MAAA;AACiC,MAAA;AACA,MAAA;AACG,QAAA;AACvB,QAAA;AACb,MAAA;AACyD,MAAA;AACtC,MAAA;AACb,QAAA;AAC6D,QAAA;AAClD,QAAA;AACA,QAAA;AACA,QAAA;AACsC,QAAA;AACD,QAAA;AACtD,MAAA;AACgD,MAAA;AACrC,MAAA;AACgB,MAAA;AACN,MAAA;AACL,MAAA;AACT,MAAA;AACT,IAAA;AAEgC,IAAA;AACG,IAAA;AACR,MAAA;AACH,MAAA;AACxB,IAAA;AACe,IAAA;AACW,IAAA;AACiC,IAAA;AACpB,IAAA;AACD,IAAA;AACoB,IAAA;AAEC,IAAA;AACC,IAAA;AACH,IAAA;AAC3D,EAAA;AAEmF,EAAA;AACxC,IAAA;AACtB,IAAA;AAEK,IAAA;AAC+B,IAAA;AACE,IAAA;AAEhC,IAAA;AACG,IAAA;AACM,IAAA;AACkB,IAAA;AACA,IAAA;AAC3B,MAAA;AACiC,QAAA;AACxD,MAAA;AACF,IAAA;AAEwB,IAAA;AACE,IAAA;AACH,MAAA;AACD,QAAA;AAChB,UAAA;AACsC,YAAA;AACA,YAAA;AACQ,YAAA;AACE,YAAA;AAChD,UAAA;AACgB,UAAA;AACA,UAAA;AAClB,QAAA;AACF,MAAA;AACwD,MAAA;AACjB,MAAA;AACR,QAAA;AACoB,UAAA;AACjD,QAAA;AACF,MAAA;AACF,IAAA;AAEqC,IAAA;AACiB,IAAA;AAEhC,IAAA;AACkC,MAAA;AACpB,MAAA;AACpC,IAAA;AACoB,IAAA;AACR,IAAA;AACd,EAAA;AAgBgJ,EAAA;AAC9G,IAAA;AACkB,IAAA;AACM,IAAA;AACE,IAAA;AAC5D,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAO6H,EAAA;AAC9D,IAAA;AACzC,MAAA;AAClB,MAAA;AACF,IAAA;AAC4B,IAAA;AAC4B,IAAA;AACvB,IAAA;AACa,IAAA;AAChD,EAAA;AAAA;AAGwD,EAAA;AACnC,IAAA;AACE,IAAA;AACiB,IAAA;AAClB,IAAA;AACd,MAAA;AACuD,QAAA;AACnD,MAAA;AACM,QAAA;AACd,MAAA;AACF,IAAA;AACuB,IAAA;AACM,IAAA;AACjB,IAAA;AACd,EAAA;AAAA;AAGqC,mBAAA;AAE+B,EAAA;AACnD,IAAA;AACiE,IAAA;AACvD,IAAA;AACJ,IAAA;AACS,MAAA;AAC6B,MAAA;AACjC,MAAA;AACgB,MAAA;AACN,MAAA;AACX,MAAA;AAC+B,MAAA;AACxC,MAAA;AACsC,MAAA;AACJ,MAAA;AACQ,QAAA;AACxD,MAAA;AACK,IAAA;AAC0B,MAAA;AACmB,MAAA;AACjC,MAAA;AACO,MAAA;AACoB,MAAA;AAC9B,MAAA;AAC0B,MAAA;AACN,MAAA;AAChB,MAAA;AACpB,IAAA;AACiB,IAAA;AACD,IAAA;AACD,IAAA;AAC4C,IAAA;AAC7D,EAAA;AAEqD,EAAA;AACpB,IAAA;AAC0B,IAAA;AACzC,IAAA;AACP,MAAA;AAAA;AAE0C,QAAA;AACE,QAAA;AACZ,QAAA;AACa,QAAA;AACvC,QAAA;AACD,UAAA;AACiC,UAAA;AAC3C,QAAA;AACwD,QAAA;AAC1D,MAAA;AACF,IAAA;AAC4D,IAAA;AACb,IAAA;AACpC,MAAA;AACT,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAC0B,MAAA;AACuB,MAAA;AACZ,MAAA;AACjB,MAAA;AACtB,IAAA;AACmD,IAAA;AAC5C,IAAA;AACT,EAAA;AACF;AAM+E;AAC9B,EAAA;AACgB,EAAA;AAC/C,EAAA;AAC6B,EAAA;AAChC,IAAA;AACe,IAAA;AAC5B,EAAA;AACO,EAAA;AACT;AAGwF;AAC7C,EAAA;AACY,EAAA;AACvC,EAAA;AACI,IAAA;AACwC,MAAA;AAChD,MAAA;AACG,MAAA;AACG,MAAA;AACF,MAAA;AACE,MAAA;AACF,MAAA;AACG,MAAA;AACP,MAAA;AACG,MAAA;AACV,IAAA;AACG,IAAA;AACN,EAAA;AACgD,EAAA;AAClB,EAAA;AACT,IAAA;AACwC,IAAA;AAC7D,EAAA;AACyC,EAAA;AAC5B,EAAA;AACc,EAAA;AAC7B;AAGiE;AACF,EAAA;AACiB,EAAA;AAC9D,EAAA;AACD,IAAA;AACwC,MAAA;AAC7C,MAAA;AACG,MAAA;AACT,MAAA;AACD,IAAA;AACH,EAAA;AACwB,EAAA;AACW,EAAA;AAClB,EAAA;AACV,EAAA;AACT;AAGqI;AACtH,EAAA;AACR,IAAA;AACU,IAAA;AACmC,IAAA;AACpB,IAAA;AACZ,IAAA;AACF,IAAA;AACO,IAAA;AACvB,EAAA;AAC8B,EAAA;AACzB,EAAA;AAC0D,IAAA;AACH,IAAA;AACC,IAAA;AACtB,MAAA;AACD,QAAA;AACA,QAAA;AACC,QAAA;AAClC,MAAA;AACI,IAAA;AACoD,MAAA;AACtC,MAAA;AACF,MAAA;AACnB,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAG4E;AACvC,EAAA;AACF,EAAA;AACiB,IAAA;AACpB,IAAA;AACX,IAAA;AACnB,EAAA;AACiC,EAAA;AACoB,IAAA;AAC7B,IAAA;AACL,IAAA;AACnB,EAAA;AACiC,EAAA;AACA,IAAA;AACX,IAAA;AACtB,EAAA;AACO,EAAA;AACT;AAG2G;AAC5D,EAAA;AACC,EAAA;AACH,EAAA;AACC,EAAA;AACT,EAAA;AACzB,EAAA;AAC6B,EAAA;AACoB,IAAA;AAC3C,IAAA;AAC+C,IAAA;AAClD,IAAA;AACgD,IAAA;AAC7D,EAAA;AACO,EAAA;AACT;AAGuF;AAC1C,EAAA;AACV,EAAA;AACuB,IAAA;AACM,IAAA;AAC9D,EAAA;AACO,EAAA;AACT;AAEiF;AACxC,EAAA;AACc,IAAA;AACQ,IAAA;AAC7D,EAAA;AACF;AAGiE;AACpC,EAAA;AACH,EAAA;AACO,IAAA;AACsB,IAAA;AACD,IAAA;AAC/B,MAAA;AACR,MAAA;AACC,MAAA;AACK,IAAA;AACnB,EAAA;AACO,EAAA;AACT;AAG0F;AACzE,EAAA;AACqC,EAAA;AAC9B,EAAA;AACC,EAAA;AACU,EAAA;AAC4B,IAAA;AACnC,IAAA;AACiB,IAAA;AACzB,IAAA;AACgB,IAAA;AAC4B,MAAA;AAClC,MAAA;AACgB,MAAA;AACxB,MAAA;AACY,MAAA;AAC+B,QAAA;AACA,QAAA;AACP,QAAA;AACpD,MAAA;AACF,IAAA;AACF,EAAA;AACkC,EAAA;AACpC;AAEyE;AAC5C,EAAA;AACH,EAAA;AACO,IAAA;AACmB,IAAA;AAClD,EAAA;AACO,EAAA;AACT;AAGuF;AAC3D,EAAA;AACkC,IAAA;AAC1B,MAAA;AAChC,IAAA;AACyD,IAAA;AAC3D,EAAA;AAC2D,EAAA;AACG,EAAA;AACjD,EAAA;AAC+C,EAAA;AACH,EAAA;AACF,EAAA;AACzD;AAGoD;AACO,EAAA;AAC3D;AAG8E;AAC/C,EAAA;AAC/B;AAEoE;AACT,EAAA;AAC7C,EAAA;AACA,EAAA;AACiD,EAAA;AACtD,EAAA;AACT;AAG8E;AACvC,EAAA;AACe,EAAA;AACjC,EAAA;AACZ,IAAA;AACsD,MAAA;AACtD,IAAA;AACoD,MAAA;AACpD,IAAA;AACoD,MAAA;AACpD,IAAA;AACoD,MAAA;AACpD,IAAA;AACA,IAAA;AACuD,MAAA;AACvD,IAAA;AAC2C,MAAA;AAClD,EAAA;AACF;A7BqzMiE;AACA;A8B9iTvB;AAMgC;AAWjC;AAEH;AAEuC;AAC9B,EAAA;AACtC,EAAA;AACT;AAME;AACiD,EAAA;AACe,IAAA;AACL,MAAA;AACzD,IAAA;AACF,EAAA;AACF;AAE+C;AAEI;AACtB,EAAA;AACY,EAAA;AACqB,EAAA;AACD,IAAA;AAC3D,EAAA;AACyC,EAAA;AAC3C;AAEwD;AACL,EAAA;AACY,EAAA;AACY,EAAA;AAC3E;AAE2B;AACzB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACF;AAE4D;AACL,EAAA;AACrD,EAAA;AACE,IAAA;AACA,IAAA;AAC0D,MAAA;AACvB,MAAA;AACnC,IAAA;AACA,IAAA;AACF,EAAA;AAEyD,EAAA;AAC3D;AAE0D;AACZ,EAAA;AAC5C,EAAA;AACE,IAAA;AACA,IAAA;AACW,MAAA;AACqC,MAAA;AACN,MAAA;AACmB,MAAA;AACjD,MAAA;AACF,MAAA;AACV,IAAA;AACA,IAAA;AACF,EAAA;AAC+E,EAAA;AACJ,EAAA;AAC7E;AAEiD;AAIlC;AAoBkE;AACzD,EAAA;AACS,EAAA;AACzB,IAAA;AACuB,MAAA;AACnB,IAAA;AACG,MAAA;AACX,IAAA;AACF,EAAA;AACqD,EAAA;AAED,EAAA;AACoB,EAAA;AACZ,EAAA;AAC9C,EAAA;AACC,EAAA;AACb,IAAA;AACA,IAAA;AAC2C,MAAA;AACiB,MAAA;AAC7C,MAAA;AACJ,QAAA;AAAA;AAE6C,QAAA;AACtD,MAAA;AACW,MAAA;AACF,QAAA;AACgD,QAAA;AACzD,MAAA;AACa,MAAA;AACO,QAAA;AAC2B,UAAA;AACL,UAAA;AACR,UAAA;AACQ,UAAA;AACxC,QAAA;AACiB,QAAA;AACnB,MAAA;AAAA;AAEW,MAAA;AACS,QAAA;AACuB,UAAA;AAC7B,UAAA;AAC4C,UAAA;AACxD,QAAA;AACU,QAAA;AACZ,MAAA;AACF,IAAA;AACA,IAAA;AAAA;AAEuD,MAAA;AAC9C,MAAA;AACT,IAAA;AACF,EAAA;AACiB,EAAA;AACa,IAAA;AACjB,MAAA;AACgD,MAAA;AAC1D,IAAA;AACH,EAAA;AAC4C,EAAA;AAClC,EAAA;AAC8C,IAAA;AAC9B,IAAA;AAC1B,EAAA;AACgD,EAAA;AACe,EAAA;AACjE;AAEsC;AAGgD;AACpD,EAAA;AACP,EAAA;AACgC,IAAA;AAC7C,MAAA;AACR,MAAA;AACF,IAAA;AAC0B,IAAA;AACmC,MAAA;AACqB,MAAA;AAChB,IAAA;AACpE,EAAA;AACO,EAAA;AACT;AAGoF;AACvD,EAAA;AACZ,EAAA;AACb,IAAA;AACA,IAAA;AAC4D,MAAA;AAAmD,QAAA;AAAsB,MAAA;AACxE,MAAA;AAAoD,QAAA;AAAsB,MAAA;AAC7H,MAAA;AACY,QAAA;AAC2B,UAAA;AACV,UAAA;AACnC,QAAA;AACiB,QAAA;AACnB,MAAA;AACQ,MAAA;AACY,QAAA;AAC2B,UAAA;AACN,UAAA;AACY,UAAA;AAC1C,UAAA;AACT,QAAA;AACkB,QAAA;AACpB,MAAA;AAC2D,MAAA;AAClB,MAAA;AACiB,MAAA;AACjD,MAAA;AACO,QAAA;AACsB,UAAA;AACD,UAAA;AACnC,QAAA;AACU,QAAA;AACZ,MAAA;AACkB,MAAA;AACF,QAAA;AACsB,UAAA;AACd,UAAA;AACtB,QAAA;AACU,QAAA;AACZ,MAAA;AACW,MAAA;AACS,QAAA;AACuB,UAAA;AAC7B,UAAA;AAC4C,UAAA;AACxD,QAAA;AACU,QAAA;AACZ,MAAA;AACF,IAAA;AAC+B,IAAA;AACjC,EAAA;AACuB,EAAA;AACO,IAAA;AACjB,MAAA;AACyC,MAAA;AACnD,IAAA;AACH,EAAA;AAC4C,EAAA;AACI,EAAA;AACO,EAAA;AACQ,EAAA;AACjE;A9B0/SiE;AACA;A+BlvTrB;AACM;AAGe;AACJ,EAAA;AACC,EAAA;AAC5D,EAAA;AACM,IAAA;AAAqB,IAAA;AAAuB,IAAA;AAAc,IAAA;AAAW,IAAA;AAAW,IAAA;AAAY,IAAA;AAAa,IAAA;AACnD,IAAA;AAC5D,EAAA;AAC4D,EAAA;AACC,EAAA;AACE,EAAA;AACD,EAAA;AACC,EAAA;AACjE;AAGE;AAC0C,EAAA;AACK,EAAA;AACF,EAAA;AAChB;AACT,EAAA;AAAwB,EAAA;AAAa,EAAA;AAAc,EAAA;AAAW,EAAA;AAAW,EAAA;AAAY,EAAA;AAAa,EAAA;AAChF,EAAA;AACpC;AAE+D;AACF,EAAA;AACR,EAAA;AACQ,EAAA;AACP,EAAA;AACA,EAAA;AAC1D;AAE2D;AAOwC;AACxF,EAAA;AACD,EAAA;AACyB,IAAA;AACH,IAAA;AACI,IAAA;AACF,IAAA;AAChC,EAAA;AACe,EAAA;AACgD,IAAA;AACF,IAAA;AACF,IAAA;AACN,IAAA;AACO,MAAA;AACV,MAAA;AACa,MAAA;AAC5D,IAAA;AACH,EAAA;AACU,EAAA;AACoD,IAAA;AACD,IAAA;AAC7D,EAAA;AACsD,EAAA;AAC1C,EAAA;AACoB,IAAA;AACM,IAAA;AACF,IAAA;AACF,IAAA;AACA,IAAA;AACM,IAAA;AACP,EAAA;AAC/B,IAAA;AAAkB,IAAA;AAAiC,IAAA;AAAuB,IAAA;AAAW,IAAA;AAAW,IAAA;AAAY,IAAA;AAAyB,IAAA;AAC/D,IAAA;AAAS,IAAA;AAC/E,EAAA;AACO,EAAA;AACsD,EAAA;AACD,EAAA;AACJ,EAAA;AAC5D;AAG6D;AACvD,EAAA;AAAkB,EAAA;AAAoB,EAAA;AAAc,EAAA;AAAQ,EAAA;AAAS,EAAA;AAAe,EAAA;AAAa,EAAA;AAAU,EAAA;AAAqB,EAAA;AAAW,EAAA;AACjJ;AAEmE;AAGQ;AACjD,EAAA;AAC1B;AAG4F;AAC7D,EAAA;AACkC,EAAA;AACjE;AAGyE;AACT,EAAA;AACN,EAAA;AACC,EAAA;AACjB,EAAA;AAEY,EAAA;AACtD;AAGyE;AACT,EAAA;AACzB,EAAA;AACR,EAAA;AAC/B;AAGsE;AACV,EAAA;AACzB,EAAA;AACnC;AAG8E;AAC7D,EAAA;AACC,EAAA;AACT,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACgD,MAAA;AAChD,IAAA;AACI,MAAA;AACT,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAGoE;AACnD,EAAA;AACqB,EAAA;AACY,IAAA;AAChC,IAAA;AAChB,EAAA;AACO,EAAA;AACT;AAG2E;AAC7C,EAAA;AAC0D,EAAA;AAC5E,EAAA;AACiC,EAAA;AAChB,IAAA;AACiC,IAAA;AACxC,IAAA;AACpB,EAAA;AAC+B,EAAA;AACxB,EAAA;AACT;AAG8F;AACvD,EAAA;AACM,EAAA;AACyB,IAAA;AACR,IAAA;AAC5D,EAAA;AACO,EAAA;AACT;A/BkvTiE;AACA;AgCx8TmB;AAChD,EAAA;AACD,EAAA;AACC,EAAA;AACJ,EAAA;AAChC;AAEoE;AACzD,EAAA;AACJ,EAAA;AACK,EAAA;AACC,EAAA;AACb;AAiBoE;AAC5D,EAAA;AACA,EAAA;AACK,EAAA;AACC,EAAA;AACD,EAAA;AACG,EAAA;AACJ,EAAA;AACZ;AASiB;AACoC,EAAA;AACpB,EAAA;AACF,EAAA;AACtB,EAAA;AACT;AAYkE;AACL,EAAA;AACC,EAAA;AACG,EAAA;AACF,EAAA;AACA,EAAA;AAC/D;AAGkF;AAC9C,EAAA;AACxB,EAAA;AACmD,EAAA;AAChB,EAAA;AACkB,EAAA;AACZ,EAAA;AACrD;AhCo6TiE;AACA;AiCj/TlC;AACvB,EAAA;AACmB,EAAA;AACZ,EAAA;AACf;AAEkF;AAE9B;AACd,EAAA;AACY,EAAA;AAClB,EAAA;AACvB,EAAA;AACG,IAAA;AACe,IAAA;AAAA;AAEF,IAAA;AAC2C,IAAA;AAC9B,IAAA;AACpC,EAAA;AACF;AAEiE;AAC/D,EAAA;AACc,EAAA;AACG,EAAA;AACD,EAAA;AACD,EAAA;AACjB;AAO0F;AACxD,EAAA;AACxB,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AACW,IAAA;AACzB,IAAA;AACnC,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACyC,QAAA;AACjB,QAAA;AACA,QAAA;AACI,QAAA;AACC,QAAA;AACjC,QAAA;AAC8C,QAAA;AACtD,MAAA;AACkB,MAAA;AACI,MAAA;AACxB,IAAA;AACqD,IAAA;AACO,IAAA;AACFC,MAAAA;AACzC,MAAA;AACR,QAAA;AACkB,UAAA;AACrB,UAAA;AACG,QAAA;AACiB,UAAA;AACpB,UAAA;AACG,QAAA;AACwB,UAAA;AAC3B,UAAA;AACG,QAAA;AACW,QAAA;AACiC,UAAA;AACM,UAAA;AACnC,YAAA;AAClB,UAAA;AACsD,UAAA;AACd,UAAA;AACxC,UAAA;AACF,QAAA;AACA,QAAA;AACiD,UAAA;AACnD,MAAA;AACyB,MAAA;AAC3B,IAAA;AACF,EAAA;AAEiC,EAAA;AACzB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACwB,QAAA;AACA,QAAA;AACI,QAAA;AACiB,QAAA;AACX,QAAA;AACW,QAAA;AACT,QAAA;AACtC,QAAA;AACA,UAAA;AAC4B,UAAA;AACrB,UAAA;AACf,QAAA;AACM,QAAA;AACR,MAAA;AACsB,MAAA;AACxB,IAAA;AACsD,IAAA;AACA,IAAA;AACrC,MAAA;AACgB,QAAA;AACE,QAAA;AACiC,QAAA;AAClE,MAAA;AAC+B,MAAA;AACuB,QAAA;AACK,MAAA;AACP,QAAA;AAC7C,MAAA;AACW,QAAA;AAClB,MAAA;AACyB,MAAA;AAC3B,IAAA;AACF,EAAA;AAE0B,EAAA;AAClB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AAC6C,QAAA;AACH,QAAA;AACtD,MAAA;AACyB,MAAA;AACH,MAAA;AACxB,IAAA;AACoD,IAAA;AACvB,IAAA;AAC8D,MAAA;AACzC,MAAA;AAC8BA,MAAAA;AACrD,MAAA;AAC3B,IAAA;AACF,EAAA;AAE0B,EAAA;AAClB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACyC,QAAA;AACN,QAAA;AACO,QAAA;AACC,QAAA;AACC,QAAA;AACD,QAAA;AACvD,MAAA;AACsB,MAAA;AACxB,IAAA;AACyD,IAAA;AAC9B,IAAA;AACuB,MAAA;AACf,MAAA;AACH,QAAA;AACQ,QAAA;AACR,QAAA;AACE,QAAA;AACE,QAAA;AACE,QAAA;AACnC,MAAA;AACwB,MAAA;AAC3B,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC8B,QAAA;AACY,QAAA;AACb,QAAA;AACY,QAAA;AACG,QAAA;AACxD,MAAA;AACsB,MAAA;AACxB,IAAA;AAC8B,IAAA;AAC4B,IAAA;AACpB,MAAA;AACY,MAAA;AAClC,MAAA;AACqC,QAAA;AACA,QAAA;AACnD,MAAA;AACI,MAAA;AAC6B,MAAA;AACwB,QAAA;AACJ,QAAA;AAC9C,QAAA;AAC4B,MAAA;AACE,QAAA;AACjC,UAAA;AACA,UAAA;AACuC,UAAA;AACxC,QAAA;AACI,MAAA;AACW,QAAA;AAClB,MAAA;AAC8C,MAAA;AACT,MAAA;AACoB,MAAA;AAC3D,IAAA;AACF,EAAA;AAE2D,EAAA;AAC7D;AjC+9TiE;AACA;AkCltU/C;AAGyE;AAChD,EAAA;AAChB,EAAA;AACC,EAAA;AACY,EAAA;AACjB,EAAA;AACmC,EAAA;AACR,EAAA;AACW,IAAA;AAC3D,EAAA;AACiC,EAAA;AAC1B,EAAA;AACT;AAGoH;AAChF,EAAA;AAEC,EAAA;AACwB,EAAA;AAChC,EAAA;AACmC,EAAA;AACnB,EAAA;AACvB,EAAA;AACC,EAAA;AACc,EAAA;AACI,EAAA;AACtB,EAAA;AAE+B,EAAA;AACjB,EAAA;AACT,IAAA;AACwB,IAAA;AAC9C,EAAA;AAC8C,EAAA;AACC,EAAA;AACgB,EAAA;AAClC,EAAA;AACT,IAAA;AACM,IAAA;AAC2B,IAAA;AACD,MAAA;AACS,MAAA;AAC3D,IAAA;AACgB,IAAA;AACD,MAAA;AACS,MAAA;AACF,MAAA;AACM,MAAA;AAC6B,MAAA;AACvC,MAAA;AAClB,IAAA;AACF,EAAA;AACsD,EAAA;AACS,EAAA;AACxD,EAAA;AACT;AAS6F;AACvE,EAAA;AAC8B,IAAA;AACa,IAAA;AAC/D,EAAA;AACO,EAAA;AACE,IAAA;AACE,IAAA;AACkD,IAAA;AACjD,IAAA;AACA,IAAA;AACgD,IAAA;AAC5D,EAAA;AACF;AlCssUiE;AACA;AmCrxUN;AACE;AACI;AAEjB;AAEwB;AAGZ;AACH,EAAA;AAC7C,EAAA;AACC,EAAA;AACa,EAAA;AACC,EAAA;AACa,EAAA;AACnB,IAAA;AACH,IAAA;AACkC,IAAA;AACE,IAAA;AACpD,EAAA;AACgD,EAAA;AAClD;AAGiE;AACpB,EAAA;AACf,EAAA;AACmC,EAAA;AACxD,EAAA;AACT;AAEuE;AACE,EAAA;AACV,EAAA;AACtD,EAAA;AACM,IAAA;AACI,IAAA;AACL,IAAA;AAC+C,MAAA;AACI,MAAA;AAC7D,IAAA;AAC6D,IAAA;AACA,IAAA;AAC/D,EAAA;AACF;AAQqF;AAC9C,EAAA;AACW,IAAA;AACG,IAAA;AAC/B,IAAA;AACkB,MAAA;AACuB,MAAA;AAC3D,IAAA;AACqC,IAAA;AACvC,EAAA;AACuE,EAAA;AAChD,EAAA;AACS,IAAA;AAC8B,IAAA;AACF,IAAA;AACnD,IAAA;AACyB,MAAA;AACC,MAAA;AACY,MAAA;AACxB,MAAA;AACA,MAAA;AACwC,MAAA;AAC7D,IAAA;AACF,EAAA;AAC+D,EAAA;AACpC,EAAA;AACuB,IAAA;AACT,IAAA;AAChB,IAAA;AACzB,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AACM,IAAA;AACD,IAAA;AACtD,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AAEL,IAAA;AAEW,IAAA;AACL,MAAA;AACM,MAAA;AACwC,QAAA;AACpD,MAAA;AACsB,MAAA;AACxB,IAAA;AACoD,IAAA;AAC7B,IAAA;AACa,MAAA;AACV,MAAA;AACO,MAAA;AACjC,IAAA;AACF,EAAA;AAE+B,EAAA;AACvB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACH,QAAA;AACkC,QAAA;AACe,QAAA;AACL,QAAA;AACd,QAAA;AACvC,MAAA;AACkB,MAAA;AACI,MAAA;AACxB,IAAA;AACuD,IAAA;AACA,IAAA;AACnC,MAAA;AAC8C,MAAA;AAC9C,QAAA;AAClB,MAAA;AAC0D,MAAA;AACgC,MAAA;AACzD,MAAA;AACnC,IAAA;AACF,EAAA;AAE+B,EAAA;AACvB,IAAA;AACC,IAAA;AACM,IAAA;AACgD,IAAA;AACT,IAAA;AAC5B,IAAA;AACJ,MAAA;AACoB,MAAA;AACM,MAAA;AAC9C,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACH,QAAA;AACyC,QAAA;AAClD,MAAA;AACwB,MAAA;AACF,MAAA;AACxB,IAAA;AACwD,IAAA;AAC5B,IAAA;AACU,MAAA;AACkB,MAAA;AACvB,MAAA;AACsB,MAAA;AACT,MAAA;AACM,MAAA;AACpD,IAAA;AACF,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACoC,QAAA;AACA,QAAA;AACA,QAAA;AACC,QAAA;AACjD,MAAA;AACsC,MAAA;AAChB,MAAA;AACxB,IAAA;AAC4D,IAAA;AACtB,IAAA;AACT,MAAA;AAC6B,MAAA;AACJ,QAAA;AACpD,MAAA;AACyD,MAAA;AACzC,MAAA;AAClB,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AACM,IAAA;AACkC,IAAA;AACc,IAAA;AAC9C,IAAA;AACoC,MAAA;AAC5B,MAAA;AACL,MAAA;AAClB,IAAA;AACF,EAAA;AAE+B,EAAA;AACvB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACkD,MAAA;AACpC,MAAA;AACE,MAAA;AACxB,IAAA;AAC6C,IAAA;AACnB,IAAA;AACX,MAAA;AACiB,MAAA;AACd,MAAA;AAClB,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACqD,MAAA;AAC1C,MAAA;AACK,MAAA;AACxB,IAAA;AACiD,IAAA;AAC1B,IAAA;AACR,MAAA;AACwC,MAAA;AACxB,MAAA;AACb,MAAA;AAClB,IAAA;AACF,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AACM,IAAA;AACkC,IAAA;AACM,IAAA;AACtC,IAAA;AAC6C,MAAA;AAC3C,MAAA;AACjB,IAAA;AACF,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AACM,IAAA;AACkC,IAAA;AACM,IAAA;AACtC,IAAA;AAC6C,MAAA;AAC3C,MAAA;AACjB,IAAA;AACF,EAAA;AAE8DC,EAAAA;AAChE;AnCsvUiE;AACA;AoCjhVN;AACE;AACI;AACC;AAEM;AAGkB;AACtE,EAAA;AACmB,EAAA;AACL,IAAA;AACJ,IAAA;AACD,IAAA;AACA,IAAA;AAC3B,EAAA;AACkB,EAAA;AACQ,IAAA;AACsB,IAAA;AAChD,EAAA;AACO,EAAA;AACT;AAW2B;AACY,EAAA;AACzB,IAAA;AACE,IAAA;AACuC,IAAA;AACpC,IAAA;AACa,IAAA;AACf,IAAA;AACiB,IAAA;AACa,IAAA;AACf,IAAA;AACP,IAAA;AACvB,EAAA;AACoB,EAAA;AACb,IAAA;AACwB,MAAA;AACI,MAAA;AACe,MAAA;AACW,MAAA;AACX,MAAA;AAC9C,MAAA;AACG,IAAA;AACc,MAAA;AACmC,MAAA;AACpD,MAAA;AACG,IAAA;AACqD,MAAA;AACxD,MAAA;AACG,IAAA;AACoC,MAAA;AACmB,MAAA;AAC1D,MAAA;AACJ,EAAA;AACO,EAAA;AACT;AAEgD;AAC9B;AACwB,EAAA;AACA,EAAA;AACC,EAAA;AACU,EAAA;AACrD;AACiE;AAEK;AACzC,EAAA;AAC4B,EAAA;AACA,EAAA;AACE,EAAA;AACN,EAAA;AAC5C,EAAA;AACT;AAUsF;AACnD,EAAA;AACD,IAAA;AACI,IAAA;AACjB,IAAA;AACV,IAAA;AAC+C,MAAA;AACxB,MAAA;AACjB,MAAA;AACb,IAAA;AACF,EAAA;AACkD,EAAA;AACH,IAAA;AACN,IAAA;AAChC,IAAA;AACT,EAAA;AAE8B,EAAA;AACtB,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AACI,IAAA;AACpC,IAAA;AACiB,MAAA;AACvB,MAAA;AAC0B,QAAA;AACd,QAAA;AACD,QAAA;AAC+B,QAAA;AACjD,MAAA;AACF,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC2B,QAAA;AACe,QAAA;AACtD,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACiDC,IAAAA;AACf,IAAA;AACP,MAAA;AACW,MAAA;AACY,MAAA;AACmC,MAAA;AAC5B,MAAA;AACzD,IAAA;AACF,EAAA;AAEiC,EAAA;AACzB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC2B,QAAA;AACC,QAAA;AACA,QAAA;AACH,QAAA;AACA,QAAA;AACgB,QAAA;AAC5C,QAAA;AACJ,QAAA;AACL,MAAA;AACsB,MAAA;AACxB,IAAA;AACoD,IAAA;AAC3B,IAAA;AACyC,MAAA;AACC,MAAA;AACpD,MAAA;AACA,QAAA;AACH,QAAA;AACA,QAAA;AACM,QAAA;AACF,QAAA;AACS,QAAA;AACvB,MAAA;AAGY,MAAA;AAEiB,MAAA;AAC/B,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACmD,MAAA;AACrC,MAAA;AACE,MAAA;AACxB,IAAA;AACoD,IAAA;AACD,IAAA;AACrD,EAAA;AAE0B,EAAA;AAClB,IAAA;AACC,IAAA;AAEL,IAAA;AACoDC,IAAAA;AACC,IAAA;AAC7B,IAAA;AACC,MAAA;AACA,MAAA;AACiC,MAAA;AAC5D,IAAA;AACF,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACwC,MAAA;AAC1B,MAAA;AACE,MAAA;AACxB,IAAA;AAC4D,IAAA;AACzC,IAAA;AACa,MAAA;AACoB,MAAA;AACpD,IAAA;AACF,EAAA;AAE4B,EAAA;AACpB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDA,QAAAA;AACwC,QAAA;AACR,QAAA;AACU,QAAA;AACI,QAAA;AACH,QAAA;AACX,QAAA;AAClC,QAAA;AACC,UAAA;AACO,UAAA;AACD,UAAA;AAC8B,YAAA;AACM,YAAA;AACJ,YAAA;AACL,YAAA;AACvC,UAAA;AACsB,UAAA;AACxB,QAAA;AAC2C,QAAA;AAC7C,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACiDD,IAAAA;AAC9B,IAAA;AACa,MAAA;AACU,MAAA;AACO,MAAA;AACL,MAAA;AACH,MAAA;AACV,MAAA;AAC/B,IAAA;AACF,EAAA;AAEyB,EAAA;AACjB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC4C,QAAA;AACjB,QAAA;AACvC,MAAA;AACqB,MAAA;AACC,MAAA;AACxB,IAAA;AAC+C,IAAA;AACd,IAAA;AACwC,MAAA;AACS,MAAA;AAClF,IAAA;AACF,EAAA;AAE4B,EAAA;AACpB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC2B,QAAA;AACe,QAAA;AACjD,QAAA;AACL,MAAA;AACsB,MAAA;AACxB,IAAA;AACqD,IAAA;AAClC,IAAA;AACgD,MAAA;AACP,MAAA;AAC5D,IAAA;AACF,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AACM,IAAA;AAC8CC,IAAAA;AACT,IAAA;AACA,IAAA;AACpD,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACyDA,IAAAA;AACA,IAAA;AACF,IAAA;AAC3D,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACkC,IAAA;AACc,IAAA;AACJ,IAAA;AAC3D,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AACM,IAAA;AACkC,IAAA;AACW,IAAA;AACD,IAAA;AAC3D,EAAA;AAE0B,EAAA;AAClB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDA,QAAAA;AAC0B,QAAA;AACA,QAAA;AACC,QAAA;AACA,QAAA;AACtC,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACyD,IAAA;AACjB,IAAA;AACb,MAAA;AACmC,MAAA;AACK,MAAA;AACiB,MAAA;AACpF,IAAA;AACF,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACDA,QAAAA;AACgD,QAAA;AACN,QAAA;AACG,QAAA;AACxD,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACmD,IAAA;AACD,IAAA;AACvB,MAAA;AACiF,MAAA;AACxG,QAAA;AACA,QAAA;AACD,MAAA;AAC6C,MAAA;AAChD,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AACM,IAAA;AACyCA,IAAAA;AACK,IAAA;AAC3B,IAAA;AACL,MAAA;AACS,MAAA;AACL,MAAA;AAC/B,IAAA;AACF,EAAA;AAE8B,EAAA;AACtB,IAAA;AACC,IAAA;AACM,IAAA;AACyCA,IAAAA;AACM,IAAA;AAClC,IAAA;AACC,MAAA;AACI,MAAA;AACA,MAAA;AAC/B,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AACM,MAAA;AACDA,QAAAA;AAC0C,QAAA;AACG,QAAA;AACb,QAAA;AAC3C,MAAA;AACoB,MAAA;AACE,MAAA;AACxB,IAAA;AACsDD,IAAAA;AACD,IAAA;AAC1B,MAAA;AACsC,MAAA;AACyB,MAAA;AAC1C,MAAA;AAChD,IAAA;AACF,EAAA;AAE2B,EAAA;AACnB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC2B,QAAA;AACA,QAAA;AACA,QAAA;AACvC,MAAA;AACsB,MAAA;AACxB,IAAA;AACyDE,IAAAA;AACpB,IAAA;AACM,MAAA;AACU,MAAA;AACrD,IAAA;AACF,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACoDD,IAAAA;AACI,IAAA;AACD,IAAA;AAC3D,EAAA;AAEmB,EAAA;AACwB,IAAA;AACU,IAAA;AACQ,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACG,IAAA;AACP,IAAA;AACvD,EAAA;AACwD,EAAA;AAChB,IAAA;AACmB,IAAA;AAClD,IAAA;AACT,EAAA;AAE6B,EAAA;AACrB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACmD,MAAA;AAC7B,MAAA;AACN,MAAA;AACxB,IAAA;AACqD,IAAA;AAClC,IAAA;AACa,MAAA;AAC6B,MAAA;AAC7B,MAAA;AAChC,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AAC6C,MAAA;AACnB,MAAA;AACV,MAAA;AACxB,IAAA;AACkDD,IAAAA;AAC/B,IAAA;AACa,MAAA;AACG,MAAA;AACW,MAAA;AACF,MAAA;AAC5C,IAAA;AACF,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AACM,IAAA;AACA,IAAA;AACL,MAAA;AAC6C,MAAA;AACnB,MAAA;AACV,MAAA;AACxB,IAAA;AACkD,IAAA;AACd,IAAA;AACT,MAAA;AACiB,MAAA;AAC5C,IAAA;AACF,EAAA;AAEO,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;ApCi9UiE;AACA;AqCniWJ;AACK;AAEnD;AAC2B,EAAA;AACa,EAAA;AACvD;AAEc;AACN,EAAA;AACO,EAAA;AACD,EAAA;AACmD,IAAA;AACT,IAAA;AACG,IAAA;AACD,IAAA;AACE,IAAA;AACL,IAAA;AACU,IAAA;AACL,IAAA;AACV,IAAA;AACG,IAAA;AACY,IAAA;AAC/D,EAAA;AACsB,EAAA;AACxB;AAEgG;AACS,EAAA;AACzG;AAOsF;AACrD,EAAA;AACvB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACyC,QAAA;AAC3C,QAAA;AACA,UAAA;AACI,UAAA;AACA,UAAA;AACG,UAAA;AACN,UAAA;AACC,YAAA;AACM,YAAA;AACwB,cAAA;AACA,cAAA;AACe,cAAA;AACnD,YAAA;AACmB,YAAA;AACG,YAAA;AACxB,UAAA;AACF,QAAA;AACsC,QAAA;AAC/B,QAAA;AACJ,QAAA;AACL,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACuD,IAAA;AAE9B,IAAA;AACV,MAAA;AACE,MAAA;AACD,MAAA;AACA,MAAA;AACM,MAAA;AACnB,IAAA;AACL,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC4B,QAAA;AACa,QAAA;AACK,QAAA;AACV,QAAA;AAC3C,QAAA;AACL,MAAA;AACsB,MAAA;AACxB,IAAA;AAC8C,IAAA;AAEtB,IAAA;AACR,MAAA;AACE,MAAA;AACF,MAAA;AACO,MAAA;AACD,MAAA;AACnB,IAAA;AACL,EAAA;AAEyB,EAAA;AACjB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACyC,QAAA;AACL,QAAA;AAC3C,QAAA;AACL,MAAA;AACsB,MAAA;AACxB,IAAA;AACqD,IAAA;AAEZ,IAAA;AAC3C,EAAA;AAEgC,EAAA;AAClC;ArCmhWiE;AACA;AsClpWN;AACO;AAErD;AACL,EAAA;AACK,EAAA;AACE,EAAA;AACf;AAG8D;AACJ,EAAA;AACG,EAAA;AACC,EAAA;AAC5B,EAAA;AAC6B,EAAA;AACtD,EAAA;AACT;AAMoF;AACvD,EAAA;AACnB,IAAA;AACC,IAAA;AAEL,IAAA;AACyD,IAAA;AACZ,IAAA;AACxB,IAAA;AAC6B,MAAA;AAC3C,MAAA;AACM,QAAA;AACC,QAAA;AACL,QAAA;AACuB,QAAA;AACkB,QAAA;AAClC,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AAEL,IAAA;AACyD,IAAA;AACJG,IAAAA;AAChC,IAAA;AACkC,MAAA;AACzB,MAAA;AACvB,MAAA;AACM,QAAA;AACC,QAAA;AACE,QAAA;AACiC,QAAA;AAC9B,QAAA;AACH,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AAEwB,EAAA;AAChB,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AACUC,IAAAA;AACpC,IAAA;AACgB,MAAA;AAC5B,MAAA;AACwB,QAAA;AACnB,QAAA;AACM,QAAA;AACP,QAAA;AACE,QAAA;AACC,QAAA;AACE,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AAE2B,EAAA;AAC7B;AtCsoWiE;AACA;AuCnsW9C;AAEwC;AAClD,EAAA;AAC+C,IAAA;AACV,IAAA;AACe,IAAA;AAC3D,EAAA;AACF;AAuBqB;AACe,EAAA;AAC1B,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AAClC,IAAA;AACJ,MAAA;AACO,MAAA;AACG,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACwD,IAAA;AAC1D,EAAA;AAEgC,EAAA;AACxB,IAAA;AACC,IAAA;AAEL,IAAA;AAC6C,IAAA;AAClC,IAAA;AACJ,MAAA;AACO,MAAA;AACG,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACgB,IAAA;AACwC,MAAA;AACpD,QAAA;AAAI,QAAA;AAAO,QAAA;AAAS,QAAA;AAAK,QAAA;AAAK,QAAA;AAAe,QAAA;AAAkD,QAAA;AAC/F,MAAA;AACqD,MAAA;AACrD,QAAA;AACA,QAAA;AAC+C,QAAA;AAC7C,UAAA;AAAW,UAAA;AAAG,UAAA;AAAK,UAAA;AAAK,UAAA;AAAe,UAAA;AACvC,QAAA;AACF,MAAA;AACqC,MAAA;AACtB,MAAA;AACnB,IAAA;AACF,EAAA;AAEkC,EAAA;AAC1B,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AACF,QAAA;AACA,UAAA;AACO,UAAA;AAC0B,UAAA;AACzC,QAAA;AACsC,QAAA;AACxC,MAAA;AACmB,MAAA;AACG,MAAA;AACxB,IAAA;AACa,IAAA;AACJ,MAAA;AACO,MAAA;AACG,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACgC,IAAA;AAC0C,MAAA;AACnC,MAAA;AACH,MAAA;AACO,MAAA;AACY,MAAA;AACZ,MAAA;AACd,MAAA;AACA,MAAA;AACM,MAAA;AACH,QAAA;AACrB,QAAA;AACyC,UAAA;AACQ,UAAA;AACjC,UAAA;AACJ,YAAA;AACf,YAAA;AACF,UAAA;AACqC,UAAA;AACP,UAAA;AACad,UAAAA;AACW,UAAA;AACxCA,UAAAA;AACd,UAAA;AACF,QAAA;AACqC,QAAA;AAC1B,QAAA;AACM,UAAA;AACf,UAAA;AACF,QAAA;AACqC,QAAA;AACP,QAAA;AACS,QAAA;AACzB,QAAA;AAChB,MAAA;AACoB,MAAA;AACsC,QAAA;AAC9C,QAAA;AACoC,UAAA;AAC9C,QAAA;AACF,MAAA;AAC2D,MAAA;AACN,MAAA;AACM,MAAA;AACJ,MAAA;AAC9B,MAAA;AACwB,MAAA;AACI,QAAA;AACrD,MAAA;AACiC,MAAA;AAC1B,MAAA;AACT,IAAA;AACF,EAAA;AAE4B,EAAA;AACpB,IAAA;AACC,IAAA;AAEL,IAAA;AAGW,IAAA;AACL,MAAA;AACM,MAAA;AAC0C,QAAA;AACb,QAAA;AACY,QAAA;AACK,QAAA;AACnB,QAAA;AACG,QAAA;AACC,QAAA;AAC3C,MAAA;AACiB,MAAA;AACK,MAAA;AACxB,IAAA;AACa,IAAA;AACJ,MAAA;AACO,MAAA;AACG,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACyD,IAAA;AACV,MAAA;AACa,MAAA;AACK,MAAA;AACxB,MAAA;AAC+B,QAAA;AACtE,MAAA;AACuE,MAAA;AACU,MAAA;AACnC,MAAA;AACY,MAAA;AAC5D,IAAA;AACF,EAAA;AAEiC,EAAA;AACzB,IAAA;AACC,IAAA;AAEL,IAAA;AACW,IAAA;AACL,MAAA;AACM,MAAA;AAC8C,QAAA;AAChB,QAAA;AAC1C,MAAA;AACsB,MAAA;AACxB,IAAA;AACa,IAAA;AACJ,MAAA;AACO,MAAA;AACG,MAAA;AACD,MAAA;AACD,MAAA;AACjB,IAAA;AACyC,IAAA;AAC+B,MAAA;AACX,MAAA;AACzB,MAAA;AACgB,MAAA;AACG,MAAA;AACd,MAAA;AACD,MAAA;AACQ,MAAA;AACH,MAAA;AACjC,QAAA;AAC4B,QAAA;AACC,QAAA;AAC4B,QAAA;AAClE,MAAA;AACgC,MAAA;AACyB,MAAA;AACxC,QAAA;AAClB,MAAA;AACO,MAAA;AACI,QAAA;AAC0C,UAAA;AACC,UAAA;AACpD,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAEO,EAAA;AACa,IAAA;AACC,IAAA;AACZ,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAC2D,MAAA;AACpB,MAAA;AACL,MAAA;AACC,MAAA;AACA,MAAA;AACF,MAAA;AACnC,IAAA;AACF,EAAA;AACF;AvC8qWiE;AACA;AwCh9WS;AAG8B;AACzC,EAAA;AAC/D;AAGiG;AACjC,EAAA;AAChE;AAGiH;AAC1E,EAAA;AACd,EAAA;AACf,EAAA;AAC2B,EAAA;AACnB,IAAA;AACiB,IAAA;AAC4B,IAAA;AAC7D,EAAA;AACO,EAAA;AACT;AASW;AAEE,EAAA;AACU,EAAA;AACI,EAAA;AACmB,IAAA;AACF,IAAA;AACxB,IAAA;AACP,MAAA;AACC,MAAA;AACV,IAAA;AACD,EAAA;AACM,EAAA;AACT;AAGwH;AACvD,EAAA;AACL,EAAA;AACJ,EAAA;AACxB,EAAA;AACJ,EAAA;AACE,EAAA;AAC9B;AAG0G;AAChE,EAAA;AACK,EAAA;AACK,EAAA;AACa,EAAA;AACF,EAAA;AACtD,EAAA;AACT;AAG6F;AAC5B,EAAA;AACFD,EAAAA;AAC/D;AAEoD;AxC47Wa;AACA;AyCz/WnC;AAW1B,EAAA;AAFS,IAAA;AACQ,IAAA;AAChB,EAAA;AAFQ,EAAA;AACQ,EAAA;AATgB,mBAAA;AACH,mBAAA;AACK,mBAAA;AACE,mBAAA;AAAA;AAET,mBAAA;AAOR,EAAA;AACE,IAAA;AACxB,EAAA;AAAA;AAGqC,EAAA;AACM,IAAA;AACR,IAAA;AAC0B,IAAA;AAChC,IAAA;AACX,MAAA;AACA,MAAA;AACqB,MAAA;AACC,MAAA;AACrC,IAAA;AAC6C,IAAA;AACG,IAAA;AAC1C,IAAA;AACkB,MAAA;AACa,MAAA;AACc,MAAA;AACb,MAAA;AACvC,IAAA;AACF,EAAA;AAAA;AAGgF,EAAA;AAC9C,IAAA;AACnB,IAAA;AACI,IAAA;AACY,IAAA;AACd,MAAA;AACT,MAAA;AACqC,QAAA;AAClB,UAAA;AACC,UAAA;AACD,UAAA;AACD,UAAA;AACI,UAAA;AACN,UAAA;AACoC,UAAA;AACrD,QAAA;AACW,MAAA;AAC6C,QAAA;AAC3D,MAAA;AACA,MAAA;AACF,IAAA;AACa,IAAA;AACE,IAAA;AACK,IAAA;AACtB,EAAA;AAAA;AAGiC,EAAA;AACP,IAAA;AACT,IAAA;AACyB,IAAA;AACS,MAAA;AACW,MAAA;AACpC,MAAA;AACxB,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAG+D,EAAA;AACrC,IAAA;AACE,IAAA;AACM,IAAA;AAC0B,IAAA;AACxC,IAAA;AACS,IAAA;AACX,MAAA;AACA,MAAA;AACsB,MAAA;AACC,MAAA;AACtC,IAAA;AACY,IAAA;AACE,IAAA;AACA,IAAA;AACwC,IAAA;AACL,IAAA;AACW,IAAA;AACT,IAAA;AACtD,EAAA;AAAA;AAGe,EAAA;AACA,IAAA;AACE,IAAA;AACA,IAAA;AACjB,EAAA;AACF;AzCs/WiE;AACA;A0CtlXJ;AACA,EAAA;AACpD,IAAA;AACR,EAAA;AAEgB,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAOf,EAAA;AASgB,EAAA;AACN,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAC4B,EAAA;AAEnB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACpB,EAAA;AAEyD,EAAA;AACC,EAAA;AACG,EAAA;AACC,EAAA;AAE7B,EAAA;AACT,EAAA;AACzB;AAUK;AAC4B,EAAA;AACc,EAAA;AAEhB,EAAA;AAC2B,IAAA;AAC3C,IAAA;AAEc,IAAA;AAEY,IAAA;AACL,MAAA;AACM,MAAA;AACL,QAAA;AACtB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACQ,QAAA;AAEmB,QAAA;AACd,UAAA;AACe,UAAA;AACN,YAAA;AACf,YAAA;AACI,YAAA;AACA,YAAA;AACA,YAAA;AACrB,UAAA;AACD,QAAA;AAEoC,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AAEE,QAAA;AACd,UAAA;AACe,UAAA;AACN,YAAA;AACnB,YAAA;AACI,YAAA;AACA,YAAA;AACA,YAAA;AACjB,UAAA;AACD,QAAA;AACD,MAAA;AACD,IAAA;AACD,EAAA;AAEO,EAAA;AACR;AAUK;AAC4B,EAAA;AACwB,EAAA;AAE1B,EAAA;AAC2B,IAAA;AAC3C,IAAA;AAEc,IAAA;AACX,IAAA;AACA,IAAA;AAC8B,IAAA;AAGjB,IAAA;AACV,MAAA;AACW,MAAA;AACjB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACC,QAAA;AAEsB,QAAA;AACG,QAAA;AACG,QAAA;AACK,UAAA;AACzB,UAAA;AACI,UAAA;AACA,UAAA;AACA,UAAA;AACvB,UAAA;AACD,QAAA;AAE8B,QAAA;AACQ,QAAA;AACI,QAAA;AACA,QAAA;AACA,QAAA;AAC3C,MAAA;AACD,IAAA;AAG6B,IAAA;AACV,MAAA;AACW,MAAA;AACjB,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACC,QAAA;AAEsB,QAAA;AACG,QAAA;AACG,QAAA;AACR,UAAA;AACZ,UAAA;AACI,UAAA;AACA,UAAA;AACA,UAAA;AACvB,UAAA;AACD,QAAA;AAE2C,QAAA;AACL,QAAA;AACI,QAAA;AACA,QAAA;AACA,QAAA;AAC3C,MAAA;AACD,IAAA;AACD,EAAA;AAEO,EAAA;AACR;A1CkiXiE;AACA;A2CpuXuC;AACvE,EAAA;AACuD,EAAA;AACzC,IAAA;AACrB,MAAA;AACV,MAAA;AACR,QAAA;AACA,QAAA;AACA,QAAA;AACO,QAAA;AACc,QAAA;AACE,QAAA;AACO,QAAA;AAC/B,MAAA;AAC0D,MAAA;AAC7D,IAAA;AACF,EAAA;AACoB,EAAA;AACb,EAAA;AACT;AAEsD;AACvB,EAAA;AAC+B,EAAA;AAClB,EAAA;AAC5C;AAGsD;AAChC,EAAA;AACb,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AAC6C,MAAA;AACpD,EAAA;AACF;AAMuE;AACb,EAAA;AACtB,EAAA;AACpC;AAGsI;AACpG,EAAA;AACS,EAAA;AACrB,EAAA;AAC2C,EAAA;AAEF,EAAA;AAC/D;AA6B8E;AAC9C,EAAA;AACW,EAAA;AACa,EAAA;AACO,EAAA;AACf,EAAA;AACpB,EAAA;AACO,EAAA;AAC1B,EAAA;AACoD,IAAA;AACA,IAAA;AACC,IAAA;AACE,IAAA;AACD,IAAA;AACH,IAAA;AACF,IAAA;AACM,IAAA;AACJ,IAAA;AACA,IAAA;AACF,IAAA;AACE,IAAA;AACjB,IAAA;AACqB,IAAA;AACP,IAAA;AACK,IAAA;AACA,IAAA;AACR,IAAA;AACpD,EAAA;AACF;AAG6H;AAClF,EAAA;AACtB,EAAA;AACf,EAAA;AACc,IAAA;AACT,MAAA;AACsB,QAAA;AACzB,QAAA;AACG,MAAA;AACiB,QAAA;AACpB,QAAA;AACG,MAAA;AACiC,QAAA;AACpC,QAAA;AACG,MAAA;AAC8B,QAAA;AACjC,QAAA;AACG,MAAA;AACgC,QAAA;AACnC,QAAA;AACG,MAAA;AAC+B,QAAA;AAClC,QAAA;AACG,MAAA;AACkC,QAAA;AACrC,QAAA;AACG,MAAA;AAC2B,QAAA;AAC9B,QAAA;AACG,MAAA;AAC2B,QAAA;AAC9B,QAAA;AACG,MAAA;AACA,MAAA;AACkD,QAAA;AACrD,QAAA;AACG,MAAA;AAC6C,QAAA;AAChD,QAAA;AACG,MAAA;AAC4C,QAAA;AAC/C,QAAA;AACG,MAAA;AACuC,QAAA;AAC1C,QAAA;AACG,MAAA;AACkC,QAAA;AACrC,QAAA;AACG,MAAA;AACsD,QAAA;AACzD,QAAA;AACG,MAAA;AACuC,QAAA;AAC1C,QAAA;AACG,MAAA;AACmD,QAAA;AACtD,QAAA;AACJ,IAAA;AACO,IAAA;AACK,EAAA;AAC0C,IAAA;AACxD,EAAA;AACF;A3C6rXiE;AACA;A4Cj4XnC;AAGkG;AAClE,EAAA;AACG,EAAA;AACF,EAAA;AACE,EAAA;AACD,EAAA;AAChE;AAG2B;AACkB,EAAA;AACE,EAAA;AACE,EAAA;AACjD;AAI4F;AAGpD;AACmB,EAAA;AAC3D;AAYiC;AACpB,EAAA;AACkD,EAAA;AAC/D;AAOkG;AAClC,EAAA;AACpB,EAAA;AACwC,EAAA;AACpE,EAAA;AACa,EAAA;AACpB,IAAA;AACuB,MAAA;AACnB,MAAA;AACJ,IAAA;AACwB,MAAA;AACpB,MAAA;AACJ,IAAA;AACsB,MAAA;AAClB,MAAA;AACJ,IAAA;AACyB,MAAA;AACrB,MAAA;AACJ,IAAA;AACwB,MAAA;AACpB,MAAA;AACX,EAAA;AACgC,EAAA;AAC6B,IAAA;AACF,IAAA;AAClD,IAAA;AACT,EAAA;AACgC,EAAA;AAC6B,IAAA;AACJ,IAAA;AAChD,IAAA;AACT,EAAA;AAC6D,EAAA;AACR,IAAA;AAC5C,IAAA;AACT,EAAA;AACO,EAAA;AACT;AASwF;AACjC,EAAA;AAC9C,EAAA;AACT;AAM0B;AAMwC,EAAA;AAAnC,IAAA;AAAoC,EAAA;AAApC,EAAA;AALe,mBAAA;AAC+D,mBAAA;AAAA;AAEpF,mBAAA;AAID,EAAA;AACmB,IAAA;AACzC,EAAA;AAEgD,EAAA;AACjC,IAAA;AAC2B,IAAA;AACpC,IAAA;AACmB,MAAA;AAC0B,QAAA;AACjB,QAAA;AACgB,QAAA;AACa,QAAA;AACJ,QAAA;AAC9C,QAAA;AACT,MAAA;AAC2C,MAAA;AACS,QAAA;AACzB,QAAA;AAClB,QAAA;AACT,MAAA;AACY,IAAA;AAC6C,MAAA;AAC3C,MAAA;AACF,MAAA;AACd,IAAA;AACY,IAAA;AACd,EAAA;AAAA;AAG4C,EAAA;AACrB,IAAA;AACJ,IAAA;AACS,MAAA;AACxB,MAAA;AACF,IAAA;AACe,IAAA;AACR,IAAA;AACkC,MAAA;AACa,MAAA;AACA,MAAA;AAChD,MAAA;AAEkD,QAAA;AAC1C,QAAA;AACE,MAAA;AAC6C,QAAA;AAC7C,QAAA;AACd,MAAA;AACF,IAAA;AACF,EAAA;AAEW,EAAA;AACQ,IAAA;AACX,MAAA;AACc,QAAA;AACJ,MAAA;AAC6C,QAAA;AAC3D,MAAA;AACF,IAAA;AACc,IAAA;AACF,IAAA;AACd,EAAA;AAEe,EAAA;AACO,oBAAA;AACN,IAAA;AACF,IAAA;AACd,EAAA;AACF;A5Cw1XiE;AACA;A6ClhYlC;AAEqB;AACM,EAAA;AAChB,EAAA;AACR,IAAA;AAC4C,IAAA;AAClB,IAAA;AAC3B,IAAA;AAC9B,EAAA;AACH;AAGoG;AACjD,EAAA;AACnD;AAGgE;AACR,EAAA;AACjB,EAAA;AACvC;AAMwI;AACnG,EAAA;AACO,EAAA;AACY,EAAA;AACG,IAAA;AACnB,IAAA;AACA,IAAA;AAC3B,IAAA;AACI,IAAA;AACL,IAAA;AACoC,IAAA;AAC9C,EAAA;AAC0C,EAAA;AAC5C;A7C0gYiE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from lightcraft-main/crates/color/src/cct.rs, lightcraft-main/crates/color/src/lib.rs,\n// lightcraft-main/crates/pipeline/src/tone.rs, lightcraft-main/crates/pipeline/src/local.rs and\n// lightcraft-main/crates/develop/src/settings.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\n\n/**\n * Colour science for the image editor's process 2 rendering: sRGB transfer functions,\n * correlated colour temperature / tint (Kim et al. Planckian locus, tint as Duv), Bradford\n * white-balance adaptation relative to \"as shot\", and the display-referred tone curve.\n */\n\n/** A CIE 1931 chromaticity. */\nexport interface Chromaticity {\n x: number;\n y: number;\n}\n\n/** A row-major 3×3 matrix (`[m00, m01, m02, m10, …, m22]`). */\nexport type Matrix3 = [number, number, number, number, number, number, number, number, number];\n\n// ---------------------------------------------------------------------------\n// Process versions\n// ---------------------------------------------------------------------------\n\n/** The rendering of adjustments from before process versions existed. Frozen. */\nexport const LEGACY_PROCESS_VERSION = 1 as const;\n/** The process new edits and Reset get. */\nexport const LATEST_PROCESS_VERSION = 2 as const;\nexport type ImageProcessVersion = 1 | 2;\n\n/**\n * Reads a stored process number leniently: a missing or damaged value (not a whole number) is\n * the legacy process, so a saved edit from before process versions renders exactly as it did. A\n * whole number newer than this build knows renders with the latest process.\n */\nexport function normalizeProcessVersion(value: unknown): ImageProcessVersion {\n if (typeof value !== 'number' || !Number.isInteger(value)) return LEGACY_PROCESS_VERSION;\n return value >= LATEST_PROCESS_VERSION ? LATEST_PROCESS_VERSION : LEGACY_PROCESS_VERSION;\n}\n\n// ---------------------------------------------------------------------------\n// sRGB transfer\n// ---------------------------------------------------------------------------\n\nexport function srgbToLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\n\nexport function linearToSrgb(v: number): number {\n return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\n/** Rec.709 / sRGB luminance weights (linear light). */\nexport const LUMA_709: readonly [number, number, number] = [0.2126, 0.7152, 0.0722];\n\n// ---------------------------------------------------------------------------\n// 3×3 matrices\n// ---------------------------------------------------------------------------\n\nexport const IDENTITY_MATRIX3: Matrix3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];\n\nexport function mulMatrix3(a: Matrix3, b: Matrix3): Matrix3 {\n const out = new Array(9).fill(0) as Matrix3;\n for (let r = 0; r < 3; r++) {\n for (let c = 0; c < 3; c++) {\n out[r * 3 + c] = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];\n }\n }\n return out;\n}\n\nexport function applyMatrix3(m: Matrix3, v: readonly [number, number, number]): [number, number, number] {\n return [\n m[0] * v[0] + m[1] * v[1] + m[2] * v[2],\n m[3] * v[0] + m[4] * v[1] + m[5] * v[2],\n m[6] * v[0] + m[7] * v[1] + m[8] * v[2],\n ];\n}\n\nexport function invertMatrix3(m: Matrix3): Matrix3 | null {\n const [a, b, c, d, e, f, g, h, i] = m;\n const A = e * i - f * h;\n const B = -(d * i - f * g);\n const C = d * h - e * g;\n const det = a * A + b * B + c * C;\n if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;\n const k = 1 / det;\n return [\n A * k, -(b * i - c * h) * k, (b * f - c * e) * k,\n B * k, (a * i - c * g) * k, -(a * f - c * d) * k,\n C * k, -(a * h - b * g) * k, (a * e - b * d) * k,\n ];\n}\n\nfunction diag3(x: number, y: number, z: number): Matrix3 {\n return [x, 0, 0, 0, y, 0, 0, 0, z];\n}\n\n// ---------------------------------------------------------------------------\n// Chromaticity, sRGB primaries, Bradford adaptation\n// ---------------------------------------------------------------------------\n\nexport const D65: Chromaticity = { x: 0.3127, y: 0.329 };\n\nfunction xyToXyz(p: Chromaticity): [number, number, number] {\n return [p.x / p.y, 1, (1 - p.x - p.y) / p.y];\n}\n\nfunction xyzToXy(v: readonly [number, number, number]): Chromaticity {\n const s = v[0] + v[1] + v[2];\n return s <= 0 ? { ...D65 } : { x: v[0] / s, y: v[1] / s };\n}\n\n/** Linear sRGB → XYZ (D65 white, Y = 1), derived from the primaries. */\nconst SRGB_TO_XYZ: Matrix3 = (() => {\n const p = [{ x: 0.64, y: 0.33 }, { x: 0.3, y: 0.6 }, { x: 0.15, y: 0.06 }].map(xyToXyz);\n const m: Matrix3 = [p[0][0], p[1][0], p[2][0], p[0][1], p[1][1], p[2][1], p[0][2], p[1][2], p[2][2]];\n const inv = invertMatrix3(m);\n const s = inv ? applyMatrix3(inv, xyToXyz(D65)) : [1, 1, 1];\n return mulMatrix3(m, diag3(s[0], s[1], s[2]));\n})();\nconst XYZ_TO_SRGB: Matrix3 = invertMatrix3(SRGB_TO_XYZ) ?? IDENTITY_MATRIX3;\n\n/** The Bradford cone-response matrix (XYZ → sharpened cones). */\nconst BRADFORD: Matrix3 = [0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296];\nconst BRADFORD_INV: Matrix3 = invertMatrix3(BRADFORD) ?? IDENTITY_MATRIX3;\n\n/** Bradford chromatic adaptation (XYZ → XYZ) from white `src` to white `dst`. */\nexport function bradfordAdaptation(src: Chromaticity, dst: Chromaticity): Matrix3 {\n if (src.x === dst.x && src.y === dst.y) return [...IDENTITY_MATRIX3];\n const s = applyMatrix3(BRADFORD, xyToXyz(src));\n const d = applyMatrix3(BRADFORD, xyToXyz(dst));\n return mulMatrix3(BRADFORD_INV, mulMatrix3(diag3(d[0] / s[0], d[1] / s[1], d[2] / s[2]), BRADFORD));\n}\n\n/**\n * Matrix (linear sRGB → linear sRGB) that white-balances a scene lit by `src` so that it\n * renders as if lit by D65.\n */\nexport function wbMatrix(src: Chromaticity): Matrix3 {\n return mulMatrix3(XYZ_TO_SRGB, mulMatrix3(bradfordAdaptation(src, D65), SRGB_TO_XYZ));\n}\n\n/** The chromaticity of a linear sRGB colour. */\nexport function linearSrgbToXy(rgb: readonly [number, number, number]): Chromaticity {\n return xyzToXy(applyMatrix3(SRGB_TO_XYZ, rgb));\n}\n\n// ---------------------------------------------------------------------------\n// Correlated colour temperature and tint\n// ---------------------------------------------------------------------------\n\n/** Tint units per unit of Duv. */\nexport const TINT_SCALE = 3000;\nexport const MIN_KELVIN = 1667;\nexport const MAX_KELVIN = 25000;\n\nfunction kimXy(tIn: number): Chromaticity {\n const t = Math.min(MAX_KELVIN, Math.max(MIN_KELVIN, tIn));\n const t2 = t * t;\n const t3 = t2 * t;\n const x =\n t <= 4000\n ? -0.2661239e9 / t3 - 0.2343589e6 / t2 + 0.8776956e3 / t + 0.17991\n : -3.0258469e9 / t3 + 2.1070379e6 / t2 + 0.2226347e3 / t + 0.24039;\n const x2 = x * x;\n const x3 = x2 * x;\n const y =\n t <= 2222\n ? -1.1063814 * x3 - 1.3481102 * x2 + 2.18555832 * x - 0.20219683\n : t <= 4000\n ? -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867\n : 3.081758 * x3 - 5.8733867 * x2 + 3.75112997 * x - 0.37001483;\n return { x, y };\n}\n\n/** CIE 1931 xy → CIE 1960 uv. */\nexport function xyToUv(p: Chromaticity): [number, number] {\n const d = -2 * p.x + 12 * p.y + 3;\n return [(4 * p.x) / d, (6 * p.y) / d];\n}\n\n/** CIE 1960 uv → CIE 1931 xy. */\nexport function uvToXy(u: number, v: number): Chromaticity {\n const d = 2 * u - 8 * v + 4;\n return { x: (3 * u) / d, y: (2 * v) / d };\n}\n\n/** Planckian locus in (u, v), extended linearly in mired space outside the approximation's range. */\nfunction locusUv(tIn: number): [number, number] {\n const t = Math.max(1000, tIn);\n if (t >= MIN_KELVIN && t <= MAX_KELVIN) return xyToUv(kimXy(t));\n const [a, b] = t < MIN_KELVIN ? [MIN_KELVIN, MIN_KELVIN + 50] : [MAX_KELVIN - 500, MAX_KELVIN];\n const [ua, va] = xyToUv(kimXy(a));\n const [ub, vb] = xyToUv(kimXy(b));\n const ma = 1e6 / a;\n const mb = 1e6 / b;\n const s = (1e6 / t - ma) / (mb - ma);\n return [ua + (ub - ua) * s, va + (vb - va) * s];\n}\n\n/** Unit normal of the locus at `t`, pointing towards green (increasing v). */\nfunction locusNormal(t: number): [number, number] {\n const m = 1e6 / t;\n const [u0, v0] = locusUv(1e6 / (m + 1));\n const [u1, v1] = locusUv(1e6 / Math.max(m - 1, 1));\n const du = u1 - u0;\n const dv = v1 - v0;\n const l = Math.max(Math.hypot(du, dv), 1e-12);\n const nx = -dv / l;\n const ny = du / l;\n return ny < 0 ? [-nx, -ny] : [nx, ny];\n}\n\n/**\n * Source illuminant chromaticity for temperature `kelvin` and correction `tint`.\n * Positive tint means a green source white, whose correction adds magenta to the image.\n */\nexport function tempTintToXy(kelvin: number, tint: number): Chromaticity {\n const [u, v] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = tint / TINT_SCALE;\n return uvToXy(u + nu * duv, v + nv * duv);\n}\n\n/** Inverse of {@link tempTintToXy}: nearest locus temperature (searched in mired space) and tint. */\nexport function xyToTempTint(p: Chromaticity): { kelvin: number; tint: number } {\n const [u, v] = xyToUv(p);\n const dist = (m: number) => {\n const [lu, lv] = locusUv(1e6 / m);\n return (u - lu) ** 2 + (v - lv) ** 2;\n };\n // Coarse scan, then golden-section refine over mired 20..1000 (50000 K .. 1000 K).\n let best = 20;\n let bd = Infinity;\n for (let m = 20; m <= 1000; m += 5) {\n const d = dist(m);\n if (d < bd) {\n bd = d;\n best = m;\n }\n }\n let a = Math.max(best - 5, 20);\n let b = Math.min(best + 5, 1000);\n const g = 0.61803398875;\n for (let i = 0; i < 60; i++) {\n const c = b - g * (b - a);\n const d = a + g * (b - a);\n if (dist(c) < dist(d)) b = d;\n else a = c;\n }\n const kelvin = 1e6 / ((a + b) / 2);\n const [lu, lv] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = (u - lu) * nu + (v - lv) * nv;\n return { kelvin, tint: duv * TINT_SCALE };\n}\n\n// ---------------------------------------------------------------------------\n// Relative white balance for rendered (JPEG / PNG / WebP) sources\n// ---------------------------------------------------------------------------\n\n/** Rendered images carry no as-shot white: \"as shot\" is 6500 K / tint 0 and maps to the identity. */\nexport const AS_SHOT_KELVIN = 6500;\n/** Mireds per unit of the `temperature` slider (−100…100). */\nexport const MIRED_PER_TEMPERATURE_UNIT = 1;\nconst AS_SHOT_MIRED = 1e6 / AS_SHOT_KELVIN;\n\n/**\n * Maps the `temperature` slider (−100 cool … +100 warm) to the Kelvin white-balance setting:\n * one mired per unit around 6500 K, so 0 → 6500 K, +100 → ~18 570 K (a strong warming\n * correction), −100 → ~3 940 K (a strong cooling correction). Mireds are perceptually even,\n * so equal slider steps look like equal changes.\n */\nexport function temperatureToKelvin(temperature: number): number {\n return 1e6 / (AS_SHOT_MIRED - temperature * MIRED_PER_TEMPERATURE_UNIT);\n}\n\n/** Inverse of {@link temperatureToKelvin}. */\nexport function kelvinToTemperature(kelvin: number): number {\n return (AS_SHOT_MIRED - 1e6 / kelvin) / MIRED_PER_TEMPERATURE_UNIT;\n}\n\nconst AS_SHOT_WB_INV: Matrix3 = invertMatrix3(wbMatrix(tempTintToXy(AS_SHOT_KELVIN, 0))) ?? IDENTITY_MATRIX3;\n\n/**\n * White-balance matrix (linear sRGB) for the `temperature` / `tint` sliders, relative to\n * \"as shot\": the identity at 0 / 0. Normalised so neutral luminance is preserved (white\n * balance doesn't change exposure).\n */\nexport function whiteBalanceMatrix(temperature: number, tint = 0): Matrix3 {\n if (!temperature && !tint) return [...IDENTITY_MATRIX3];\n const set = wbMatrix(tempTintToXy(temperatureToKelvin(temperature), tint));\n const m = mulMatrix3(set, AS_SHOT_WB_INV);\n const g = applyMatrix3(m, [1, 1, 1]);\n const y = g[0] * LUMA_709[0] + g[1] * LUMA_709[1] + g[2] * LUMA_709[2];\n return mulMatrix3(m, diag3(1 / y, 1 / y, 1 / y));\n}\n\n/**\n * Neutral-point eyedropper: the `temperature` / `tint` that render the sampled sRGB colour\n * (0–255 per channel, as it appears in the unadjusted image) neutral. Results are clamped to\n * the slider range −100…100.\n */\nexport function whiteBalanceFromSample(\n rgb: readonly [number, number, number] | { r: number; g: number; b: number }\n): { temperature: number; tint: number } {\n const [r, g, b] = Array.isArray(rgb) ? rgb : [(rgb as any).r, (rgb as any).g, (rgb as any).b];\n const lin: [number, number, number] = [r, g, b].map((c) => srgbToLinear(Math.min(255, Math.max(0, c)) / 255)) as [\n number,\n number,\n number,\n ];\n if (lin[0] + lin[1] + lin[2] <= 1e-6) return { temperature: 0, tint: 0 };\n // Under relative WB the matrix is wb(set) · wb(asShot)⁻¹, so the sample is neutral when the\n // chosen source white is the chromaticity of wb(asShot)⁻¹ · sample.\n const src = linearSrgbToXy(applyMatrix3(AS_SHOT_WB_INV, lin));\n const { kelvin, tint } = xyToTempTint(src);\n const clamp = (v: number) => Math.max(-100, Math.min(100, v));\n return { temperature: clamp(kelvinToTemperature(kelvin)), tint: clamp(tint) };\n}\n\n// ---------------------------------------------------------------------------\n// Tone curve (display-referred, identity at neutral)\n// ---------------------------------------------------------------------------\n\n/** Number of intervals in a tone LUT (the table has `TONE_LUT_SIZE + 1` entries). */\nexport const TONE_LUT_SIZE = 4096;\nconst GREY = 0.18;\nconst GREY_P = Math.pow(GREY, 1 / 2.2);\n/** Linear value where the highlight shoulder starts when there is headroom above 1. */\nconst SHOULDER_KNEE = 0.6;\n\n/**\n * The process 2 tone curve for one linear input value in `[0, headroom]`:\n *\n * 1. A highlight shoulder (extended Reinhard with white point `headroom`) above a knee of 0.6\n * rolls values above 1 (after exposure / white balance) back into range instead of clipping.\n * With no headroom (`headroom = 1`) it is exactly the identity.\n * 2. In a gamma-2.2 perceptual domain, contrast is an S-curve anchored at 0, middle grey and 1\n * (as `ToneMap::display`), and brightness a midtone lift anchored at 0 and 1.\n *\n * `contrast` and `brightness` are −100…100; both 0 with `headroom = 1` is the identity.\n */\nexport function toneCurve(y: number, contrast: number, brightness: number, headroom = 1): number {\n if (!(y > 0)) return 0;\n const h = Math.max(1, headroom);\n let o = Math.min(y, h);\n if (h > 1 && o > SHOULDER_KNEE) {\n const span = 1 - SHOULDER_KNEE;\n const x = (o - SHOULDER_KNEE) / span;\n const X = (h - SHOULDER_KNEE) / span;\n o = SHOULDER_KNEE + span * ((x * (1 + x / (X * X))) / (1 + x));\n }\n if (contrast === 0 && brightness === 0) return Math.min(1, o);\n let p = Math.pow(Math.min(1, o), 1 / 2.2);\n const c = contrast / 100;\n if (c !== 0) {\n p += c * 0.4 * (p - GREY_P) * (1 - (2 * p - 1) ** 2);\n }\n const b = brightness / 100;\n if (b !== 0) {\n // Smooth midtone lift that leaves black and white in place (slope stays positive).\n p += b * 0.8 * p * (1 - p);\n }\n return Math.pow(Math.min(1, Math.max(0, p)), 2.2);\n}\n\n/** Tabulates {@link toneCurve} over `[0, headroom]` in {@link TONE_LUT_SIZE} steps. */\nexport function buildToneLut(contrast: number, brightness: number, headroom = 1): Float32Array {\n const h = Math.max(1, headroom);\n const lut = new Float32Array(TONE_LUT_SIZE + 1);\n for (let i = 0; i <= TONE_LUT_SIZE; i++) {\n lut[i] = toneCurve((h * i) / TONE_LUT_SIZE, contrast, brightness, h);\n }\n return lut;\n}\n\n/** Linearly interpolated lookup into a LUT from {@link buildToneLut}. */\nexport function sampleToneLut(lut: Float32Array, headroom: number, y: number): number {\n if (!(y > 0)) return lut[0];\n const n = lut.length - 1;\n const f = (y * n) / Math.max(1, headroom);\n if (f >= n) return lut[n];\n const i = f | 0;\n return lut[i] + (lut[i + 1] - lut[i]) * (f - i);\n}\n","import type { Annotation, CropBox, ImageEditorState, ImagePoint, ImageTransform } from './types';\n\n/*\n * Coordinate spaces\n * -----------------\n * The crop box and every annotation are stored in ONE canonical space: natural image\n * pixels of the current source image (origin top-left of the un-rotated, un-flipped,\n * un-cropped image). Rotation, flips, crop and the viewport fit/zoom/pan are pure view\n * transforms applied on top of that space by a single forward mapping:\n *\n * target = origin + scale * R(angle) * F(flipH, flipV) * (image - regionCenter)\n *\n * where `regionCenter` is the centre of the visible region (the crop box, or the whole\n * image while the crop tool is active). `projectImagePoint` / `unprojectCanvasPoint`\n * implement the mapping and its exact inverse, and `applyImageProjectionToContext`\n * applies the very same transform to a 2D context, so rendering, hit-testing, viewport\n * metrics, export and mask generation cannot drift apart.\n */\n\nexport type CropHandle = 'nw' | 'ne' | 'se' | 'sw' | 'n' | 's' | 'e' | 'w' | 'inside' | null;\n\nexport interface ViewportMetrics {\n vpWidth: number;\n vpHeight: number;\n centerX: number;\n centerY: number;\n baseW: number;\n baseH: number;\n fitScale: number;\n renderW: number;\n renderH: number;\n totalScale: number;\n imageRect: { x: number; y: number; width: number; height: number };\n cropRect: { x: number; y: number; width: number; height: number } | null;\n}\n\n/**\n * A complete description of the natural-image -> target mapping (see module comment).\n */\nexport interface ImageProjection {\n /** Visible region in natural image pixels (crop box or full image). */\n region: CropBox;\n /** Rotation in degrees, normalised to [0, 360). */\n angle: number;\n flipH: boolean;\n flipV: boolean;\n /** Uniform scale from image pixels to target pixels. */\n scale: number;\n /** Target-space point that the centre of `region` maps to. */\n originX: number;\n originY: number;\n}\n\n/** Normalises any rotation in degrees to [0, 360). */\nexport function normalizeAngle(degrees: number): number {\n const a = ((degrees % 360) + 360) % 360;\n return a === 360 ? 0 : a;\n}\n\nfunction cosSin(angle: number): [number, number] {\n // Exact values for quarter turns so 90deg steps never accumulate float noise.\n switch (angle) {\n case 0:\n return [1, 0];\n case 90:\n return [0, 1];\n case 180:\n return [-1, 0];\n case 270:\n return [0, -1];\n default: {\n const rad = (angle * Math.PI) / 180;\n return [Math.cos(rad), Math.sin(rad)];\n }\n }\n}\n\n/** Whether the rotation swaps the displayed width and height. */\nexport function isQuarterTurnSwapped(transform: Pick<ImageTransform, 'rotate'>): boolean {\n const angle = normalizeAngle(transform.rotate);\n return angle === 90 || angle === 270;\n}\n\n/** Size of a region once rotated (bounding box for arbitrary angles). */\nexport function getRotatedSize(\n width: number,\n height: number,\n transform: Pick<ImageTransform, 'rotate'>\n): { width: number; height: number } {\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n return {\n width: Math.abs(width * c) + Math.abs(height * s),\n height: Math.abs(width * s) + Math.abs(height * c),\n };\n}\n\n/** Maps a natural-image point to target space. */\nexport function projectImagePoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n let x = pt.x - (p.region.x + p.region.width / 2);\n let y = pt.y - (p.region.y + p.region.height / 2);\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n const rx = x * c - y * s;\n const ry = x * s + y * c;\n return { x: p.originX + rx * p.scale, y: p.originY + ry * p.scale };\n}\n\n/** Exact inverse of {@link projectImagePoint}. */\nexport function unprojectCanvasPoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n const rx = (pt.x - p.originX) / p.scale;\n const ry = (pt.y - p.originY) / p.scale;\n let x = rx * c + ry * s;\n let y = -rx * s + ry * c;\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n return { x: x + p.region.x + p.region.width / 2, y: y + p.region.y + p.region.height / 2 };\n}\n\n/**\n * Applies the projection to a 2D context so that subsequent drawing in natural image\n * pixels lands exactly where {@link projectImagePoint} says it does.\n */\nexport function applyImageProjectionToContext(\n ctx: Pick<CanvasRenderingContext2D, 'translate' | 'scale' | 'rotate'>,\n p: ImageProjection\n): void {\n ctx.translate(p.originX, p.originY);\n ctx.scale(p.scale, p.scale);\n if (p.angle !== 0) ctx.rotate((p.angle * Math.PI) / 180);\n if (p.flipH || p.flipV) ctx.scale(p.flipH ? -1 : 1, p.flipV ? -1 : 1);\n ctx.translate(-(p.region.x + p.region.width / 2), -(p.region.y + p.region.height / 2));\n}\n\n/** Projects an image-space rectangle and returns its target-space bounding box. */\nexport function projectImageRect(\n rect: CropBox,\n p: ImageProjection\n): { x: number; y: number; width: number; height: number } {\n const corners = [\n projectImagePoint({ x: rect.x, y: rect.y }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y }, p),\n projectImagePoint({ x: rect.x, y: rect.y + rect.height }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, p),\n ];\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const minX = Math.min(...xs);\n const minY = Math.min(...ys);\n return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };\n}\n\n/** Clamps a crop box to the image bounds. */\nexport function clampCropToImage(crop: CropBox, width: number, height: number): CropBox {\n const x = Math.max(0, Math.min(width - 1, crop.x));\n const y = Math.max(0, Math.min(height - 1, crop.y));\n return {\n x,\n y,\n width: Math.max(1, Math.min(width - x, crop.width)),\n height: Math.max(1, Math.min(height - y, crop.height)),\n };\n}\n\nfunction resolveViewport(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping: boolean,\n cropOverride?: CropBox | null\n) {\n const { imageDimensions, crop: stateCrop, transform, zoom, pan, fit = 'margin' } = state;\n const naturalWidth = imageDimensions.width || 800;\n const naturalHeight = imageDimensions.height || 600;\n const full: CropBox = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const activeCrop =\n cropOverride !== undefined && cropOverride !== null\n ? cropOverride\n : (stateCrop ?? (isActivelyCropping ? full : null));\n\n const region =\n !isActivelyCropping && activeCrop ? clampCropToImage(activeCrop, naturalWidth, naturalHeight) : full;\n const { width: baseW, height: baseH } = getRotatedSize(region.width, region.height, transform);\n\n const fitScale =\n fit === 'contain'\n ? Math.min(vpWidth / baseW, vpHeight / baseH)\n : Math.min((vpWidth * 0.85) / baseW, (vpHeight * 0.85) / baseH, 1);\n const centerX = vpWidth / 2 + pan.x;\n const centerY = vpHeight / 2 + pan.y;\n\n const projection: ImageProjection = {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: zoom * fitScale,\n originX: centerX,\n originY: centerY,\n };\n\n return { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection };\n}\n\n/**\n * Returns the natural-image -> canvas projection used by the interactive viewport.\n */\nexport function getViewportProjection(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ImageProjection {\n return resolveViewport(state, vpWidth, vpHeight, isActivelyCropping, cropOverride).projection;\n}\n\n/**\n * Calculates current viewport projection metrics for canvas <-> image coordinate mapping.\n * `cropRect` is the on-screen (rotated/flipped) rectangle of the crop box.\n */\nexport function getViewportMetrics(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ViewportMetrics {\n const { zoom } = state;\n const { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection } = resolveViewport(\n state,\n vpWidth,\n vpHeight,\n isActivelyCropping,\n cropOverride\n );\n\n const renderW = baseW * fitScale;\n const renderH = baseH * fitScale;\n\n const imageRect = {\n x: centerX - (renderW * zoom) / 2,\n y: centerY - (renderH * zoom) / 2,\n width: renderW * zoom,\n height: renderH * zoom,\n };\n\n const cropRect = activeCrop ? projectImageRect(activeCrop, projection) : null;\n\n return {\n vpWidth,\n vpHeight,\n centerX,\n centerY,\n baseW,\n baseH,\n fitScale,\n renderW,\n renderH,\n totalScale: zoom * fitScale,\n imageRect,\n cropRect,\n };\n}\n\n/**\n * Converts a point on the canvas (in internal canvas pixel space) to natural image pixel\n * space, accounting for zoom, pan, fit scale, crop offset, rotation and flips.\n */\nexport function canvasToImagePoint(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return unprojectCanvasPoint(canvasPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\n/**\n * Converts a point in natural image pixel space to internal canvas pixel space.\n * Exact inverse of {@link canvasToImagePoint}.\n */\nexport function imageToCanvasPoint(\n imgPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return projectImagePoint(imgPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\nconst HANDLE_VECTORS: Record<Exclude<CropHandle, 'inside' | null>, [number, number]> = {\n nw: [-1, -1],\n n: [0, -1],\n ne: [1, -1],\n e: [1, 0],\n se: [1, 1],\n s: [0, 1],\n sw: [-1, 1],\n w: [-1, 0],\n};\n\nfunction vectorToHandle(x: number, y: number): CropHandle {\n const vx = Math.abs(x) < 0.38 ? 0 : Math.sign(x);\n const vy = Math.abs(y) < 0.38 ? 0 : Math.sign(y);\n for (const [name, [hx, hy]] of Object.entries(HANDLE_VECTORS)) {\n if (hx === vx && hy === vy) return name as CropHandle;\n }\n return null;\n}\n\n/**\n * Converts a crop handle in screen orientation to the equivalent edge/corner of the crop\n * box in natural image space, or the reverse with `toScreen = true`.\n */\nexport function mapCropHandle(handle: CropHandle, transform: ImageTransform, toScreen = false): CropHandle {\n if (handle === null || handle === 'inside') return handle;\n const [hx, hy] = HANDLE_VECTORS[handle];\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n const fx = transform.flipH ? -1 : 1;\n const fy = transform.flipV ? -1 : 1;\n if (toScreen) {\n const x = hx * fx;\n const y = hy * fy;\n return vectorToHandle(x * c - y * s, x * s + y * c);\n }\n const x = hx * c + hy * s;\n const y = -hx * s + hy * c;\n return vectorToHandle(x * fx, y * fy);\n}\n\n/**\n * Hit-tests the crop box and its corner/edge handles given a canvas point.\n * The returned handle names the edge/corner of the crop box in natural image space\n * (what {@link calculateCropDrag} expects); use `getCropCursor(handle, state.transform)`\n * to obtain the matching on-screen cursor.\n */\nexport function hitTestCrop(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n handleThreshold = 14,\n cropOverride?: CropBox | null\n): CropHandle {\n const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);\n if (!metrics.cropRect) return null;\n\n const screenHandle = hitTestCropRect(canvasPt, metrics.cropRect, handleThreshold);\n return mapCropHandle(screenHandle, state.transform);\n}\n\nfunction hitTestCropRect(\n canvasPt: ImagePoint,\n rect: { x: number; y: number; width: number; height: number },\n handleThreshold: number\n): CropHandle {\n const { x, y, width: w, height: h } = rect;\n const px = canvasPt.x;\n const py = canvasPt.y;\n\n // 1. Corners (highest priority)\n const dist = (x1: number, y1: number, x2: number, y2: number) =>\n Math.hypot(x1 - x2, y1 - y2);\n\n if (dist(px, py, x, y) <= handleThreshold) return 'nw';\n if (dist(px, py, x + w, y) <= handleThreshold) return 'ne';\n if (dist(px, py, x + w, y + h) <= handleThreshold) return 'se';\n if (dist(px, py, x, y + h) <= handleThreshold) return 'sw';\n\n // 2. Edges\n const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;\n const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;\n\n if (nearX && Math.abs(py - y) <= handleThreshold) return 'n';\n if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return 's';\n if (nearY && Math.abs(px - x) <= handleThreshold) return 'w';\n if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return 'e';\n\n // 3. Inside crop region\n if (px >= x && px <= x + w && py >= y && py <= y + h) {\n return 'inside';\n }\n\n return null;\n}\n\n/**\n * Returns CSS cursor style corresponding to a crop handle. Pass the current transform when\n * the handle comes from {@link hitTestCrop} (image space) so the cursor matches the\n * on-screen orientation under rotation/flip.\n */\nexport function getCropCursor(handle: CropHandle, transform?: ImageTransform): string {\n const screenHandle = transform ? mapCropHandle(handle, transform, true) : handle;\n switch (screenHandle) {\n case 'nw':\n case 'se':\n return 'nwse-resize';\n case 'ne':\n case 'sw':\n return 'nesw-resize';\n case 'n':\n case 's':\n return 'ns-resize';\n case 'e':\n case 'w':\n return 'ew-resize';\n case 'inside':\n return 'move';\n default:\n return 'crosshair';\n }\n}\n\n/**\n * Calculates updated crop rectangle given a drag interaction.\n */\nexport function calculateCropDrag(\n startCrop: CropBox,\n handle: CropHandle,\n startImgPt: ImagePoint,\n currentImgPt: ImagePoint,\n imgWidth: number,\n imgHeight: number,\n aspectRatio?: number | null,\n transform?: ImageTransform\n): CropBox {\n // `aspectRatio` is the on-screen ratio; under a quarter turn it is inverted in image space.\n if (aspectRatio && transform && isQuarterTurnSwapped(transform)) {\n aspectRatio = 1 / aspectRatio;\n }\n const dx = currentImgPt.x - startImgPt.x;\n const dy = currentImgPt.y - startImgPt.y;\n\n // Whole crop box move\n if (handle === 'inside') {\n const maxX = Math.max(0, imgWidth - startCrop.width);\n const maxY = Math.max(0, imgHeight - startCrop.height);\n const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));\n const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));\n return {\n x: Math.round(newX),\n y: Math.round(newY),\n width: startCrop.width,\n height: startCrop.height,\n };\n }\n\n // Handle resizing\n let left = startCrop.x;\n let top = startCrop.y;\n let right = startCrop.x + startCrop.width;\n let bottom = startCrop.y + startCrop.height;\n\n const minSize = 20;\n\n if (handle === 'e' || handle === 'ne' || handle === 'se') {\n right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));\n }\n if (handle === 'w' || handle === 'nw' || handle === 'sw') {\n left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));\n }\n if (handle === 's' || handle === 'se' || handle === 'sw') {\n bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));\n }\n if (handle === 'n' || handle === 'nw' || handle === 'ne') {\n top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));\n }\n\n // Maintain aspect ratio if requested\n if (aspectRatio && aspectRatio > 0) {\n let currentW = right - left;\n let currentH = bottom - top;\n\n if (handle === 'e' || handle === 'w') {\n currentH = Math.min(imgHeight, currentW / aspectRatio);\n bottom = Math.min(imgHeight, top + currentH);\n } else if (handle === 'n' || handle === 's') {\n currentW = Math.min(imgWidth, currentH * aspectRatio);\n right = Math.min(imgWidth, left + currentW);\n } else {\n // Corner resizing\n const targetH = currentW / aspectRatio;\n if (top + targetH <= imgHeight) {\n bottom = top + targetH;\n } else {\n const targetW = currentH * aspectRatio;\n right = Math.min(imgWidth, left + targetW);\n }\n }\n }\n\n return {\n x: Math.round(left),\n y: Math.round(top),\n width: Math.round(Math.max(minSize, right - left)),\n height: Math.round(Math.max(minSize, bottom - top)),\n };\n}\n\n/**\n * Calculates bounding box of an annotation.\n */\nexport function getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n switch (ann.type) {\n case 'rect':\n case 'image':\n return {\n x: Math.min(ann.x, ann.x + ann.width),\n y: Math.min(ann.y, ann.y + ann.height),\n width: Math.abs(ann.width),\n height: Math.abs(ann.height),\n };\n case 'circle':\n return {\n x: ann.x - Math.abs(ann.radiusX),\n y: ann.y - Math.abs(ann.radiusY),\n width: Math.abs(ann.radiusX) * 2,\n height: Math.abs(ann.radiusY) * 2,\n };\n case 'arrow':\n case 'line': {\n const minX = Math.min(ann.x, ann.endX);\n const minY = Math.min(ann.y, ann.endY);\n return {\n x: minX,\n y: minY,\n width: Math.max(Math.abs(ann.endX - ann.x), 10),\n height: Math.max(Math.abs(ann.endY - ann.y), 10),\n };\n }\n case 'pen': {\n if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };\n let minX = ann.points[0].x;\n let maxX = ann.points[0].x;\n let minY = ann.points[0].y;\n let maxY = ann.points[0].y;\n for (const pt of ann.points) {\n if (pt.x < minX) minX = pt.x;\n if (pt.x > maxX) maxX = pt.x;\n if (pt.y < minY) minY = pt.y;\n if (pt.y > maxY) maxY = pt.y;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n case 'text':\n return {\n x: ann.x,\n y: ann.y,\n width: ann.text.length * ann.fontSize * 0.6,\n height: ann.fontSize * 1.2,\n };\n }\n}\n\n/**\n * Hit tests if a point in image space is within an annotation's bounds.\n */\nexport function hitTestAnnotation(imgPt: ImagePoint, ann: Annotation, padding = 10): boolean {\n const b = getAnnotationBounds(ann);\n return (\n imgPt.x >= b.x - padding &&\n imgPt.x <= b.x + b.width + padding &&\n imgPt.y >= b.y - padding &&\n imgPt.y <= b.y + b.height + padding\n );\n}\n\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\n\n/**\n * A selection: one 8-bit coverage value per image pixel (0 = not selected, 255 = fully\n * selected), row-major over the whole source image in natural pixels — the same space as the\n * crop box and annotations. Soft values are partial selection (feathered or anti-aliased edges).\n *\n * Treat a mask as immutable: every operation returns a new one, so history entries can share\n * them without copying.\n */\nexport interface SelectionMask {\n readonly width: number;\n readonly height: number;\n readonly data: Uint8Array;\n /** Bounding box of the non-zero coverage, or `null` when nothing is selected. */\n readonly bounds: CropBox | null;\n}\n\n/** How a new selection combines with the current one. */\nexport type SelectionMode = 'replace' | 'add' | 'subtract' | 'intersect';\n\nexport const SELECTION_MODES: readonly SelectionMode[] = ['replace', 'add', 'subtract', 'intersect'];\n\nexport function isSelectionMode(value: unknown): value is SelectionMode {\n return typeof value === 'string' && (SELECTION_MODES as readonly string[]).includes(value);\n}\n\n/** Parses `replace|add|subtract|intersect`; anything else is `replace`. */\nexport function parseSelectionMode(value: unknown): SelectionMode {\n return isSelectionMode(value) ? value : 'replace';\n}\n\n/** RGBA pixels, 8 bits per channel, straight alpha (an `ImageData` qualifies). */\nexport interface PixelBuffer {\n width: number;\n height: number;\n data: ArrayLike<number>;\n}\n\n/** Serialized selection: a run-length encoding of the 8-bit coverage. */\nexport interface SerializedSelection {\n width: number;\n height: number;\n /** `r1:` + base64 of (value byte, LEB128 run length) pairs. */\n rle: string;\n}\n\nconst MAX_SIDE = 1 << 15;\n\nfunction checkSize(width: number, height: number): void {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE) {\n throw new Error(`Invalid selection size ${width}×${height}`);\n }\n}\n\n/** Bounding box of non-zero bytes. */\nexport function computeSelectionBounds(width: number, height: number, data: ArrayLike<number>): CropBox | null {\n let x0 = width;\n let x1 = -1;\n let y0 = -1;\n let y1 = -1;\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let first = -1;\n for (let x = 0; x < width; x++) {\n if (data[row + x] !== 0) {\n first = x;\n break;\n }\n }\n if (first < 0) continue;\n let last = first;\n for (let x = width - 1; x > first; x--) {\n if (data[row + x] !== 0) {\n last = x;\n break;\n }\n }\n if (y0 < 0) y0 = y;\n y1 = y;\n if (first < x0) x0 = first;\n if (last > x1) x1 = last;\n }\n if (y0 < 0) return null;\n return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };\n}\n\n/** Wraps coverage bytes (taken over, not copied) as a selection, computing its bounds. */\nexport function createSelectionMask(width: number, height: number, data?: Uint8Array): SelectionMask {\n checkSize(width, height);\n const bytes = data ?? new Uint8Array(width * height);\n if (bytes.length !== width * height) throw new Error('Selection data does not match its size');\n return { width, height, data: bytes, bounds: data ? computeSelectionBounds(width, height, bytes) : null };\n}\n\n/** Select › All. */\nexport function fullSelection(width: number, height: number): SelectionMask {\n checkSize(width, height);\n return { width, height, data: new Uint8Array(width * height).fill(255), bounds: { x: 0, y: 0, width, height } };\n}\n\nexport function isSelectionEmpty(mask: SelectionMask | null | undefined): boolean {\n return !mask || mask.bounds === null;\n}\n\n/** Coverage at a pixel, 0..1 (0 outside the image). */\nexport function selectionValueAt(mask: SelectionMask, x: number, y: number): number {\n const xi = Math.floor(x);\n const yi = Math.floor(y);\n if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;\n return mask.data[yi * mask.width + xi] / 255;\n}\n\n/** 8-bit value of a coverage (clamped and rounded; NaN → 0). */\nexport function coverageToByte(v: number): number {\n if (!(v > 0)) return 0;\n if (v >= 1) return 255;\n return Math.floor(v * 255 + 0.5);\n}\n\n/** A selection from 0..1 coverage. */\nexport function selectionFromCoverage(width: number, height: number, coverage: ArrayLike<number>): SelectionMask {\n const n = width * height;\n const out = new Uint8Array(n);\n for (let i = 0; i < n; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** The selection's coverage as 0..1 floats. */\nexport function selectionToCoverage(mask: SelectionMask): Float32Array {\n const out = new Float32Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = mask.data[i] / 255;\n return out;\n}\n\n/** Number of fully-or-partly selected pixels, weighted by coverage (Σ coverage). */\nexport function selectionArea(mask: SelectionMask | null | undefined): number {\n if (!mask || !mask.bounds) return 0;\n let sum = 0;\n const { x, y, width, height } = mask.bounds;\n for (let yy = y; yy < y + height; yy++) {\n const row = yy * mask.width;\n for (let xx = x; xx < x + width; xx++) sum += mask.data[row + xx];\n }\n return sum / 255;\n}\n\n/**\n * Combines `next` with `prev` by `mode` (8-bit, exact: combining a mask with itself is a no-op).\n * Add is the maximum, Subtract `max(prev − next, 0)`, Intersect the minimum. A missing `prev`\n * reads as nothing selected. Returns `null` when nothing remains selected.\n */\nexport function combineSelections(\n prev: SelectionMask | null | undefined,\n next: SelectionMask,\n mode: SelectionMode\n): SelectionMask | null {\n let result: SelectionMask;\n if (mode === 'replace' || (!prev && mode === 'add')) {\n result = next;\n } else if (!prev) {\n return null;\n } else {\n if (prev.width !== next.width || prev.height !== next.height) throw new Error('Selections differ in size');\n const a = prev.data;\n const b = next.data;\n const out = new Uint8Array(a.length);\n if (mode === 'add') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] : b[i];\n else if (mode === 'subtract') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] - b[i] : 0;\n else for (let i = 0; i < a.length; i++) out[i] = a[i] < b[i] ? a[i] : b[i];\n result = createSelectionMask(prev.width, prev.height, out);\n }\n return result.bounds ? result : null;\n}\n\n/** Select › Inverse. */\nexport function invertSelection(mask: SelectionMask): SelectionMask {\n const out = new Uint8Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = 255 - mask.data[i];\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Bilinear resample to another size (e.g. an AI mask returned at a different resolution). */\nexport function resizeSelection(mask: SelectionMask, width: number, height: number): SelectionMask {\n if (mask.width === width && mask.height === height) return mask;\n checkSize(width, height);\n const out = new Uint8Array(width * height);\n const sx = mask.width / width;\n const sy = mask.height / height;\n const src = mask.data;\n const mw = mask.width;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(mask.height - 1, Math.max(0, (y + 0.5) * sy - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(mask.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(mw - 1, Math.max(0, (x + 0.5) * sx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(mw - 1, x0 + 1);\n const tx = fx - x0;\n const top = src[y0 * mw + x0] * (1 - tx) + src[y0 * mw + x1] * tx;\n const bot = src[y1 * mw + x0] * (1 - tx) + src[y1 * mw + x1] * tx;\n out[y * width + x] = Math.round(top * (1 - ty) + bot * ty);\n }\n }\n return createSelectionMask(width, height, out);\n}\n\nexport type SelectionChannel = 'alpha' | 'luminance' | 'red' | 'green' | 'blue';\n\n/**\n * A selection from an image: by default its luminance (white = selected, the usual convention\n * of segmentation masks), or one channel. Partly transparent pixels count as black for\n * luminance, so a mask PNG with transparency works either way.\n */\nexport function selectionFromPixels(\n pixels: PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number } = {}\n): SelectionMask {\n const { width, height, data } = pixels;\n checkSize(width, height);\n if (data.length < width * height * 4) throw new Error('Pixel data is shorter than width × height × 4');\n const channel = options.channel ?? 'luminance';\n const out = new Uint8Array(width * height);\n const threshold = options.threshold;\n for (let i = 0, p = 0; i < out.length; i++, p += 4) {\n let v: number;\n switch (channel) {\n case 'alpha':\n v = data[p + 3];\n break;\n case 'red':\n v = data[p];\n break;\n case 'green':\n v = data[p + 1];\n break;\n case 'blue':\n v = data[p + 2];\n break;\n default:\n v = Math.round(((0.2126 * data[p] + 0.7152 * data[p + 1] + 0.0722 * data[p + 2]) * data[p + 3]) / 255);\n }\n if (options.invert) v = 255 - v;\n if (threshold !== undefined) v = v >= threshold ? 255 : 0;\n out[i] = v;\n }\n return createSelectionMask(width, height, out);\n}\n\n/** The selection as RGBA pixels: grey (white = selected, opaque) or white with coverage as alpha. */\nexport function selectionToPixels(mask: SelectionMask, as: 'gray' | 'alpha' = 'gray'): { width: number; height: number; data: Uint8ClampedArray } {\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n const v = mask.data[i];\n if (as === 'gray') {\n out[p] = out[p + 1] = out[p + 2] = v;\n out[p + 3] = 255;\n } else {\n out[p] = out[p + 1] = out[p + 2] = 255;\n out[p + 3] = v;\n }\n }\n return { width: mask.width, height: mask.height, data: out };\n}\n\n// ---------------------------------------------------------------------------\n// Run-length encoding\n// ---------------------------------------------------------------------------\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => {\n const t = new Int16Array(128).fill(-1);\n for (let i = 0; i < B64.length; i++) t[B64.charCodeAt(i)] = i;\n return t;\n})();\n\nfunction toBase64(bytes: Uint8Array, length: number): string {\n const parts: string[] = [];\n let chunk = '';\n for (let i = 0; i < length; i += 3) {\n const a = bytes[i];\n const b = i + 1 < length ? bytes[i + 1] : 0;\n const c = i + 2 < length ? bytes[i + 2] : 0;\n chunk += B64[a >> 2] + B64[((a & 3) << 4) | (b >> 4)];\n chunk += i + 1 < length ? B64[((b & 15) << 2) | (c >> 6)] : '=';\n chunk += i + 2 < length ? B64[c & 63] : '=';\n if (chunk.length >= 8192) {\n parts.push(chunk);\n chunk = '';\n }\n }\n parts.push(chunk);\n return parts.join('');\n}\n\nfunction fromBase64(text: string): Uint8Array {\n if (text.length % 4 !== 0) throw new Error('Invalid base64 length');\n let pad = 0;\n if (text.endsWith('==')) pad = 2;\n else if (text.endsWith('=')) pad = 1;\n const out = new Uint8Array((text.length / 4) * 3 - pad);\n let o = 0;\n const digit = (i: number, k: number): number => {\n const ch = text.charCodeAt(i + k);\n if (ch === 61 /* = */ && i + 4 === text.length && k >= 4 - pad) return 0;\n const idx = ch < 128 ? B64_INDEX[ch] : -1;\n if (idx < 0) throw new Error('Invalid base64 character');\n return idx;\n };\n for (let i = 0; i < text.length; i += 4) {\n const n = (digit(i, 0) << 18) | (digit(i, 1) << 12) | (digit(i, 2) << 6) | digit(i, 3);\n if (o < out.length) out[o++] = (n >> 16) & 255;\n if (o < out.length) out[o++] = (n >> 8) & 255;\n if (o < out.length) out[o++] = n & 255;\n }\n return out;\n}\n\n/** Run-length encodes coverage bytes: `r1:` + base64 of (value, LEB128 run length) pairs. */\nexport function encodeSelectionRle(data: Uint8Array): string {\n let buf = new Uint8Array(64);\n let len = 0;\n const push = (b: number) => {\n if (len === buf.length) {\n const next = new Uint8Array(buf.length * 2);\n next.set(buf);\n buf = next;\n }\n buf[len++] = b;\n };\n let i = 0;\n while (i < data.length) {\n const v = data[i];\n let j = i + 1;\n while (j < data.length && data[j] === v) j++;\n push(v);\n let run = j - i;\n while (run >= 128) {\n push((run & 127) | 128);\n run = Math.floor(run / 128);\n }\n push(run);\n i = j;\n }\n return 'r1:' + toBase64(buf, len);\n}\n\n/** Decodes {@link encodeSelectionRle}; throws unless the runs cover exactly `length` bytes. */\nexport function decodeSelectionRle(rle: string, length: number): Uint8Array {\n if (typeof rle !== 'string' || !rle.startsWith('r1:')) throw new Error('Unknown selection encoding');\n const bytes = fromBase64(rle.slice(3));\n const out = new Uint8Array(length);\n let o = 0;\n let i = 0;\n while (i < bytes.length) {\n const v = bytes[i++];\n let run = 0;\n let scale = 1;\n for (;;) {\n if (i >= bytes.length) throw new Error('Truncated selection run');\n const b = bytes[i++];\n run += (b & 127) * scale;\n if (b < 128) break;\n scale *= 128;\n if (scale > 2 ** 35) throw new Error('Selection run too long');\n }\n if (run === 0 || o + run > length) throw new Error('Selection runs do not match its size');\n if (v !== 0) out.fill(v, o, o + run);\n o += run;\n }\n if (o !== length) throw new Error('Selection runs do not match its size');\n return out;\n}\n\nexport function serializeSelection(mask: SelectionMask): SerializedSelection {\n return { width: mask.width, height: mask.height, rle: encodeSelectionRle(mask.data) };\n}\n\n/** Restores a {@link SerializedSelection}; throws on damaged data. */\nexport function deserializeSelection(value: unknown): SelectionMask {\n if (!value || typeof value !== 'object') throw new Error('selection must be an object');\n const v = value as Record<string, unknown>;\n const width = v.width as number;\n const height = v.height as number;\n checkSize(width, height);\n return createSelectionMask(width, height, decodeSelectionRle(v.rle as string, width * height));\n}\n\n/** A short, stable hash of a string or bytes (FNV-1a, 32-bit, hex) for cache keys. */\nexport function hashMaskContent(content: string | Uint8Array): string {\n let h = 0x811c9dc5;\n if (typeof content === 'string') {\n for (let i = 0; i < content.length; i++) {\n h ^= content.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n } else {\n for (let i = 0; i < content.length; i++) {\n h ^= content[i];\n h = Math.imul(h, 0x01000193);\n }\n }\n return `${(h >>> 0).toString(16)}:${content.length}`;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs, photocraft/crates/algo/src/selection/distance.rs, photocraft/crates/algo/src/selection_blur.rs and photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport { createSelectionMask, coverageToByte, selectionToCoverage, type SelectionMask } from './mask';\nimport { gaussianBlur } from '../../raster/blur';\n\nexport { gaussianBlur, DIRECT_GAUSSIAN_MAX_SIGMA } from '../../raster/blur';\n\n/** Distance reported for every pixel when there is no seed at all. */\nexport const EMPTY_DISTANCE = 1e10;\n\n/**\n * Exact Euclidean distance from every pixel centre to the nearest seed pixel centre (row-major\n * `inside`, non-zero = seed): nearest seeds along columns, then the lower envelope of parabolas\n * along rows (Felzenszwalb & Huttenlocher). Linear in the pixel count.\n */\nexport function distanceTransform(inside: ArrayLike<number | boolean>, width: number, height: number): Float32Array {\n const n = width * height;\n const out = new Float32Array(n);\n let any = false;\n let all = true;\n for (let i = 0; i < n; i++) {\n if (inside[i]) any = true;\n else all = false;\n }\n if (!any) return out.fill(EMPTY_DISTANCE);\n if (all) return out;\n const NONE = 0xffffffff;\n // Vertical distance to the nearest seed in the same column.\n const vertical = new Uint32Array(n).fill(NONE);\n for (let x = 0; x < width; x++) {\n let nearest = -1;\n for (let y = 0; y < height; y++) {\n const i = y * width + x;\n if (inside[i]) nearest = y;\n if (nearest >= 0) vertical[i] = y - nearest;\n }\n nearest = -1;\n for (let y = height - 1; y >= 0; y--) {\n const i = y * width + x;\n if (vertical[i] === 0) nearest = y;\n if (nearest >= 0) vertical[i] = Math.min(vertical[i], nearest - y);\n }\n }\n const sites = new Int32Array(width);\n const starts = new Float64Array(width);\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let count = 0;\n for (let q = 0; q < width; q++) {\n const dq = vertical[row + q];\n if (dq === NONE) continue;\n const fq = dq * dq;\n let start = -Infinity;\n while (count > 0) {\n const p = sites[count - 1];\n const dp = vertical[row + p];\n start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;\n if (start > starts[count - 1]) break;\n count--;\n }\n if (count === 0) start = -Infinity;\n sites[count] = q;\n starts[count] = start;\n count++;\n }\n if (count === 0) {\n out.fill(EMPTY_DISTANCE, row, row + width);\n continue;\n }\n let k = 0;\n for (let x = 0; x < width; x++) {\n while (k + 1 < count && starts[k + 1] < x) k++;\n const site = sites[k];\n const dx = x - site;\n const dv = vertical[row + site];\n out[row + x] = Math.sqrt(dx * dx + dv * dv);\n }\n }\n return out;\n}\n\nfunction thresholdInside(m: ArrayLike<number>, threshold = 0.5): Uint8Array {\n const out = new Uint8Array(m.length);\n for (let i = 0; i < m.length; i++) out[i] = m[i] >= threshold ? 1 : 0;\n return out;\n}\n\n/** Grows coverage (0..1) by `r` pixels: covered when the nearest selected centre is ≤ r away (fractional r → partial). */\nexport function expandCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const d = distanceTransform(thresholdInside(m), w, h);\n const out = new Float32Array(m.length);\n for (let i = 0; i < out.length; i++) out[i] = Math.max(m[i], Math.min(1, Math.max(0, r + 1 - d[i])));\n return out;\n}\n\n/** Shrinks coverage by `r` pixels (expands the inverse). */\nexport function contractCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inv = new Float32Array(m.length);\n for (let i = 0; i < inv.length; i++) inv[i] = 1 - m[i];\n const e = expandCoverage(inv, w, h, r);\n for (let i = 0; i < e.length; i++) e[i] = 1 - e[i];\n return e;\n}\n\n/**\n * Select › Modify › Border: a soft band `2r` wide centred on the edge. A pixel `D` away (centre\n * to centre) from the nearest pixel on the other side of the edge is selected `1 − (D − 1) / r`.\n */\nexport function borderCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inside = thresholdInside(m);\n const outside = new Uint8Array(inside.length);\n for (let i = 0; i < inside.length; i++) outside[i] = inside[i] ? 0 : 1;\n const out = new Float32Array(m.length);\n if (!(r > 0)) return out;\n const toInside = distanceTransform(inside, w, h);\n const toOutside = distanceTransform(outside, w, h);\n for (let i = 0; i < out.length; i++) {\n const d = inside[i] ? toOutside[i] : toInside[i];\n out[i] = Math.min(1, Math.max(0, 1 - (d - 1) / r));\n }\n return out;\n}\n\n/** Box average of radius `r` along one line, edges replicated, via a running sum (O(1) per sample). */\nfunction boxLineReplicate(src: Float32Array, srcOff: number, stride: number, len: number, r: number, dst: Float32Array) {\n const at = (i: number) => src[srcOff + Math.min(len - 1, Math.max(0, i)) * stride];\n let sum = 0;\n for (let i = -r; i <= r; i++) sum += at(i);\n const k = 2 * r + 1;\n for (let i = 0; i < len; i++) {\n dst[i] = sum / k;\n sum += at(i + r + 1) - at(i - r);\n }\n}\n\n/**\n * Select › Modify › Smooth: box average of radius `round(r)` with replicated edges, then\n * re-thresholded at ½ — specks drop, corners round, straight edges stay hard.\n */\nexport function smoothCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const rr = Number.isFinite(r) ? Math.max(0, Math.round(r)) : 0;\n const src = Float32Array.from(m);\n const tmp = new Float32Array(src.length);\n const line = new Float32Array(Math.max(w, h));\n if (rr > 0) {\n for (let y = 0; y < h; y++) {\n boxLineReplicate(src, y * w, 1, w, rr, line);\n tmp.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n boxLineReplicate(tmp, x, w, h, rr, line);\n for (let y = 0; y < h; y++) src[y * w + x] = line[y];\n }\n }\n for (let i = 0; i < src.length; i++) src[i] = src[i] >= 0.5 ? 1 : 0;\n return src;\n}\n\n\n/** Select › Modify › Feather: Gaussian blur with σ = radius (as Photoshop), zero beyond the image. */\nexport function featherCoverage(m: ArrayLike<number>, w: number, h: number, radius: number): Float32Array {\n const out = gaussianBlur(m, w, h, Number.isFinite(radius) ? Math.max(0, radius) : 0, 'zero');\n for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) < 1e-7) out[i] = 0;\n return out;\n}\n\n/**\n * Running max (or min) over a window of radius `r` along one line (edge-clipped), van Herk /\n * Gil–Werman: about three comparisons per sample whatever the radius.\n */\nfunction runningExtreme(src: Float32Array, off: number, stride: number, len: number, r: number, max: boolean, g: Float32Array, hb: Float32Array, dst: Float32Array) {\n const k = 2 * r + 1;\n const pick = max ? (a: number, b: number) => (a > b ? a : b) : (a: number, b: number) => (a < b ? a : b);\n const fill = max ? -Infinity : Infinity;\n // Padded line: r fills on each side.\n const plen = len + 2 * r;\n const at = (i: number) => (i < r || i >= r + len ? fill : src[off + (i - r) * stride]);\n for (let start = 0; start < plen; start += k) {\n const end = Math.min(plen, start + k);\n g[start] = at(start);\n for (let i = start + 1; i < end; i++) g[i] = pick(g[i - 1], at(i));\n hb[end - 1] = at(end - 1);\n for (let i = end - 2; i >= start; i--) hb[i] = pick(hb[i + 1], at(i));\n }\n for (let i = 0; i < len; i++) {\n // Window over padded indices i .. i + 2r.\n const a = hb[i];\n const b = g[i + 2 * r];\n dst[i] = pick(a, b);\n }\n}\n\n/** Grey dilation (`grow`) or erosion with a square of radius `r`, separable, O(1) per pixel. */\nexport function greyMorphology(src: ArrayLike<number>, w: number, h: number, r: number, grow: boolean): Float32Array {\n const out = Float32Array.from(src);\n if (r <= 0) return out;\n const n = Math.max(w, h);\n const g = new Float32Array(n + 2 * r);\n const hb = new Float32Array(n + 2 * r);\n const line = new Float32Array(n);\n for (let y = 0; y < h; y++) {\n runningExtreme(out, y * w, 1, w, r, grow, g, hb, line);\n out.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n runningExtreme(out, x, w, h, r, grow, g, hb, line);\n for (let y = 0; y < h; y++) out[y * w + x] = line[y];\n }\n return out;\n}\n\n/**\n * Grows (positive) or shrinks (negative) soft coverage by a fractional distance: whole pixels\n * use one morphology, the remainder blends with the next pixel, so 0.3 px differs from 1 px.\n */\nexport function shiftCoverage(src: ArrayLike<number>, w: number, h: number, amount: number): Float32Array {\n const mag = Math.abs(amount);\n if (!Number.isFinite(mag) || mag < 1e-4) return Float32Array.from(src);\n const grow = amount > 0;\n const whole = Math.floor(mag);\n const frac = mag - whole;\n if (frac < 1e-4) return greyMorphology(src, w, h, whole, grow);\n const lo = whole === 0 ? Float32Array.from(src) : greyMorphology(src, w, h, whole, grow);\n const hi = greyMorphology(src, w, h, whole + 1, grow);\n for (let i = 0; i < lo.length; i++) lo[i] += (hi[i] - lo[i]) * frac;\n return lo;\n}\n\n// ---------------------------------------------------------------------------\n// Selection-level wrappers\n// ---------------------------------------------------------------------------\n\nexport type SelectionModifyOp = 'grow' | 'shrink' | 'feather' | 'border' | 'smooth';\n\nexport const SELECTION_MODIFY_OPS: readonly SelectionModifyOp[] = ['grow', 'shrink', 'feather', 'border', 'smooth'];\n\nfunction wrap(mask: SelectionMask, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Select › Modify (grow = Expand, shrink = Contract, feather, border, smooth) by `radius` pixels. */\nexport function modifySelection(mask: SelectionMask, op: SelectionModifyOp, radius: number): SelectionMask {\n const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;\n if (r === 0 && op !== 'border') return mask;\n const m = selectionToCoverage(mask);\n const { width: w, height: h } = mask;\n switch (op) {\n case 'grow':\n return wrap(mask, expandCoverage(m, w, h, r));\n case 'shrink':\n return wrap(mask, contractCoverage(m, w, h, r));\n case 'feather':\n return wrap(mask, featherCoverage(m, w, h, r));\n case 'border':\n return wrap(mask, borderCoverage(m, w, h, r));\n case 'smooth':\n return wrap(mask, smoothCoverage(m, w, h, r));\n default:\n throw new Error(`Unknown selection modification \"${op as string}\"`);\n }\n}\n","// Portions ported from photocraft/crates/engine/src/layer_mask_props_cmds.rs and photocraft/crates/compose/src/masks.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentLayer, AdjustmentLayerMask } from '../types';\nimport { unprojectCanvasPoint, type ImageProjection } from '../geometry';\nimport {\n createSelectionMask,\n decodeSelectionRle,\n encodeSelectionRle,\n hashMaskContent,\n type SelectionMask,\n} from './mask';\nimport { gaussianBlur } from './modify';\n\n/** Properties of a layer mask that leave its pixels intact (Photoshop's Properties › Masks). */\nexport interface LayerMaskProps {\n /** 0..1 (default 1): at 0.5 the masked-out areas still get half of the adjustment. */\n density?: number;\n /** Gaussian feather σ in pixels, 0..1000 (default 0). */\n feather?: number;\n /** Disabled masks are kept but ignored (the layer applies everywhere). Default true. */\n enabled?: boolean;\n}\n\nconst MAX_SIDE = 1 << 15;\n\n/**\n * A clean layer mask from stored data, or `null` when it is unusable. Defaults are left out\n * (density 1, feather 0, enabled), so a plain mask serializes as `{width, height, rle}`. The\n * RLE is only decoded when rendering.\n */\nexport function normalizeLayerMask(raw: unknown): AdjustmentLayerMask | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const { width, height, rle } = r;\n if (\n typeof width !== 'number' ||\n typeof height !== 'number' ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width < 1 ||\n height < 1 ||\n width > MAX_SIDE ||\n height > MAX_SIDE ||\n typeof rle !== 'string' ||\n !rle.startsWith('r1:')\n ) {\n return null;\n }\n const out: AdjustmentLayerMask = { width, height, rle };\n if (typeof r.density === 'number' && Number.isFinite(r.density)) {\n const d = Math.min(1, Math.max(0, r.density));\n if (d !== 1) out.density = d;\n }\n if (typeof r.feather === 'number' && Number.isFinite(r.feather)) {\n const f = Math.min(1000, Math.max(0, r.feather));\n if (f !== 0) out.feather = f;\n }\n if (r.enabled === false) out.enabled = false;\n return out;\n}\n\n/** A layer mask holding the selection's pixels (Add Layer Mask › Reveal Selection). */\nexport function layerMaskFromSelection(selection: SelectionMask, props: LayerMaskProps = {}): AdjustmentLayerMask {\n return normalizeLayerMask({ width: selection.width, height: selection.height, rle: encodeSelectionRle(selection.data), ...props })!;\n}\n\n/** Applies property changes; `undefined` fields keep their value. */\nexport function updateLayerMaskProps(mask: AdjustmentLayerMask, props: LayerMaskProps): AdjustmentLayerMask {\n return normalizeLayerMask({\n ...mask,\n ...(props.density !== undefined ? { density: props.density } : {}),\n ...(props.feather !== undefined ? { feather: props.feather } : {}),\n ...(props.enabled !== undefined ? { enabled: props.enabled } : {}),\n })!;\n}\n\n// Small LRU caches: masks are stored as RLE strings and layers are deep-copied often.\nconst DECODE_LIMIT = 8;\nconst decoded = new Map<string, SelectionMask | null>();\nconst coverageCache = new Map<string, Float32Array | null>();\n\nfunction lru<T>(cache: Map<string, T>, key: string, make: () => T): T {\n if (cache.has(key)) {\n const v = cache.get(key)!;\n cache.delete(key);\n cache.set(key, v);\n return v;\n }\n const v = make();\n cache.set(key, v);\n if (cache.size > DECODE_LIMIT) cache.delete(cache.keys().next().value as string);\n return v;\n}\n\n/** The mask's pixels as a selection (Load Selection from mask), or `null` when the data is damaged. */\nexport function decodeLayerMask(mask: AdjustmentLayerMask): SelectionMask | null {\n const key = `${mask.width}x${mask.height}:${mask.rle}`;\n return lru(decoded, key, () => {\n try {\n return createSelectionMask(mask.width, mask.height, decodeSelectionRle(mask.rle, mask.width * mask.height));\n } catch {\n return null;\n }\n });\n}\n\n/** Cache key of everything about a mask that changes pixels. */\nexport function layerMaskKey(mask: AdjustmentLayerMask | undefined): string {\n if (!mask) return '';\n return `${mask.width}x${mask.height}:${hashMaskContent(mask.rle)}:${mask.density ?? 1}:${mask.feather ?? 0}:${mask.enabled !== false}`;\n}\n\n/**\n * The mask's effective coverage (0..1, mask resolution): feathered (σ = feather, borders\n * replicated), then density applied as `1 − density·(1 − v)`. `null` for a disabled or damaged\n * mask, meaning \"no mask\".\n */\nexport function layerMaskCoverage(mask: AdjustmentLayerMask): Float32Array | null {\n if (mask.enabled === false) return null;\n return lru(coverageCache, layerMaskKey(mask), () => {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n let v: Float32Array = new Float32Array(sel.data.length);\n for (let i = 0; i < v.length; i++) v[i] = sel.data[i] / 255;\n const feather = mask.feather ?? 0;\n if (feather > 0) v = gaussianBlur(v, mask.width, mask.height, feather, 'clamp');\n const density = mask.density ?? 1;\n if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);\n return v;\n });\n}\n\n/**\n * Resamples mask coverage (natural-image aligned, any resolution) into a target raster drawn\n * through `projection` (the base canvas of the renderer: crop, rotation, flips): each target\n * pixel centre is unprojected to natural pixels and sampled bilinearly.\n */\nexport function projectMaskCoverage(\n coverage: Float32Array,\n maskWidth: number,\n maskHeight: number,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Float32Array {\n const out = new Float32Array(targetWidth * targetHeight);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, projection);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, projection);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, projection);\n const kx = maskWidth / naturalWidth;\n const ky = maskHeight / naturalHeight;\n // Affine map target → mask coordinates (pixel centres at +0.5).\n const ux = (ox.x - o.x) * kx;\n const vx = (ox.y - o.y) * ky;\n const uy = (oy.x - o.x) * kx;\n const vy = (oy.y - o.y) * ky;\n const u0 = o.x * kx - 0.5;\n const v0 = o.y * ky - 0.5;\n const mw1 = maskWidth - 1;\n const mh1 = maskHeight - 1;\n for (let y = 0; y < targetHeight; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n const row = y * targetWidth;\n for (let x = 0; x < targetWidth; x++, u += ux, v += vx) {\n const fu = u < 0 ? 0 : u > mw1 ? mw1 : u;\n const fv = v < 0 ? 0 : v > mh1 ? mh1 : v;\n const x0 = fu | 0;\n const y0 = fv | 0;\n const x1 = x0 < mw1 ? x0 + 1 : x0;\n const y1 = y0 < mh1 ? y0 + 1 : y0;\n const tx = fu - x0;\n const ty = fv - y0;\n const a = coverage[y0 * maskWidth + x0] * (1 - tx) + coverage[y0 * maskWidth + x1] * tx;\n const b = coverage[y1 * maskWidth + x0] * (1 - tx) + coverage[y1 * maskWidth + x1] * tx;\n out[row + x] = a * (1 - ty) + b * ty;\n }\n }\n return out;\n}\n\n/**\n * Per-layer mask coverage for a render target, keyed by layer id; `undefined` when no layer has\n * an enabled, valid mask (so mask-free stacks take the exact same path as before).\n */\nexport function buildLayerMaskPlanes(\n layers: readonly AdjustmentLayer[] | undefined,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Map<string, Float32Array> | undefined {\n if (!layers?.some((l) => l.mask && l.mask.enabled !== false)) return undefined;\n const planes = new Map<string, Float32Array>();\n for (const layer of layers) {\n if (!layer.mask) continue;\n const coverage = layerMaskCoverage(layer.mask);\n if (!coverage) continue;\n planes.set(\n layer.id,\n projectMaskCoverage(coverage, layer.mask.width, layer.mask.height, naturalWidth, naturalHeight, projection, targetWidth, targetHeight)\n );\n }\n return planes.size ? planes : undefined;\n}\n","// Portions ported from photocraft-main/crates/compose/src/adjust.rs, photocraft-main/crates/doc/src/adjust.rs and photocraft-main/crates/color/src/convert.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentLayerMask,\n AdjustmentLayerOf,\n AdjustmentParamsMap,\n BlackWhiteParams,\n ColorBalanceParams,\n ColorBalanceTone,\n CurvePoint,\n HueRange,\n HueSaturationParams,\n LevelsChannel,\n PhotoFilterParams,\n VibranceParams,\n} from './types';\nimport { type BlendMode, blendRgb, normalizeBlendMode, setLum } from './blend';\nimport { linearToSrgb, srgbToLinear } from './color';\nimport { layerMaskKey, normalizeLayerMask } from './selection/layer-mask';\n\n/**\n * Adjustment layers: Photoshop-matching evaluation on encoded sRGB colours in 0..1.\n *\n * Every layer is compiled once per parameter change into a per-pixel function (per-channel kinds\n * into three 4096-entry LUTs); {@link applyAdjustmentLayers} runs the stack on an ImageData.\n */\n\ntype Rgb = [number, number, number];\n\n/** Entries in a tone LUT (curves, levels, colour balance). */\nexport const ADJUSTMENT_LUT_SIZE = 4096;\n\nconst clamp01 = (v: number) => (v <= 0 ? 0 : v >= 1 ? 1 : v);\nconst finite = (v: unknown, fallback: number) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);\nconst clampTo = (v: unknown, lo: number, hi: number, fallback: number) => Math.min(hi, Math.max(lo, finite(v, fallback)));\nconst remEuclid = (a: number, b: number) => ((a % b) + b) % b;\n\n/** Linear interpolation into a 0..1-domain LUT. */\nexport function sampleLut(table: ArrayLike<number>, v: number): number {\n const n = table.length - 1;\n const x = clamp01(v) * n;\n const i = Math.floor(x);\n const j = Math.min(i + 1, n);\n const f = x - i;\n return table[i] * (1 - f) + table[j] * f;\n}\n\n// ---------------------------------------------------------------------------\n// Kinds, labels, defaults\n// ---------------------------------------------------------------------------\n\nexport interface AdjustmentKindInfo {\n kind: AdjustmentKind;\n label: string;\n}\n\nexport const ADJUSTMENT_KINDS: readonly AdjustmentKindInfo[] = [\n { kind: 'curves', label: 'Curves' },\n { kind: 'levels', label: 'Levels' },\n { kind: 'hueSaturation', label: 'Hue/Saturation' },\n { kind: 'vibrance', label: 'Vibrance' },\n { kind: 'colorBalance', label: 'Color Balance' },\n { kind: 'blackWhite', label: 'Black & White' },\n { kind: 'photoFilter', label: 'Photo Filter' },\n { kind: 'invert', label: 'Invert' },\n { kind: 'posterize', label: 'Posterize' },\n { kind: 'threshold', label: 'Threshold' },\n];\n\nconst KIND_LABELS = new Map(ADJUSTMENT_KINDS.map((k) => [k.kind, k.label]));\n\nexport function isAdjustmentKind(value: unknown): value is AdjustmentKind {\n return typeof value === 'string' && KIND_LABELS.has(value as AdjustmentKind);\n}\n\nexport function adjustmentKindLabel(kind: AdjustmentKind): string {\n return KIND_LABELS.get(kind) ?? kind;\n}\n\nexport const HUE_RANGE_LABELS = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'] as const;\n\n/** The neutral range `i` (0 reds … 5 magentas): fully on for 30° around its centre, fading over 30° each side. */\nexport function neutralHueRange(i: number): HueRange {\n const c = 60 * i;\n return { hue: 0, saturation: 0, lightness: 0, bounds: [c - 45, c - 15, c + 15, c + 45] };\n}\n\nexport const identityCurve = (): CurvePoint[] => [\n { input: 0, output: 0 },\n { input: 255, output: 255 },\n];\n\nexport const identityLevels = (): LevelsChannel => ({ inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 });\n\n/** Neutral parameters of every kind (fresh objects on every call). */\nexport function defaultAdjustmentParams<K extends AdjustmentKind>(kind: K): AdjustmentParamsMap[K] {\n const defaults: { [P in AdjustmentKind]: () => AdjustmentParamsMap[P] } = {\n curves: () => ({ master: identityCurve(), red: identityCurve(), green: identityCurve(), blue: identityCurve() }),\n levels: () => ({ master: identityLevels(), red: identityLevels(), green: identityLevels(), blue: identityLevels() }),\n hueSaturation: () => ({\n hue: 0,\n saturation: 0,\n lightness: 0,\n colorize: false,\n ranges: [0, 1, 2, 3, 4, 5].map(neutralHueRange),\n }),\n vibrance: () => ({ vibrance: 0, saturation: 0 }),\n colorBalance: () => ({ shadows: [0, 0, 0], midtones: [0, 0, 0], highlights: [0, 0, 0], preserveLuminosity: true }),\n blackWhite: () => ({ reds: 40, yellows: 60, greens: 40, cyans: 60, blues: 20, magentas: 80, tint: null }),\n invert: () => ({}) as Record<string, never>,\n posterize: () => ({ levels: 4 }),\n threshold: () => ({ level: 128 }),\n photoFilter: () => ({ color: '#ec8a00', density: 25, preserveLuminosity: true }),\n };\n return defaults[kind]() as AdjustmentParamsMap[K];\n}\n\nlet layerCounter = 0;\n\n/** A new layer of `kind` with neutral (or the given) parameters, enabled, opacity 1, normal blend. */\nexport function createAdjustmentLayer<K extends AdjustmentKind>(\n kind: K,\n options: {\n id?: string;\n name?: string;\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n mask?: AdjustmentLayerMask;\n } = {}\n): AdjustmentLayerOf<K> {\n const raw = {\n id: options.id ?? `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind,\n name: options.name,\n params: { ...defaultAdjustmentParams(kind), ...(options.params ?? {}) },\n enabled: options.enabled ?? true,\n opacity: options.opacity ?? 1,\n blend: options.blend ?? 'normal',\n mask: options.mask,\n };\n return normalizeAdjustmentLayer(raw) as AdjustmentLayerOf<K>;\n}\n\n// ---------------------------------------------------------------------------\n// Normalisation of stored layers\n// ---------------------------------------------------------------------------\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\nfunction normCurve(points: unknown): CurvePoint[] {\n if (!Array.isArray(points)) return identityCurve();\n const out = points\n .filter((p) => p && typeof p === 'object')\n .map((p) => ({ input: clampTo(p.input, 0, 255, 0), output: clampTo(p.output, 0, 255, 0) }))\n .sort((a, b) => a.input - b.input);\n return out.length >= 2 ? out : identityCurve();\n}\n\nfunction normLevels(ch: unknown): LevelsChannel {\n const c = (ch && typeof ch === 'object' ? ch : {}) as Partial<LevelsChannel>;\n return {\n inBlack: clampTo(c.inBlack, 0, 254, 0),\n inWhite: clampTo(c.inWhite, 1, 255, 255),\n gamma: clampTo(c.gamma, 0.1, 9.99, 1),\n outBlack: clampTo(c.outBlack, 0, 255, 0),\n outWhite: clampTo(c.outWhite, 0, 255, 255),\n };\n}\n\nfunction normTone(t: unknown): ColorBalanceTone {\n const a = Array.isArray(t) ? t : [];\n return [clampTo(a[0], -100, 100, 0), clampTo(a[1], -100, 100, 0), clampTo(a[2], -100, 100, 0)];\n}\n\nfunction normParams(kind: AdjustmentKind, raw: unknown): AdjustmentParamsMap[AdjustmentKind] {\n const p = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>;\n const d = defaultAdjustmentParams(kind) as Record<string, any>;\n switch (kind) {\n case 'curves':\n return { master: normCurve(p.master), red: normCurve(p.red), green: normCurve(p.green), blue: normCurve(p.blue) };\n case 'levels':\n return { master: normLevels(p.master), red: normLevels(p.red), green: normLevels(p.green), blue: normLevels(p.blue) };\n case 'hueSaturation': {\n const ranges: HueRange[] = [0, 1, 2, 3, 4, 5].map((i) => {\n const r = Array.isArray(p.ranges) && p.ranges[i] && typeof p.ranges[i] === 'object' ? p.ranges[i] : {};\n const n = neutralHueRange(i);\n const b = Array.isArray(r.bounds) && r.bounds.length === 4 && r.bounds.every((v: unknown) => Number.isFinite(v)) ? r.bounds : n.bounds;\n return {\n hue: clampTo(r.hue, -180, 180, 0),\n saturation: clampTo(r.saturation, -100, 100, 0),\n lightness: clampTo(r.lightness, -100, 100, 0),\n bounds: [b[0], b[1], b[2], b[3]] as [number, number, number, number],\n };\n });\n const colorize = p.colorize === true;\n return {\n hue: colorize ? clampTo(p.hue, 0, 360, 0) : clampTo(p.hue, -180, 180, 0),\n saturation: clampTo(p.saturation, -100, 100, 0),\n lightness: clampTo(p.lightness, -100, 100, 0),\n colorize,\n ranges,\n } satisfies HueSaturationParams;\n }\n case 'vibrance':\n return { vibrance: clampTo(p.vibrance, -100, 100, 0), saturation: clampTo(p.saturation, -100, 100, 0) } satisfies VibranceParams;\n case 'colorBalance':\n return {\n shadows: normTone(p.shadows),\n midtones: normTone(p.midtones),\n highlights: normTone(p.highlights),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies ColorBalanceParams;\n case 'blackWhite': {\n const out = {} as BlackWhiteParams;\n for (const k of ['reds', 'yellows', 'greens', 'cyans', 'blues', 'magentas'] as const) out[k] = clampTo(p[k], -200, 300, d[k]);\n out.tint = typeof p.tint === 'string' && HEX.test(p.tint) ? p.tint.toLowerCase() : null;\n return out;\n }\n case 'photoFilter':\n return {\n color: typeof p.color === 'string' && HEX.test(p.color) ? p.color.toLowerCase() : d.color,\n density: clampTo(p.density, 0, 100, d.density),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies PhotoFilterParams;\n case 'posterize':\n return { levels: Math.round(clampTo(p.levels, 2, 255, 4)) };\n case 'threshold':\n return { level: Math.round(clampTo(p.level, 1, 255, 128)) };\n case 'invert':\n return {} as Record<string, never>;\n }\n}\n\n/**\n * A clean, deep-copied layer from stored or hand-written data: missing fields get neutral\n * defaults, numbers are clamped to their ranges. `null` for an unknown kind.\n */\nexport function normalizeAdjustmentLayer(raw: unknown): AdjustmentLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n if (!isAdjustmentKind(r.kind)) return null;\n const layer: Record<string, unknown> = {\n id: typeof r.id === 'string' && r.id ? r.id : `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind: r.kind,\n enabled: r.enabled !== false,\n opacity: clampTo(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n params: normParams(r.kind, r.params),\n };\n if (typeof r.name === 'string' && r.name) layer.name = r.name;\n // Only present when the layer has a usable mask, so mask-free layers serialize as before.\n const mask = normalizeLayerMask(r.mask);\n if (mask) layer.mask = mask;\n return layer as unknown as AdjustmentLayer;\n}\n\n/** Normalises a stored layer list (drops unknown kinds, de-duplicates ids). */\nexport function normalizeAdjustmentLayers(raw: unknown): AdjustmentLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const out: AdjustmentLayer[] = [];\n for (const item of raw) {\n const layer = normalizeAdjustmentLayer(item);\n if (!layer) continue;\n while (seen.has(layer.id)) layer.id = `${layer.id}_${(++layerCounter).toString(36)}`;\n seen.add(layer.id);\n out.push(layer);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Curves\n// ---------------------------------------------------------------------------\n\n/**\n * Photoshop curve: a natural cubic spline through the points (0..1 in and out; it may overshoot\n * between points, as Photoshop's does), constant outside the first / last point, clamped to\n * 0..1, sampled into a LUT. Fewer than two distinct points: the identity.\n */\nexport function curveLut(points: ReadonlyArray<{ input: number; output: number }>, size = ADJUSTMENT_LUT_SIZE): Float32Array {\n const pts = points\n .filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output))\n .map((p) => [p.input, p.output] as [number, number])\n .sort((a, b) => a[0] - b[0]);\n const dedup: [number, number][] = [];\n for (const p of pts) {\n if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;\n dedup.push(p);\n }\n const out = new Float32Array(size);\n const n = dedup.length;\n if (n < 2) {\n for (let k = 0; k < size; k++) out[k] = k / (size - 1);\n return out;\n }\n const x = dedup.map((p) => p[0]);\n const y = dedup.map((p) => p[1]);\n // Second derivatives of the natural spline (tridiagonal solve).\n const m2 = new Float64Array(n);\n if (n > 2) {\n const c = new Float64Array(n);\n const d = new Float64Array(n);\n for (let i = 1; i < n - 1; i++) {\n const h0 = x[i] - x[i - 1];\n const h1 = x[i + 1] - x[i];\n const a = h0 / 6;\n const b = (h0 + h1) / 3;\n const cc = h1 / 6;\n const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;\n const denom = b - a * c[i - 1];\n c[i] = cc / denom;\n d[i] = (r - a * d[i - 1]) / denom;\n }\n for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];\n }\n let seg = 0;\n for (let k = 0; k < size; k++) {\n const xv = k / (size - 1);\n if (xv <= x[0]) {\n out[k] = clamp01(y[0]);\n continue;\n }\n if (xv >= x[n - 1]) {\n out[k] = clamp01(y[n - 1]);\n continue;\n }\n while (seg < n - 2 && xv > x[seg + 1]) seg++;\n const x0 = x[seg], x1 = x[seg + 1];\n const h = x1 - x0;\n const a = (x1 - xv) / h;\n const b = (xv - x0) / h;\n const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;\n out[k] = clamp01(v);\n }\n return out;\n}\n\n/** {@link curveLut} for Curves points in levels (0..255). */\nexport function curveLutFromLevels(points: readonly CurvePoint[]): Float32Array {\n return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));\n}\n\n// ---------------------------------------------------------------------------\n// Levels\n// ---------------------------------------------------------------------------\n\n/** A Levels record with values in 0..1 (as `levelsValue` takes them). */\nexport interface UnitLevels {\n inBlack: number;\n inWhite: number;\n gamma: number;\n outBlack: number;\n outWhite: number;\n}\n\nconst toUnitLevels = (c: LevelsChannel): UnitLevels => ({\n inBlack: c.inBlack / 255,\n inWhite: c.inWhite / 255,\n gamma: c.gamma,\n outBlack: c.outBlack / 255,\n outWhite: c.outWhite / 255,\n});\n\n/**\n * One Levels record on `v` (0..1). With `quantum` (8-bit: 255) Photoshop works on whole levels:\n * the input is a level and the stretch is rounded (half up) before the gamma. A gamma above 1 has\n * Photoshop's soft shadow toe: a p-norm soft-min of the power curve and a line of slope ≈ 2^gamma.\n */\nexport function levelsValue(ch: UnitLevels, v: number, quantum?: number): number {\n const range = Math.max(1e-6, ch.inWhite - ch.inBlack);\n let u: number;\n if (quantum !== undefined && Number.isFinite(quantum) && quantum >= 1) {\n const q = quantum;\n const vq = Math.round(v * q) / q;\n u = Math.floor(clamp01((vq - ch.inBlack) / range) * q + 0.5 + 1e-3) / q;\n } else {\n u = clamp01((v - ch.inBlack) / range);\n }\n const g = Math.max(0.01, ch.gamma);\n let t: number;\n if (g > 1) {\n const power = Math.pow(u, 1 / g);\n const line = 0.93 * Math.pow(2, g) * u;\n if (power <= 1e-6 || line <= 1e-6) t = Math.min(power, line);\n else {\n const p = 10;\n t = Math.pow(Math.pow(power, -p) + Math.pow(line, -p), -1 / p);\n }\n } else {\n t = Math.pow(u, 1 / g);\n }\n return ch.outBlack + t * (ch.outWhite - ch.outBlack);\n}\n\n// ---------------------------------------------------------------------------\n// HSL and Hue/Saturation\n// ---------------------------------------------------------------------------\n\n/** RGB (0..1) → HSL with hue in turns (0..1). */\nexport function rgbToHsl(c: readonly number[]): [number, number, number] {\n const max = Math.max(c[0], c[1], c[2]);\n const min = Math.min(c[0], c[1], c[2]);\n const l = (max + min) / 2;\n if (Math.abs(max - min) < 1e-7) return [0, 0, l];\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (max === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return [h / 6, s, l];\n}\n\nexport function hslToRgb(h: number, s: number, l: number): Rgb {\n if (s <= 0) return [l, l, l];\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const f = (t: number) => {\n t = remEuclid(t, 1);\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 0.5) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n return [f(h + 1 / 3), f(h), f(h - 1 / 3)];\n}\n\n/** How much a pixel of hue `deg` belongs to `range` (0..1): a trapezoid that may wrap past 0/360. */\nexport function hueRangeWeight(range: Pick<HueRange, 'bounds'>, deg: number): number {\n const [a, b0, c0, d0] = range.bounds;\n const rel = (x: number) => (Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0);\n const b = rel(b0);\n const c = Math.max(rel(c0), b);\n const d = Math.max(rel(d0), c);\n const h = rel(deg);\n if (h <= b) return b <= 0 ? 1 : h / b;\n if (h <= c) return 1;\n if (h <= d) return d <= c ? 1 : (d - h) / (d - c);\n return 0;\n}\n\nconst isNeutralRange = (r: HueRange) => r.hue === 0 && r.saturation === 0 && r.lightness === 0;\n\n/** Range edits as three hue-indexed tables (hue shift in degrees, saturation and lightness as fractions). */\nexport function hueRangeTables(ranges: readonly HueRange[]): [Float32Array, Float32Array, Float32Array] {\n const n = ADJUSTMENT_LUT_SIZE;\n const t: [Float32Array, Float32Array, Float32Array] = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];\n const active = ranges.filter((r) => !isNeutralRange(r));\n for (let k = 0; k < n; k++) {\n const deg = (k / (n - 1)) * 360;\n for (const r of active) {\n const w = hueRangeWeight(r, deg);\n t[0][k] += w * r.hue;\n t[1][k] += (w * r.saturation) / 100;\n t[2][k] += (w * r.lightness) / 100;\n }\n }\n return t;\n}\n\n/**\n * One Hue/Saturation evaluation: hue shift in degrees, saturation and lightness in -1..1.\n * Lightness first, then hue and saturation (raising divides by 1 − s, lowering multiplies by\n * 1 + s). Colorize sets hue and saturation at the lightened lightness.\n */\nexport function hueSaturation(c: readonly number[], hue: number, s: number, l: number, colorize: boolean): Rgb {\n const lighten = (v: number) => (l > 0 ? v + (1 - v) * l : v * (1 + l));\n if (colorize) {\n const ll = rgbToHsl(c)[2];\n return hslToRgb(remEuclid(hue, 360) / 360, Math.min(1, Math.abs(s)), lighten(ll));\n }\n const [hh0, ss0, ll] = rgbToHsl([lighten(c[0]), lighten(c[1]), lighten(c[2])]);\n const hh = remEuclid(hh0 + hue / 360, 1);\n const ss = Math.min(1, Math.max(0, s > 0 ? ss0 / Math.max(1 / 512, 1 - s) : ss0 * (1 + s)));\n return hslToRgb(hh, ss, ll);\n}\n\n/** A colour range's Lightness: towards the brightest channel when raised, the darkest when lowered. */\nexport function rangeLightness(c: readonly number[], l: number): Rgb {\n const k = Math.min(1, Math.max(-1, l));\n const target = k > 0 ? Math.max(c[0], c[1], c[2]) : Math.min(c[0], c[1], c[2]);\n const a = Math.abs(k);\n return [c[0] + (target - c[0]) * a, c[1] + (target - c[1]) * a, c[2] + (target - c[2]) * a];\n}\n\n// ---------------------------------------------------------------------------\n// Vibrance\n// ---------------------------------------------------------------------------\n\n/** Grey Vibrance › Saturation desaturates to (linear light; blue has no weight). */\nexport const VIBRANCE_GREY: readonly [number, number] = [0.288, 0.712];\nconst VIBRANCE_BOOST = [0.5001, 0.0887, 0.4374, 0.7573, -0.8113, 0.5154, 2.8026] as const;\n\nfunction vibranceSkin(h: number): number {\n if (h <= 30) return 1;\n if (h < 45) return (45 - h) / 15;\n if (h < 300) return 0;\n return (h - 300) / 60;\n}\n\nfunction hsvHue(c: readonly number[], mx: number, mn: number): number {\n const d = mx - mn;\n let h: number;\n if (mx === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (mx === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return h * 60;\n}\n\n/** The Vibrance slider (`v` in -1..1) on a linear-light pixel: an HSV change that keeps the hue. */\nexport function vibranceLinear(c: Rgb, v: number): Rgb {\n const mx = Math.max(c[0], c[1], c[2]);\n const mn = Math.min(c[0], c[1], c[2]);\n if (mx <= 0 || mx - mn <= 1e-9) return c;\n const t = 1 - mn / mx;\n const shape = t * (1 - t) * (2 - t + t * t) * mx * Math.max(0, 1 - mx);\n let t2: number, v2: number;\n if (v < 0) {\n const a = -v;\n t2 = t * (1 - a / 4) * (1 - a + (a * t * (1 + t)) / 2);\n v2 = mx - a * shape;\n } else {\n const [c0, c1, p, q, r0, ra, rt] = VIBRANCE_BOOST;\n const w = vibranceSkin(hsvHue(c, mx, mn));\n const a = v * (1 - w) + 0.64 * Math.pow(v, 1.6) * w;\n const boost =\n (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));\n t2 = Math.min(1, t * (1 + boost));\n v2 = mx + (v / 4) * shape;\n }\n const m2 = v2 * (1 - t2);\n const k = (v2 - m2) / (mx - mn);\n return [m2 + (c[0] - mn) * k, m2 + (c[1] - mn) * k, m2 + (c[2] - mn) * k];\n}\n\n/** Photoshop's Vibrance on one encoded sRGB pixel (`v`, `s`: the sliders / 100). */\nexport function vibrancePixel(c: readonly number[], v: number, s: number): Rgb {\n let l: Rgb = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n if (v !== 0) l = vibranceLinear(l, v);\n if (s !== 0) {\n const g = VIBRANCE_GREY[0] * l[0] + VIBRANCE_GREY[1] * l[1];\n l = l.map((x) => Math.min(1, Math.max(0, g + (1 + s) * (x - g)))) as Rgb;\n }\n return l.map((x) => linearToSrgb(clamp01(x))) as Rgb;\n}\n\n// ---------------------------------------------------------------------------\n// Color Balance\n// ---------------------------------------------------------------------------\n\n/** How far one Color Balance slider at ±100 moves the black or white point. */\nconst BALANCE_LEVELS = 0.39;\n\n/**\n * Color Balance on channel `ch` (0 red, 1 green, 2 blue) of `v` (0..1); `tones` are the\n * shadows, midtones and highlights slider triples (-100..100). Highlights, shadows, then midtones.\n */\nexport function colorBalanceChannel(\n tones: readonly [readonly number[], readonly number[], readonly number[]],\n preserveLuminosity: boolean,\n ch: number,\n v: number\n): number {\n const [shadows, midtones, highlights] = tones;\n const black = (x: number, b: number) => Math.max(0, (x - b) / (1 - b));\n const white = (x: number, w: number) => Math.min(1, x / w);\n let out = clamp01(v);\n for (const [tone, sliders] of [\n [2, highlights],\n [0, shadows],\n [1, midtones],\n ] as const) {\n const a = [0, 1, 2].map((i) => Math.min(1, Math.max(-1, sliders[i] / 100)));\n if (preserveLuminosity) {\n const own = a[ch] ?? 0;\n const lo = Math.min(a[0], a[1], a[2]);\n const hi = Math.max(a[0], a[1], a[2]);\n if (tone === 0) out = black(out, BALANCE_LEVELS * (hi - own));\n else if (tone === 1) out = Math.pow(out, Math.pow(2, (hi + lo) / 2 - own));\n else out = white(out, 1 - BALANCE_LEVELS * (own - lo));\n continue;\n }\n const s = a[ch];\n if (s === 0) continue;\n if (tone === 1) out = Math.pow(out, Math.pow(2, -s));\n else if (tone === 0 && s < 0) out = Math.pow(black(out, BALANCE_LEVELS * -s), Math.pow(2, -s / 2));\n else if (tone === 2 && s > 0) out = Math.pow(white(out, 1 - BALANCE_LEVELS * s), Math.pow(2, -s / 2));\n else out = Math.pow(out, Math.pow(2, -s / 2));\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Black & White, Photo Filter, Posterize, Threshold\n// ---------------------------------------------------------------------------\n\n/**\n * Black & White grey: the smallest channel, plus the secondary part (middle − smallest) and the\n * primary part (largest − middle) weighted by the sliders (percent: reds, yellows, greens, cyans,\n * blues, magentas).\n */\nexport function blackWhiteGray(c: readonly number[], weights: readonly number[]): number {\n const [r, g, b] = c;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const mid = r + g + b - max - min;\n const primary = r >= g && r >= b ? 0 : g >= b ? 2 : 4;\n const secondary = b <= r && b <= g ? 1 : r <= g ? 3 : 5;\n return clamp01(min + ((mid - min) * weights[secondary]) / 100 + ((max - mid) * weights[primary]) / 100);\n}\n\n/** D50 white (ICC PCS). */\nexport const D50 = [0.96422, 1.0, 0.82521] as const;\n/** Linear sRGB → D50 XYZ (Bradford-adapted, as in the ICC sRGB profile). */\nconst SRGB_TO_XYZ_D50 = [\n [0.436074, 0.385064, 0.14308],\n [0.222504, 0.716878, 0.060618],\n [0.013932, 0.097104, 0.714173],\n] as const;\n/** D50 XYZ → linear sRGB. */\nexport const XYZ_D50_TO_SRGB = [\n [3.133856, -1.616867, -0.490615],\n [-0.978768, 1.916142, 0.033454],\n [0.071945, -0.228991, 1.405243],\n] as const;\n\n/**\n * Photo Filter's linear-light RGB matrix (row-major 3×3): the pixel's D50 XYZ, relative to white,\n * multiplied by the filter colour's mixed in by `density` (0..1). `normalizeY` keeps luminance Y.\n */\nexport function photoFilterMatrix(color: readonly number[], density: number, normalizeY = false): number[][] {\n const d = Number.isFinite(density) ? clamp01(density) : 0;\n const lin = [0, 1, 2].map((i) => srgbToLinear(clamp01(color[i])));\n const xyz = SRGB_TO_XYZ_D50.map((r) => r[0] * lin[0] + r[1] * lin[1] + r[2] * lin[2]);\n let s = [0, 1, 2].map((i) => 1 - d + (d * xyz[i]) / D50[i]);\n if (normalizeY && s[1] > 1e-6) {\n const y = s[1];\n s = s.map((v) => v / y);\n }\n return [0, 1, 2].map((r) =>\n [0, 1, 2].map((c) => XYZ_D50_TO_SRGB[r][0] * s[0] * SRGB_TO_XYZ_D50[0][c] + XYZ_D50_TO_SRGB[r][1] * s[1] * SRGB_TO_XYZ_D50[1][c] + XYZ_D50_TO_SRGB[r][2] * s[2] * SRGB_TO_XYZ_D50[2][c])\n );\n}\n\n/** Photoshop posterize: `levels` equal input bins over 0..255, outputs `floor(k·255 / (levels − 1))`. */\nexport function posterizeValue(v: number, levels: number): number {\n const n = Math.min(255, Math.max(2, Math.round(levels)));\n const x = Math.round(clamp01(v) * 255);\n const bin = Math.min(n - 1, Math.floor((x * n) / 256));\n return Math.floor((bin * 255) / (n - 1)) / 255;\n}\n\n/** Rec.601 luma (Photoshop's grey for Threshold). */\nexport const rgbToGray = (c: readonly number[]) => 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];\n\n/** `#rrggbb` → 0..1 RGB (black for anything else). */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX.test(hex)) return [0, 0, 0];\n const n = parseInt(hex.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n// ---------------------------------------------------------------------------\n// Compilation and evaluation\n// ---------------------------------------------------------------------------\n\n/** Mutates an encoded sRGB colour (0..1) in place. */\nexport type PixelOp = (c: Rgb) => void;\n\nfunction lutOp(luts: [Float32Array, Float32Array, Float32Array]): PixelOp {\n const [lr, lg, lb] = luts;\n return (c) => {\n c[0] = sampleLut(lr, c[0]);\n c[1] = sampleLut(lg, c[1]);\n c[2] = sampleLut(lb, c[2]);\n };\n}\n\nconst isIdentityCurve = (pts: readonly CurvePoint[]) =>\n pts.length === 2 && pts[0].input === 0 && pts[0].output === 0 && pts[1].input === 255 && pts[1].output === 255;\n\nconst isIdentityLevels = (c: LevelsChannel) =>\n c.inBlack === 0 && c.inWhite === 255 && c.gamma === 1 && c.outBlack === 0 && c.outWhite === 255;\n\n/** Quantum Levels works in (8-bit documents: whole levels). */\nconst LEVELS_QUANTUM = 255;\n\n/**\n * Per-channel LUTs of a Curves or Levels layer: each channel's record first, then the master.\n * `null` for any other kind.\n */\nexport function toneLuts(layer: AdjustmentLayer): [Float32Array, Float32Array, Float32Array] | null {\n const n = ADJUSTMENT_LUT_SIZE;\n if (layer.kind === 'curves') {\n const m = curveLutFromLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((pts) => {\n const ch = curveLutFromLevels(pts);\n for (let k = 0; k < n; k++) ch[k] = sampleLut(m, ch[k]);\n return ch;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n if (layer.kind === 'levels') {\n const m = toUnitLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((rec) => {\n const c = toUnitLevels(rec);\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = levelsValue(m, levelsValue(c, k / (n - 1), LEVELS_QUANTUM), LEVELS_QUANTUM);\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n return null;\n}\n\n/**\n * Compiles a layer into a pixel function, or `null` when it is the identity (neutral parameters).\n * Pure: depends on `kind` and `params` only.\n */\nexport function compileAdjustmentLayer(layer: AdjustmentLayer): PixelOp | null {\n switch (layer.kind) {\n case 'curves': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityCurve)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'levels': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityLevels)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'hueSaturation': {\n const p = layer.params;\n const rangesActive = !p.colorize && p.ranges.some((r) => !isNeutralRange(r));\n if (!p.colorize && !rangesActive && p.hue === 0 && p.saturation === 0 && p.lightness === 0) return null;\n const tables = rangesActive ? hueRangeTables(p.ranges) : null;\n const sat = p.saturation / 100;\n const light = Math.min(1, Math.max(-1, p.lightness / 100));\n return (c) => {\n let dh = 0, ds = 0, dl = 0;\n if (tables) {\n const h0 = rgbToHsl(c)[0];\n dh = sampleLut(tables[0], h0);\n ds = sampleLut(tables[1], h0);\n dl = sampleLut(tables[2], h0);\n }\n const out = rangeLightness(hueSaturation(c, p.hue + dh, Math.min(1, Math.max(-1, sat + ds)), light, p.colorize), dl);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'vibrance': {\n const v = layer.params.vibrance / 100;\n const s = layer.params.saturation / 100;\n if (v === 0 && s === 0) return null;\n return (c) => {\n const out = vibrancePixel(c, v, s);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'colorBalance': {\n const p = layer.params;\n const tones = [p.shadows, p.midtones, p.highlights] as const;\n if (tones.every((t) => t[0] === 0 && t[1] === 0 && t[2] === 0)) return null;\n const n = ADJUSTMENT_LUT_SIZE;\n const luts = [0, 1, 2].map((ch) => {\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = colorBalanceChannel(tones, p.preserveLuminosity, ch, k / (n - 1));\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n return lutOp(luts);\n }\n case 'blackWhite': {\n const p = layer.params;\n const weights = [p.reds, p.yellows, p.greens, p.cyans, p.blues, p.magentas];\n const tint = p.tint ? hexToRgb(p.tint) : null;\n return (c) => {\n const g = blackWhiteGray(c, weights);\n if (tint) {\n const t = setLum(tint, g);\n c[0] = clamp01(t[0]);\n c[1] = clamp01(t[1]);\n c[2] = clamp01(t[2]);\n } else {\n c[0] = c[1] = c[2] = g;\n }\n };\n }\n case 'photoFilter': {\n const p = layer.params;\n if (p.density <= 0) return null;\n const m = photoFilterMatrix(hexToRgb(p.color), p.density / 100, false);\n const preserve = p.preserveLuminosity;\n return (c) => {\n const l = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n let f: Rgb = [0, 1, 2].map((r) => linearToSrgb(Math.max(0, m[r][0] * l[0] + m[r][1] * l[1] + m[r][2] * l[2]))) as Rgb;\n if (preserve) {\n const target = 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];\n f = setLum(f, target);\n }\n c[0] = clamp01(f[0]);\n c[1] = clamp01(f[1]);\n c[2] = clamp01(f[2]);\n };\n }\n case 'invert':\n return (c) => {\n c[0] = 1 - c[0];\n c[1] = 1 - c[1];\n c[2] = 1 - c[2];\n };\n case 'posterize': {\n const levels = layer.params.levels;\n return (c) => {\n c[0] = posterizeValue(c[0], levels);\n c[1] = posterizeValue(c[1], levels);\n c[2] = posterizeValue(c[2], levels);\n };\n }\n case 'threshold': {\n const t = Math.round(layer.params.level);\n return (c) => {\n const v = Math.round(rgbToGray(c) * 255) >= t ? 1 : 0;\n c[0] = c[1] = c[2] = v;\n };\n }\n }\n return null;\n}\n\n// Compiled layers keyed by kind + params, so a stack is compiled once per parameter change.\nconst COMPILE_CACHE_LIMIT = 64;\nconst compileCache = new Map<string, PixelOp | null>();\n\nfunction compiledFor(layer: AdjustmentLayer): PixelOp | null {\n const key = `${layer.kind}:${JSON.stringify(layer.params)}`;\n if (compileCache.has(key)) {\n const op = compileCache.get(key)!;\n // Refresh recency.\n compileCache.delete(key);\n compileCache.set(key, op);\n return op;\n }\n const op = compileAdjustmentLayer(layer);\n compileCache.set(key, op);\n if (compileCache.size > COMPILE_CACHE_LIMIT) compileCache.delete(compileCache.keys().next().value as string);\n return op;\n}\n\n/** Layers that change pixels: enabled, visible, not the identity. */\nexport function activeAdjustmentLayers(layers: readonly AdjustmentLayer[] | undefined): AdjustmentLayer[] {\n if (!layers?.length) return [];\n return layers.filter((l) => l.enabled !== false && (l.opacity ?? 1) > 0 && compiledFor(l) !== null);\n}\n\n/**\n * Evaluates one colour (encoded sRGB, 0..1) through a layer stack: each layer adjusts the result\n * of the layers below and is blended over it with its blend mode and opacity (an opaque\n * backdrop, so `(1 − α)·Cb + α·B(Cb, Cs)`).\n */\nexport function evaluateAdjustmentLayers(color: readonly number[], layers: readonly AdjustmentLayer[]): Rgb {\n const c: Rgb = [color[0], color[1], color[2]];\n for (const layer of layers) applyLayer(c, layer, compiledFor(layer));\n return c;\n}\n\nfunction applyLayer(c: Rgb, layer: AdjustmentLayer, op: PixelOp | null, coverage = 1): void {\n if (!op || layer.enabled === false) return;\n const alpha = Math.min(1, Math.max(0, layer.opacity ?? 1)) * coverage;\n if (alpha <= 0) return;\n const blend = layer.blend ?? 'normal';\n if (blend === 'normal' && alpha >= 1) {\n op(c);\n return;\n }\n const cb0 = c[0], cb1 = c[1], cb2 = c[2];\n op(c);\n const b = blend === 'normal' ? c : blendRgb(blend, [cb0, cb1, cb2], c);\n c[0] = (1 - alpha) * cb0 + alpha * b[0];\n c[1] = (1 - alpha) * cb1 + alpha * b[1];\n c[2] = (1 - alpha) * cb2 + alpha * b[2];\n}\n\n/**\n * Applies an adjustment-layer stack to 8-bit straight RGBA pixels in place. Each pixel is\n * decoded once, run through every layer in float, and encoded once. Transparent pixels are\n * left alone; alpha is never changed.\n *\n * `masks` (layer id → 0..1 coverage per pixel of `imageData`, see `buildLayerMaskPlanes`)\n * scales each masked layer's opacity per pixel: coverage 0 leaves the pixel as the layers below\n * made it, 1 applies the layer fully.\n */\nexport function applyAdjustmentLayers(\n imageData: { data: Uint8ClampedArray },\n layers: readonly AdjustmentLayer[] | undefined,\n masks?: ReadonlyMap<string, ArrayLike<number>>\n): void {\n const active = activeAdjustmentLayers(layers);\n if (!active.length) return;\n const ops = active.map((l) => compiledFor(l));\n const planes = active.map((l) => masks?.get(l.id));\n const data = imageData.data;\n const len = data.length;\n const c: Rgb = [0, 0, 0];\n for (let i = 0; i < len; i += 4) {\n if (data[i + 3] === 0) continue;\n c[0] = data[i] / 255;\n c[1] = data[i + 1] / 255;\n c[2] = data[i + 2] / 255;\n for (let k = 0; k < active.length; k++) {\n const plane = planes[k];\n if (plane) {\n const cov = plane[i >> 2];\n if (cov > 0) applyLayer(c, active[k], ops[k], cov >= 1 ? 1 : cov);\n } else {\n applyLayer(c, active[k], ops[k]);\n }\n }\n data[i] = Math.round(clamp01(c[0]) * 255);\n data[i + 1] = Math.round(clamp01(c[1]) * 255);\n data[i + 2] = Math.round(clamp01(c[2]) * 255);\n }\n}\n\n/** Stable cache key of a layer stack's pixel effect (for renderer caches). */\nexport function adjustmentLayersKey(layers: readonly AdjustmentLayer[] | undefined): string {\n if (!layers?.length) return '';\n return JSON.stringify(\n layers.map((l) => {\n const key: unknown[] = [l.kind, l.enabled !== false, l.opacity ?? 1, l.blend ?? 'normal', l.params];\n if (l.mask) key.push(layerMaskKey(l.mask));\n return key;\n })\n );\n}\n","import type { DepthMaskOptions, ImageAdjustments } from './types';\nimport {\n LUMA_709,\n LATEST_PROCESS_VERSION,\n TONE_LUT_SIZE,\n buildToneLut,\n linearToSrgb,\n normalizeProcessVersion,\n srgbToLinear,\n whiteBalanceMatrix,\n} from './color';\nimport { applyAdjustmentLayers } from './adjustment-layers';\n\nfunction clamp(value: number, min = 0, max = 255): number {\n return Math.max(min, Math.min(max, value));\n}\n\n/**\n * Applies color and tone adjustments (brightness, contrast, saturation, exposure, temperature, tint,\n * opacity, then any adjustment `layers`, then blur) directly to the pixel data of an ImageData buffer.\n *\n * `adjustments.process` selects the rendering: `2` is the linear-light pipeline\n * ({@link applyProcess2Adjustments}); anything else (including a missing value, as in edits saved\n * before process versions existed) is the frozen legacy rendering ({@link applyLegacyAdjustments}).\n */\nexport function applyAdjustments(\n imageData: ImageData,\n adjustments: ImageAdjustments,\n options: { layerMasks?: ReadonlyMap<string, ArrayLike<number>> } = {}\n): void {\n if (normalizeProcessVersion(adjustments.process) === LATEST_PROCESS_VERSION) {\n applyProcess2Adjustments(imageData, adjustments);\n } else {\n applyLegacyAdjustments(imageData, adjustments);\n }\n\n // Adjustment layers, bottom to top, on the encoded result (no-op without layers).\n // Layer masks (see `buildLayerMaskPlanes`) are aligned with `imageData`.\n applyAdjustmentLayers(imageData, adjustments.layers, options.layerMasks);\n\n // Apply blur if requested\n if (adjustments.blur > 0) {\n applyFastBoxBlur(imageData, Math.min(Math.round(adjustments.blur), 40));\n }\n}\n\n/**\n * Process 1: the original 8-bit gamma-space rendering, clamping after every step. Frozen so\n * that edits saved without a `process` look exactly as they always have. `tint` is ignored.\n */\nexport function applyLegacyAdjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const { brightness, contrast, saturation, exposure, temperature, opacity } = adjustments;\n const data = imageData.data;\n const len = data.length;\n\n const hasBrightness = brightness !== 0;\n const hasContrast = contrast !== 0;\n const hasSaturation = saturation !== 0;\n const hasExposure = exposure !== 0;\n const hasTemperature = temperature !== 0;\n const hasOpacity = opacity < 1;\n\n // Pre-calculate contrast factor\n const contrastFactor = hasContrast\n ? (259 * (contrast + 255)) / (255 * (259 - contrast))\n : 1;\n\n // Pre-calculate exposure multiplier\n const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;\n\n // Pre-calculate saturation multiplier\n const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;\n\n // Pre-calculate temperature offsets\n const tempOffset = hasTemperature ? temperature * 0.8 : 0;\n\n if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {\n for (let i = 0; i < len; i += 4) {\n let r = data[i];\n let g = data[i + 1];\n let b = data[i + 2];\n let a = data[i + 3];\n\n // Exposure\n if (hasExposure) {\n r = clamp(r * exposureMultiplier);\n g = clamp(g * exposureMultiplier);\n b = clamp(b * exposureMultiplier);\n }\n\n // Brightness\n if (hasBrightness) {\n const bOffset = brightness * 2.55;\n r = clamp(r + bOffset);\n g = clamp(g + bOffset);\n b = clamp(b + bOffset);\n }\n\n // Contrast\n if (hasContrast) {\n r = clamp(contrastFactor * (r - 128) + 128);\n g = clamp(contrastFactor * (g - 128) + 128);\n b = clamp(contrastFactor * (b - 128) + 128);\n }\n\n // Saturation\n if (hasSaturation) {\n const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;\n r = clamp(luminance + (r - luminance) * satMultiplier);\n g = clamp(luminance + (g - luminance) * satMultiplier);\n b = clamp(luminance + (b - luminance) * satMultiplier);\n }\n\n // Temperature / Warmth\n if (hasTemperature) {\n r = clamp(r + tempOffset);\n b = clamp(b - tempOffset);\n }\n\n // Opacity\n if (hasOpacity) {\n a = clamp(a * opacity);\n }\n\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n data[i + 3] = a;\n }\n }\n}\n\n/** sRGB byte → linear light (exact; shared by every process 2 render). */\nconst DECODE_LUT: Float32Array = (() => {\n const lut = new Float32Array(256);\n for (let i = 0; i < 256; i++) lut[i] = srgbToLinear(i / 255);\n return lut;\n})();\n\n/** Intervals of the linear → sRGB byte encode table over [0, 1]. */\nconst ENCODE_LUT_SIZE = 65535;\nlet encodeLut: Uint8Array | null = null;\nfunction getEncodeLut(): Uint8Array {\n if (!encodeLut) {\n encodeLut = new Uint8Array(ENCODE_LUT_SIZE + 1);\n for (let i = 0; i <= ENCODE_LUT_SIZE; i++) {\n encodeLut[i] = Math.round(linearToSrgb(i / ENCODE_LUT_SIZE) * 255);\n }\n }\n return encodeLut;\n}\n\ninterface Process2Plan {\n key: string;\n /** Exposure gain folded into the white-balance matrix (row-major). */\n m: Float64Array;\n /** Per-channel tone LUTs over [0, headroom[c]]. */\n luts: [Float32Array, Float32Array, Float32Array];\n /** LUT index scale per channel: TONE_LUT_SIZE / headroom[c]. */\n scale: [number, number, number];\n saturation: number;\n}\n\nlet cachedPlan: Process2Plan | null = null;\n\n/**\n * Precomputes everything per parameter change: the 3×3 colour matrix (exposure gain × white\n * balance) and one tone LUT per channel whose domain is that channel's largest possible value,\n * so the highlight shoulder absorbs it instead of clipping.\n */\nfunction getProcess2Plan(adjustments: ImageAdjustments): Process2Plan {\n const exposure = adjustments.exposure || 0;\n const temperature = adjustments.temperature || 0;\n const tint = adjustments.tint || 0;\n const contrast = adjustments.contrast || 0;\n const brightness = adjustments.brightness || 0;\n const saturation = 1 + (adjustments.saturation || 0) / 100;\n const key = `${exposure},${temperature},${tint},${contrast},${brightness},${saturation}`;\n if (cachedPlan && cachedPlan.key === key) return cachedPlan;\n\n const gain = Math.pow(2, exposure / 50);\n const wb = whiteBalanceMatrix(temperature, tint);\n const m = new Float64Array(9);\n for (let i = 0; i < 9; i++) m[i] = wb[i] * gain;\n const luts = [] as unknown as [Float32Array, Float32Array, Float32Array];\n const scale = [0, 0, 0] as [number, number, number];\n for (let c = 0; c < 3; c++) {\n // Largest value this channel can reach from inputs in [0, 1].\n let reach = 0;\n for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);\n const headroom = Math.max(1, reach);\n luts[c] = buildToneLut(contrast, brightness, headroom);\n scale[c] = TONE_LUT_SIZE / headroom;\n }\n cachedPlan = { key, m, luts, scale, saturation };\n return cachedPlan;\n}\n\n/**\n * Process 2: decode sRGB to linear float once; apply exposure (gain) and white balance (a\n * Bradford matrix relative to as-shot) in linear light; a per-channel tone LUT for the\n * highlight shoulder, contrast and brightness; luminance-preserving saturation in linear light;\n * then encode back to sRGB through a LUT. Nothing is clamped between stages, only at the final\n * encode. Neutral settings are the identity.\n */\nexport function applyProcess2Adjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const data = imageData.data;\n const len = data.length;\n const opacity = adjustments.opacity ?? 1;\n const hasOpacity = opacity < 1;\n const hasColor =\n !!adjustments.brightness ||\n !!adjustments.contrast ||\n !!adjustments.saturation ||\n !!adjustments.exposure ||\n !!adjustments.temperature ||\n !!adjustments.tint;\n\n if (!hasColor) {\n if (hasOpacity) {\n for (let i = 3; i < len; i += 4) data[i] = clamp(data[i] * opacity);\n }\n return;\n }\n\n const plan = getProcess2Plan(adjustments);\n const dec = DECODE_LUT;\n const enc = getEncodeLut();\n const { m, luts, scale, saturation } = plan;\n const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3], m4 = m[4], m5 = m[5], m6 = m[6], m7 = m[7], m8 = m[8];\n const lutR = luts[0], lutG = luts[1], lutB = luts[2];\n const sR = scale[0], sG = scale[1], sB = scale[2];\n const N = TONE_LUT_SIZE;\n const hasSaturation = saturation !== 1;\n const wr = LUMA_709[0], wg = LUMA_709[1], wb = LUMA_709[2];\n\n for (let i = 0; i < len; i += 4) {\n const r0 = dec[data[i]];\n const g0 = dec[data[i + 1]];\n const b0 = dec[data[i + 2]];\n\n // Exposure × white balance (linear light, unclamped).\n let r = m0 * r0 + m1 * g0 + m2 * b0;\n let g = m3 * r0 + m4 * g0 + m5 * b0;\n let b = m6 * r0 + m7 * g0 + m8 * b0;\n\n // Tone LUTs (linear interpolation).\n let f: number;\n let k: number;\n if (r <= 0) r = lutR[0];\n else if ((f = r * sR) >= N) r = lutR[N];\n else { k = f | 0; r = lutR[k] + (lutR[k + 1] - lutR[k]) * (f - k); }\n if (g <= 0) g = lutG[0];\n else if ((f = g * sG) >= N) g = lutG[N];\n else { k = f | 0; g = lutG[k] + (lutG[k + 1] - lutG[k]) * (f - k); }\n if (b <= 0) b = lutB[0];\n else if ((f = b * sB) >= N) b = lutB[N];\n else { k = f | 0; b = lutB[k] + (lutB[k + 1] - lutB[k]) * (f - k); }\n\n // Luminance-preserving saturation in linear light.\n if (hasSaturation) {\n const y = wr * r + wg * g + wb * b;\n r = y + (r - y) * saturation;\n g = y + (g - y) * saturation;\n b = y + (b - y) * saturation;\n }\n\n // Encode (the only clamp).\n data[i] = enc[r <= 0 ? 0 : r >= 1 ? ENCODE_LUT_SIZE : (r * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 1] = enc[g <= 0 ? 0 : g >= 1 ? ENCODE_LUT_SIZE : (g * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 2] = enc[b <= 0 ? 0 : b >= 1 ? ENCODE_LUT_SIZE : (b * ENCODE_LUT_SIZE + 0.5) | 0];\n if (hasOpacity) data[i + 3] = clamp(data[i + 3] * opacity);\n }\n}\n\n/**\n * Fast separable box blur running horizontally and vertically (`adjustments.blur` and the `blur`\n * layer filter): `radius` pixels each side, edges clamped, rounded after each pass.\n */\nexport function applyBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n applyFastBoxBlur(imageData, radius);\n}\n\nfunction applyFastBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n if (radius < 1) return;\n const w = imageData.width;\n const h = imageData.height;\n const src = imageData.data;\n const temp = new Uint8ClampedArray(src.length);\n\n // Horizontal blur pass\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const px = Math.min(w - 1, Math.max(0, x + k));\n const idx = (y * w + px) * 4;\n r += src[idx];\n g += src[idx + 1];\n b += src[idx + 2];\n a += src[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n temp[dstIdx] = Math.round(r / count);\n temp[dstIdx + 1] = Math.round(g / count);\n temp[dstIdx + 2] = Math.round(b / count);\n temp[dstIdx + 3] = Math.round(a / count);\n }\n }\n\n // Vertical blur pass back into src\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const py = Math.min(h - 1, Math.max(0, y + k));\n const idx = (py * w + x) * 4;\n r += temp[idx];\n g += temp[idx + 1];\n b += temp[idx + 2];\n a += temp[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n src[dstIdx] = Math.round(r / count);\n src[dstIdx + 1] = Math.round(g / count);\n src[dstIdx + 2] = Math.round(b / count);\n src[dstIdx + 3] = Math.round(a / count);\n }\n }\n}\n\n/**\n * Blends a depth/alpha mask into the main image data based on Gleamforge depth masking.\n * Supports threshold range with softness gradation, brightness mode, and optional inversion.\n */\nexport function applyDepthMask(\n mainImageData: ImageData,\n maskImageData: ImageData,\n options: DepthMaskOptions\n): void {\n const { mode, depthRange, softness, invert } = options;\n const mainData = mainImageData.data;\n const maskData = maskImageData.data;\n const len = Math.min(mainData.length, maskData.length);\n const effectiveSoftness = Math.max(1, softness || 10);\n const minDepth = depthRange[0];\n const maxDepth = depthRange[1];\n\n for (let i = 0; i < len; i += 4) {\n let maskValue = maskData[i]; // Use red channel of mask (or grayscale)\n if (invert) {\n maskValue = 255 - maskValue;\n }\n\n let alphaValue: number;\n\n if (mode === 'threshold') {\n if (maskValue >= maxDepth + effectiveSoftness) {\n alphaValue = 0;\n } else if (maskValue > maxDepth) {\n alphaValue = Math.round(((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness) * 255);\n } else if (maskValue >= minDepth) {\n alphaValue = 255;\n } else if (maskValue < minDepth - effectiveSoftness) {\n alphaValue = 0;\n } else {\n alphaValue = Math.round(((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness) * 255);\n }\n } else {\n // Brightness mode\n alphaValue = clamp(maskValue + minDepth);\n }\n\n // Multiply existing alpha with mask alpha\n mainData[i + 3] = clamp((mainData[i + 3] * alphaValue) / 255);\n }\n}\n\n/**\n * Removes alpha channel by setting all alpha bytes to 255 (completely opaque).\n * From Gleamforge image-filters.ts.\n */\nexport function removeAlpha(imageData: ImageData): void {\n const data = imageData.data;\n const len = data.length;\n for (let i = 3; i < len; i += 4) {\n data[i] = 255;\n }\n}\n","// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * The image editor's layer stack: pixel, adjustment, group and annotation layers in one ordered\n * tree (bottom to top), all in document space (natural pixels of the document).\n *\n * Layer objects are immutable. Every change builds new objects along the path from the root to\n * the changed layer and shares everything else, so undo history, the compositor's caches and\n * serialization can compare layers by identity.\n */\n\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n Annotation,\n CropBox,\n DepthMaskOptions,\n} from '../types';\nimport type { PixelSurface } from '../../raster/surface';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n type GroupBlendMode,\n type LayerLocation as RasterLayerLocation,\n type LayerLocks,\n type RasterLayerBase,\n type RasterMask as BaseRasterMask,\n type RasterPixelLayer,\n} from '../../raster/layers';\n\n/** Blend modes of a layer; groups also accept `pass-through`. */\nexport type LayerBlendMode = GroupBlendMode;\n\nexport type { LayerLocks };\n\n/**\n * A layer mask (see the raster module's `RasterMask`) whose parametric `source` is a depth map:\n * a depth mask is generated from it instead of painted, and its surface is unused.\n */\nexport type RasterMask = BaseRasterMask<DepthMaskOptions>;\n\nexport interface LayerBase extends RasterLayerBase {\n mask?: RasterMask;\n}\n\n/** The base sliders of the old document-level adjustments, as a filter on one layer. */\nexport interface DevelopSettings {\n brightness: number;\n contrast: number;\n saturation: number;\n exposure: number;\n temperature: number;\n tint?: number;\n /** Rendering process (see `ImageAdjustments.process`). */\n process?: 1 | 2;\n}\n\nexport type DevelopFilter = { id: string; kind: 'develop'; enabled: boolean } & DevelopSettings;\n/** Box blur of `radius` pixels (the old `adjustments.blur`), clamped at the canvas edges. */\nexport interface BlurFilter {\n id: string;\n kind: 'blur';\n enabled: boolean;\n radius: number;\n}\nexport type LayerFilter = DevelopFilter | BlurFilter;\nexport type LayerFilterKind = LayerFilter['kind'];\n\nexport interface PixelLayer extends LayerBase, Omit<RasterPixelLayer, keyof RasterLayerBase> {\n /** Live filters, applied bottom (first) to top before the mask and blend. */\n filters?: LayerFilter[];\n /**\n * `'document'`: the pixels are the document's source image, unmodified (the Background of an\n * opened image). Such a layer's `surface` is a placeholder; read its pixels through the\n * controller. Painting it drops the flag.\n */\n source?: 'document';\n}\n\nexport type AdjustmentLayerNode<K extends AdjustmentKind = AdjustmentKind> = {\n [P in K]: LayerBase & { type: 'adjustment'; kind: P; params: AdjustmentParamsMap[P] };\n}[K];\n\nexport interface GroupLayer extends Omit<LayerBase, 'blend'> {\n type: 'group';\n blend: LayerBlendMode;\n /** Bottom to top. */\n children: ImageLayer[];\n /** UI only. */\n collapsed?: boolean;\n}\n\n/** One annotation (pen, shape, arrow, text, image) as a layer, drawn by the annotation renderer. */\nexport interface AnnotationLayer extends LayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\nexport type ImageLayer = PixelLayer | AdjustmentLayerNode | GroupLayer | AnnotationLayer;\nexport type ImageLayerType = ImageLayer['type'];\n\n/** Which part of the active layer painting targets. */\nexport type LayerTarget = 'pixels' | 'mask';\n\n// ---------------------------------------------------------------------------\n// Ids\n// ---------------------------------------------------------------------------\n\nlet idCounter = 0;\n\n/** A fresh id with `prefix` (`layer`, `flt`, …). */\nexport function newLayerId(prefix = 'layer'): string {\n return `${prefix}_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Tree helpers (pure)\n// ---------------------------------------------------------------------------\n\nexport type LayerLocation = RasterLayerLocation<ImageLayer>;\n\nexport { flattenLayers, findLayer, mapLayer, insertLayer, isLayerLocked };\n\n/** The top-level and nested pixel layers. */\nexport function pixelLayers(layers: readonly ImageLayer[]): PixelLayer[] {\n return flattenLayers(layers).filter((l): l is PixelLayer => l.type === 'pixel');\n}\n\n/** The develop filter of a pixel layer, if any. */\nexport function developFilter(layer: PixelLayer): DevelopFilter | undefined {\n return layer.filters?.find((f): f is DevelopFilter => f.kind === 'develop');\n}\n\nexport function blurFilter(layer: PixelLayer): BlurFilter | undefined {\n return layer.filters?.find((f): f is BlurFilter => f.kind === 'blur');\n}\n\n/** True for filters that change pixels (enabled, non-neutral). */\nexport function isActiveFilter(f: LayerFilter): boolean {\n if (!f.enabled) return false;\n if (f.kind === 'blur') return Math.min(Math.round(f.radius), 40) >= 1;\n return Boolean(f.brightness || f.contrast || f.saturation || f.exposure || f.temperature || f.tint);\n}\n\n/** True when a filter needs neighbouring pixels (it can't run tile by tile). */\nexport function isSpatialFilter(f: LayerFilter): boolean {\n return f.kind === 'blur';\n}\n\n/** Document-space bounds of a pixel layer's stored tiles, or `null`. */\nexport function pixelLayerBounds(layer: PixelLayer, surface: PixelSurface = layer.surface): CropBox | null {\n const b = surface.bounds;\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };\n}\n\n/** Human label of a layer kind. */\nexport function layerTypeLabel(type: ImageLayerType): string {\n return type === 'pixel' ? 'Pixel layer' : type === 'adjustment' ? 'Adjustment layer' : type === 'group' ? 'Group' : 'Annotation';\n}\n","/**\n * The image editor's compositor: the raster engine (`@pixerate/editor/raster`) with the photo\n * layer kinds plugged in. Adjustment layers filter what is below them (a run of adjacent ones is\n * one float pass with one rounding at the end, exactly as `applyAdjustmentLayers` does, so a\n * document migrated from adjustment layers renders identically); annotation layers are drawn by\n * the host; pixel layers can carry develop and blur filters; masks can be depth masks.\n */\n\nimport { applyAdjustmentLayers } from '../adjustment-layers';\nimport { applyBoxBlur, applyLegacyAdjustments, applyProcess2Adjustments } from '../filters';\nimport { LATEST_PROCESS_VERSION, normalizeProcessVersion } from '../color';\nimport type { BlendMode } from '../blend';\nimport type { AdjustmentLayer, Annotation, CropBox, DepthMaskOptions, ImageAdjustments } from '../types';\nimport {\n RasterCompositor,\n type CompositeCallOptions,\n type CompositeStats,\n type CompositorOptions,\n type CompositorProfile,\n type PixelFilterSpec,\n type RasterSources,\n} from '../../raster/compositor';\nimport type { PixelSurface } from '../../raster/surface';\nimport { isActiveFilter, isSpatialFilter, type AdjustmentLayerNode, type ImageLayer, type LayerFilter, type PixelLayer } from './model';\n\nexport { blendTile, changedTiles } from '../../raster/compositor';\nexport type { CompositeCallOptions, CompositeStats, CompositorOptions };\n\n/** What the compositor needs from its host besides the layers. */\nexport interface CompositorSources extends RasterSources {\n /** The pixels of a pixel layer (resolves layers whose pixels are the document source). */\n surfaceOf?(layer: PixelLayer): PixelSurface;\n /**\n * A parametric mask's depth map as RGBA at canvas size (the red channel is the depth), or\n * `null` while it is unavailable (the mask then reveals everything).\n */\n depthMap?(source: DepthMaskOptions): { width: number; height: number; data: ArrayLike<number> } | null;\n /** Draws one annotation into RGBA for `rect` (canvas hosts only); `null` when unavailable. */\n rasterizeAnnotation?(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null;\n}\n\nconst legacyViewCache = new WeakMap<AdjustmentLayerNode, AdjustmentLayer>();\n\n/** The `AdjustmentLayer` shape `applyAdjustmentLayers` evaluates, for a node (memoised). */\nexport function adjustmentNodeAsLayer(node: AdjustmentLayerNode): AdjustmentLayer {\n let view = legacyViewCache.get(node);\n if (!view) {\n view = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n } as AdjustmentLayer;\n legacyViewCache.set(node, view);\n }\n return view;\n}\n\n/** Develop settings as `ImageAdjustments` for the process functions (opacity is document-level). */\nfunction developAdjustments(f: Extract<LayerFilter, { kind: 'develop' }>): ImageAdjustments {\n return {\n brightness: f.brightness,\n contrast: f.contrast,\n saturation: f.saturation,\n exposure: f.exposure,\n temperature: f.temperature,\n tint: f.tint ?? 0,\n blur: 0,\n opacity: 1,\n process: f.process,\n };\n}\n\n/** Runs per-pixel filters over RGBA `data` in place. */\nfunction applyPixelFilters(data: Uint8ClampedArray, filters: readonly LayerFilter[]): void {\n for (const f of filters) {\n if (!isActiveFilter(f) || f.kind !== 'develop') continue;\n const adj = developAdjustments(f);\n if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Adjustments({ data }, adj);\n else applyLegacyAdjustments({ data }, adj);\n }\n}\n\n/** Evaluates a depth mask (see `applyDepthMask`) into one coverage byte per depth sample. */\nexport function depthMaskAlpha(depth: number, options: DepthMaskOptions): number {\n const { mode, depthRange, softness, invert } = options;\n let v = invert ? 255 - depth : depth;\n const soft = Math.max(1, softness || 10);\n const [lo, hi] = depthRange;\n if (mode === 'threshold') {\n if (v >= hi + soft) return 0;\n if (v > hi) return Math.round(((hi + soft - v) / soft) * 255);\n if (v >= lo) return 255;\n if (v < lo - soft) return 0;\n return Math.round(((v - (lo - soft)) / soft) * 255);\n }\n v += lo;\n return v < 0 ? 0 : v > 255 ? 255 : v;\n}\n\nconst filterSpecCache = new WeakMap<object, PixelFilterSpec | null>();\n\n/** A pixel layer's active filters for the engine (memoised per filter list). */\nfunction pixelFilterSpec(layer: PixelLayer): PixelFilterSpec | null {\n const list = layer.filters;\n if (!list?.length) return null;\n if (filterSpecCache.has(list)) return filterSpecCache.get(list)!;\n const filters = list.filter(isActiveFilter);\n let spec: PixelFilterSpec | null = null;\n if (filters.length) {\n const spatial = filters.some(isSpatialFilter);\n spec = {\n key: JSON.stringify(filters),\n spatial,\n apply: (data, width, height) => {\n for (const f of filters) {\n if (f.kind === 'develop') applyPixelFilters(data, [f]);\n else if (f.kind === 'blur') applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));\n }\n },\n };\n }\n filterSpecCache.set(list, spec);\n return spec;\n}\n\nconst PHOTO_PROFILE: CompositorProfile<ImageLayer, CompositorSources> = {\n isFilterLayer: (l) => l.type === 'adjustment',\n applyFilterRun: (layers, acc, _r, masks) => {\n const views = (layers as AdjustmentLayerNode[]).map(adjustmentNodeAsLayer);\n applyAdjustmentLayers({ data: acc }, views, masks);\n },\n contentTile: (l, r, sources) => (l.type === 'annotation' ? (sources.rasterizeAnnotation?.(l.annotation, r) ?? null) : null),\n /** Annotation layers draw with their annotation's blend mode unless the layer sets one. */\n contentBlend: (l) => (l.type === 'annotation' && l.blend === 'normal' && l.annotation.blend ? l.annotation.blend : (l.blend as BlendMode)),\n layerKey: (l) =>\n l.type === 'pixel' ? [l.filters ?? null, l.source ?? null] : l.type === 'adjustment' ? [l.kind, l.params] : l.type === 'annotation' ? l.annotation : undefined,\n pixelFilters: (l) => (l.type === 'pixel' ? pixelFilterSpec(l) : null),\n maskSourcePlane: (source, W, H, sources) => {\n const opts = source as DepthMaskOptions;\n const depth = sources.depthMap?.(opts) ?? null;\n if (!depth || depth.width !== W || depth.height !== H) return null;\n const v = new Float32Array(W * H);\n for (let i = 0; i < v.length; i++) v[i] = depthMaskAlpha(depth.data[i * 4], opts) / 255;\n return v;\n },\n};\n\n/** The per-tile compositor for image editor layer stacks (see the raster module for the caches). */\nexport class LayerCompositor extends RasterCompositor<ImageLayer, CompositorSources> {\n constructor(options: CompositorOptions = {}) {\n super(PHOTO_PROFILE, options);\n }\n}\n","import { LATEST_PROCESS_VERSION } from '../color';\nimport type { DepthMaskOptions, ImageAdjustments, ImageTransform } from '../types';\n\n/** Neutral base adjustments, latest process (re-exported as `DEFAULT_ADJUSTMENTS`). */\nexport const DEFAULT_ADJUSTMENTS_VALUES: ImageAdjustments = {\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n blur: 0,\n opacity: 1,\n process: LATEST_PROCESS_VERSION,\n};\n\nexport const DEFAULT_TRANSFORM_VALUES: ImageTransform = {\n rotate: 0,\n flipH: false,\n flipV: false,\n};\n\nexport const DEFAULT_DEPTH_MASK_VALUES: DepthMaskOptions = {\n enabled: false,\n maskSource: null,\n mode: 'threshold',\n depthRange: [0, 255],\n softness: 10,\n invert: false,\n};\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","/**\n * Schema version 3 of the image editor document: the layer tree, with pixel layers and painted\n * masks stored as PNG assets (cropped to their content and placed by position), plus the\n * document-level crop, transform, output opacity, migrated blur and selection.\n *\n * Encoding is synchronous and exact (see `png.ts`). Each surface's encoding is memoised, so\n * saving an unchanged layer again costs nothing.\n */\n\nimport { normalizeAdjustmentLayer } from '../adjustment-layers';\nimport { isBlendMode, normalizeBlendMode } from '../blend';\nimport { normalizeProcessVersion } from '../color';\nimport type { Annotation, CropBox, DepthMaskOptions, ImageTransform } from '../types';\nimport type { SerializedSelection } from '../selection/mask';\nimport { DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport {\n newLayerId,\n type AdjustmentLayerNode,\n type GroupLayer,\n type ImageLayer,\n type LayerBase,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { bytesToBase64, decodePng, parseDataUrl, pngDataUrl } from '../../raster/png';\nimport { contentBounds, emptySurface, readRegion, surfaceFromPixels, type TiledSurface } from '../../raster/surface';\n\nexport const LAYERED_SCHEMA_VERSION = 3 as const;\n\n/** A stored raster mask. Pixels: `asset` (grey PNG) placed at `x`, `y`; elsewhere `defaultValue`. */\nexport interface SerializedRasterMask {\n defaultValue: 0 | 255;\n asset?: string;\n x?: number;\n y?: number;\n density?: number;\n feather?: number;\n enabled?: boolean;\n source?: DepthMaskOptions;\n}\n\ninterface SerializedLayerBase {\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: string;\n clipped?: boolean;\n mask?: SerializedRasterMask;\n locks?: LayerLocks;\n}\n\nexport interface SerializedPixelLayer extends SerializedLayerBase {\n type: 'pixel';\n /** Asset id of the pixels (RGBA PNG) placed at `x`, `y` in layer space; missing when empty. */\n asset?: string;\n x?: number;\n y?: number;\n offset: { x: number; y: number };\n fillOpacity?: number;\n filters?: LayerFilter[];\n /** `'document'`: the pixels are the document's source image (no asset is stored). */\n source?: 'document';\n}\n\nexport interface SerializedAdjustmentLayer extends SerializedLayerBase {\n type: 'adjustment';\n kind: string;\n params: unknown;\n}\n\nexport interface SerializedGroupLayer extends SerializedLayerBase {\n type: 'group';\n children: SerializedLayer[];\n collapsed?: boolean;\n}\n\nexport interface SerializedAnnotationLayer extends SerializedLayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\nexport type SerializedLayer = SerializedPixelLayer | SerializedAdjustmentLayer | SerializedGroupLayer | SerializedAnnotationLayer;\n\n/** A version 3 document (layers). */\nexport interface SerializedLayeredImageEditorState {\n version: 3;\n /** Canvas size in pixels. */\n width: number;\n height: number;\n /** Bottom to top. */\n layers: SerializedLayer[];\n /** Asset id → PNG data URL (or an external reference, see `ImageEditorAssetStore`). */\n assets: Record<string, string>;\n activeLayerId: string | null;\n crop: CropBox | null;\n transform: ImageTransform;\n /** Export alpha, 0..1. */\n opacity: number;\n /** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */\n legacyOutputBlur?: number;\n selection?: SerializedSelection;\n}\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\ninterface EncodedSurface {\n url: string;\n x: number;\n y: number;\n}\n\nconst encodedCache = new WeakMap<TiledSurface, EncodedSurface | null>();\n\n/** The surface's content (non-default pixels) as a PNG data URL and its position, or `null`. */\nexport function encodeSurface(surface: TiledSurface): EncodedSurface | null {\n if (encodedCache.has(surface)) return encodedCache.get(surface)!;\n const b = contentBounds(surface);\n let out: EncodedSurface | null = null;\n if (b) {\n const data = readRegion(surface, b);\n out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };\n }\n encodedCache.set(surface, out);\n return out;\n}\n\nexport interface SerializeContext {\n /** Adds an asset and returns its id. */\n addAsset(url: string, hint: string): string;\n}\n\nfunction baseFields(layer: ImageLayer, ctx: SerializeContext): SerializedLayerBase {\n const out: SerializedLayerBase = {\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend: layer.blend,\n };\n if (layer.clipped) out.clipped = true;\n if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);\n if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };\n return out;\n}\n\nfunction serializeMask(mask: RasterMask, hint: string, ctx: SerializeContext): SerializedRasterMask {\n const out: SerializedRasterMask = { defaultValue: mask.defaultValue };\n if (!mask.source) {\n const enc = encodeSurface(mask.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, hint);\n out.x = enc.x;\n out.y = enc.y;\n }\n } else {\n out.source = { ...mask.source, depthRange: [...mask.source.depthRange] as [number, number] };\n }\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\nexport function serializeLayer(layer: ImageLayer, ctx: SerializeContext): SerializedLayer {\n const base = baseFields(layer, ctx);\n switch (layer.type) {\n case 'pixel': {\n const out: SerializedPixelLayer = { ...base, type: 'pixel', offset: { ...layer.offset } };\n if (layer.source === 'document') out.source = 'document';\n else {\n const enc = encodeSurface(layer.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, layer.id);\n out.x = enc.x;\n out.y = enc.y;\n }\n }\n if (layer.fillOpacity !== undefined && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;\n if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));\n return out;\n }\n case 'adjustment':\n return { ...base, type: 'adjustment', kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };\n case 'group': {\n const out: SerializedGroupLayer = { ...base, type: 'group', children: layer.children.map((c) => serializeLayer(c, ctx)) };\n if (layer.collapsed) out.collapsed = true;\n return out;\n }\n case 'annotation':\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify(layer.annotation)) };\n }\n}\n\n/** Serializes a layer tree with inline assets (asset ids are derived from layer ids). */\nexport function serializeLayers(layers: readonly ImageLayer[]): { layers: SerializedLayer[]; assets: Record<string, string> } {\n const assets: Record<string, string> = {};\n const ctx: SerializeContext = {\n addAsset(url, hint) {\n let id = hint;\n for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;\n assets[id] = url;\n return id;\n },\n };\n return { layers: layers.map((l) => serializeLayer(l, ctx)), assets };\n}\n\n// ---------------------------------------------------------------------------\n// External assets\n// ---------------------------------------------------------------------------\n\n/**\n * Where a host keeps layer pixels outside the JSON (`toJSON({ assets: 'external', store })`):\n * `put` stores a PNG and returns a reference (a URL, a key) that `get` resolves later.\n */\nexport interface ImageEditorAssetStore {\n put(blob: Blob, info: { id: string; width: number; height: number }): Promise<string>;\n get(ref: string): Promise<Blob>;\n}\n\nconst externalRefs = new WeakMap<ImageEditorAssetStore, Map<string, string>>();\n\n/**\n * Serializes a layer tree with its assets in `store`: each PNG is uploaded once per store (by\n * content), and `assets` maps asset ids to the store's references.\n */\nexport async function serializeLayersExternal(\n layers: readonly ImageLayer[],\n store: ImageEditorAssetStore\n): Promise<{ layers: SerializedLayer[]; assets: Record<string, string> }> {\n const inline = serializeLayers(layers);\n let refs = externalRefs.get(store);\n if (!refs) {\n refs = new Map();\n externalRefs.set(store, refs);\n }\n const assets: Record<string, string> = {};\n for (const [id, url] of Object.entries(inline.assets)) {\n let ref = refs.get(url);\n if (!ref) {\n const parsed = parseDataUrl(url)!;\n const png = decodePngHeader(parsed.bytes);\n ref = await store.put(new Blob([parsed.bytes as BlobPart], { type: 'image/png' }), { id, width: png.width, height: png.height });\n if (typeof ref !== 'string' || !ref) throw new Error(`The asset store returned no reference for \"${id}\".`);\n refs.set(url, ref);\n }\n assets[id] = ref;\n }\n return { layers: inline.layers, assets };\n}\n\n/** Width and height from a PNG's header. */\nfunction decodePngHeader(bytes: Uint8Array): { width: number; height: number } {\n const u32 = (o: number) => ((bytes[o] << 24) | (bytes[o + 1] << 16) | (bytes[o + 2] << 8) | bytes[o + 3]) >>> 0;\n return { width: u32(16), height: u32(20) };\n}\n\n/**\n * Turns external asset references back into data URLs through `store.get` (data URLs pass through),\n * so the document loads like an inline one. References that fail are reported and left out.\n */\nexport async function resolveExternalAssets(\n assets: Record<string, string>,\n store: ImageEditorAssetStore | undefined,\n problems: string[]\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const [id, ref] of Object.entries(assets)) {\n if (ref.startsWith('data:')) {\n out[id] = ref;\n continue;\n }\n if (!store) {\n problems.push(`Asset \"${id}\" is external (${ref}) and no asset store was given.`);\n continue;\n }\n try {\n const blob = await store.get(ref);\n const bytes = new Uint8Array(await blob.arrayBuffer());\n out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;\n } catch (err) {\n problems.push(`Asset \"${id}\" (${ref}) could not be fetched: ${(err as Error).message}`);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nexport interface DeserializeContext {\n assets: Record<string, string>;\n /** Decodes an asset (PNG data URL by default). Throw for an unusable asset. */\n decodeAsset?(ref: string, channels: 1 | 4): { width: number; height: number; data: ArrayLike<number> };\n /** Problems found while reading (unknown kinds, broken assets); the rest still loads. */\n problems: string[];\n}\n\n/** Decodes a PNG data URL into pixels of `channels` bytes each. */\nexport function decodePngDataUrl(url: string, channels: 1 | 4): { width: number; height: number; data: Uint8ClampedArray } {\n const parsed = parseDataUrl(url);\n if (!parsed) throw new Error('Asset is not a data URL.');\n const png = decodePng(parsed.bytes, { keepGray: channels === 1 });\n if (png.channels === channels) return png;\n if (channels === 1) {\n // An RGBA PNG used as a mask: take its luminance (alpha-weighted).\n const out = new Uint8ClampedArray(png.width * png.height);\n for (let i = 0, p = 0; p < out.length; i += 4, p++) {\n out[p] = Math.round(((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3]) / 255);\n }\n return { width: png.width, height: png.height, data: out };\n }\n const out = new Uint8ClampedArray(png.width * png.height * 4);\n for (let p = 0; p < png.width * png.height; p++) {\n out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];\n out[p * 4 + 3] = 255;\n }\n return { width: png.width, height: png.height, data: out };\n}\n\nconst num = (v: unknown, lo: number, hi: number, fallback: number) =>\n typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;\n\nfunction readSurface(ref: unknown, x: unknown, y: unknown, channels: 1 | 4, defaultValue: number, ctx: DeserializeContext, what: string): TiledSurface {\n if (typeof ref !== 'string' || !ref) return emptySurface(channels, defaultValue);\n const url = ctx.assets[ref];\n if (typeof url !== 'string') {\n ctx.problems.push(`${what}: asset \"${ref}\" is missing`);\n return emptySurface(channels, defaultValue);\n }\n try {\n const px = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);\n return surfaceFromPixels(px, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);\n } catch (err) {\n ctx.problems.push(`${what}: asset \"${ref}\" could not be decoded (${(err as Error).message})`);\n return emptySurface(channels, defaultValue);\n }\n}\n\nfunction readMask(raw: unknown, ctx: DeserializeContext, what: string): RasterMask | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, unknown>;\n const defaultValue: 0 | 255 = r.defaultValue === 255 ? 255 : 0;\n const mask: RasterMask = {\n surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),\n defaultValue: r.source ? 255 : defaultValue,\n };\n if (typeof r.density === 'number' && r.density !== 1) mask.density = num(r.density, 0, 1, 1);\n if (typeof r.feather === 'number' && r.feather > 0) mask.feather = num(r.feather, 0, 1000, 0);\n if (r.enabled === false) mask.enabled = false;\n if (r.source && typeof r.source === 'object') {\n const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...(r.source as Partial<DepthMaskOptions>) };\n mask.source = { ...s, depthRange: [...s.depthRange] as [number, number] };\n }\n return mask;\n}\n\nfunction readFilters(raw: unknown): LayerFilter[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: LayerFilter[] = [];\n for (const f of raw) {\n if (!f || typeof f !== 'object') continue;\n const r = f as Record<string, unknown>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId('flt');\n const enabled = r.enabled !== false;\n if (r.kind === 'blur') out.push({ id, kind: 'blur', enabled, radius: num(r.radius, 0, 50, 0) });\n else if (r.kind === 'develop') {\n out.push({\n id,\n kind: 'develop',\n enabled,\n brightness: num(r.brightness, -100, 100, 0),\n contrast: num(r.contrast, -100, 100, 0),\n saturation: num(r.saturation, -100, 100, 0),\n exposure: num(r.exposure, -100, 100, 0),\n temperature: num(r.temperature, -100, 100, 0),\n tint: num(r.tint, -100, 100, 0),\n process: normalizeProcessVersion(r.process),\n });\n }\n }\n return out.length ? out : undefined;\n}\n\nexport function deserializeLayer(raw: unknown, ctx: DeserializeContext): ImageLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId();\n const base: LayerBase = {\n id,\n name: typeof r.name === 'string' ? r.name : '',\n visible: r.visible !== false,\n opacity: num(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n };\n if (r.clipped === true) base.clipped = true;\n const mask = readMask(r.mask, ctx, `Layer \"${base.name || id}\"`);\n if (mask) base.mask = mask;\n if (r.locks && typeof r.locks === 'object') {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (r.locks[k] === true) locks[k] = true;\n if (Object.keys(locks).length) base.locks = locks;\n }\n switch (r.type) {\n case 'pixel': {\n const layer: PixelLayer = {\n ...base,\n type: 'pixel',\n surface: r.source === 'document' ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer \"${base.name || id}\"`),\n offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) },\n };\n if (r.source === 'document') layer.source = 'document';\n if (typeof r.fillOpacity === 'number' && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);\n const filters = readFilters(r.filters);\n if (filters) layer.filters = filters;\n return layer;\n }\n case 'adjustment': {\n const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });\n if (!norm) {\n ctx.problems.push(`Layer \"${base.name || id}\": unknown adjustment kind \"${r.kind}\"`);\n return null;\n }\n return { ...base, type: 'adjustment', kind: norm.kind, params: norm.params } as AdjustmentLayerNode;\n }\n case 'group': {\n const children = Array.isArray(r.children) ? r.children.map((c: unknown) => deserializeLayer(c, ctx)).filter(Boolean) : [];\n const blend = r.blend === 'pass-through' ? 'pass-through' : isBlendMode(r.blend) ? r.blend : 'pass-through';\n const group: GroupLayer = { ...base, type: 'group', blend, children: children as ImageLayer[] };\n if (r.collapsed === true) group.collapsed = true;\n return group;\n }\n case 'annotation': {\n const a = r.annotation;\n if (!a || typeof a !== 'object' || typeof a.type !== 'string') {\n ctx.problems.push(`Layer \"${base.name || id}\": annotation is missing`);\n return null;\n }\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === 'string' ? a.id : id })) };\n }\n default:\n ctx.problems.push(`Layer \"${base.name || id}\": unknown layer type \"${r.type}\"`);\n return null;\n }\n}\n\n/** Reads a stored layer tree (layers that cannot be read are dropped and reported). */\nexport function deserializeLayers(raw: unknown, ctx: DeserializeContext): ImageLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const dedupe = (list: ImageLayer[]): ImageLayer[] =>\n list.map((l) => {\n let next = l;\n if (seen.has(l.id)) next = { ...l, id: newLayerId() } as ImageLayer;\n seen.add(next.id);\n if (next.type === 'group') next = { ...next, children: dedupe(next.children) };\n return next;\n });\n return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean) as ImageLayer[]);\n}\n","/**\n * Layer tree operations as pure functions: each validates its arguments against the stack and\n * throws a `LayerOperationError` that says what is wrong before changing anything, and returns a\n * new stack that shares every untouched layer. The controller wraps them with undo history.\n */\n\nimport type { BlendMode } from '../blend';\nimport { isBlendMode } from '../blend';\nimport type { CropBox } from '../types';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n newLayerId,\n type GroupLayer,\n type ImageLayer,\n type LayerBlendMode,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\n\n/** A refused layer operation; the message says why and how to fix it. */\nexport class LayerOperationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LayerOperationError';\n }\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\n/** Where a layer goes: above or below a layer (in its group), or at a position in a group. */\nexport interface LayerPlacement {\n /** Directly above this layer, in the same group. */\n aboveId?: string;\n /** Directly below this layer, in the same group. */\n belowId?: string;\n /** Inside this group (at `index`, default the top). */\n parentId?: string;\n /** Index in the parent's children (or the top level), 0 = bottom. */\n index?: number;\n}\n\n/** The layer with `id`, or a `LayerOperationError` listing the ids that exist. */\nexport function requireLayer(layers: readonly ImageLayer[], id: string) {\n const loc = findLayer(layers, id);\n if (!loc) {\n const ids = flattenLayers(layers).map((l) => l.id);\n fail(`Unknown layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'The document has no layers.'} Call list_layers for ids.`);\n }\n return loc!;\n}\n\n/** Refuses when the layer is locked for `what` (and always for `all`). */\nexport function requireUnlocked(layer: ImageLayer, what: keyof LayerLocks, action: string): void {\n if (isLayerLocked(layer, 'all')) fail(`Layer \"${layer.name || layer.id}\" is locked (all); unlock it before you ${action}.`);\n if (what !== 'all' && isLayerLocked(layer, what)) fail(`Layer \"${layer.name || layer.id}\" has its ${what} locked; unlock it before you ${action}.`);\n}\n\n/** Inserts `layer` at `placement` (default: the top level at `defaultIndex`, or the top). */\nexport function placeLayer(layers: readonly ImageLayer[], layer: ImageLayer, placement: LayerPlacement = {}, defaultIndex?: number): ImageLayer[] {\n const given = [placement.aboveId, placement.belowId, placement.parentId].filter((v) => v !== undefined).length;\n if (given > 1) fail('Give only one of aboveId, belowId and parentId.');\n if (placement.aboveId || placement.belowId) {\n const ref = requireLayer(layers, (placement.aboveId ?? placement.belowId)!);\n return insertLayer(layers, layer, ref.parent?.id ?? null, ref.index + (placement.aboveId ? 1 : 0));\n }\n if (placement.parentId) {\n const ref = requireLayer(layers, placement.parentId);\n if (ref.layer.type !== 'group') fail(`Layer \"${placement.parentId}\" is not a group; parentId must be a group id.`);\n return insertLayer(layers, layer, ref.layer.id, placement.index);\n }\n return insertLayer(layers, layer, null, placement.index ?? defaultIndex);\n}\n\n/** Removes a layer (refused when it is locked). */\nexport function removeLayer(layers: readonly ImageLayer[], id: string): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'delete it');\n return mapLayer(layers, id, () => null);\n}\n\n/** Moves a layer to `placement`. A group cannot go inside itself. Returns the new stack. */\nexport function moveLayer(layers: readonly ImageLayer[], id: string, placement: LayerPlacement): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'move it');\n const target = placement.aboveId ?? placement.belowId ?? placement.parentId;\n if (target === id) fail('A layer cannot be placed relative to itself.');\n if (target && loc.layer.type === 'group' && flattenLayers(loc.layer.children).some((l) => l.id === target)) {\n fail(`Group \"${id}\" cannot be moved into its own child \"${target}\".`);\n }\n if (target) requireLayer(layers, target);\n if (!target && placement.index === undefined) fail('Give aboveId, belowId, parentId or index.');\n const without = mapLayer(layers, id, () => null);\n return placeLayer(without, loc.layer, placement);\n}\n\n/** Every id in a subtree gets a fresh one (for duplicates). */\nexport function withNewIds<T extends ImageLayer>(layer: T): T {\n const next = { ...layer, id: newLayerId(layer.type === 'adjustment' ? 'adj' : layer.type) } as T;\n if (next.type === 'group') (next as GroupLayer).children = (next as GroupLayer).children.map(withNewIds);\n if (next.type === 'annotation') next.annotation = { ...next.annotation, id: next.id };\n return next;\n}\n\n/** Duplicates a layer (and its children) directly above it. Returns the stack and the copy's id. */\nexport function duplicateLayer(layers: readonly ImageLayer[], id: string, name?: string): { layers: ImageLayer[]; id: string } {\n const loc = requireLayer(layers, id);\n const copy = withNewIds(loc.layer);\n copy.name = name ?? `${loc.layer.name || 'Layer'} copy`;\n delete copy.locks;\n return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), id: copy.id };\n}\n\n/** Properties `setLayerProperties` changes. */\nexport interface LayerProperties {\n name?: string;\n visible?: boolean;\n /** 0..1 */\n opacity?: number;\n /** 0..1, pixel layers only. */\n fillOpacity?: number;\n blend?: LayerBlendMode;\n clipped?: boolean;\n locks?: LayerLocks;\n /** Groups only (UI state). */\n collapsed?: boolean;\n}\n\n/** Validates and applies property changes to one layer. */\nexport function setLayerProperties(layers: readonly ImageLayer[], id: string, props: LayerProperties): ImageLayer[] {\n const loc = requireLayer(layers, id);\n const layer = loc.layer;\n const onlyUnlocking = Object.keys(props).every((k) => k === 'locks' || k === 'visible' || k === 'collapsed');\n if (!onlyUnlocking) requireUnlocked(layer, 'all', 'change its properties');\n const next = { ...layer } as ImageLayer;\n if (props.name !== undefined) {\n if (typeof props.name !== 'string' || !props.name.trim()) fail('name must be a non-empty string.');\n next.name = props.name.trim();\n }\n if (props.visible !== undefined) next.visible = Boolean(props.visible);\n if (props.opacity !== undefined) {\n if (!(typeof props.opacity === 'number' && props.opacity >= 0 && props.opacity <= 1)) fail('opacity must be a number from 0 to 1.');\n next.opacity = props.opacity;\n }\n if (props.fillOpacity !== undefined) {\n if (layer.type !== 'pixel') fail(`fillOpacity applies to pixel layers; \"${id}\" is a ${layer.type} layer.`);\n if (!(typeof props.fillOpacity === 'number' && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail('fillOpacity must be a number from 0 to 1.');\n const p = next as PixelLayer;\n if (props.fillOpacity === 1) delete p.fillOpacity;\n else p.fillOpacity = props.fillOpacity;\n }\n if (props.blend !== undefined) {\n if (props.blend === 'pass-through') {\n if (layer.type !== 'group') fail('pass-through is only for groups.');\n } else if (!isBlendMode(props.blend)) fail(`Unknown blend mode \"${props.blend}\".`);\n (next as { blend: LayerBlendMode }).blend = props.blend;\n }\n if (props.clipped !== undefined) {\n if (props.clipped && loc.index === 0) fail('The bottom layer of a group or the stack has nothing below to clip to.');\n if (props.clipped) next.clipped = true;\n else delete next.clipped;\n }\n if (props.locks !== undefined) {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (props.locks[k]) locks[k] = true;\n if (Object.keys(locks).length) next.locks = locks;\n else delete next.locks;\n }\n if (props.collapsed !== undefined) {\n if (layer.type !== 'group') fail('collapsed applies to groups.');\n const g = next as GroupLayer;\n if (props.collapsed) g.collapsed = true;\n else delete g.collapsed;\n }\n return mapLayer(layers, id, () => next);\n}\n\n/**\n * Puts layers (siblings in one group or the top level) into a new group at the position of the\n * topmost one, keeping their order. Returns the stack and the group id.\n */\nexport function groupLayers(layers: readonly ImageLayer[], ids: string[], name = 'Group'): { layers: ImageLayer[]; id: string } {\n if (!ids.length) fail('Give at least one layer id to group.');\n const locs = ids.map((id) => requireLayer(layers, id));\n const parent = locs[0].parent?.id ?? null;\n if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail('Layers to group must share one parent; move them next to each other first.');\n for (const l of locs) requireUnlocked(l.layer, 'all', 'group it');\n const ordered = [...new Set(locs)].sort((a, b) => a.index - b.index);\n const top = ordered[ordered.length - 1].index;\n const below = ordered.filter((l) => l.index < top).length;\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name,\n visible: true,\n opacity: 1,\n blend: 'pass-through',\n children: ordered.map((l) => {\n const c = { ...l.layer } as ImageLayer;\n return c;\n }),\n };\n // The bottom child cannot be clipped to anything inside the new group.\n if (group.children[0]?.clipped) {\n const c = { ...group.children[0] };\n delete c.clipped;\n group.children[0] = c;\n }\n let out = layers as ImageLayer[];\n for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);\n return { layers: insertLayer(out, group, parent, top - below), id: group.id };\n}\n\n/** Replaces a group by its children (the group's own opacity, blend and mask are dropped). */\nexport function ungroupLayer(layers: readonly ImageLayer[], id: string): { layers: ImageLayer[]; ids: string[] } {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'group') fail(`Layer \"${id}\" is not a group.`);\n requireUnlocked(loc.layer, 'all', 'ungroup it');\n const group = loc.layer as GroupLayer;\n let out = mapLayer(layers, id, () => null);\n group.children.forEach((c, i) => {\n out = insertLayer(out, c, loc.parent?.id ?? null, loc.index + i);\n });\n return { layers: out, ids: group.children.map((c) => c.id) };\n}\n\n/** Sets a pixel layer's offset (refused when its position is locked). */\nexport function offsetLayer(layers: readonly ImageLayer[], id: string, offset: { x: number; y: number }): ImageLayer[] {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Only pixel layers move; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'position', 'move it');\n if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail('x and y must be numbers.');\n const layer = loc.layer as PixelLayer;\n return mapLayer(layers, id, () => ({ ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } }));\n}\n\n/** Sets or removes (`null`) a layer's mask. */\nexport function setLayerMask(layers: readonly ImageLayer[], id: string, mask: RasterMask | null): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'change its mask');\n return mapLayer(layers, id, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Filters\n// ---------------------------------------------------------------------------\n\nconst DEVELOP_FIELDS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/** A clean filter from loose input (for tools and the UI). */\nexport function normalizeFilter(raw: Partial<LayerFilter> & { kind: LayerFilter['kind'] }, id = newLayerId('flt')): LayerFilter {\n const enabled = raw.enabled !== false;\n if (raw.kind === 'blur') {\n const r = (raw as { radius?: unknown }).radius;\n if (r !== undefined && !(typeof r === 'number' && r >= 0 && r <= 50)) fail('blur radius must be a number from 0 to 50 px.');\n return { id, kind: 'blur', enabled, radius: typeof r === 'number' ? r : 2 };\n }\n if (raw.kind !== 'develop') fail(`Unknown filter kind \"${(raw as { kind: string }).kind}\". Use develop or blur.`);\n const out = { id, kind: 'develop', enabled, brightness: 0, contrast: 0, saturation: 0, exposure: 0, temperature: 0, tint: 0, process: 2 } as LayerFilter & { kind: 'develop' };\n for (const k of DEVELOP_FIELDS) {\n const v = (raw as Record<string, unknown>)[k];\n if (v === undefined) continue;\n if (!(typeof v === 'number' && v >= -100 && v <= 100)) fail(`${k} must be a number from -100 to 100.`);\n out[k] = v as number;\n }\n const p = (raw as { process?: unknown }).process;\n if (p !== undefined) {\n if (p !== 1 && p !== 2) fail('process must be 1 or 2.');\n out.process = p as 1 | 2;\n }\n return out;\n}\n\nfunction requirePixelLayer(layers: readonly ImageLayer[], id: string): PixelLayer {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Filters belong to pixel layers; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'all', 'change its filters');\n return loc.layer as PixelLayer;\n}\n\n/** Adds a filter on top of a pixel layer's filters. A layer has at most one filter of each kind. */\nexport function addLayerFilter(layers: readonly ImageLayer[], id: string, filter: LayerFilter): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (layer.filters?.some((f) => f.kind === filter.kind)) fail(`Layer \"${id}\" already has a ${filter.kind} filter; change it with update_layer_filter.`);\n return mapLayer(layers, id, () => ({ ...layer, filters: [...(layer.filters ?? []), filter] }));\n}\n\n/** Changes one filter of a pixel layer (`changes` merged and validated). */\nexport function updateLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string, changes: Record<string, unknown>): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n const i = (layer.filters ?? []).findIndex((f) => f.id === filterId);\n if (i === -1) fail(`Layer \"${id}\" has no filter \"${filterId}\". Filters: ${(layer.filters ?? []).map((f) => f.id).join(', ') || 'none'}.`);\n const current = layer.filters![i];\n if ('kind' in changes && changes.kind !== current.kind) fail('A filter kind cannot change; remove it and add another.');\n const merged = normalizeFilter({ ...current, ...changes, kind: current.kind } as LayerFilter, current.id);\n const filters = [...layer.filters!];\n filters[i] = merged;\n return mapLayer(layers, id, () => ({ ...layer, filters }));\n}\n\nexport function removeLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (!(layer.filters ?? []).some((f) => f.id === filterId)) fail(`Layer \"${id}\" has no filter \"${filterId}\".`);\n const filters = layer.filters!.filter((f) => f.id !== filterId);\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return mapLayer(layers, id, () => next);\n}\n\n/** A blend mode check for tools. */\nexport function parseBlend(value: unknown, allowPassThrough = false): BlendMode | 'pass-through' | undefined {\n if (value === undefined) return undefined;\n if (value === 'pass-through' && allowPassThrough) return value;\n if (!isBlendMode(value)) fail(`Unknown blend mode \"${String(value)}\".`);\n return value as BlendMode;\n}\n\n/** The union of two rectangles (either may be null). */\nexport function unionRect(a: CropBox | null, b: CropBox | null): CropBox | null {\n if (!a) return b;\n if (!b) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n","// Portions ported from photocraft/crates/psd/src/io.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** A malformed or unsupported PSD; the message says what was wrong and where. */\nexport class PsdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PsdError';\n }\n}\n\n/** Big-endian reader over a byte range that never reads past its end (it throws `PsdError`). */\nexport class PsdReader {\n pos: number;\n\n constructor(\n readonly data: Uint8Array,\n private readonly start = 0,\n readonly end = data.length\n ) {\n this.pos = start;\n }\n\n get remaining(): number {\n return this.end - this.pos;\n }\n\n get offset(): number {\n return this.pos - this.start;\n }\n\n private need(n: number, what: string): void {\n if (n < 0 || this.pos + n > this.end) throw new PsdError(`Unexpected end of data reading ${what} at byte ${this.pos}.`);\n }\n\n u8(): number {\n this.need(1, 'a byte');\n return this.data[this.pos++];\n }\n\n i8(): number {\n const v = this.u8();\n return v > 127 ? v - 256 : v;\n }\n\n u16(): number {\n this.need(2, 'a 16-bit value');\n const v = (this.data[this.pos] << 8) | this.data[this.pos + 1];\n this.pos += 2;\n return v;\n }\n\n i16(): number {\n const v = this.u16();\n return v > 32767 ? v - 65536 : v;\n }\n\n u32(): number {\n this.need(4, 'a 32-bit value');\n const d = this.data;\n const p = this.pos;\n this.pos += 4;\n return ((d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]) >>> 0;\n }\n\n i32(): number {\n return this.u32() | 0;\n }\n\n u64(): number {\n const hi = this.u32();\n const lo = this.u32();\n const v = hi * 2 ** 32 + lo;\n if (!Number.isSafeInteger(v)) throw new PsdError('A 64-bit length is too large.');\n return v;\n }\n\n i64(): number {\n const hi = this.i32();\n const lo = this.u32();\n return hi * 2 ** 32 + lo;\n }\n\n f32(): number {\n this.need(4, 'a float');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 4).getFloat32(0, false);\n this.pos += 4;\n return v;\n }\n\n f64(): number {\n this.need(8, 'a double');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 8).getFloat64(0, false);\n this.pos += 8;\n return v;\n }\n\n /** A length field: 8 bytes in PSB where the format says so, else 4. */\n len(long: boolean): number {\n return long ? this.u64() : this.u32();\n }\n\n bytes(n: number): Uint8Array {\n this.need(n, `${n} bytes`);\n const out = this.data.subarray(this.pos, this.pos + n);\n this.pos += n;\n return out;\n }\n\n ascii(n: number): string {\n return String.fromCharCode(...this.bytes(n));\n }\n\n skip(n: number): void {\n this.need(n, `${n} bytes`);\n this.pos += n;\n }\n\n /** A reader over the next `n` bytes (and skips them here). */\n sub(n: number): PsdReader {\n this.need(n, `a ${n}-byte section`);\n const r = new PsdReader(this.data, this.pos, this.pos + n);\n this.pos += n;\n return r;\n }\n\n peek(n: number): Uint8Array {\n return this.data.subarray(this.pos, Math.min(this.end, this.pos + n));\n }\n\n rest(): Uint8Array {\n const out = this.data.subarray(this.pos, this.end);\n this.pos = this.end;\n return out;\n }\n\n /** Refuses counts that could not fit in what is left (each item at least `size` bytes). */\n checkCount(n: number, size: number, what: string): void {\n if (n * size > this.remaining) throw new PsdError(`${what}: count ${n} does not fit in the remaining ${this.remaining} bytes.`);\n }\n\n /** Pascal string padded to a multiple of `align` (length byte included). */\n pascal(align: number): Uint8Array {\n const n = this.u8();\n const s = this.bytes(n);\n const pad = (align - ((1 + n) % align)) % align;\n this.skip(Math.min(pad, this.remaining));\n return s;\n }\n\n /** Photoshop Unicode string: u32 code-unit count, UTF-16BE units (trailing NULs dropped). */\n unicode(): string {\n const n = this.u32();\n this.checkCount(n, 2, 'Unicode string');\n let s = '';\n for (let i = 0; i < n; i++) s += String.fromCharCode(this.u16());\n return s.replace(/\\u0000+$/, '');\n }\n}\n\n/** A growable big-endian writer. */\nexport class PsdWriter {\n private buf = new Uint8Array(1 << 16);\n length = 0;\n\n private ensure(n: number): void {\n if (this.length + n <= this.buf.length) return;\n let size = this.buf.length * 2;\n while (size < this.length + n) size *= 2;\n const next = new Uint8Array(size);\n next.set(this.buf.subarray(0, this.length));\n this.buf = next;\n }\n\n u8(v: number): void {\n this.ensure(1);\n this.buf[this.length++] = v & 255;\n }\n\n u16(v: number): void {\n this.ensure(2);\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i16(v: number): void {\n this.u16(v < 0 ? v + 65536 : v);\n }\n\n u32(v: number): void {\n this.ensure(4);\n this.buf[this.length++] = (v >>> 24) & 255;\n this.buf[this.length++] = (v >>> 16) & 255;\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i32(v: number): void {\n this.u32(v >>> 0);\n }\n\n u64(v: number): void {\n this.u32(Math.floor(v / 2 ** 32));\n this.u32(v >>> 0);\n }\n\n f64(v: number): void {\n this.ensure(8);\n new DataView(this.buf.buffer, this.length, 8).setFloat64(0, v, false);\n this.length += 8;\n }\n\n bytes(b: ArrayLike<number>): void {\n this.ensure(b.length);\n this.buf.set(b as Uint8Array, this.length);\n this.length += b.length;\n }\n\n ascii(s: string): void {\n for (let i = 0; i < s.length; i++) this.u8(s.charCodeAt(i));\n }\n\n zeros(n: number): void {\n this.ensure(n);\n this.buf.fill(0, this.length, this.length + n);\n this.length += n;\n }\n\n /** Reserves a length field; `endLen` fills it with the bytes written since. */\n beginLen(long: boolean): { at: number; long: boolean } {\n const at = this.length;\n if (long) this.u64(0);\n else this.u32(0);\n return { at, long };\n }\n\n endLen(mark: { at: number; long: boolean }): number {\n const n = this.length - mark.at - (mark.long ? 8 : 4);\n const save = this.length;\n this.length = mark.at;\n if (mark.long) this.u64(n);\n else this.u32(n);\n this.length = save;\n return n;\n }\n\n /** Pascal string padded to a multiple of `align`. */\n pascal(s: Uint8Array, align: number): void {\n const n = Math.min(255, s.length);\n this.u8(n);\n this.bytes(s.subarray(0, n));\n this.zeros((align - ((1 + n) % align)) % align);\n }\n\n unicode(s: string, nul = false): void {\n const n = s.length + (nul ? 1 : 0);\n this.u32(n);\n for (let i = 0; i < s.length; i++) this.u16(s.charCodeAt(i));\n if (nul) this.u16(0);\n }\n\n result(): Uint8Array {\n return this.buf.slice(0, this.length);\n }\n}\n\n/** A legacy (Pascal) name: UTF-8 when valid, else Latin-1. */\nexport function decodeLegacyName(b: Uint8Array): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(b);\n } catch {\n return String.fromCharCode(...b);\n }\n}\n\n/** Encodes a name for a legacy Pascal field: non-ASCII characters become `?`. */\nexport function encodeLegacyName(s: string): Uint8Array {\n const out: number[] = [];\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n out.push(c >= 32 && c < 127 ? c : 63);\n if (out.length >= 255) break;\n }\n return Uint8Array.from(out);\n}\n","// Portions ported from photocraft/crates/psd/src/descriptor.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Photoshop ActionDescriptors (the structured values inside `vibA`, `blwh` and other blocks):\n * read and write. Keys and class ids are 4-character codes, or longer strings.\n */\n\nimport { PsdError, PsdReader, PsdWriter } from './binary';\n\nconst MAX_DEPTH = 64;\n/** Ids that are 4 characters long but stored as strings, never codes. */\nconst STRING_IDS = new Set(['warp', 'list', 'time']);\n\nexport interface DescClass {\n name: string;\n classId: string;\n}\n\nexport type ReferenceItem =\n | { type: 'prop'; class: DescClass; key: string }\n | { type: 'Clss'; class: DescClass }\n | { type: 'Enmr'; class: DescClass; typeId: string; value: string }\n | { type: 'rele'; class: DescClass; offset: number }\n | { type: 'Idnt'; class: DescClass; id: number }\n | { type: 'indx'; class: DescClass; index: number }\n | { type: 'name'; class: DescClass; name: string };\n\nexport type DescValue =\n | { type: 'obj '; items: ReferenceItem[] }\n | { type: 'Objc' | 'GlbO'; value: Descriptor }\n | { type: 'VlLs'; items: DescValue[] }\n | { type: 'doub'; value: number }\n | { type: 'UntF'; unit: string; value: number }\n | { type: 'UnFl'; unit: string; values: number[] }\n | { type: 'TEXT'; value: string }\n | { type: 'enum'; typeId: string; value: string }\n | { type: 'long'; value: number }\n | { type: 'comp'; value: number }\n | { type: 'bool'; value: boolean }\n | { type: 'type' | 'GlbC'; class: DescClass }\n | { type: 'alis' | 'Pth ' | 'tdta'; data: Uint8Array }\n | { type: 'ObAr'; prefix: number; body: Descriptor };\n\nexport interface Descriptor {\n name: string;\n classId: string;\n items: Array<[string, DescValue]>;\n}\n\nexport function descriptor(classId: string, items: Array<[string, DescValue]> = []): Descriptor {\n return { name: '', classId, items };\n}\n\nexport function descGet(d: Descriptor, key: string): DescValue | undefined {\n return d.items.find(([k]) => k === key)?.[1];\n}\n\n/** A number value (long, double or unit float), or `undefined`. */\nexport function descNumber(d: Descriptor, key: string): number | undefined {\n const v = descGet(d, key);\n if (!v) return undefined;\n if (v.type === 'long' || v.type === 'doub' || v.type === 'UntF' || v.type === 'comp') return v.value;\n return undefined;\n}\n\nexport function descBool(d: Descriptor, key: string): boolean | undefined {\n const v = descGet(d, key);\n return v?.type === 'bool' ? v.value : undefined;\n}\n\nexport function descObject(d: Descriptor, key: string): Descriptor | undefined {\n const v = descGet(d, key);\n return v && (v.type === 'Objc' || v.type === 'GlbO') ? v.value : undefined;\n}\n\nfunction readId(r: PsdReader): string {\n const n = r.u32();\n return n === 0 ? r.ascii(4) : r.ascii(n);\n}\n\nfunction writeId(w: PsdWriter, id: string): void {\n if (id.length === 4 && !STRING_IDS.has(id)) {\n w.u32(0);\n w.ascii(id);\n } else {\n w.u32(id.length);\n w.ascii(id);\n }\n}\n\nfunction readClass(r: PsdReader): DescClass {\n return { name: r.unicode(), classId: readId(r) };\n}\n\nfunction writeClass(w: PsdWriter, c: DescClass): void {\n w.unicode(c.name);\n writeId(w, c.classId);\n}\n\nfunction readReferenceItem(r: PsdReader): ReferenceItem {\n const type = r.ascii(4);\n switch (type) {\n case 'prop':\n return { type, class: readClass(r), key: readId(r) };\n case 'Clss':\n return { type, class: readClass(r) };\n case 'Enmr':\n return { type, class: readClass(r), typeId: readId(r), value: readId(r) };\n case 'rele':\n return { type, class: readClass(r), offset: r.i32() };\n case 'Idnt':\n return { type, class: readClass(r), id: r.u32() };\n case 'indx':\n return { type, class: readClass(r), index: r.u32() };\n case 'name':\n return { type, class: readClass(r), name: r.unicode() };\n default:\n throw new PsdError(`Unsupported descriptor reference item \"${type}\".`);\n }\n}\n\nfunction writeReferenceItem(w: PsdWriter, it: ReferenceItem): void {\n w.ascii(it.type);\n writeClass(w, it.class);\n switch (it.type) {\n case 'prop':\n writeId(w, it.key);\n break;\n case 'Enmr':\n writeId(w, it.typeId);\n writeId(w, it.value);\n break;\n case 'rele':\n w.i32(it.offset);\n break;\n case 'Idnt':\n w.u32(it.id);\n break;\n case 'indx':\n w.u32(it.index);\n break;\n case 'name':\n w.unicode(it.name);\n break;\n }\n}\n\nfunction readValue(r: PsdReader, depth: number): DescValue {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const type = r.ascii(4);\n switch (type) {\n case 'obj ': {\n const n = r.u32();\n r.checkCount(n, 8, 'Descriptor reference');\n const items: ReferenceItem[] = [];\n for (let i = 0; i < n; i++) items.push(readReferenceItem(r));\n return { type, items };\n }\n case 'Objc':\n case 'GlbO':\n return { type, value: readDescriptorBody(r, depth + 1) };\n case 'VlLs': {\n const n = r.u32();\n r.checkCount(n, 4, 'Descriptor list');\n const items: DescValue[] = [];\n for (let i = 0; i < n; i++) items.push(readValue(r, depth + 1));\n return { type, items };\n }\n case 'doub':\n return { type, value: r.f64() };\n case 'UntF':\n return { type, unit: r.ascii(4), value: r.f64() };\n case 'UnFl': {\n const unit = r.ascii(4);\n const n = r.u32();\n r.checkCount(n, 8, 'Unit float list');\n const values: number[] = [];\n for (let i = 0; i < n; i++) values.push(r.f64());\n return { type, unit, values };\n }\n case 'TEXT':\n return { type, value: r.unicode() };\n case 'enum':\n return { type, typeId: readId(r), value: readId(r) };\n case 'long':\n return { type, value: r.i32() };\n case 'comp':\n return { type, value: r.i64() };\n case 'bool':\n return { type, value: r.u8() !== 0 };\n case 'type':\n case 'GlbC':\n return { type, class: readClass(r) };\n case 'alis':\n case 'Pth ':\n case 'tdta':\n return { type, data: r.bytes(r.u32()) };\n case 'ObAr':\n return { type, prefix: r.u32(), body: readDescriptorBody(r, depth + 1) };\n default:\n throw new PsdError(`Unsupported descriptor value type \"${type}\".`);\n }\n}\n\nfunction writeValue(w: PsdWriter, v: DescValue): void {\n w.ascii(v.type);\n switch (v.type) {\n case 'obj ':\n w.u32(v.items.length);\n for (const it of v.items) writeReferenceItem(w, it);\n break;\n case 'Objc':\n case 'GlbO':\n writeDescriptorBody(w, v.value);\n break;\n case 'VlLs':\n w.u32(v.items.length);\n for (const it of v.items) writeValue(w, it);\n break;\n case 'doub':\n w.f64(v.value);\n break;\n case 'UntF':\n w.ascii(v.unit);\n w.f64(v.value);\n break;\n case 'UnFl':\n w.ascii(v.unit);\n w.u32(v.values.length);\n for (const x of v.values) w.f64(x);\n break;\n case 'TEXT':\n w.unicode(v.value, true);\n break;\n case 'enum':\n writeId(w, v.typeId);\n writeId(w, v.value);\n break;\n case 'long':\n w.i32(v.value);\n break;\n case 'comp':\n w.u32(Math.floor(v.value / 2 ** 32) >>> 0);\n w.u32(v.value >>> 0);\n break;\n case 'bool':\n w.u8(v.value ? 1 : 0);\n break;\n case 'type':\n case 'GlbC':\n writeClass(w, v.class);\n break;\n case 'alis':\n case 'Pth ':\n case 'tdta':\n w.u32(v.data.length);\n w.bytes(v.data);\n break;\n case 'ObAr':\n w.u32(v.prefix);\n writeDescriptorBody(w, v.body);\n break;\n }\n}\n\nfunction readDescriptorBody(r: PsdReader, depth: number): Descriptor {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const name = r.unicode();\n const classId = readId(r);\n const n = r.u32();\n r.checkCount(n, 10, 'Descriptor items');\n const items: Array<[string, DescValue]> = [];\n for (let i = 0; i < n; i++) {\n const key = readId(r);\n items.push([key, readValue(r, depth + 1)]);\n }\n return { name, classId, items };\n}\n\nfunction writeDescriptorBody(w: PsdWriter, d: Descriptor): void {\n w.unicode(d.name, true);\n writeId(w, d.classId);\n w.u32(d.items.length);\n for (const [k, v] of d.items) {\n writeId(w, k);\n writeValue(w, v);\n }\n}\n\n/** A versioned descriptor (u32 version 16, then the descriptor) at the reader's position. */\nexport function readVersionedDescriptor(r: PsdReader): Descriptor {\n const version = r.u32();\n if (version !== 16) throw new PsdError(`Descriptor version ${version}, expected 16.`);\n return readDescriptorBody(r, 0);\n}\n\nexport function writeVersionedDescriptor(w: PsdWriter, d: Descriptor): void {\n w.u32(16);\n writeDescriptorBody(w, d);\n}\n\n/** A versioned descriptor from the start of `bytes` (trailing bytes are ignored). */\nexport function parseVersionedDescriptor(bytes: Uint8Array): Descriptor {\n return readVersionedDescriptor(new PsdReader(bytes));\n}\n\nexport function versionedDescriptorBytes(d: Descriptor): Uint8Array {\n const w = new PsdWriter();\n writeVersionedDescriptor(w, d);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/adjust_map.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD adjustment-layer blocks ↔ our ten adjustment kinds. Parameters use our units (levels\n * 0..255, degrees, percent), which are also Photoshop's, so most fields copy straight across.\n */\n\nimport { D50, XYZ_D50_TO_SRGB, defaultAdjustmentParams, hexToRgb, neutralHueRange } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n BlackWhiteParams,\n CurvePoint,\n CurvesParams,\n HueRange,\n LevelsChannel,\n} from '../types';\nimport { PsdReader, PsdWriter } from './binary';\nimport {\n type Descriptor,\n type DescValue,\n descBool,\n descNumber,\n descObject,\n descriptor,\n parseVersionedDescriptor,\n versionedDescriptorBytes,\n} from './descriptor';\n\n/** Every adjustment block key Photoshop writes. */\nexport const ADJUSTMENT_KEYS = ['levl', 'curv', 'hue2', 'brit', 'nvrt', 'thrs', 'post', 'expA', 'vibA', 'blnc', 'mixr', 'grdm', 'phfl', 'selc', 'blwh', 'clrL'];\n\n/** Keys we read and write. */\nexport const SUPPORTED_ADJUSTMENT_KEYS: Record<string, AdjustmentKind> = {\n levl: 'levels',\n curv: 'curves',\n hue2: 'hueSaturation',\n vibA: 'vibrance',\n blnc: 'colorBalance',\n blwh: 'blackWhite',\n nvrt: 'invert',\n post: 'posterize',\n thrs: 'threshold',\n phfl: 'photoFilter',\n};\n\nconst LABELS: Record<string, string> = {\n levl: 'Levels',\n curv: 'Curves',\n hue2: 'Hue/Saturation',\n brit: 'Brightness/Contrast',\n nvrt: 'Invert',\n thrs: 'Threshold',\n post: 'Posterize',\n expA: 'Exposure',\n vibA: 'Vibrance',\n blnc: 'Color Balance',\n mixr: 'Channel Mixer',\n grdm: 'Gradient Map',\n phfl: 'Photo Filter',\n selc: 'Selective Color',\n blwh: 'Black & White',\n clrL: 'Color Lookup',\n};\n\nexport function adjustmentLabel(key: string): string {\n return LABELS[key] ?? 'adjustment';\n}\n\nexport type ParsedAdjustment = {\n [K in AdjustmentKind]: { kind: K; params: AdjustmentParamsMap[K]; approximated?: string };\n}[AdjustmentKind];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst toHex = (rgb: readonly number[]) =>\n '#' + rgb.map((v) => Math.round(clamp(v, 0, 1) * 255).toString(16).padStart(2, '0')).join('');\n\n/** The canonical form of four range sliders: increasing, within one turn, centre in -30..330. */\nexport function canonicalBounds(b: readonly number[]): [number, number, number, number] {\n if (b.some((v) => !Number.isFinite(v))) return [-45, -15, 15, 45];\n const rem = (v: number) => ((v % 360) + 360) % 360;\n const out = [b[0], b[0], b[0], b[0]];\n for (let k = 1; k < 4; k++) out[k] = Math.min(out[k - 1] + rem(b[k] - out[k - 1]), out[0] + 359);\n const centre = (out[1] + out[2]) / 2;\n const shift = rem(centre + 30) - 30 - centre;\n return out.map((v) => v + shift) as [number, number, number, number];\n}\n\nfunction levelsRecord(r: PsdReader): LevelsChannel {\n const inBlack = r.u16();\n const inWhite = r.u16();\n const outBlack = r.u16();\n const outWhite = r.u16();\n const gamma = r.u16() / 100;\n return {\n inBlack: clamp(inBlack, 0, 255),\n inWhite: clamp(inWhite, 0, 255),\n gamma: clamp(gamma || 1, 0.1, 9.99),\n outBlack: clamp(outBlack, 0, 255),\n outWhite: clamp(outWhite, 0, 255),\n };\n}\n\n/** A pencil (256-entry map) curve as points every 15 levels. */\nfunction curveFromMap(map: Uint8Array): CurvePoint[] {\n const pts: CurvePoint[] = [];\n for (let i = 0; i <= 255; i += 15) pts.push({ input: i, output: map[i] });\n return pts;\n}\n\nfunction parseCurves(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const pencil = r.u8() !== 0;\n const version = r.u16();\n if (version !== 1 && version !== 4) return null;\n const bits = r.u32();\n const params = defaultAdjustmentParams('curves');\n const order: Array<keyof CurvesParams> = ['master', 'red', 'green', 'blue'];\n for (let bit = 0; bit < 32; bit++) {\n if (!(bits & (1 << bit))) continue;\n let pts: CurvePoint[];\n if (pencil) {\n pts = curveFromMap(r.bytes(256));\n } else {\n const n = r.u16();\n r.checkCount(n, 4, 'Curve points');\n pts = [];\n for (let i = 0; i < n; i++) {\n const output = r.u16();\n const input = r.u16();\n pts.push({ input: clamp(input, 0, 255), output: clamp(output, 0, 255) });\n }\n pts.sort((a, b) => a.input - b.input);\n }\n if (bit < 4 && pts.length >= 2) params[order[bit]] = pts;\n }\n return { kind: 'curves', params, approximated: pencil ? 'its pencil curve is approximated by points' : undefined };\n}\n\nfunction parseHueSaturation(d: Uint8Array): ParsedAdjustment {\n const r = new PsdReader(d);\n r.u16();\n const colorize = r.u8() !== 0;\n r.u8();\n const triple = () => [r.i16(), r.i16(), r.i16()];\n const col = triple();\n const master = triple();\n const ranges: HueRange[] = [];\n for (let i = 0; i < 6; i++) {\n if (r.remaining < 14) {\n ranges.push(neutralHueRange(i));\n continue;\n }\n const b = [r.u16(), r.u16(), r.u16(), r.u16()];\n const [hue, saturation, lightness] = triple();\n ranges.push({ hue, saturation, lightness, bounds: canonicalBounds(b) });\n }\n const [hue, saturation, lightness] = colorize ? col : master;\n return { kind: 'hueSaturation', params: { hue, saturation, lightness, colorize, ranges } };\n}\n\nconst BW_KEYS: Array<[keyof Omit<BlackWhiteParams, 'tint'>, string]> = [\n ['reds', 'Rd '],\n ['yellows', 'Yllw'],\n ['greens', 'Grn '],\n ['cyans', 'Cyn '],\n ['blues', 'Bl '],\n ['magentas', 'Mgnt'],\n];\nconst BW_TINT = [225 / 255, 211 / 255, 179 / 255];\n\nfunction descRgb(d: Descriptor | undefined): number[] | null {\n if (!d) return null;\n if (descNumber(d, 'redFloat') !== undefined) {\n return [descNumber(d, 'redFloat')!, descNumber(d, 'greenFloat') ?? 0, descNumber(d, 'blueFloat') ?? 0].map((v) => clamp(v, 0, 1));\n }\n const r = descNumber(d, 'Rd ');\n const g = descNumber(d, 'Grn ');\n const b = descNumber(d, 'Bl ');\n if (r === undefined || g === undefined || b === undefined) return null;\n return [r, g, b].map((v) => clamp(v / 255, 0, 1));\n}\n\nfunction parseBlackWhite(d: Uint8Array): ParsedAdjustment {\n const desc = parseVersionedDescriptor(d);\n const params = defaultAdjustmentParams('blackWhite');\n for (const [field, key] of BW_KEYS) {\n const v = descNumber(desc, key);\n if (v !== undefined) params[field] = clamp(v, -200, 300);\n }\n params.tint = descBool(desc, 'useTint') ? toHex(descRgb(descObject(desc, 'tintColor')) ?? BW_TINT) : null;\n return { kind: 'blackWhite', params };\n}\n\nfunction labToRgb(l: number, a: number, b: number): number[] {\n const fy = (l + 16) / 116;\n const fx = fy + a / 500;\n const fz = fy - b / 200;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const xyz = [f(fx) * D50[0], f(fy) * D50[1], f(fz) * D50[2]];\n return XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n}\n\n/** A PSD colour structure (space id + four u16) as 0..1 RGB, or `null` for spaces we cannot read. */\nexport function decodePsdColor(space: number, c: readonly number[]): number[] | null {\n const n = (v: number) => v / 65535;\n switch (space) {\n case 0:\n return [n(c[0]), n(c[1]), n(c[2])];\n case 1: {\n const h = n(c[0]) * 360;\n const s = n(c[1]);\n const v = n(c[2]);\n const k = (m: number) => (m + h / 60) % 6;\n const f = (m: number) => v - v * s * Math.max(0, Math.min(k(m), 4 - k(m), 1));\n return [f(5), f(3), f(1)];\n }\n case 2: {\n // Stored inverted: 65535 is no ink. A naive conversion; CMYK profiles are not read.\n const [cy, m, y, k] = c.map(n);\n return [cy * k, m * k, y * k];\n }\n case 7: {\n const signed = (v: number) => (v << 16) >> 16;\n return labToRgb(Math.min(c[0], 10000) / 100, signed(c[1]) / 100, signed(c[2]) / 100);\n }\n case 8: {\n const v = 1 - Math.min(c[0], 10000) / 10000;\n return [v, v, v];\n }\n default:\n return null;\n }\n}\n\nfunction parsePhotoFilter(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const version = r.u16();\n let rgb: number[] | null;\n if (version === 2) {\n const space = r.u16();\n const c = [r.u16(), r.u16(), r.u16(), r.u16()];\n rgb = [0, 2, 7, 8].includes(space) ? decodePsdColor(space, c) : null;\n } else if (version === 3) {\n const rel = [r.u32(), r.u32(), r.u32()].map((v) => v / 32768);\n if (rel.some((v) => v > 2)) return null;\n const xyz = [rel[0] * D50[0], rel[1], rel[2] * D50[2]];\n rgb = XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n } else {\n return null;\n }\n if (!rgb) return null;\n const density = clamp(r.u32(), 0, 100);\n const preserveLuminosity = r.u8() !== 0;\n return { kind: 'photoFilter', params: { color: toHex(rgb), density, preserveLuminosity } };\n}\n\n/**\n * Our parameters for an adjustment block, or `null` when the key is not one of our kinds or the\n * block cannot be read.\n */\nexport function parseAdjustmentBlock(key: string, data: Uint8Array): ParsedAdjustment | null {\n try {\n const r = new PsdReader(data);\n switch (key) {\n case 'nvrt':\n return { kind: 'invert', params: {} as Record<string, never> };\n case 'thrs':\n return { kind: 'threshold', params: { level: clamp(r.u16(), 1, 255) } };\n case 'post':\n return { kind: 'posterize', params: { levels: clamp(r.u16(), 2, 255) } };\n case 'levl': {\n r.u16();\n const master = levelsRecord(r);\n const red = levelsRecord(r);\n const green = levelsRecord(r);\n const blue = levelsRecord(r);\n return { kind: 'levels', params: { master, red, green, blue } };\n }\n case 'curv':\n return parseCurves(data);\n case 'hue2':\n return parseHueSaturation(data);\n case 'vibA': {\n const desc = parseVersionedDescriptor(data);\n const get = (k: string) => clamp(descNumber(desc, k) ?? 0, -100, 100);\n return { kind: 'vibrance', params: { vibrance: get('vibrance'), saturation: get('Strt') } };\n }\n case 'blnc': {\n const tone = () => [r.i16(), r.i16(), r.i16()].map((v) => clamp(v, -100, 100)) as [number, number, number];\n const shadows = tone();\n const midtones = tone();\n const highlights = tone();\n return { kind: 'colorBalance', params: { shadows, midtones, highlights, preserveLuminosity: r.u8() !== 0 } };\n }\n case 'blwh':\n return parseBlackWhite(data);\n case 'phfl':\n return parsePhotoFilter(data);\n default:\n return null;\n }\n } catch {\n return null;\n }\n}\n\nconst q = (v: number) => Math.round(clamp(v, 0, 255));\nconst i16 = (v: number) => Math.round(clamp(v, -32768, 32767));\n\nfunction writeLevels(w: PsdWriter, c: LevelsChannel): void {\n w.u16(q(c.inBlack));\n w.u16(q(c.inWhite));\n w.u16(q(c.outBlack));\n w.u16(q(c.outWhite));\n w.u16(Math.round(clamp(c.gamma * 100, 1, 999)));\n}\n\nconst long = (value: number): DescValue => ({ type: 'long', value: i16(value) });\n\n/** The PSD block (key and data) for one of our adjustment layers. */\nexport function writeAdjustmentBlock<K extends AdjustmentKind>(kind: K, params: AdjustmentParamsMap[K]): { key: string; data: Uint8Array } {\n const w = new PsdWriter();\n switch (kind) {\n case 'invert':\n return { key: 'nvrt', data: new Uint8Array(0) };\n case 'threshold': {\n const p = params as AdjustmentParamsMap['threshold'];\n w.u16(Math.max(1, q(p.level)));\n w.u16(0);\n return { key: 'thrs', data: w.result() };\n }\n case 'posterize': {\n const p = params as AdjustmentParamsMap['posterize'];\n w.u16(Math.round(clamp(p.levels, 2, 255)));\n w.u16(0);\n return { key: 'post', data: w.result() };\n }\n case 'levels': {\n const p = params as AdjustmentParamsMap['levels'];\n w.u16(2);\n for (const c of [p.master, p.red, p.green, p.blue]) writeLevels(w, c);\n const ident: LevelsChannel = { inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 };\n for (let i = 4; i < 29; i++) writeLevels(w, ident);\n return { key: 'levl', data: w.result() };\n }\n case 'curves': {\n const p = params as AdjustmentParamsMap['curves'];\n w.u8(0);\n w.u16(1);\n w.u32(0b1111);\n for (const c of [p.master, p.red, p.green, p.blue]) {\n const pts = c.slice(0, 19);\n w.u16(pts.length);\n for (const pt of pts) {\n w.u16(q(pt.output));\n w.u16(q(pt.input));\n }\n }\n return { key: 'curv', data: w.result() };\n }\n case 'hueSaturation': {\n const p = params as AdjustmentParamsMap['hueSaturation'];\n w.u16(2);\n w.u8(p.colorize ? 1 : 0);\n w.u8(0);\n for (let k = 0; k < 2; k++) for (const x of [p.hue, p.saturation, p.lightness]) w.i16(i16(x));\n for (let i = 0; i < 6; i++) {\n const r = p.ranges[i] ?? neutralHueRange(i);\n for (const b of r.bounds) w.u16(Math.round(((b % 360) + 360) % 360) % 360);\n for (const x of [r.hue, r.saturation, r.lightness]) w.i16(i16(x));\n }\n return { key: 'hue2', data: w.result() };\n }\n case 'vibrance': {\n const p = params as AdjustmentParamsMap['vibrance'];\n const d = descriptor('null', [\n ['vibrance', long(clamp(p.vibrance, -100, 100))],\n ['Strt', long(clamp(p.saturation, -100, 100))],\n ]);\n return { key: 'vibA', data: versionedDescriptorBytes(d) };\n }\n case 'colorBalance': {\n const p = params as AdjustmentParamsMap['colorBalance'];\n for (const x of [...p.shadows, ...p.midtones, ...p.highlights]) w.i16(i16(clamp(x, -100, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.u8(0);\n return { key: 'blnc', data: w.result() };\n }\n case 'blackWhite': {\n const p = params as AdjustmentParamsMap['blackWhite'];\n const tint = p.tint ? hexToRgb(p.tint) : BW_TINT;\n const dbl = (v: number): DescValue => ({ type: 'doub', value: clamp(v, 0, 1) * 255 });\n const items: Array<[string, DescValue]> = BW_KEYS.map(([field, key]) => [key, long(clamp(p[field], -200, 300))]);\n items.push(\n ['useTint', { type: 'bool', value: p.tint !== null }],\n ['tintColor', { type: 'Objc', value: descriptor('RGBC', [['Rd ', dbl(tint[0])], ['Grn ', dbl(tint[1])], ['Bl ', dbl(tint[2])]]) }],\n ['bwPresetKind', { type: 'long', value: 1 }],\n ['blackAndWhitePresetFileName', { type: 'TEXT', value: '' }]\n );\n return { key: 'blwh', data: versionedDescriptorBytes(descriptor('null', items)) };\n }\n case 'photoFilter': {\n const p = params as AdjustmentParamsMap['photoFilter'];\n const rgb = hexToRgb(p.color);\n w.u16(2);\n w.u16(0);\n for (const x of rgb) w.u16(Math.round(clamp(x, 0, 1) * 65535));\n w.u16(0);\n w.u32(Math.round(clamp(p.density, 0, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.zeros(3);\n return { key: 'phfl', data: w.result() };\n }\n }\n throw new Error(`Unknown adjustment kind \"${kind}\".`);\n}\n","// Portions ported from photocraft/crates/psd/src/compression.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Channel and image data compression: raw, RLE (PackBits), ZIP and ZIP with prediction. */\n\nimport { ZlibError, deflate, inflate } from '../../raster/zlib';\nimport { PsdError, PsdReader } from './binary';\n\nexport type PsdCompression = 0 | 1 | 2 | 3;\nexport const COMPRESSION_NAMES = ['raw', 'rle', 'zip', 'zip-prediction'] as const;\n\n/** Largest decoded channel set accepted from one file (bytes). */\nexport const MAX_DECODED_BYTES = 2 ** 31;\n\nexport function rowBytes(width: number, depth: number): number {\n return depth === 1 ? Math.ceil(width / 8) : width * Math.max(1, depth >> 3);\n}\n\nexport interface PlaneLayout {\n planes: number;\n width: number;\n height: number;\n depth: number;\n /** PSB: RLE row counts are 4 bytes. */\n psb: boolean;\n}\n\nfunction decodedLength(l: PlaneLayout): number {\n const total = l.planes * l.height * rowBytes(l.width, l.depth);\n if (total > MAX_DECODED_BYTES) throw new PsdError(`Decoded channel data of ${total} bytes is larger than the ${MAX_DECODED_BYTES}-byte limit.`);\n return total;\n}\n\n/** Worst-case PackBits size of `n` bytes. */\nexport function packbitsBound(n: number): number {\n return n + Math.ceil(n / 128);\n}\n\n/** PackBits: writes `src` encoded into `out` at `at` (room for `packbitsBound`); returns the new offset. */\nexport function packbitsEncode(src: Uint8Array, out: Uint8Array, at: number): number {\n const n = src.length;\n let i = 0;\n while (i < n) {\n let run = 1;\n while (i + run < n && run < 128 && src[i + run] === src[i]) run++;\n if (run >= 2) {\n out[at++] = (257 - run) & 255;\n out[at++] = src[i];\n i += run;\n continue;\n }\n const start = i;\n let len = 0;\n while (i < n && len < 128) {\n if (i + 2 < n && src[i] === src[i + 1] && src[i] === src[i + 2]) break;\n i++;\n len++;\n }\n out[at++] = len - 1;\n out.set(src.subarray(start, start + len), at);\n at += len;\n }\n return at;\n}\n\n/** PackBits: decodes exactly `expected` bytes from `src` into `out` at `at`; returns the new offset. */\nexport function packbitsDecodeInto(src: Uint8Array, expected: number, out: Uint8Array, at: number): number {\n const target = at + expected;\n let i = 0;\n while (at < target) {\n if (i >= src.length) throw new PsdError('PackBits row ended early.');\n const h = src[i++];\n if (h < 128) {\n const len = h + 1;\n if (i + len > src.length) throw new PsdError('PackBits literal is truncated.');\n if (at + len > target) throw new PsdError('PackBits literal overflows its row.');\n out.set(src.subarray(i, i + len), at);\n at += len;\n i += len;\n } else if (h !== 128) {\n const len = 257 - h;\n if (i >= src.length) throw new PsdError('PackBits run is truncated.');\n const b = src[i++];\n if (at + len > target) throw new PsdError('PackBits run overflows its row.');\n out.fill(b, at, at + len);\n at += len;\n }\n }\n return at;\n}\n\nfunction inflateChecked(data: Uint8Array, expected: number): Uint8Array {\n let out: Uint8Array;\n try {\n out = inflate(data, { maxOutput: expected + (1 << 20), sizeHint: expected, verify: false });\n } catch (e) {\n throw new PsdError(`ZIP channel data: ${e instanceof ZlibError || e instanceof Error ? e.message : String(e)}`);\n }\n if (out.length < expected) throw new PsdError(`ZIP stream produced ${out.length} of ${expected} bytes.`);\n return out.length === expected ? out : out.subarray(0, expected);\n}\n\nfunction unpredict(buf: Uint8Array, l: PlaneLayout): void {\n const rb = rowBytes(l.width, l.depth);\n if (rb === 0) return;\n const w = l.width;\n const scratch = l.depth === 32 ? new Uint8Array(rb) : null;\n for (let o = 0; o + rb <= buf.length; o += rb) {\n if (l.depth === 1 || l.depth === 8) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n } else if (l.depth === 16) {\n for (let i = 1; i < w; i++) {\n const a = (buf[o + 2 * i] << 8) | buf[o + 2 * i + 1];\n const b = (buf[o + 2 * i - 2] << 8) | buf[o + 2 * i - 1];\n const v = (a + b) & 0xffff;\n buf[o + 2 * i] = v >> 8;\n buf[o + 2 * i + 1] = v & 255;\n }\n } else if (l.depth === 32) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n scratch!.set(buf.subarray(o, o + rb));\n for (let i = 0; i < w; i++) for (let k = 0; k < 4; k++) buf[o + 4 * i + k] = scratch![k * w + i];\n } else {\n throw new PsdError(`ZIP prediction is not defined for depth ${l.depth}.`);\n }\n }\n}\n\nfunction decodeRle(r: PsdReader, l: PlaneLayout, total: number): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n r.checkCount(rows, l.psb ? 4 : 2, 'RLE row counts');\n const counts = new Array<number>(rows);\n for (let i = 0; i < rows; i++) counts[i] = l.psb ? r.u32() : r.u16();\n if (total > r.remaining * 130) throw new PsdError('RLE data is too short for its declared size.');\n const out = new Uint8Array(total);\n let at = 0;\n for (const c of counts) at = packbitsDecodeInto(r.bytes(c), rb, out, at);\n return out;\n}\n\n/** Decodes planar channel data (`planes` planes of `height` rows each). */\nexport function decodePlanes(compression: number, data: Uint8Array, l: PlaneLayout): Uint8Array {\n const total = decodedLength(l);\n switch (compression) {\n case 0:\n if (data.length < total) throw new PsdError(`Raw channel data has ${data.length} of ${total} bytes.`);\n return data.slice(0, total);\n case 1:\n return decodeRle(new PsdReader(data), l, total);\n case 2:\n return inflateChecked(data, total).slice();\n case 3: {\n const v = inflateChecked(data, total).slice();\n unpredict(v, l);\n return v;\n }\n default:\n throw new PsdError(`Unsupported compression method ${compression}.`);\n }\n}\n\n/** RLE-encodes planar data: row counts (u16, or u32 in PSB), then the packed rows. */\nexport function encodeRle(decoded: Uint8Array, l: PlaneLayout): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n const cs = l.psb ? 4 : 2;\n const packed = new Uint8Array(rows * packbitsBound(rb));\n let at = 0;\n const counts = new Uint8Array(rows * cs);\n for (let row = 0; row < rows; row++) {\n const start = at;\n at = packbitsEncode(decoded.subarray(row * rb, (row + 1) * rb), packed, at);\n const n = at - start;\n if (cs === 2) {\n if (n > 0xffff) throw new PsdError('An RLE row is longer than 65535 bytes; write a PSB instead.');\n counts[row * 2] = n >> 8;\n counts[row * 2 + 1] = n & 255;\n } else {\n counts[row * 4] = n >>> 24;\n counts[row * 4 + 1] = (n >>> 16) & 255;\n counts[row * 4 + 2] = (n >>> 8) & 255;\n counts[row * 4 + 3] = n & 255;\n }\n }\n const out = new Uint8Array(counts.length + at);\n out.set(counts);\n out.set(packed.subarray(0, at), counts.length);\n return out;\n}\n\n/** ZIP without prediction (used by tests to build fixtures). */\nexport function encodeZip(decoded: Uint8Array): Uint8Array {\n return deflate(decoded);\n}\n","// Portions ported from photocraft/crates/psd/src/{header,file,layer,tagged,tree,resources,image_data}.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The PSD / PSB container: header, colour mode data, image resources, layer and mask info\n * (records, channel data, tagged blocks) and the merged image. Channel data stays encoded\n * until a caller decodes it, so inspecting a file is cheap.\n */\n\nimport { PsdError, PsdReader, PsdWriter, decodeLegacyName } from './binary';\nimport { decodePlanes, rowBytes } from './compression';\n\nexport const PSD_MAX_DIMENSION = 30_000;\nexport const PSB_MAX_DIMENSION = 300_000;\nconst MAX_CHANNELS = 56;\nconst MAX_LAYER_CHANNELS = 64;\n\nexport const COLOR_MODES: Record<number, string> = {\n 0: 'bitmap',\n 1: 'grayscale',\n 2: 'indexed',\n 3: 'rgb',\n 4: 'cmyk',\n 7: 'multichannel',\n 8: 'duotone',\n 9: 'lab',\n};\n\n/** Tagged block keys whose length is 8 bytes in a PSB. */\nconst PSB_LONG_KEYS = new Set([\n 'LMsk', 'Lr16', 'Lr32', 'Layr', 'Mt16', 'Mt32', 'Mtrn', 'Alph', 'FMsk', 'lnk2', 'FEid', 'FXid', 'PxSD',\n 'lnk3', 'lnkE', 'pths', 'extd', 'extn', 'FELS', 'cinf', 'artd',\n]);\n\nexport const CHANNEL_ALPHA = -1;\nexport const CHANNEL_USER_MASK = -2;\nexport const CHANNEL_REAL_USER_MASK = -3;\n\nexport const LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;\nexport const LAYER_FLAG_HIDDEN = 2;\nexport const MASK_FLAG_DISABLED = 2;\nexport const MASK_FLAG_PARAMETERS = 16;\n\n/** Section divider (`lsct`) kinds. */\nexport const SECTION_OTHER = 0;\nexport const SECTION_OPEN = 1;\nexport const SECTION_CLOSED = 2;\nexport const SECTION_DIVIDER = 3;\n\nexport interface PsdHeader {\n psb: boolean;\n channels: number;\n height: number;\n width: number;\n depth: number;\n colorMode: number;\n}\n\nexport interface PsdRect {\n top: number;\n left: number;\n bottom: number;\n right: number;\n}\n\nexport interface PsdChannel {\n id: number;\n /** `null` for a zero-length channel. */\n compression: number | null;\n /** Encoded bytes, without the compression field. */\n data: Uint8Array;\n}\n\nexport interface PsdLayerMask {\n rect: PsdRect;\n defaultColor: number;\n flags: number;\n userDensity?: number;\n userFeather?: number;\n vectorDensity?: number;\n vectorFeather?: number;\n /** The real (vector-combined) user mask, when present. */\n real?: { flags: number; background: number; rect: PsdRect };\n}\n\nexport interface PsdTaggedBlock {\n key: string;\n data: Uint8Array;\n}\n\nexport interface PsdLayerRecord {\n rect: PsdRect;\n channels: PsdChannel[];\n blendKey: string;\n opacity: number;\n clipping: number;\n flags: number;\n mask?: PsdLayerMask;\n /** The legacy (Pascal) name; `layerName` prefers `luni`. */\n legacyName: string;\n blocks: PsdTaggedBlock[];\n}\n\nexport interface PsdResource {\n id: number;\n name: string;\n data: Uint8Array;\n}\n\nexport interface PsdFile {\n header: PsdHeader;\n colorModeData: Uint8Array;\n resources: PsdResource[];\n /** Bottom-most first, as stored. */\n layers: PsdLayerRecord[];\n /** The layer count was negative: the merged image's first extra channel is its alpha. */\n mergedAlpha: boolean;\n globalBlocks: PsdTaggedBlock[];\n imageCompression: number;\n imageData: Uint8Array;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nfunction readRect(r: PsdReader): PsdRect {\n return { top: r.i32(), left: r.i32(), bottom: r.i32(), right: r.i32() };\n}\n\nexport function rectSize(rect: PsdRect): { width: number; height: number } {\n const width = rect.right - rect.left;\n const height = rect.bottom - rect.top;\n if (width < 0 || height < 0) throw new PsdError('A layer rectangle has a negative size.');\n if (width > PSB_MAX_DIMENSION || height > PSB_MAX_DIMENSION) throw new PsdError('A layer is larger than 300000 pixels.');\n return { width, height };\n}\n\nfunction readHeader(r: PsdReader): PsdHeader {\n if (r.ascii(4) !== '8BPS') throw new PsdError('Not a PSD file: the signature is not \"8BPS\".');\n const version = r.u16();\n if (version !== 1 && version !== 2) throw new PsdError(`Unsupported PSD version ${version} (1 is PSD, 2 is PSB).`);\n r.skip(6);\n const h: PsdHeader = { psb: version === 2, channels: r.u16(), height: r.u32(), width: r.u32(), depth: r.u16(), colorMode: r.u16() };\n if (h.channels < 1 || h.channels > MAX_CHANNELS) throw new PsdError(`Channel count ${h.channels} is outside 1 to ${MAX_CHANNELS}.`);\n if (h.width < 1 || h.height < 1) throw new PsdError(`Image size ${h.width} × ${h.height} must be at least 1 × 1.`);\n const max = h.psb ? PSB_MAX_DIMENSION : PSD_MAX_DIMENSION;\n if (h.width > max || h.height > max) throw new PsdError(`Image size ${h.width} × ${h.height} exceeds ${max} pixels.`);\n if (![1, 8, 16, 32].includes(h.depth)) throw new PsdError(`Unsupported bit depth ${h.depth}.`);\n return h;\n}\n\nfunction readResources(r: PsdReader): PsdResource[] {\n const s = r.sub(r.u32());\n const out: PsdResource[] = [];\n while (s.remaining >= 12) {\n s.ascii(4); // 8BIM, or another vendor signature\n const id = s.u16();\n const name = decodeLegacyName(s.pascal(2));\n const len = s.u32();\n const data = s.bytes(len);\n if (len % 2 && s.remaining) s.skip(1);\n out.push({ id, name, data });\n }\n return out;\n}\n\nconst isSig = (r: PsdReader, at = 0) => {\n const b = r.data.subarray(r.pos + at, r.pos + at + 4);\n if (b.length < 4) return false;\n const s = String.fromCharCode(b[0], b[1], b[2], b[3]);\n return s === '8BIM' || s === '8B64';\n};\n\n/** Tagged blocks until the end of `r` (or something that is not a block). */\nfunction readBlocks(r: PsdReader, psb: boolean, alignment: number): PsdTaggedBlock[] {\n const out: PsdTaggedBlock[] = [];\n while (r.remaining >= 12 && isSig(r)) {\n r.skip(4);\n const key = r.ascii(4);\n const len = r.len(psb && PSB_LONG_KEYS.has(key));\n const data = r.bytes(len);\n // Writers disagree on padding: take the gap up to the next signature, else the default.\n let pad = -1;\n for (let k = 0; k <= 3; k++) {\n if (k === r.remaining || (k < r.remaining && isSig(r, k))) {\n pad = k;\n break;\n }\n }\n if (pad < 0) {\n const def = (alignment - (len % alignment)) % alignment;\n pad = r.peek(def).every((b) => b === 0) && r.remaining >= def ? def : 0;\n }\n r.skip(pad);\n out.push({ key, data });\n }\n return out;\n}\n\nfunction maskParameterBytes(pf: number): number {\n return (pf & 1 ? 1 : 0) + (pf & 2 ? 8 : 0) + (pf & 4 ? 1 : 0) + (pf & 8 ? 8 : 0);\n}\n\n/** Photoshop writes the real mask before the parameters; the spec says after. Guess which. */\nfunction realBeforeParameters(d: Uint8Array): boolean {\n const fits = (at: number) => {\n if (at >= d.length) return false;\n const end = at + 1 + maskParameterBytes(d[at]);\n return d.length >= end && d.length <= end + 3;\n };\n if (fits(18)) return false;\n if (!fits(36)) return false;\n const known = (pf: number) => (pf & 0xf0) === 0;\n const bgOk = (at: number) => at + 1 < d.length && (d[at + 1] === 0 || d[at + 1] === 255);\n const photoshop = bgOk(18) && known(d[36]);\n const realAt = 19 + maskParameterBytes(d[18]);\n const spec = known(d[18]) && d.length >= realAt + 18 && d.length <= realAt + 21 && bgOk(realAt);\n return photoshop || !spec;\n}\n\nfunction parseMask(d: Uint8Array): PsdLayerMask | undefined {\n try {\n const r = new PsdReader(d);\n const m: PsdLayerMask = { rect: readRect(r), defaultColor: r.u8(), flags: r.u8() };\n const readReal = () => (r.remaining >= 18 ? { flags: r.u8(), background: r.u8(), rect: readRect(r) } : undefined);\n const params = (m.flags & MASK_FLAG_PARAMETERS) !== 0;\n const realFirst = params && realBeforeParameters(d);\n if (realFirst) m.real = readReal();\n if (params) {\n const pf = r.u8();\n if (pf & 1) m.userDensity = r.u8();\n if (pf & 2) m.userFeather = r.f64();\n if (pf & 4) m.vectorDensity = r.u8();\n if (pf & 8) m.vectorFeather = r.f64();\n }\n if (!realFirst) m.real = readReal();\n return m;\n } catch {\n return undefined;\n }\n}\n\nfunction readLayerRecord(r: PsdReader, psb: boolean): { record: PsdLayerRecord; lengths: number[] } {\n const rect = readRect(r);\n const n = r.u16();\n if (n > MAX_LAYER_CHANNELS) throw new PsdError(`A layer has ${n} channels (at most ${MAX_LAYER_CHANNELS}).`);\n const channels: PsdChannel[] = [];\n const lengths: number[] = [];\n for (let i = 0; i < n; i++) {\n channels.push({ id: r.i16(), compression: null, data: new Uint8Array(0) });\n lengths.push(r.len(psb));\n }\n if (r.ascii(4) !== '8BIM') throw new PsdError('A layer record has no \"8BIM\" blend-mode signature.');\n const blendKey = r.ascii(4);\n const opacity = r.u8();\n const clipping = r.u8();\n const flags = r.u8();\n r.u8();\n const x = r.sub(r.u32());\n const maskBytes = x.bytes(x.u32());\n const mask = maskBytes.length ? parseMask(maskBytes) : undefined;\n x.skip(x.u32()); // blending ranges\n const legacyName = decodeLegacyName(x.pascal(4));\n const blocks = readBlocks(x, psb, 2);\n return { record: { rect, channels, blendKey, opacity, clipping, flags, mask, legacyName, blocks }, lengths };\n}\n\nfunction readLayerInfo(data: Uint8Array, psb: boolean): { layers: PsdLayerRecord[]; mergedAlpha: boolean } {\n const r = new PsdReader(data);\n const count = r.i16();\n const n = Math.abs(count);\n r.checkCount(n, 34, 'Layer records');\n const read = [];\n for (let i = 0; i < n; i++) read.push(readLayerRecord(r, psb));\n for (const { record, lengths } of read) {\n record.channels.forEach((ch, i) => {\n const len = lengths[i];\n if (len === 0) return;\n if (len === 1) throw new PsdError('A channel has a data length of 1.');\n ch.compression = r.u16();\n ch.data = r.bytes(len - 2);\n });\n }\n return { layers: read.map((x) => x.record), mergedAlpha: count < 0 };\n}\n\n/** Parses a PSD or PSB. Throws `PsdError` with what was wrong. */\nexport function readPsd(bytes: Uint8Array): PsdFile {\n const r = new PsdReader(bytes);\n const header = readHeader(r);\n const { psb } = header;\n const colorModeData = r.bytes(r.u32());\n const resources = readResources(r);\n const s = r.sub(r.len(psb));\n let layers: PsdLayerRecord[] = [];\n let mergedAlpha = false;\n let globalBlocks: PsdTaggedBlock[] = [];\n if (s.remaining) {\n const liLen = s.len(psb);\n if (liLen > 0) ({ layers, mergedAlpha } = readLayerInfo(s.bytes(liLen), psb));\n if (s.remaining >= 4) {\n s.skip(s.u32()); // global layer mask\n globalBlocks = readBlocks(s, psb, 4);\n // 16- and 32-bit files keep their layers in an Lr16 / Lr32 block.\n const key = header.depth === 16 ? 'Lr16' : header.depth === 32 ? 'Lr32' : null;\n const block = globalBlocks.find((b) => (key ? b.key === key : !layers.length && ['Layr', 'Lr16', 'Lr32'].includes(b.key)));\n if (block) {\n ({ layers, mergedAlpha } = readLayerInfo(block.data, psb));\n globalBlocks = globalBlocks.filter((b) => b !== block);\n }\n }\n }\n const imageCompression = r.remaining >= 2 ? r.u16() : 0;\n return { header, colorModeData, resources, layers, mergedAlpha, globalBlocks, imageCompression, imageData: r.rest() };\n}\n\n// ---------------------------------------------------------------------------\n// Queries\n// ---------------------------------------------------------------------------\n\nexport function layerBlock(rec: PsdLayerRecord, key: string): Uint8Array | undefined {\n return rec.blocks.find((b) => b.key === key)?.data;\n}\n\nexport function layerName(rec: PsdLayerRecord): string {\n const luni = layerBlock(rec, 'luni');\n if (luni) {\n try {\n return new PsdReader(luni).unicode();\n } catch {\n // fall back to the legacy name\n }\n }\n return rec.legacyName;\n}\n\nexport interface SectionDivider {\n kind: number;\n blendKey?: string;\n}\n\nexport function sectionDivider(rec: PsdLayerRecord): SectionDivider | undefined {\n const d = layerBlock(rec, 'lsct') ?? layerBlock(rec, 'lsdk');\n if (!d || d.length < 4) return undefined;\n const r = new PsdReader(d);\n const kind = r.u32();\n if (r.remaining >= 8 && r.ascii(4) === '8BIM') return { kind, blendKey: r.ascii(4) };\n return { kind };\n}\n\nexport function u32Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 4 ? new PsdReader(d).u32() : undefined;\n}\n\nexport function u8Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 1 ? d[0] : undefined;\n}\n\nexport interface PsdTreeNode {\n /** Index into `PsdFile.layers` (the folder record for a group). */\n index: number;\n children?: PsdTreeNode[];\n}\n\n/** The layer tree, bottom-most first (the stored order, as in our model), from the `lsct` dividers. */\nexport function layerTree(file: PsdFile): PsdTreeNode[] {\n const stack: PsdTreeNode[][] = [[]];\n file.layers.forEach((rec, index) => {\n const kind = sectionDivider(rec)?.kind ?? SECTION_OTHER;\n if (kind === SECTION_DIVIDER) {\n stack.push([]);\n } else if (kind === SECTION_OPEN || kind === SECTION_CLOSED) {\n const children = stack.length > 1 ? stack.pop()! : [];\n stack[stack.length - 1].push({ index, children });\n } else {\n stack[stack.length - 1].push({ index });\n }\n });\n while (stack.length > 1) {\n const children = stack.pop()!;\n stack[stack.length - 1].push(...children);\n }\n return stack[0];\n}\n\nexport function channelRect(rec: PsdLayerRecord, id: number): PsdRect {\n if (id === CHANNEL_USER_MASK && rec.mask) return rec.mask.rect;\n if (id === CHANNEL_REAL_USER_MASK && rec.mask) return rec.mask.real?.rect ?? rec.mask.rect;\n return rec.rect;\n}\n\n/** One layer channel decoded to `depth`-bit planar bytes, or `undefined` if the layer has none. */\nexport function decodeLayerChannel(file: PsdFile, rec: PsdLayerRecord, id: number): Uint8Array | undefined {\n const ch = rec.channels.find((c) => c.id === id);\n if (!ch) return undefined;\n const { width, height } = rectSize(channelRect(rec, id));\n if (ch.compression === null) {\n if (width * height === 0) return new Uint8Array(0);\n throw new PsdError(`Channel ${id} of layer \"${layerName(rec)}\" has no data.`);\n }\n return decodePlanes(ch.compression, ch.data, { planes: 1, width, height, depth: file.header.depth, psb: file.header.psb });\n}\n\n/** The merged image's channels decoded, planar. */\nexport function decodeMergedImage(file: PsdFile): Uint8Array {\n const { channels, width, height, depth, psb } = file.header;\n return decodePlanes(file.imageCompression, file.imageData, { planes: channels, width, height, depth, psb });\n}\n\nexport function resource(file: PsdFile, id: number): Uint8Array | undefined {\n return file.resources.find((r) => r.id === id)?.data;\n}\n\n/** Resolution (1005) in pixels per inch, or `undefined`. */\nexport function resolution(file: PsdFile): number | undefined {\n const d = resource(file, 1005);\n if (!d || d.length < 4) return undefined;\n return new PsdReader(d).u32() / 65536;\n}\n\nexport { rowBytes };\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\nfunction writeRect(w: PsdWriter, r: PsdRect): void {\n w.i32(r.top);\n w.i32(r.left);\n w.i32(r.bottom);\n w.i32(r.right);\n}\n\nfunction writeBlocks(w: PsdWriter, blocks: PsdTaggedBlock[], psb: boolean, alignment: number): void {\n for (const b of blocks) {\n const long = psb && PSB_LONG_KEYS.has(b.key);\n w.ascii(long ? '8B64' : '8BIM');\n w.ascii(b.key);\n // Pad inside the declared length, as Photoshop does, so readers that skip by length stay aligned.\n const pad = (alignment - (b.data.length % alignment)) % alignment;\n if (long) w.u64(b.data.length + pad);\n else w.u32(b.data.length + pad);\n w.bytes(b.data);\n w.zeros(pad);\n }\n}\n\nfunction writeMask(w: PsdWriter, m: PsdLayerMask): void {\n writeRect(w, m.rect);\n w.u8(m.defaultColor);\n const pf = (m.userDensity !== undefined ? 1 : 0) | (m.userFeather !== undefined ? 2 : 0);\n w.u8(pf ? m.flags | MASK_FLAG_PARAMETERS : m.flags & ~MASK_FLAG_PARAMETERS);\n if (pf) {\n w.u8(pf);\n if (m.userDensity !== undefined) w.u8(m.userDensity);\n if (m.userFeather !== undefined) w.f64(m.userFeather);\n }\n // Pad to the 20-byte minimum the spec asks for.\n const len = 18 + (pf ? 1 + maskParameterBytes(pf) : 0);\n w.zeros(Math.max(0, 20 - len) + ((4 - (Math.max(20, len) % 4)) % 4));\n}\n\nfunction writeLayerRecord(w: PsdWriter, rec: PsdLayerRecord, psb: boolean, nameBytes: Uint8Array): void {\n writeRect(w, rec.rect);\n w.u16(rec.channels.length);\n for (const c of rec.channels) {\n w.i16(c.id);\n const len = c.compression === null ? 0 : 2 + c.data.length;\n if (psb) w.u64(len);\n else w.u32(len);\n }\n if (rec.blendKey.length !== 4) throw new PsdError(`Blend key \"${rec.blendKey}\" must be 4 characters.`);\n w.ascii('8BIM');\n w.ascii(rec.blendKey);\n w.u8(rec.opacity);\n w.u8(rec.clipping);\n w.u8(rec.flags);\n w.u8(0);\n const extra = w.beginLen(false);\n const mask = w.beginLen(false);\n if (rec.mask) writeMask(w, rec.mask);\n w.endLen(mask);\n // Blending ranges: composite gray plus one per colour channel, all full.\n w.u32(40);\n for (let i = 0; i < 5; i++) w.bytes([0, 0, 255, 255, 0, 0, 255, 255]);\n w.pascal(nameBytes, 4);\n writeBlocks(w, rec.blocks, psb, 2);\n w.endLen(extra);\n}\n\n/** Serializes a file. Channel data must already be encoded; `encodeName` writes legacy (Pascal) names. */\nexport function writePsd(file: PsdFile, encodeName: (s: string) => Uint8Array): Uint8Array {\n const { header } = file;\n const { psb } = header;\n const w = new PsdWriter();\n w.ascii('8BPS');\n w.u16(psb ? 2 : 1);\n w.zeros(6);\n w.u16(header.channels);\n w.u32(header.height);\n w.u32(header.width);\n w.u16(header.depth);\n w.u16(header.colorMode);\n w.u32(file.colorModeData.length);\n w.bytes(file.colorModeData);\n const res = w.beginLen(false);\n for (const r of file.resources) {\n w.ascii('8BIM');\n w.u16(r.id);\n w.pascal(encodeName(r.name), 2);\n w.u32(r.data.length);\n w.bytes(r.data);\n if (r.data.length % 2) w.u8(0);\n }\n w.endLen(res);\n const lm = w.beginLen(psb);\n if (file.layers.length || file.globalBlocks.length) {\n const li = w.beginLen(psb);\n if (file.layers.length) {\n const start = w.length;\n w.i16(file.mergedAlpha ? -file.layers.length : file.layers.length);\n for (const rec of file.layers) writeLayerRecord(w, rec, psb, encodeName(rec.legacyName));\n for (const rec of file.layers) {\n for (const c of rec.channels) {\n if (c.compression === null) continue;\n w.u16(c.compression);\n w.bytes(c.data);\n }\n }\n if ((w.length - start) % 2) w.u8(0);\n }\n w.endLen(li);\n w.u32(0); // no global layer mask\n writeBlocks(w, file.globalBlocks, psb, 4);\n }\n w.endLen(lm);\n w.u16(file.imageCompression);\n w.bytes(file.imageData);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/pixels.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Planar PSD channels (any colour mode, 1 / 8 / 16 / 32 bits) → 8-bit sRGB RGBA, and back. */\n\nimport { D50, XYZ_D50_TO_SRGB } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport { PsdError } from './binary';\n\nexport const MODE_BITMAP = 0;\nexport const MODE_GRAYSCALE = 1;\nexport const MODE_INDEXED = 2;\nexport const MODE_RGB = 3;\nexport const MODE_CMYK = 4;\nexport const MODE_MULTICHANNEL = 7;\nexport const MODE_DUOTONE = 8;\nexport const MODE_LAB = 9;\n\n/** Colour channels of a mode (alpha and spot channels come after them). */\nexport function colorChannelCount(mode: number, headerChannels: number): number {\n switch (mode) {\n case MODE_RGB:\n case MODE_LAB:\n return 3;\n case MODE_CMYK:\n return 4;\n case MODE_MULTICHANNEL:\n return Math.min(3, headerChannels);\n default:\n return 1;\n }\n}\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** A plane's sample at `i` as 0..1 (`gamma`: 32-bit samples are linear light, so encode them). */\nfunction sampler(plane: Uint8Array | undefined, depth: number, width: number, encode: boolean, fill: number): (i: number) => number {\n if (!plane) return () => fill;\n switch (depth) {\n case 8:\n return (i) => plane[i] / 255;\n case 16:\n return (i) => ((plane[2 * i] << 8) | plane[2 * i + 1]) / 65535;\n case 32: {\n const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);\n return (i) => {\n const v = clamp01(view.getFloat32(4 * i, false) || 0);\n return encode ? linearToSrgb(v) : v;\n };\n }\n case 1: {\n const rb = Math.ceil(width / 8);\n // Bitmap: a set bit is black.\n return (i) => {\n const x = i % width;\n const y = (i - x) / width;\n return (plane[y * rb + (x >> 3)] >> (7 - (x & 7))) & 1 ? 0 : 1;\n };\n }\n default:\n throw new PsdError(`Unsupported bit depth ${depth}.`);\n }\n}\n\nfunction labToSrgb(l: number, a: number, b: number): [number, number, number] {\n const fy = (l + 16) / 116;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const x = f(fy + a / 500) * D50[0];\n const y = f(fy) * D50[1];\n const z = f(fy - b / 200) * D50[2];\n const m = XYZ_D50_TO_SRGB;\n return [\n linearToSrgb(clamp01(m[0][0] * x + m[0][1] * y + m[0][2] * z)),\n linearToSrgb(clamp01(m[1][0] * x + m[1][1] * y + m[1][2] * z)),\n linearToSrgb(clamp01(m[2][0] * x + m[2][1] * y + m[2][2] * z)),\n ];\n}\n\nexport interface ChannelSet {\n width: number;\n height: number;\n depth: number;\n mode: number;\n /** Colour planes in mode order (missing planes read as 0, or no ink for CMYK). */\n color: Array<Uint8Array | undefined>;\n /** Missing: opaque. */\n alpha?: Uint8Array;\n /** Indexed: 768-byte palette (all reds, all greens, all blues). */\n palette?: Uint8Array;\n}\n\n/** Interleaved 8-bit sRGB RGBA. */\nexport function toRgba8(set: ChannelSet): Uint8ClampedArray {\n const { width, height, depth, mode } = set;\n const n = width * height;\n const out = new Uint8ClampedArray(n * 4);\n const linear = depth === 32;\n const isLab = mode === MODE_LAB;\n const cmyk = mode === MODE_CMYK;\n // CMYK planes store 1 for no ink; a missing plane is no ink too.\n const planes = set.color.map((p) => sampler(p, depth, width, linear && !isLab && !cmyk, cmyk ? 1 : 0));\n const alpha = sampler(set.alpha, depth, width, false, 1);\n const fast8 = depth === 8 && mode === MODE_RGB && set.color.every(Boolean);\n if (fast8) {\n const [r, g, b] = set.color as Uint8Array[];\n const a = set.alpha;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n out[o] = r[i];\n out[o + 1] = g[i];\n out[o + 2] = b[i];\n out[o + 3] = a ? a[i] : 255;\n }\n return out;\n }\n const palette = set.palette;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n let r: number;\n let g: number;\n let b: number;\n switch (mode) {\n case MODE_RGB:\n case MODE_MULTICHANNEL:\n r = planes[0](i);\n g = planes[1] ? planes[1](i) : r;\n b = planes[2] ? planes[2](i) : r;\n break;\n case MODE_CMYK: {\n const k = planes[3] ? planes[3](i) : 1;\n r = planes[0](i) * k;\n g = planes[1](i) * k;\n b = planes[2](i) * k;\n break;\n }\n case MODE_LAB:\n [r, g, b] = labToSrgb(planes[0](i) * 100, planes[1](i) * 255 - 128, planes[2](i) * 255 - 128);\n break;\n case MODE_INDEXED: {\n const idx = Math.round(planes[0](i) * 255);\n if (palette && palette.length >= 768) {\n out[o] = palette[idx];\n out[o + 1] = palette[256 + idx];\n out[o + 2] = palette[512 + idx];\n out[o + 3] = Math.round(alpha(i) * 255);\n continue;\n }\n r = g = b = idx / 255;\n break;\n }\n default:\n r = g = b = planes[0](i);\n }\n out[o] = Math.round(r * 255);\n out[o + 1] = Math.round(g * 255);\n out[o + 2] = Math.round(b * 255);\n out[o + 3] = Math.round(alpha(i) * 255);\n }\n return out;\n}\n\n/** An 8-bit coverage plane (masks) from any depth. */\nexport function toMask8(plane: Uint8Array, width: number, height: number, depth: number): Uint8ClampedArray {\n if (depth === 8) return new Uint8ClampedArray(plane.buffer, plane.byteOffset, width * height);\n const s = sampler(plane, depth, width, false, 0);\n const out = new Uint8ClampedArray(width * height);\n for (let i = 0; i < out.length; i++) out[i] = Math.round(s(i) * 255);\n return out;\n}\n\n/** Splits interleaved RGBA into planes `[r, g, b, a]` (8-bit). */\nexport function rgbaToPlanes(data: ArrayLike<number>, n: number): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {\n const planes: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] = [new Uint8Array(n), new Uint8Array(n), new Uint8Array(n), new Uint8Array(n)];\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n planes[0][i] = data[o];\n planes[1][i] = data[o + 1];\n planes[2][i] = data[o + 2];\n planes[3][i] = data[o + 3];\n }\n return planes;\n}\n\n/**\n * Photoshop stores a transparent merged image matted over white (colour × alpha + white × (1 −\n * alpha)). `unmatteWhite` recovers the colour in place; `matteWhite` applies the matte.\n */\nexport function unmatteWhite(data: Uint8ClampedArray): void {\n for (let i = 0; i < data.length; i += 4) {\n const a = data[i + 3];\n if (a === 255 || a === 0) continue;\n const k = 255 - a;\n for (let c = 0; c < 3; c++) data[i + c] = Math.round(((data[i + c] - k) * 255) / a);\n }\n}\n\nexport function matteWhite(data: ArrayLike<number>): Uint8ClampedArray {\n const out = Uint8ClampedArray.from(data);\n for (let i = 0; i < out.length; i += 4) {\n const a = out[i + 3];\n if (a === 255) continue;\n for (let c = 0; c < 3; c++) out[i + c] = Math.round((out[i + c] * a + 255 * (255 - a)) / 255);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/io/src/psd_import.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD / PSB → our layer tree. Everything the model holds maps directly; everything else comes in\n * as close as it can (type, smart-object, fill and shape layers as their stored pixels) with a\n * warning naming the layers.\n */\n\nimport type { BlendMode } from '../blend';\nimport { emptySurface, surfaceFromPixels } from '../../raster/surface';\nimport type { GroupLayer, ImageLayer, LayerBlendMode, LayerLocks, PixelLayer, RasterMask } from '../layers/model';\nimport { ADJUSTMENT_KEYS, adjustmentLabel, parseAdjustmentBlock } from './adjust';\nimport { PsdError } from './binary';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_REAL_USER_MASK,\n CHANNEL_USER_MASK,\n COLOR_MODES,\n LAYER_FLAG_HIDDEN,\n MASK_FLAG_DISABLED,\n SECTION_CLOSED,\n type PsdFile,\n type PsdLayerRecord,\n type PsdRect,\n type PsdTreeNode,\n decodeLayerChannel,\n decodeMergedImage,\n layerName,\n layerTree,\n readPsd,\n rectSize,\n resolution,\n resource,\n rowBytes,\n sectionDivider,\n u32Block,\n u8Block,\n} from './file';\nimport { MODE_INDEXED, colorChannelCount, toMask8, toRgba8, unmatteWhite } from './pixels';\n\n/** PSD blend keys → our blend modes (`diss` is the one we lack). */\nexport const PSD_BLEND_KEYS: Record<string, LayerBlendMode> = {\n pass: 'pass-through',\n norm: 'normal',\n dark: 'darken',\n 'mul ': 'multiply',\n idiv: 'color-burn',\n lbrn: 'linear-burn',\n dkCl: 'darker-color',\n lite: 'lighten',\n scrn: 'screen',\n 'div ': 'color-dodge',\n lddg: 'linear-dodge',\n lgCl: 'lighter-color',\n over: 'overlay',\n sLit: 'soft-light',\n hLit: 'hard-light',\n vLit: 'vivid-light',\n lLit: 'linear-light',\n pLit: 'pin-light',\n hMix: 'hard-mix',\n diff: 'difference',\n smud: 'exclusion',\n fsub: 'subtract',\n fdiv: 'divide',\n 'hue ': 'hue',\n 'sat ': 'saturation',\n colr: 'color',\n 'lum ': 'luminosity',\n};\n\nexport const BLEND_TO_PSD: Record<LayerBlendMode, string> = Object.fromEntries(\n Object.entries(PSD_BLEND_KEYS).map(([k, v]) => [v, k])\n) as Record<LayerBlendMode, string>;\n\nconst FILL_KEYS = ['SoCo', 'GdFl', 'PtFl'];\nconst TYPE_KEYS = ['TySh', 'tySh'];\nconst SMART_KEYS = ['SoLd', 'SoLE', 'PlLd'];\nconst VECTOR_KEYS = ['vmsk', 'vsms'];\nconst EFFECT_KEYS = ['lfx2', 'lfxs', 'lmfx'];\n\n/** What happened to one PSD layer on import. */\nexport interface PsdLayerSummary {\n name: string;\n /** Our layer type, or `null` when the layer was dropped. */\n type: 'pixel' | 'adjustment' | 'group' | null;\n /** `mapped`: held exactly; `approximated`: imported with losses; `dropped`: left out. */\n status: 'mapped' | 'approximated' | 'dropped';\n /** What Photoshop calls it (`pixel`, `type`, `smart-object`, `fill`, `shape`, `Levels` …). */\n psdKind: string;\n /** Why it is not `mapped`. */\n notes: string[];\n /** Our layer id (when imported). */\n id?: string;\n depth: number;\n}\n\nexport interface PsdImportResult {\n width: number;\n height: number;\n /** Bottom to top, as in our model. */\n layers: ImageLayer[];\n /** One sentence per problem, naming the affected layers. */\n warnings: string[];\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n /** Pixels per inch, when the file says. */\n resolution?: number;\n info: { psb: boolean; depth: number; colorMode: string; channels: number };\n}\n\nexport interface PsdImportOptions {\n /** Makes layer ids (defaults to `psd-<n>`). */\n newId?: (hint: string) => string;\n /** Skip decoding pixels (inspection only). */\n skipPixels?: boolean;\n}\n\n/** A PSD byte (0..255) as the whole percent Photoshop shows, 0..1. */\nconst percent = (v: number) => Math.round((v / 255) * 100) / 100;\n\nclass Warnings {\n private groups = new Map<string, string[]>();\n readonly extra: string[] = [];\n\n add(message: string, layer: string): void {\n const list = this.groups.get(message) ?? [];\n if (!list.includes(layer)) list.push(layer);\n this.groups.set(message, list);\n }\n\n list(): string[] {\n const out = [...this.extra];\n for (const [message, layers] of this.groups) {\n const names = layers.map((n) => `\"${n}\"`);\n const shown = names.length > 6 ? `${names.slice(0, 6).join(', ')} and ${names.length - 6} more` : names.join(', ');\n out.push(`${message} (${layers.length === 1 ? 'layer' : 'layers'} ${shown}).`);\n }\n return out;\n }\n}\n\nfunction psdKind(rec: PsdLayerRecord): string {\n if (rec.blocks.some((b) => TYPE_KEYS.includes(b.key))) return 'type';\n if (rec.blocks.some((b) => SMART_KEYS.includes(b.key))) return 'smart-object';\n const fill = rec.blocks.some((b) => FILL_KEYS.includes(b.key));\n const vector = rec.blocks.some((b) => VECTOR_KEYS.includes(b.key) || b.key === 'vstk');\n if (fill && vector) return 'shape';\n if (fill) return 'fill';\n const adj = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adj) return adjustmentLabel(adj.key);\n return 'pixel';\n}\n\nfunction locksOf(rec: PsdLayerRecord): LayerLocks | undefined {\n const v = u32Block(rec, 'lspf');\n if (!v) return undefined;\n const locks: LayerLocks = {};\n if (v & 0x80000000) locks.all = true;\n if (v & 1) locks.transparency = true;\n if (v & 2) locks.pixels = true;\n if (v & 4) locks.position = true;\n return Object.keys(locks).length ? locks : undefined;\n}\n\ninterface Ctx {\n file: PsdFile;\n warn: Warnings;\n newId: (hint: string) => string;\n skipPixels: boolean;\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n colorCount: number;\n palette?: Uint8Array;\n}\n\nfunction decodeOr(ctx: Ctx, rec: PsdLayerRecord, id: number, name: string): Uint8Array | undefined {\n try {\n return decodeLayerChannel(ctx.file, rec, id);\n } catch (e) {\n ctx.warn.add(`A channel could not be decoded (${e instanceof Error ? e.message : String(e)}) and the layer is treated as empty`, name);\n return undefined;\n }\n}\n\n/** A rectangle's size, or 0 × 0 (with a warning) for a malformed one. */\nfunction safeSize(ctx: Ctx, rect: PsdRect, name: string): { width: number; height: number } {\n try {\n return rectSize(rect);\n } catch (e) {\n ctx.warn.add(`A malformed rectangle (${e instanceof Error ? e.message : String(e)}) left the layer or its mask empty`, name);\n return { width: 0, height: 0 };\n }\n}\n\nfunction recordPixels(ctx: Ctx, rec: PsdLayerRecord, name: string): PixelLayer['surface'] {\n const { width, height } = safeSize(ctx, rec.rect, name);\n if (!width || !height || ctx.skipPixels) return emptySurface(4);\n const ids = [...Array.from({ length: ctx.colorCount }, (_, i) => i), CHANNEL_ALPHA];\n const planes = ids.map((id) => (rec.channels.some((c) => c.id === id) ? decodeOr(ctx, rec, id, name) ?? null : undefined));\n // A channel that is in the file but unreadable leaves the layer empty (as the warning says).\n if (planes.some((p) => p === null)) return emptySurface(4);\n if (planes.every((p) => p === undefined)) return emptySurface(4);\n const data = toRgba8({\n width,\n height,\n depth: ctx.file.header.depth,\n mode: ctx.file.header.colorMode,\n color: planes.slice(0, ctx.colorCount) as Array<Uint8Array | undefined>,\n alpha: planes[ctx.colorCount] as Uint8Array | undefined,\n palette: ctx.palette,\n });\n return surfaceFromPixels({ width, height, data }, 4, rec.rect.left, rec.rect.top);\n}\n\nfunction recordMask(ctx: Ctx, rec: PsdLayerRecord, name: string): RasterMask | undefined {\n const m = rec.mask;\n if (!m) return undefined;\n const useReal = rec.channels.some((c) => c.id === CHANNEL_REAL_USER_MASK) && m.real;\n const id = useReal ? CHANNEL_REAL_USER_MASK : CHANNEL_USER_MASK;\n if (!rec.channels.some((c) => c.id === id)) return undefined;\n const rect = useReal ? m.real!.rect : m.rect;\n const def: 0 | 255 = (useReal ? m.real!.background : m.defaultColor) >= 128 ? 255 : 0;\n const flags = useReal ? m.real!.flags : m.flags;\n let surface = emptySurface(1, def);\n const { width, height } = safeSize(ctx, rect, name);\n if (width && height && !ctx.skipPixels) {\n const plane = decodeOr(ctx, rec, id, name);\n if (plane) surface = surfaceFromPixels({ width, height, data: toMask8(plane, width, height, ctx.file.header.depth) }, 1, rect.left, rect.top, def);\n }\n const mask: RasterMask = { surface, defaultValue: def };\n if (m.userDensity !== undefined && m.userDensity < 255) mask.density = percent(m.userDensity);\n if (m.userFeather !== undefined && m.userFeather > 0) mask.feather = Math.min(1000, m.userFeather);\n if (flags & MASK_FLAG_DISABLED) mask.enabled = false;\n return mask;\n}\n\nfunction baseProps(ctx: Ctx, rec: PsdLayerRecord, name: string, notes: string[]) {\n let blend = PSD_BLEND_KEYS[rec.blendKey];\n if (rec.blendKey === 'diss') {\n ctx.warn.add('Dissolve is not supported and became Normal', name);\n notes.push('Dissolve became Normal');\n } else if (!blend) {\n ctx.warn.add(`An unknown blend mode became Normal`, name);\n notes.push(`unknown blend mode \"${rec.blendKey}\"`);\n }\n if (!blend || blend === 'pass-through') blend = 'normal';\n const props = {\n name,\n visible: (rec.flags & LAYER_FLAG_HIDDEN) === 0,\n opacity: percent(rec.opacity),\n blend: blend as BlendMode,\n } as { name: string; visible: boolean; opacity: number; blend: BlendMode; clipped?: boolean; locks?: LayerLocks };\n if (rec.clipping) props.clipped = true;\n const locks = locksOf(rec);\n if (locks) props.locks = locks;\n if (rec.blocks.some((b) => EFFECT_KEYS.includes(b.key))) {\n ctx.warn.add('Layer effects (lfx2) are not rendered', name);\n notes.push('layer effects are not rendered');\n }\n if (rec.blocks.some((b) => b.key === 'artb' || b.key === 'artd' || b.key === 'abdd')) {\n ctx.warn.add('Artboards are not supported; they came in as plain groups, without their background colour and clipping', name);\n notes.push('artboard imported as a group');\n }\n if (rec.blocks.some((b) => VECTOR_KEYS.includes(b.key)) && psdKind(rec) === 'pixel') {\n ctx.warn.add('Vector masks are not supported and were ignored', name);\n notes.push('vector mask ignored');\n }\n return props;\n}\n\nfunction importNode(ctx: Ctx, node: PsdTreeNode, depth: number): ImageLayer | null {\n const rec = ctx.file.layers[node.index];\n const name = layerName(rec);\n const notes: string[] = [];\n const kind = psdKind(rec);\n const entry: PsdLayerSummary = { name, type: null, status: 'mapped', psdKind: node.children ? 'group' : kind, notes, depth };\n ctx.summary.push(entry);\n\n if (node.children) {\n const props = baseProps(ctx, rec, name, notes);\n const divider = sectionDivider(rec);\n const key = divider?.blendKey ?? rec.blendKey;\n const groupBlend: LayerBlendMode = key === 'pass' ? 'pass-through' : (PSD_BLEND_KEYS[key] ?? 'normal');\n const children: ImageLayer[] = [];\n for (const child of node.children) {\n const layer = importNode(ctx, child, depth + 1);\n if (layer) children.push(layer);\n }\n const group: GroupLayer = { type: 'group', id: ctx.newId(name), ...props, blend: groupBlend, children };\n if (divider?.kind === SECTION_CLOSED) group.collapsed = true;\n const mask = recordMask(ctx, rec, name);\n if (mask) group.mask = mask;\n entry.type = 'group';\n entry.id = group.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return group;\n }\n\n const adjBlock = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adjBlock) {\n const parsed = parseAdjustmentBlock(adjBlock.key, adjBlock.data);\n if (!parsed) {\n const label = adjustmentLabel(adjBlock.key);\n ctx.warn.add(`${label} adjustment layers are not supported and were left out`, name);\n notes.push(`${label} is not supported`);\n entry.status = 'dropped';\n return null;\n }\n const props = baseProps(ctx, rec, name, notes);\n if (parsed.approximated) {\n ctx.warn.add(`A ${adjustmentLabel(adjBlock.key)} layer was approximated: ${parsed.approximated}`, name);\n notes.push(parsed.approximated);\n }\n const layer = { type: 'adjustment', id: ctx.newId(name), ...props, kind: parsed.kind, params: parsed.params } as ImageLayer;\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'adjustment';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n }\n\n const props = baseProps(ctx, rec, name, notes);\n if (kind !== 'pixel') {\n const what = kind === 'smart-object' ? 'Smart objects' : kind === 'type' ? 'Type layers' : kind === 'shape' ? 'Shape layers' : 'Fill layers';\n ctx.warn.add(`${what} came in as pixel layers (their stored pixels)`, name);\n notes.push(`${kind} layer imported as pixels`);\n }\n const layer: PixelLayer = { type: 'pixel', id: ctx.newId(name), ...props, surface: recordPixels(ctx, rec, name), offset: { x: 0, y: 0 } };\n const fill = u8Block(rec, 'iOpa');\n if (fill !== undefined && fill < 255) layer.fillOpacity = percent(fill);\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'pixel';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n}\n\nfunction mergedLayer(ctx: Ctx): PixelLayer | null {\n const { header } = ctx.file;\n const { width, height } = header;\n let planes: Uint8Array;\n try {\n planes = decodeMergedImage(ctx.file);\n } catch (e) {\n ctx.warn.extra.push(`The merged image could not be decoded (${e instanceof Error ? e.message : String(e)}).`);\n return null;\n }\n const size = rowBytes(width, header.depth) * height;\n const plane = (i: number) => (i < header.channels ? planes.subarray(i * size, (i + 1) * size) : undefined);\n const cc = ctx.colorCount;\n const hasAlpha = header.channels > cc && (ctx.file.layers.length === 0 || ctx.file.mergedAlpha);\n const data = toRgba8({\n width,\n height,\n depth: header.depth,\n mode: header.colorMode,\n color: Array.from({ length: cc }, (_, i) => plane(i)),\n alpha: hasAlpha ? plane(cc) : undefined,\n palette: ctx.palette,\n });\n if (hasAlpha) unmatteWhite(data);\n return {\n type: 'pixel',\n id: ctx.newId('Background'),\n name: 'Background',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n}\n\n/** Maps a parsed PSD to our layers. */\nexport function psdToLayers(file: PsdFile, options: PsdImportOptions = {}): PsdImportResult {\n const { header } = file;\n const warn = new Warnings();\n let n = 0;\n const ctx: Ctx = {\n file,\n warn,\n newId: options.newId ?? (() => `psd-${++n}`),\n skipPixels: options.skipPixels ?? false,\n summary: [],\n colorCount: colorChannelCount(header.colorMode, header.channels),\n palette: header.colorMode === MODE_INDEXED ? file.colorModeData : undefined,\n };\n const mode = COLOR_MODES[header.colorMode] ?? `mode ${header.colorMode}`;\n if (mode !== 'rgb' || header.depth !== 8) {\n warn.extra.push(`The file is ${header.depth}-bit ${mode}; it was converted to 8-bit RGB.`);\n }\n if (resource(file, 1039)) warn.extra.push('The embedded ICC profile is ignored; colours are read as sRGB.');\n\n let layers: ImageLayer[] = [];\n if (file.layers.length) {\n const tree = layerTree(file);\n const built: ImageLayer[] = [];\n for (const node of tree) {\n const layer = importNode(ctx, node, 0);\n if (layer) built.push(layer);\n }\n layers = built;\n if (!layers.length && !ctx.skipPixels) {\n // Every layer was left out: the merged image is still the best picture of the file.\n const bg = mergedLayer(ctx);\n if (bg) {\n layers = [bg];\n warn.extra.push('No layer could be imported, so the merged image was opened as the Background.');\n }\n }\n } else if (!ctx.skipPixels) {\n const bg = mergedLayer(ctx);\n if (bg) layers = [bg];\n }\n return {\n width: header.width,\n height: header.height,\n layers,\n warnings: warn.list(),\n summary: ctx.summary,\n resolution: resolution(file),\n info: { psb: header.psb, depth: header.depth, colorMode: mode, channels: header.channels },\n };\n}\n\n/** Parses and maps PSD / PSB bytes. Throws `PsdError` for files that cannot be read at all. */\nexport function importPsd(bytes: Uint8Array, options: PsdImportOptions = {}): PsdImportResult {\n if (bytes.length < 26) throw new PsdError('Not a PSD file: it is shorter than a PSD header.');\n return psdToLayers(readPsd(bytes), options);\n}\n","// Portions ported from photocraft/crates/io/src/psd_export.rs and photocraft/crates/psd/src/builder.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Our layer tree → an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, RLE-compressed, plus the merged image the caller rendered.\n *\n * The stack must be *prepared*: annotation layers rasterized, layer filters and parametric masks\n * baked, and no layer reading the document source (see `ImageEditorController.toPsd`).\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport { contentBounds, readRegion } from '../../raster/surface';\nimport { isActiveFilter, type ImageLayer, type RasterMask } from '../layers/model';\nimport type { CropBox } from '../types';\nimport { PsdError, PsdWriter, encodeLegacyName } from './binary';\nimport { writeAdjustmentBlock } from './adjust';\nimport { encodeRle } from './compression';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_USER_MASK,\n LAYER_FLAG_HIDDEN,\n LAYER_FLAG_TRANSPARENCY_PROTECTED,\n MASK_FLAG_DISABLED,\n PSD_MAX_DIMENSION,\n SECTION_CLOSED,\n SECTION_DIVIDER,\n SECTION_OPEN,\n type PsdChannel,\n type PsdLayerMask,\n type PsdLayerRecord,\n type PsdRect,\n type PsdResource,\n type PsdTaggedBlock,\n writePsd,\n} from './file';\nimport { BLEND_TO_PSD } from './import';\nimport { matteWhite, rgbaToPlanes } from './pixels';\n\nexport interface PsdExportInput {\n width: number;\n height: number;\n /** Prepared stack, bottom to top. */\n layers: readonly ImageLayer[];\n /** The composite, `width × height` RGBA, written as the merged image. */\n merged: ArrayLike<number>;\n /** Pixels per inch (default 72). */\n resolution?: number;\n}\n\nexport interface PsdExportResult {\n bytes: Uint8Array;\n psb: boolean;\n /** Layer records written (group folders and dividers included). */\n records: number;\n}\n\n/** Layer flag bits 3 and 4: \"bit 4 is useful\" and \"pixel data irrelevant\" (group records). */\nconst FLAG_PIXELS_IRRELEVANT = 8 | 16;\nconst EMPTY_RECT: PsdRect = { top: 0, left: 0, bottom: 0, right: 0 };\n\nfunction unicodeBlock(s: string): Uint8Array {\n const w = new PsdWriter();\n w.unicode(s);\n return w.result();\n}\n\nfunction u32Bytes(v: number): Uint8Array {\n const w = new PsdWriter();\n w.u32(v);\n return w.result();\n}\n\nfunction sectionBlock(kind: number, blendKey?: string): Uint8Array {\n const w = new PsdWriter();\n w.u32(kind);\n if (blendKey) {\n w.ascii('8BIM');\n w.ascii(blendKey);\n }\n return w.result();\n}\n\nconst toRect = (r: CropBox): PsdRect => ({ top: r.y, left: r.x, bottom: r.y + r.height, right: r.x + r.width });\n\nfunction rleChannel(id: number, plane: Uint8Array, width: number, height: number, psb: boolean): PsdChannel {\n return { id, compression: 1, data: encodeRle(plane, { planes: 1, width, height, depth: 8, psb }) };\n}\n\nconst emptyChannel = (id: number): PsdChannel => ({ id, compression: 0, data: new Uint8Array(0) });\n\nfunction maskChannel(mask: RasterMask, ox: number, oy: number, psb: boolean): { channel: PsdChannel; mask: PsdLayerMask } {\n if (mask.source) throw new PsdError('A parametric mask must be baked before export.');\n const bounds = contentBounds(mask.surface);\n const def = mask.defaultValue;\n const m: PsdLayerMask = { rect: EMPTY_RECT, defaultColor: def, flags: mask.enabled === false ? MASK_FLAG_DISABLED : 0 };\n if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);\n if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;\n if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };\n const plane = readRegion(mask.surface, bounds);\n m.rect = toRect({ ...bounds, x: bounds.x + ox, y: bounds.y + oy });\n return { channel: rleChannel(CHANNEL_USER_MASK, new Uint8Array(plane.buffer, plane.byteOffset, plane.length), bounds.width, bounds.height, psb), mask: m };\n}\n\nfunction lockBits(layer: ImageLayer): number {\n const l = layer.locks;\n if (!l) return 0;\n return (l.all ? 0x80000000 : 0) | (l.transparency ? 1 : 0) | (l.pixels ? 2 : 0) | (l.position ? 4 : 0);\n}\n\n/** Writes the prepared stack as PSD (or PSB) bytes. */\nexport function exportPsd(input: PsdExportInput): PsdExportResult {\n const { width, height } = input;\n if (width < 1 || height < 1) throw new PsdError('The document is empty.');\n const psb = width > PSD_MAX_DIMENSION || height > PSD_MAX_DIMENSION;\n const records: PsdLayerRecord[] = [];\n let nextId = 0;\n\n const common = (layer: ImageLayer, extra: PsdTaggedBlock[] = []): Pick<PsdLayerRecord, 'opacity' | 'clipping' | 'flags' | 'legacyName' | 'blocks'> => {\n const name = layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : '');\n const blocks: PsdTaggedBlock[] = [{ key: 'luni', data: unicodeBlock(name) }, { key: 'lyid', data: u32Bytes(++nextId) }, ...extra];\n const locks = lockBits(layer);\n if (locks) blocks.push({ key: 'lspf', data: u32Bytes(locks >>> 0) });\n return {\n opacity: Math.round(Math.min(1, Math.max(0, layer.opacity)) * 255),\n clipping: layer.clipped ? 1 : 0,\n flags: (layer.visible ? 0 : LAYER_FLAG_HIDDEN) | (layer.locks?.transparency ? LAYER_FLAG_TRANSPARENCY_PROTECTED : 0),\n legacyName: name,\n blocks,\n };\n };\n\n const write = (layer: ImageLayer): void => {\n switch (layer.type) {\n case 'group': {\n records.push({\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: 'norm',\n opacity: 255,\n clipping: 0,\n flags: FLAG_PIXELS_IRRELEVANT | (layer.visible ? 0 : LAYER_FLAG_HIDDEN),\n legacyName: '</Layer group>',\n blocks: [\n { key: 'luni', data: unicodeBlock('</Layer group>') },\n { key: 'lsct', data: sectionBlock(SECTION_DIVIDER) },\n ],\n });\n for (const child of layer.children) write(child);\n const blendKey = BLEND_TO_PSD[layer.blend] ?? 'norm';\n const base = common(layer, [{ key: 'lsct', data: sectionBlock(layer.collapsed ? SECTION_CLOSED : SECTION_OPEN, blendKey) }]);\n const rec: PsdLayerRecord = {\n ...base,\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey,\n flags: base.flags | FLAG_PIXELS_IRRELEVANT,\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'adjustment': {\n const block = writeAdjustmentBlock(layer.kind, layer.params);\n const rec: PsdLayerRecord = {\n ...common(layer, [{ key: block.key, data: block.data }]),\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm',\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'pixel': {\n if (layer.source === 'document') throw new PsdError(`Layer \"${layer.name}\" reads the document image; resolve its pixels before export.`);\n if (layer.filters?.some(isActiveFilter)) throw new PsdError(`Layer \"${layer.name}\" has live filters; bake them before export.`);\n const extra: PsdTaggedBlock[] = [];\n if (layer.fillOpacity !== undefined && layer.fillOpacity < 1) {\n extra.push({ key: 'iOpa', data: Uint8Array.from([Math.round(Math.max(0, layer.fillOpacity) * 255), 0, 0, 0]) });\n }\n const rec: PsdLayerRecord = { ...common(layer, extra), rect: EMPTY_RECT, channels: [], blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm' };\n const bounds = contentBounds(layer.surface);\n if (bounds) {\n const n = bounds.width * bounds.height;\n const [r, g, b, a] = rgbaToPlanes(readRegion(layer.surface, bounds), n);\n rec.rect = toRect({ ...bounds, x: bounds.x + layer.offset.x, y: bounds.y + layer.offset.y });\n rec.channels = [\n rleChannel(CHANNEL_ALPHA, a, bounds.width, bounds.height, psb),\n rleChannel(0, r, bounds.width, bounds.height, psb),\n rleChannel(1, g, bounds.width, bounds.height, psb),\n rleChannel(2, b, bounds.width, bounds.height, psb),\n ];\n } else {\n rec.channels = [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel);\n }\n if (layer.mask) {\n const m = maskChannel(layer.mask, layer.offset.x, layer.offset.y, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'annotation':\n throw new PsdError(`Annotation layer \"${layer.name}\" must be rasterized before export.`);\n }\n };\n for (const layer of input.layers) write(layer);\n if (records.length > 32767) throw new PsdError('A PSD holds at most 32767 layer records.');\n\n // Merged image: RGB, plus alpha when anything is transparent.\n const n = width * height;\n if (input.merged.length < n * 4) throw new PsdError(`The merged image has ${input.merged.length} bytes, expected ${n * 4}.`);\n // Photoshop's merged image is matted over white where it is transparent.\n const [r, g, b, a] = rgbaToPlanes(matteWhite(input.merged), n);\n const transparent = a.some((v) => v !== 255);\n const planes = transparent ? [r, g, b, a] : [r, g, b];\n const merged = new Uint8Array(n * planes.length);\n planes.forEach((p, i) => merged.set(p, i * n));\n\n const res = Math.max(1, input.resolution ?? 72);\n const rw = new PsdWriter();\n for (let k = 0; k < 2; k++) {\n rw.u32(Math.round(res * 65536));\n rw.u16(1); // pixels per inch\n rw.u16(1); // inches\n }\n const version = new PsdWriter();\n version.u32(1);\n version.u8(1); // the merged image is real\n version.unicode('Pixerate Editor');\n version.unicode('Pixerate Editor');\n version.u32(1);\n const resources: PsdResource[] = [\n { id: 1005, name: '', data: rw.result() },\n { id: 1057, name: '', data: version.result() },\n ];\n\n const bytes = writePsd(\n {\n header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },\n colorModeData: new Uint8Array(0),\n resources,\n layers: records,\n mergedAlpha: transparent && records.length > 0,\n globalBlocks: [],\n imageCompression: 1,\n imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb }),\n },\n encodeLegacyName\n );\n return { bytes, psb, records: records.length };\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n} from './layers/model';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport {\n SurfaceDraft,\n TILE,\n collectTiles,\n contentBounds,\n emptySurface,\n packRgba,\n readPixel,\n readRegion,\n solidSurface,\n surfaceFromPixels,\n tilesInRect,\n translateSurface,\n writeRegion,\n type PixelSurface,\n type Tile,\n type TiledSurface,\n type TileKey,\n TiledSurface as TiledSurfaceClass,\n finishTile,\n tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerFilter as addLayerFilterOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n StrokeCoverage,\n applyStrokeToTile,\n normalizeBrush,\n type BrushSettings,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, isPng, parseDataUrl } from '../raster/png';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd } from './psd/export';\n\n/** Latest serialized schema version (`3`: layers). Documents with the old shape still write 1 or 2. */\nexport const IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;\n\n/** Changes to an adjustment layer; `params` is merged into the current parameters. */\nexport interface AdjustmentLayerUpdate<K extends AdjustmentKind = AdjustmentKind> {\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n name?: string;\n /** A layer mask, or `null` to remove it. */\n mask?: AdjustmentLayerMask | null;\n}\n\n/** Options shared by the selection commands. */\nexport interface SelectionCommandOptions {\n /** How the result combines with the current selection (default `replace`). */\n mode?: SelectionMode;\n /** Feather the new shape by this many pixels before combining (default 0). */\n feather?: number;\n}\n\n/** Where an AI result goes (see `applyInpaintedImage`, `applyBackgroundRemovedImage`). */\nexport interface AIResultOptions {\n /** `new-layer`: a new layer (inpainting) or a mask (background removal); `replace`: the Background's pixels. Default: by `isMultiLayer`. */\n target?: 'new-layer' | 'replace';\n}\n\n/** Where the selection tools read colours from. */\nexport interface SampleOptions {\n /**\n * `composite` (default): the image, as before (with several layers: the composite of the pixel\n * layers, or with `sampleMerged` of every layer); `active-layer`: only the active pixel layer.\n */\n sample?: 'composite' | 'active-layer';\n /** Sample the adjusted image (base adjustments and layers) instead of the source pixels. */\n sampleMerged?: boolean;\n /** Pixels to use instead of reading the source image (natural size, RGBA). */\n pixels?: PixelBuffer;\n}\n\n/** A new adjustment layer: its kind plus optional initial values and position. */\nexport interface AdjustmentLayerInit<K extends AdjustmentKind = AdjustmentKind> extends AdjustmentLayerUpdate<K> {\n kind: K;\n id?: string;\n /** Put the new layer directly above this layer (in its group). */\n aboveId?: string;\n /** Put the new layer at the top of this group. */\n parentId?: string;\n}\n\n/** A new pixel layer: empty, a solid colour over the canvas, or given pixels, plus its properties and position. */\nexport interface PixelLayerInit extends LayerPlacement {\n id?: string;\n name?: string;\n /** Fill the canvas with this colour (`#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` 0..255). */\n color?: string | number[];\n /** RGBA pixels placed at `x`, `y` (document pixels). */\n pixels?: PixelBuffer;\n x?: number;\n y?: number;\n visible?: boolean;\n /** 0..1 (default 1). */\n opacity?: number;\n fillOpacity?: number;\n blend?: BlendMode;\n}\n\n/** The paint tools' options (view state). */\nexport interface PaintOptions {\n brush: BrushSettings;\n /** `#rrggbb`: what the brush and Fill paint. */\n foreground: string;\n /** `#rrggbb`: what the eraser paints on transparency-locked layers and masks. */\n background: string;\n}\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: 'brush' | 'eraser';\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /** Paint colour (default the foreground). Masks take its grey value. */\n color?: string | number[];\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n private readonly stroke: StrokeCoverage,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.stroke.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(this.stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n this.update(this.stroke.finish(this.last ?? undefined));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.stroke.coverage.bounds;\n return { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: this.stroke.dabCount };\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => this.sourceVersion(),\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff' },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || layer.type !== 'pixel') return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || layer.type !== 'pixel') return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * Composites the layer stack in document space without a canvas (annotation layers need one\n * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and\n * output opacity are not applied. With `layerIds`, only those layers are composited.\n */\n public compositePixels(rect?: CropBox, options: { layerIds?: string[] } = {}): PixelBuffer {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n let layers = this.state.layers;\n if (options.layerIds) {\n const keep = new Set(options.layerIds);\n layers = flattenLayers(layers).filter((l) => keep.has(l.id) && l.type !== 'group');\n }\n const data = this.compositor.composite(layers, r, this.compositorSources());\n return { width: r.width, height: r.height, data };\n }\n\n private compositorSources(): CompositorSources {\n const { width, height } = this.naturalSize;\n return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion() };\n }\n\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 1;\n\n /** Changes whenever the Background's source pixels can have changed. */\n private sourceVersion(): string {\n const key = (this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels) as object | null;\n if (!key) return '0';\n let id = this.sourceIds.get(key);\n if (!id) {\n id = this.nextSourceId++;\n this.sourceIds.set(key, id);\n }\n return String(id);\n }\n\n /** The pixels a pixel layer composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer): PixelSurface {\n if (layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather or enabled flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */\n private async compositeToSurface(layers: ImageLayer[], rect: CropBox): Promise<PixelSurface> {\n const { width, height } = this.naturalSize;\n if (flattenLayers(layers).some((l) => l.type === 'annotation' && l.visible) && !this.renderer.canRasterize()) {\n throw new LayerOperationError('Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.');\n }\n await this.renderer.prepareLayerAssets(layers, width, height);\n const data = new LayerCompositor().composite(layers, rect, this.renderer.compositorSources(width, height));\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /** Document bounds a layer can paint into (pixel content, annotations, or the canvas for the rest). */\n private paintBounds(layer: ImageLayer): CropBox {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n if (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill and filters are baked), as one undo step. Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n const rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n // --- Painting ---\n\n /** Brush settings, colours and tool options of the paint tools (view state, no undo). */\n public setPaintOptions(options: Partial<Omit<PaintOptions, 'brush'>> & { brush?: Partial<BrushSettings> }): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel === 'mask' ? toMaskGrey(color) : color,\n background: target.channel === 'mask' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel === 'mask' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n this.notify(rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush or eraser. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel === 'mask' ? grey(color) : color,\n background: target.channel === 'mask' ? grey(background) : background,\n opacity,\n blend: target.channel === 'mask' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else {\n throw new LayerOperationError(`Only annotation layers and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n let out: PixelLayer = { ...layer };\n if (layer.source === 'document') {\n out.surface = this.surfaceOf(layer);\n delete out.source;\n }\n const active = layer.filters?.filter(isActiveFilter) ?? [];\n if (active.length) {\n add('Layer filters (develop, blur) were baked into the pixels', layer.name);\n const content: PixelLayer = { ...out, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const data = compositor.composite([content], canvas, sources);\n out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };\n }\n if (layer.filters?.some((f) => !f.enabled)) add('Disabled layer filters were left out', layer.name);\n delete out.filters;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n };\n\n const prepared: ImageLayer[] = [];\n for (const l of this.state.layers) {\n const p = await prepare(l);\n if (p) prepared.push(p);\n }\n const s = this.state;\n if (s.crop) warnings.push('The crop is not applied; the PSD is the uncropped document.');\n if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push('Rotation and flips are not applied; the PSD is in the source orientation.');\n if (s.outputOpacity < 1) warnings.push('The document opacity is not written; layers keep their own opacity.');\n if (s.legacyOutputBlur) warnings.push('The document-level blur of an old document is not written.');\n for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`);\n\n const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });\n const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution });\n return { bytes: result.bytes, warnings, psb: result.psb, width, height };\n }\n\n private async replaceSource(src: string, clearAnnotations: boolean): Promise<void> {\n const next = await this.resolveSource(src);\n this.recordHistory();\n\n const prev = this.state.imageDimensions;\n const sx = next.dimensions.width / (prev.width || next.dimensions.width);\n const sy = next.dimensions.height / (prev.height || next.dimensions.height);\n\n this.imageElement = next.element;\n this.state.sourceUrl = next.sourceUrl;\n this.state.imageDimensions = next.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (this.state.selection && (sx !== 1 || sy !== 1)) {\n this.state.selection = normalizeSelection(\n resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))\n );\n }\n\n let layers = this.state.layers;\n if (sx !== 1 || sy !== 1) {\n if (this.state.crop) {\n this.state.crop = clampCropToImage(\n {\n x: Math.round(this.state.crop.x * sx),\n y: Math.round(this.state.crop.y * sy),\n width: Math.round(this.state.crop.width * sx),\n height: Math.round(this.state.crop.height * sy),\n },\n next.dimensions.width,\n next.dimensions.height\n );\n }\n layers = resizeLegacyMasks(layers, prev, next.dimensions);\n for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n const out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.state.layers, options.store).then(({ layers, assets }) => ({ ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n if (view.exact) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n","import type { SelectionMask, SelectionMode } from './mask';\nimport type { ColorDistance } from './wand';\nimport type { RefineEdgeParams } from './refine';\n\n/** Selection tools of the image editor UI. */\nexport type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'lasso';\n\nexport const SELECTION_TOOLS: readonly { id: SelectionToolKind; label: string }[] = [\n { id: 'wand', label: 'Magic Wand' },\n { id: 'rect', label: 'Rectangle' },\n { id: 'ellipse', label: 'Ellipse' },\n { id: 'lasso', label: 'Lasso' },\n];\n\nexport const SELECTION_MODE_LABELS: Record<SelectionMode, string> = {\n replace: 'New',\n add: 'Add',\n subtract: 'Subtract',\n intersect: 'Intersect',\n};\n\n/** The options bar of the selection tools (shared by the React and Svelte UIs). */\nexport interface SelectionToolOptions {\n tool: SelectionToolKind;\n mode: SelectionMode;\n /** Magic wand tolerance, 0..255. */\n tolerance: number;\n contiguous: boolean;\n antiAlias: boolean;\n /** Sample the adjusted image instead of the source. */\n sampleMerged: boolean;\n /** Sample the whole image (`composite`, default) or only the active pixel layer. */\n sample?: 'composite' | 'active-layer';\n distance: ColorDistance;\n}\n\nexport const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions = {\n tool: 'wand',\n mode: 'replace',\n tolerance: 32,\n contiguous: true,\n antiAlias: true,\n sampleMerged: false,\n distance: 'rgb',\n};\n\n/**\n * Photoshop's modifier keys: Shift adds, Alt/Option subtracts, both intersect; otherwise the\n * options bar mode.\n */\nexport function selectionModeFromModifiers(\n modifiers: { shiftKey?: boolean; altKey?: boolean },\n fallback: SelectionMode\n): SelectionMode {\n if (modifiers.shiftKey && modifiers.altKey) return 'intersect';\n if (modifiers.shiftKey) return 'add';\n if (modifiers.altKey) return 'subtract';\n return fallback;\n}\n\nexport interface RefineEdgeControl {\n id: keyof Omit<RefineEdgeParams, 'smartRadius'>;\n label: string;\n min: number;\n max: number;\n step: number;\n unit: string;\n}\n\n/** Sliders of the Refine Edge panel. */\nexport const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[] = [\n { id: 'radius', label: 'Radius', min: 0, max: 64, step: 1, unit: 'px' },\n { id: 'smooth', label: 'Smooth', min: 0, max: 100, step: 1, unit: '' },\n { id: 'feather', label: 'Feather', min: 0, max: 50, step: 0.5, unit: 'px' },\n { id: 'contrast', label: 'Contrast', min: 0, max: 100, step: 1, unit: '%' },\n { id: 'shiftEdge', label: 'Shift Edge', min: -100, max: 100, step: 1, unit: '%' },\n];\n\n/** A one-line status: \"No selection\" or \"W × H px selected (n % of the image)\". */\nexport function describeSelection(mask: SelectionMask | null | undefined): string {\n if (!mask || !mask.bounds) return 'No selection';\n let sum = 0;\n for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];\n const pct = (sum / 255 / mask.data.length) * 100;\n const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;\n return `${mask.bounds.width} × ${mask.bounds.height} px selected (${shown}% of the image)`;\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind } from './types';\nimport { ADJUSTMENT_KINDS, isAdjustmentKind } from './adjustment-layers';\nimport { describeSelection } from './selection/tool-options';\nimport { SELECTION_MODES, parseSelectionMode, selectionArea } from './selection/mask';\nimport { SELECTION_MODIFY_OPS, type SelectionModifyOp } from './selection/modify';\nimport { layerMaskFromSelection } from './selection/layer-mask';\n\nconst modeSchema: JsonSchema = {\n type: 'string',\n enum: [...SELECTION_MODES],\n description: 'How the result combines with the current selection (default replace).',\n};\n\nconst num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\nfunction summary(controller: ImageEditorController) {\n const sel = controller.getSelection();\n const { width, height } = controller.getState().imageDimensions;\n const area = selectionArea(sel);\n return {\n empty: !sel,\n bounds: sel?.bounds ?? null,\n /** Σ coverage in pixels. */\n area: Math.round(area),\n percent: Math.round((area / Math.max(1, width * height)) * 10000) / 100,\n description: describeSelection(sel),\n };\n}\n\nconst writeAnnotations = (title: string, idempotent = false) => ({\n title,\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: idempotent,\n openWorldHint: false,\n});\n\n/**\n * Agent tools for selections and masks (part of `createImageEditorToolProvider`):\n * `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection`\n * and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.\n */\nexport function createImageSelectionTools(controller: ImageEditorController): AgentTool[] {\n const getSelection: AgentTool = {\n name: 'get_selection',\n title: 'Read the selection',\n description:\n 'Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read the selection', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: () => summary(controller),\n };\n\n const selectRegion: AgentTool = {\n name: 'select_region',\n title: 'Select a region',\n description:\n 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything (\"all\"), nothing (\"none\") or the inverse of the current selection (\"invert\"). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n shape: { type: 'string', enum: ['rect', 'ellipse', 'all', 'none', 'invert'], description: 'What to select.' },\n x: { type: 'number', description: 'rect / ellipse: left edge in natural image pixels.' },\n y: { type: 'number', description: 'rect / ellipse: top edge in natural image pixels.' },\n width: { type: 'number', description: 'rect / ellipse: width in natural image pixels.' },\n height: { type: 'number', description: 'rect / ellipse: height in natural image pixels.' },\n mode: modeSchema,\n feather: { type: 'number', minimum: 0, description: 'Feather radius in pixels (default 0).' },\n },\n required: ['shape'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select a region', true),\n handler: ({ shape, x, y, width, height, mode, feather }) => {\n const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };\n switch (shape) {\n case 'all':\n controller.selectAll();\n break;\n case 'none':\n controller.deselect();\n break;\n case 'invert':\n controller.invertSelection();\n break;\n case 'rect':\n case 'ellipse': {\n const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {\n throw new Error('rect and ellipse need numeric x, y, width and height.');\n }\n if (shape === 'rect') controller.selectRect(rect, opts);\n else controller.selectEllipse(rect, opts);\n break;\n }\n default:\n throw new Error(`Unknown shape \"${String(shape)}\". Use rect, ellipse, all, none or invert.`);\n }\n return summary(controller);\n },\n };\n\n const selectByColor: AgentTool = {\n name: 'select_by_color',\n title: 'Select by colour',\n description:\n 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with \"color\" (\"#rrggbb\") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Seed point x in natural image pixels.' },\n y: { type: 'number', description: 'Seed point y in natural image pixels.' },\n color: { type: 'string', description: '#rrggbb: select this colour everywhere instead of a seed point.' },\n tolerance: { type: 'number', minimum: 0, maximum: 255, description: 'Magic wand tolerance (default 32).' },\n contiguous: { type: 'boolean', description: 'Only the connected region around (x, y) (default true).' },\n fuzziness: { type: 'number', minimum: 0, maximum: 200, description: 'Color Range fuzziness (default 40).' },\n sampleMerged: { type: 'boolean', description: 'Sample the adjusted image rather than the source.' },\n sample: {\n type: 'string',\n enum: ['composite', 'active-layer'],\n description: 'composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer).',\n },\n mode: modeSchema,\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select by colour', true),\n handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {\n const common = {\n mode: parseSelectionMode(mode),\n sampleMerged: sampleMerged === true,\n sample: (sample as 'composite' | 'active-layer' | undefined) ?? 'composite',\n };\n if (typeof color === 'string') {\n await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });\n } else if (typeof x === 'number' && typeof y === 'number') {\n await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });\n } else {\n throw new Error('Pass x and y (magic wand) or color (#rrggbb).');\n }\n return summary(controller);\n },\n };\n\n const modify: AgentTool = {\n name: 'modify_selection',\n title: 'Modify the selection',\n description: 'Select › Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n op: { type: 'string', enum: [...SELECTION_MODIFY_OPS], description: 'How to modify the selection.' },\n radius: { type: 'number', minimum: 0, maximum: 500, description: 'Radius in pixels.' },\n },\n required: ['op', 'radius'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Modify the selection'),\n handler: ({ op, radius }) => {\n if (!(SELECTION_MODIFY_OPS as readonly string[]).includes(op as string)) throw new Error(`Unknown op \"${String(op)}\".`);\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n controller.modifySelection(op as SelectionModifyOp, Math.min(500, Math.max(0, num(radius, 0))));\n return summary(controller);\n },\n };\n\n const refine: AgentTool = {\n name: 'refine_selection',\n title: 'Refine the selection edge',\n description:\n 'Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n radius: { type: 'number', minimum: 0, maximum: 64, description: 'Edge detection radius in pixels.' },\n smartRadius: { type: 'boolean', description: 'Adapt the radius to hard and soft edges.' },\n smooth: { type: 'number', minimum: 0, maximum: 100, description: 'Smooth the edge outline (0-100).' },\n feather: { type: 'number', minimum: 0, maximum: 250, description: 'Feather the edge (pixels).' },\n contrast: { type: 'number', minimum: 0, maximum: 100, description: 'Sharpen the edge transition (0-100).' },\n shiftEdge: { type: 'number', minimum: -100, maximum: 100, description: 'Move the edge out (+) or in (-), in percent.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Refine the selection edge'),\n handler: async (params) => {\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n await controller.refineSelection({\n radius: num(params.radius, 8),\n smartRadius: params.smartRadius === true,\n smooth: num(params.smooth, 0),\n feather: num(params.feather, 0),\n contrast: num(params.contrast, 0),\n shiftEdge: num(params.shiftEdge, 0),\n });\n return summary(controller);\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_selection_as_mask',\n title: 'Mask a layer with the selection',\n description:\n 'Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Existing adjustment layer to mask.' },\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Kind of a new layer to add and mask (instead of layerId).' },\n params: { type: 'object', description: 'Parameters for the new layer (see list_controls).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0-1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Mask feather in pixels.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Mask a layer with the selection'),\n handler: ({ layerId, kind, params, density, feather }) => {\n const sel = controller.getSelection();\n if (!sel) throw new Error('Nothing is selected.');\n const props = {\n ...(typeof density === 'number' ? { density } : {}),\n ...(typeof feather === 'number' ? { feather } : {}),\n };\n let id: string;\n if (typeof layerId === 'string') {\n if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer \"${layerId}\".`);\n controller.applySelectionAsLayerMask(layerId, props);\n id = layerId;\n } else if (isAdjustmentKind(kind)) {\n id = controller.addAdjustmentLayer({\n kind: kind as AdjustmentKind,\n params: params as never,\n mask: layerMaskFromSelection(sel, props),\n });\n } else {\n throw new Error('Pass layerId (an existing layer) or kind (a new adjustment layer).');\n }\n const layer = controller.getAdjustmentLayer(id)!;\n const { rle: _rle, ...mask } = layer.mask!;\n return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };\n },\n };\n\n return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];\n}\n","import type { CaptureRenderRequest, CaptureSurface } from '../tools/capture.js';\nimport type { ImageEditorController } from './controller';\nimport type { SelectionMask } from './selection/mask';\n\nconst NO_CANVAS = 'Rendering is not available in this environment.';\n\n/** Darkens what is outside the selection (natural pixels), so its coverage reads at a glance. */\nfunction selectionShade(selection: SelectionMask, doc: Document): HTMLCanvasElement | null {\n const canvas = doc.createElement('canvas');\n canvas.width = selection.width;\n canvas.height = selection.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n const pixels = context.createImageData(selection.width, selection.height);\n for (let i = 0; i < selection.data.length; i++) {\n pixels.data[i * 4 + 3] = Math.round(150 * (1 - selection.data[i] / 255));\n }\n context.putImageData(pixels, 0, 0);\n return canvas;\n}\n\n/** Draws a region of the image editor's source view for `capture_view`. */\nasync function renderImageRegion(controller: ImageEditorController, request: CaptureRenderRequest): Promise<string> {\n const state = controller.getState();\n // Never render above natural size; small regions are enlarged when drawn.\n const k = Math.min(1, request.scale);\n const view = await controller.renderSourceView({ scale: k });\n if (!view) throw new Error('No image is loaded, so there is nothing to capture.');\n if (typeof document === 'undefined') throw new Error(NO_CANVAS);\n const out = document.createElement('canvas');\n out.width = request.width;\n out.height = request.height;\n const context = out.getContext('2d');\n if (!context) throw new Error(NO_CANVAS);\n const { rect } = request;\n // Upscaled regions keep hard pixel edges, so the agent sees real pixels.\n context.imageSmoothingEnabled = request.scale < 1;\n if (request.format === 'jpeg') {\n context.fillStyle = '#ffffff';\n context.fillRect(0, 0, out.width, out.height);\n }\n const kx = view.width / state.imageDimensions.width;\n const ky = view.height / state.imageDimensions.height;\n context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);\n if (request.includeOverlays) {\n const s = request.scale;\n const selection = state.selection;\n if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {\n const shade = selectionShade(selection, document);\n if (shade) context.drawImage(shade, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);\n }\n if (state.crop) {\n context.save();\n context.strokeStyle = '#ffd400';\n context.lineWidth = 2;\n context.setLineDash([6, 4]);\n context.strokeRect((state.crop.x - rect.x) * s, (state.crop.y - rect.y) * s, state.crop.width * s, state.crop.height * s);\n context.restore();\n }\n }\n const url = out.toDataURL(request.format === 'jpeg' ? 'image/jpeg' : 'image/png', request.quality);\n if (!url.startsWith('data:image/') || url.indexOf(',') === url.length - 1) throw new Error(NO_CANVAS);\n return url;\n}\n\n/**\n * The image editor as a capture surface. Units are natural pixels of the source image,\n * the same space as `get_selection` and `select_region`: crop, rotation and flips are not\n * applied (use `render_preview` for the final framing), adjustments, layers and annotations\n * are. Regions: the whole image, the selection's bounds, or a rect. Small regions are\n * enlarged up to 8× with hard pixel edges for close inspection.\n */\nexport function createImageCaptureSurface(controller: ImageEditorController): CaptureSurface {\n const whole = () => {\n const { width, height } = controller.getState().imageDimensions;\n return width > 0 && height > 0 ? { x: 0, y: 0, width, height } : null;\n };\n return {\n units: 'image pixels',\n content: whole,\n selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),\n overlays: 'a shade over unselected pixels and the crop box outline',\n maxScale: 8,\n render: (request) => renderImageRegion(controller, request),\n };\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { BlendMode } from './blend';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport { getRotatedSize, normalizeAngle } from './geometry';\nimport type { ImageHistogram } from './histogram';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Adjustment layer id from get_adjustments (e.g. \"adj_1\").' } as const;\n\nconst round = (n: number, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** Mean and the 1st / 99th percentiles of a 256-bin histogram. */\nfunction binStats(bins: ArrayLike<number>, samples: number) {\n if (!samples) return { mean: null, p1: null, p99: null };\n let sum = 0;\n let seen = 0;\n let p1: number | null = null;\n let p99: number | null = null;\n for (let i = 0; i < bins.length; i++) {\n sum += i * bins[i];\n seen += bins[i];\n if (p1 === null && seen >= samples * 0.01) p1 = i;\n if (p99 === null && seen >= samples * 0.99) p99 = i;\n }\n return { mean: round(sum / samples, 1), p1, p99 };\n}\n\n/** Sums 256 bins into `count` equal buckets. */\nfunction rebin(bins: ArrayLike<number>, count: number): number[] {\n const out = new Array<number>(count).fill(0);\n const width = bins.length / count;\n for (let i = 0; i < bins.length; i++) out[Math.floor(i / width)] += bins[i];\n return out;\n}\n\nfunction histogramSummary(h: ImageHistogram, bins: number | undefined) {\n const pct = (n: number) => (h.samples ? round((n / h.samples) * 100) : 0);\n const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };\n return {\n samples: h.samples,\n transparent: h.transparent,\n clipping: {\n shadowsPercent: { red: pct(h.shadows[0]), green: pct(h.shadows[1]), blue: pct(h.shadows[2]) },\n highlightsPercent: { red: pct(h.highlights[0]), green: pct(h.highlights[1]), blue: pct(h.highlights[2]) }\n },\n stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),\n ...(bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {})\n };\n}\n\n/**\n * Agent tools for the image editor's layers, geometry and history: `get_image_info`,\n * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,\n * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,\n * `undo_image` and `redo_image`. Each write is one undo step and returns what changed.\n */\nexport function createImageEditTools(controller: ImageEditorController): AgentTool[] {\n const layerOrThrow = (id: string) => {\n const layers = controller.getAdjustmentLayers();\n const index = layers.findIndex((l) => l.id === id);\n if (index === -1) {\n const ids = layers.map((l) => l.id);\n throw new Error(`Unknown adjustment layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'There are no adjustment layers.'}`);\n }\n return { layer: layers[index], index };\n };\n const layerJson = (id: string) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));\n const geometry = () => {\n const s = controller.getState();\n const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };\n const out = getRotatedSize(region.width, region.height, s.transform);\n return {\n image: { ...s.imageDimensions },\n crop: s.crop ? { ...s.crop } : null,\n rotation: normalizeAngle(s.transform.rotate),\n flipH: s.transform.flipH,\n flipV: s.transform.flipV,\n output: { width: Math.round(out.width), height: Math.round(out.height) }\n };\n };\n const history = () => ({ canUndo: controller.canUndo, canRedo: controller.canRedo });\n const requireImage = () => {\n const { width, height } = controller.getState().imageDimensions;\n if (!width || !height) throw new Error('No image is loaded. Ask the user to open one first.');\n return { width, height };\n };\n\n const getImageInfo: AgentTool = {\n name: 'get_image_info',\n title: 'Read image geometry',\n description:\n 'Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read image geometry', ...READ },\n handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const getHistogram: AgentTool = {\n name: 'get_histogram',\n title: 'Read the histogram',\n description:\n 'Returns the histogram of the last rendered preview (after adjustments and layers): per channel and luminance mean and 1st / 99th percentiles (0-255), and the percentage of pixels clipped to pure black or white per channel. ' +\n 'Use it to judge exposure and clipping numerically. Pass bins for the counts themselves. Fails before the first render: call render_preview first.',\n inputSchema: {\n type: 'object',\n properties: {\n bins: { type: 'integer', enum: [16, 32, 64, 256], description: 'Also return the counts in this many buckets per channel (default: no counts).' }\n },\n additionalProperties: false\n },\n annotations: { title: 'Read the histogram', ...READ },\n handler: ({ bins }) => {\n const h = controller.getHistogram();\n if (!h) throw new Error('No histogram yet: it comes from the rendered preview. Call render_preview, then try again.');\n return histogramSummary(h, bins);\n }\n };\n\n const updateLayer: AgentTool = {\n name: 'update_adjustment_layer',\n title: 'Change an adjustment layer',\n description:\n 'Shows or hides a layer, renames it, or changes its blend mode or opacity, as one undo step. Change its parameters with set_adjustments (\"layers.<id>.<control>\"). Returns the updated layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n enabled: { type: 'boolean', description: 'false hides the layer without deleting it.' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (see list_controls).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1.' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' }\n },\n required: ['layer'],\n additionalProperties: false\n },\n annotations: { title: 'Change an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, enabled, blend, opacity, name }) => {\n layerOrThrow(layer);\n if (enabled === undefined && blend === undefined && opacity === undefined && name === undefined) {\n throw new Error('Nothing to change. Pass enabled, blend, opacity or name.');\n }\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n controller.updateAdjustmentLayer(layer, { enabled, blend: blend as BlendMode | undefined, opacity, name });\n return { layer: layerJson(layer) };\n }\n };\n\n const removeLayer: AgentTool = {\n name: 'remove_adjustment_layer',\n title: 'Delete an adjustment layer',\n description: 'Deletes an adjustment layer and its mask as one undo step. To hide it instead, use update_adjustment_layer with enabled false. Returns the remaining layer ids, bottom to top.',\n inputSchema: { type: 'object', properties: { layer: LAYER_ID }, required: ['layer'], additionalProperties: false },\n annotations: { title: 'Delete an adjustment layer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: ({ layer }) => {\n layerOrThrow(layer);\n controller.removeAdjustmentLayer(layer);\n return { removed: layer, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const moveLayer: AgentTool = {\n name: 'move_adjustment_layer',\n title: 'Reorder an adjustment layer',\n description:\n 'Moves an adjustment layer to a position in the stack (0 = bottom, applied first) as one undo step. Layers apply bottom to top, so order changes the result. Returns the layer ids, bottom to top.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n to: { type: 'integer', minimum: 0, description: 'New index, 0 = bottom; past the top means the top.' }\n },\n required: ['layer', 'to'],\n additionalProperties: false\n },\n annotations: { title: 'Reorder an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, to }) => {\n const { index } = layerOrThrow(layer);\n const top = controller.getAdjustmentLayers().length - 1;\n const target = Math.min(to, top);\n if (target === index) throw new Error(`Layer \"${layer}\" is already at index ${index} (0 = bottom, ${top} = top).`);\n controller.moveAdjustmentLayer(layer, target);\n return { layer, index: target, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const setCrop: AgentTool = {\n name: 'set_crop',\n title: 'Crop the image',\n description:\n 'Sets the crop box in natural image pixels of the un-rotated source (origin top-left; see get_image_info for the size) as one undo step. Non-destructive: reset_crop restores the whole image. Returns the new geometry and output size.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, description: 'Left edge in source pixels.' },\n y: { type: 'number', minimum: 0, description: 'Top edge in source pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in source pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in source pixels.' }\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false\n },\n annotations: { title: 'Crop the image', ...WRITE_IDEMPOTENT },\n handler: ({ x, y, width, height }) => {\n const image = requireImage();\n if (x + width > image.width + 0.5 || y + height > image.height + 0.5) {\n throw new Error(`The crop box (${x}, ${y}, ${width} × ${height}) goes outside the ${image.width} × ${image.height} image.`);\n }\n controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });\n return geometry();\n }\n };\n\n const resetCrop: AgentTool = {\n name: 'reset_crop',\n title: 'Remove the crop',\n description: 'Removes the crop so the whole image shows again, as one undo step. Returns the new geometry.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Remove the crop', ...WRITE_IDEMPOTENT },\n handler: () => {\n if (!controller.getState().crop) throw new Error('The image is not cropped.');\n controller.resetCrop();\n return geometry();\n }\n };\n\n const setRotation: AgentTool = {\n name: 'set_rotation',\n title: 'Rotate the image',\n description:\n 'Sets the image rotation in degrees clockwise, as one undo step. Quarter turns (90, 180, 270) keep every pixel; other angles straighten the image inside a larger frame with transparent corners, so crop afterwards. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { degrees: { type: 'number', minimum: -360, maximum: 360, description: 'Absolute rotation in degrees clockwise (0 = upright).' } },\n required: ['degrees'],\n additionalProperties: false\n },\n annotations: { title: 'Rotate the image', ...WRITE_IDEMPOTENT },\n handler: ({ degrees }) => {\n requireImage();\n controller.setRotation(degrees);\n return geometry();\n }\n };\n\n const flipImage: AgentTool = {\n name: 'flip_image',\n title: 'Flip the image',\n description: 'Mirrors the image horizontally (left-right) or vertically (top-bottom), as one undo step. Flipping twice restores it. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left-right; vertical mirrors top-bottom.' } },\n required: ['axis'],\n additionalProperties: false\n },\n annotations: { title: 'Flip the image', ...WRITE },\n handler: ({ axis }) => {\n requireImage();\n if (axis === 'horizontal') controller.flipHorizontal();\n else controller.flipVertical();\n return geometry();\n }\n };\n\n const undo: AgentTool = {\n name: 'undo_image',\n title: 'Undo an image edit',\n description: 'Undoes the last image edit (one tool call is one step). Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Undo an image edit', ...WRITE },\n handler: () => {\n if (!controller.undo()) throw new Error('Nothing to undo.');\n return history();\n }\n };\n\n const redo: AgentTool = {\n name: 'redo_image',\n title: 'Redo an image edit',\n description: 'Redoes the last undone image edit. Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Redo an image edit', ...WRITE },\n handler: () => {\n if (!controller.redo()) throw new Error('Nothing to redo.');\n return history();\n }\n };\n\n return [getImageInfo, getHistogram, updateLayer, removeLayer, moveLayer, setCrop, resetCrop, setRotation, flipImage, undo, redo];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { contentBounds } from '../raster/surface';\nimport { findLayer, type ImageLayer, type LayerFilter, type RasterMask } from './layers/model';\nimport { LayerOperationError, requireLayer, type LayerPlacement } from './layers/ops';\nimport { adjustmentKindLabel } from './adjustment-layers';\nimport { BLEND_MODES } from './blend';\nimport type { BlendMode } from './blend';\nimport type { CropBox } from './types';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** A short description of a mask for agents (no pixels). */\nexport function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null {\n if (!mask) return null;\n const out: Record<string, unknown> = {\n kind: mask.source ? 'depth' : 'raster',\n enabled: mask.enabled !== false,\n density: mask.density ?? 1,\n feather: mask.feather ?? 0,\n };\n if (!mask.source) {\n out.defaultValue = mask.defaultValue;\n out.paintedBounds = contentBounds(mask.surface);\n }\n return out;\n}\n\n/**\n * A plain description of a layer (and its children) for agents: ids, types, names, document\n * bounds, visibility, opacity, fill, blend, clipping, locks, mask summary and whether it is\n * active. `boundsOf` gives a pixel layer's document bounds.\n */\nexport function describeLayer(\n layer: ImageLayer,\n activeId: string | null,\n boundsOf: (layer: ImageLayer) => CropBox | null\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n id: layer.id,\n type: layer.type,\n name: layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : ''),\n visible: layer.visible,\n opacity: round(layer.opacity),\n blend: layer.blend,\n clipped: Boolean(layer.clipped),\n locks: layer.locks ? { ...layer.locks } : {},\n mask: maskSummary(layer.mask),\n active: layer.id === activeId,\n };\n switch (layer.type) {\n case 'pixel':\n out.bounds = boundsOf(layer);\n out.offset = { ...layer.offset };\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.filters = (layer.filters ?? []).map((f) => ({ ...f }));\n if (layer.source === 'document') out.source = 'document';\n break;\n case 'adjustment':\n out.kind = layer.kind;\n out.params = JSON.parse(JSON.stringify(layer.params));\n break;\n case 'annotation':\n out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'group':\n out.collapsed = Boolean(layer.collapsed);\n out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n 'Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description: \"Changes a layer mask's density (0..1), feather (px) or enabled flag; its pixels are kept. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled }) => {\n const id = exists(layerId);\n if ([density, feather, enabled].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather or enabled.');\n const mask = controller.setLayerMaskProps(id, { density: density as number | undefined, feather: feather as number | undefined, enabled: enabled as boolean | undefined });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\n\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: { type: 'string', enum: ['pixels', 'mask'], description: \"pixels (default for a pixel layer) or the layer's mask: black hides, white reveals.\" },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal).' },\n },\n additionalProperties: false,\n} as const;\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush or eraser along points, on pixels or a mask),\n * `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection). Each is one undo\n * step and returns the layer, channel and changed bounds.\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush (or removes pixels with the eraser) through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask. The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count.',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser'], description: 'brush (default) or eraser.' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals.' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) =>\n controller.paintStroke({\n tool: args.tool as 'brush' | 'eraser' | undefined,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n ...readTarget(args),\n }),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n 'Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility and locks map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Open a PSD as the document', ...DESTRUCTIVE },\n handler: ({ data }) => {\n const res = controller.loadPsd(psdBytesFromBase64(data));\n const s = controller.getState();\n return {\n width: res.width,\n height: res.height,\n layerIds: res.layerIds,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n activeLayerId: s.activeLayerId,\n warnings: res.warnings,\n };\n },\n };\n\n const save: AgentTool = {\n name: 'export_psd',\n title: 'Export the document as a PSD',\n description:\n 'Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createImagePsdTools } from './psd-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImagePaintTools(controller),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-clip'\n | 'rasterize';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, E, V, [ ], Shift+[ ], number keys, D, X) and a pointer\n * gesture that turns pointer events into strokes (Brush, Eraser) or layer moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'eraser';\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool('brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","/** Framework-free helpers behind the PSD buttons of the React and Svelte layer panels. */\n\nimport type { ImageEditorController } from '../controller';\n\n/** Types the file pickers accept. */\nexport const PSD_FILE_ACCEPT = '.psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop';\n\nfunction blobBytes(file: Blob): Promise<Uint8Array> {\n if (typeof file.arrayBuffer === 'function') return file.arrayBuffer().then((b) => new Uint8Array(b));\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));\n reader.onerror = () => reject(reader.error ?? new Error('The file could not be read.'));\n reader.readAsArrayBuffer(file);\n });\n}\n\n/** Reads a picked file and opens it as the document (one undo step). Returns the import warnings. */\nexport async function openPsdFile(controller: ImageEditorController, file: Blob): Promise<string[]> {\n return controller.loadPsd(await blobBytes(file)).warnings;\n}\n\n/** A file name for the exported document: `name` with a `.psd` / `.psb` extension. */\nexport function psdFileName(name: string, psb: boolean): string {\n const base = name.replace(/\\.[a-z0-9]{2,4}$/i, '') || 'image';\n return `${base}.${psb ? 'psb' : 'psd'}`;\n}\n\n/**\n * Writes the document as a PSD and offers it as a download (in a browser). Returns the file\n * name and the export warnings.\n */\nexport async function downloadPsd(controller: ImageEditorController, name = 'image'): Promise<{ fileName: string; warnings: string[] }> {\n const res = await controller.toPsd();\n const fileName = psdFileName(name, res.psb);\n if (typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([res.bytes as BlobPart], { type: 'image/vnd.adobe.photoshop' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n return { fileName, warnings: res.warnings };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","../../src/image-editor/color.ts","../../src/image-editor/geometry.ts","../../src/image-editor/selection/mask.ts","../../src/image-editor/selection/modify.ts","../../src/image-editor/selection/layer-mask.ts","../../src/image-editor/adjustment-layers.ts","../../src/image-editor/filters.ts","../../src/image-editor/histogram.ts","../../src/image-editor/selection/overlay.ts","../../src/image-editor/layers/model.ts","../../src/image-editor/layers/compositor.ts","../../src/image-editor/layers/defaults.ts","../../src/image-editor/layers/migrate.ts","../../src/image-editor/layers/render.ts","../../src/image-editor/renderer.ts","../../src/image-editor/selection/shapes.ts","../../src/image-editor/selection/wand.ts","../../src/image-editor/selection/refine.ts","../../src/image-editor/layers/serialize.ts","../../src/image-editor/layers/ops.ts","../../src/image-editor/psd/binary.ts","../../src/image-editor/psd/descriptor.ts","../../src/image-editor/psd/adjust.ts","../../src/image-editor/psd/compression.ts","../../src/image-editor/psd/file.ts","../../src/image-editor/psd/pixels.ts","../../src/image-editor/psd/import.ts","../../src/image-editor/psd/export.ts","../../src/image-editor/controller.ts","../../src/image-editor/document.ts","../../src/image-editor/controls.ts","../../src/image-editor/selection/tool-options.ts","../../src/image-editor/selection-tools.ts","../../src/image-editor/capture.ts","../../src/image-editor/edit-tools.ts","../../src/image-editor/layer-tools.ts","../../src/image-editor/paint-tools.ts","../../src/image-editor/psd-tools.ts","../../src/image-editor/tools.ts","../../src/image-editor/curve-editing.ts","../../src/image-editor/selection/gesture.ts","../../src/image-editor/mosaic-filter.ts","../../src/image-editor/layers/panel.ts","../../src/image-editor/layers/paint-ui.ts","../../src/image-editor/psd/ui.ts"],"names":["lu","lv","clamp","x","y","q","v","op","lightness","props","mask","layer","decoded","out","num","moveLayer","WRITE_IDEMPOTENT","LAYER_ID","WRITE","DESTRUCTIVE","READ"],"mappings":"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CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/image-editor/index.cjs","sourcesContent":[null,"// Portions ported from lightcraft-main/crates/color/src/cct.rs, lightcraft-main/crates/color/src/lib.rs,\n// lightcraft-main/crates/pipeline/src/tone.rs, lightcraft-main/crates/pipeline/src/local.rs and\n// lightcraft-main/crates/develop/src/settings.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors).\n// See THIRD_PARTY_NOTICES.md.\n\n/**\n * Colour science for the image editor's process 2 rendering: sRGB transfer functions,\n * correlated colour temperature / tint (Kim et al. Planckian locus, tint as Duv), Bradford\n * white-balance adaptation relative to \"as shot\", and the display-referred tone curve.\n */\n\n/** A CIE 1931 chromaticity. */\nexport interface Chromaticity {\n x: number;\n y: number;\n}\n\n/** A row-major 3×3 matrix (`[m00, m01, m02, m10, …, m22]`). */\nexport type Matrix3 = [number, number, number, number, number, number, number, number, number];\n\n// ---------------------------------------------------------------------------\n// Process versions\n// ---------------------------------------------------------------------------\n\n/** The rendering of adjustments from before process versions existed. Frozen. */\nexport const LEGACY_PROCESS_VERSION = 1 as const;\n/** The process new edits and Reset get. */\nexport const LATEST_PROCESS_VERSION = 2 as const;\nexport type ImageProcessVersion = 1 | 2;\n\n/**\n * Reads a stored process number leniently: a missing or damaged value (not a whole number) is\n * the legacy process, so a saved edit from before process versions renders exactly as it did. A\n * whole number newer than this build knows renders with the latest process.\n */\nexport function normalizeProcessVersion(value: unknown): ImageProcessVersion {\n if (typeof value !== 'number' || !Number.isInteger(value)) return LEGACY_PROCESS_VERSION;\n return value >= LATEST_PROCESS_VERSION ? LATEST_PROCESS_VERSION : LEGACY_PROCESS_VERSION;\n}\n\n// ---------------------------------------------------------------------------\n// sRGB transfer\n// ---------------------------------------------------------------------------\n\nexport function srgbToLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\n\nexport function linearToSrgb(v: number): number {\n return v <= 0.0031308 ? v * 12.92 : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\n/** Rec.709 / sRGB luminance weights (linear light). */\nexport const LUMA_709: readonly [number, number, number] = [0.2126, 0.7152, 0.0722];\n\n// ---------------------------------------------------------------------------\n// 3×3 matrices\n// ---------------------------------------------------------------------------\n\nexport const IDENTITY_MATRIX3: Matrix3 = [1, 0, 0, 0, 1, 0, 0, 0, 1];\n\nexport function mulMatrix3(a: Matrix3, b: Matrix3): Matrix3 {\n const out = new Array(9).fill(0) as Matrix3;\n for (let r = 0; r < 3; r++) {\n for (let c = 0; c < 3; c++) {\n out[r * 3 + c] = a[r * 3] * b[c] + a[r * 3 + 1] * b[3 + c] + a[r * 3 + 2] * b[6 + c];\n }\n }\n return out;\n}\n\nexport function applyMatrix3(m: Matrix3, v: readonly [number, number, number]): [number, number, number] {\n return [\n m[0] * v[0] + m[1] * v[1] + m[2] * v[2],\n m[3] * v[0] + m[4] * v[1] + m[5] * v[2],\n m[6] * v[0] + m[7] * v[1] + m[8] * v[2],\n ];\n}\n\nexport function invertMatrix3(m: Matrix3): Matrix3 | null {\n const [a, b, c, d, e, f, g, h, i] = m;\n const A = e * i - f * h;\n const B = -(d * i - f * g);\n const C = d * h - e * g;\n const det = a * A + b * B + c * C;\n if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;\n const k = 1 / det;\n return [\n A * k, -(b * i - c * h) * k, (b * f - c * e) * k,\n B * k, (a * i - c * g) * k, -(a * f - c * d) * k,\n C * k, -(a * h - b * g) * k, (a * e - b * d) * k,\n ];\n}\n\nfunction diag3(x: number, y: number, z: number): Matrix3 {\n return [x, 0, 0, 0, y, 0, 0, 0, z];\n}\n\n// ---------------------------------------------------------------------------\n// Chromaticity, sRGB primaries, Bradford adaptation\n// ---------------------------------------------------------------------------\n\nexport const D65: Chromaticity = { x: 0.3127, y: 0.329 };\n\nfunction xyToXyz(p: Chromaticity): [number, number, number] {\n return [p.x / p.y, 1, (1 - p.x - p.y) / p.y];\n}\n\nfunction xyzToXy(v: readonly [number, number, number]): Chromaticity {\n const s = v[0] + v[1] + v[2];\n return s <= 0 ? { ...D65 } : { x: v[0] / s, y: v[1] / s };\n}\n\n/** Linear sRGB → XYZ (D65 white, Y = 1), derived from the primaries. */\nconst SRGB_TO_XYZ: Matrix3 = (() => {\n const p = [{ x: 0.64, y: 0.33 }, { x: 0.3, y: 0.6 }, { x: 0.15, y: 0.06 }].map(xyToXyz);\n const m: Matrix3 = [p[0][0], p[1][0], p[2][0], p[0][1], p[1][1], p[2][1], p[0][2], p[1][2], p[2][2]];\n const inv = invertMatrix3(m);\n const s = inv ? applyMatrix3(inv, xyToXyz(D65)) : [1, 1, 1];\n return mulMatrix3(m, diag3(s[0], s[1], s[2]));\n})();\nconst XYZ_TO_SRGB: Matrix3 = invertMatrix3(SRGB_TO_XYZ) ?? IDENTITY_MATRIX3;\n\n/** The Bradford cone-response matrix (XYZ → sharpened cones). */\nconst BRADFORD: Matrix3 = [0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296];\nconst BRADFORD_INV: Matrix3 = invertMatrix3(BRADFORD) ?? IDENTITY_MATRIX3;\n\n/** Bradford chromatic adaptation (XYZ → XYZ) from white `src` to white `dst`. */\nexport function bradfordAdaptation(src: Chromaticity, dst: Chromaticity): Matrix3 {\n if (src.x === dst.x && src.y === dst.y) return [...IDENTITY_MATRIX3];\n const s = applyMatrix3(BRADFORD, xyToXyz(src));\n const d = applyMatrix3(BRADFORD, xyToXyz(dst));\n return mulMatrix3(BRADFORD_INV, mulMatrix3(diag3(d[0] / s[0], d[1] / s[1], d[2] / s[2]), BRADFORD));\n}\n\n/**\n * Matrix (linear sRGB → linear sRGB) that white-balances a scene lit by `src` so that it\n * renders as if lit by D65.\n */\nexport function wbMatrix(src: Chromaticity): Matrix3 {\n return mulMatrix3(XYZ_TO_SRGB, mulMatrix3(bradfordAdaptation(src, D65), SRGB_TO_XYZ));\n}\n\n/** The chromaticity of a linear sRGB colour. */\nexport function linearSrgbToXy(rgb: readonly [number, number, number]): Chromaticity {\n return xyzToXy(applyMatrix3(SRGB_TO_XYZ, rgb));\n}\n\n// ---------------------------------------------------------------------------\n// Correlated colour temperature and tint\n// ---------------------------------------------------------------------------\n\n/** Tint units per unit of Duv. */\nexport const TINT_SCALE = 3000;\nexport const MIN_KELVIN = 1667;\nexport const MAX_KELVIN = 25000;\n\nfunction kimXy(tIn: number): Chromaticity {\n const t = Math.min(MAX_KELVIN, Math.max(MIN_KELVIN, tIn));\n const t2 = t * t;\n const t3 = t2 * t;\n const x =\n t <= 4000\n ? -0.2661239e9 / t3 - 0.2343589e6 / t2 + 0.8776956e3 / t + 0.17991\n : -3.0258469e9 / t3 + 2.1070379e6 / t2 + 0.2226347e3 / t + 0.24039;\n const x2 = x * x;\n const x3 = x2 * x;\n const y =\n t <= 2222\n ? -1.1063814 * x3 - 1.3481102 * x2 + 2.18555832 * x - 0.20219683\n : t <= 4000\n ? -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867\n : 3.081758 * x3 - 5.8733867 * x2 + 3.75112997 * x - 0.37001483;\n return { x, y };\n}\n\n/** CIE 1931 xy → CIE 1960 uv. */\nexport function xyToUv(p: Chromaticity): [number, number] {\n const d = -2 * p.x + 12 * p.y + 3;\n return [(4 * p.x) / d, (6 * p.y) / d];\n}\n\n/** CIE 1960 uv → CIE 1931 xy. */\nexport function uvToXy(u: number, v: number): Chromaticity {\n const d = 2 * u - 8 * v + 4;\n return { x: (3 * u) / d, y: (2 * v) / d };\n}\n\n/** Planckian locus in (u, v), extended linearly in mired space outside the approximation's range. */\nfunction locusUv(tIn: number): [number, number] {\n const t = Math.max(1000, tIn);\n if (t >= MIN_KELVIN && t <= MAX_KELVIN) return xyToUv(kimXy(t));\n const [a, b] = t < MIN_KELVIN ? [MIN_KELVIN, MIN_KELVIN + 50] : [MAX_KELVIN - 500, MAX_KELVIN];\n const [ua, va] = xyToUv(kimXy(a));\n const [ub, vb] = xyToUv(kimXy(b));\n const ma = 1e6 / a;\n const mb = 1e6 / b;\n const s = (1e6 / t - ma) / (mb - ma);\n return [ua + (ub - ua) * s, va + (vb - va) * s];\n}\n\n/** Unit normal of the locus at `t`, pointing towards green (increasing v). */\nfunction locusNormal(t: number): [number, number] {\n const m = 1e6 / t;\n const [u0, v0] = locusUv(1e6 / (m + 1));\n const [u1, v1] = locusUv(1e6 / Math.max(m - 1, 1));\n const du = u1 - u0;\n const dv = v1 - v0;\n const l = Math.max(Math.hypot(du, dv), 1e-12);\n const nx = -dv / l;\n const ny = du / l;\n return ny < 0 ? [-nx, -ny] : [nx, ny];\n}\n\n/**\n * Source illuminant chromaticity for temperature `kelvin` and correction `tint`.\n * Positive tint means a green source white, whose correction adds magenta to the image.\n */\nexport function tempTintToXy(kelvin: number, tint: number): Chromaticity {\n const [u, v] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = tint / TINT_SCALE;\n return uvToXy(u + nu * duv, v + nv * duv);\n}\n\n/** Inverse of {@link tempTintToXy}: nearest locus temperature (searched in mired space) and tint. */\nexport function xyToTempTint(p: Chromaticity): { kelvin: number; tint: number } {\n const [u, v] = xyToUv(p);\n const dist = (m: number) => {\n const [lu, lv] = locusUv(1e6 / m);\n return (u - lu) ** 2 + (v - lv) ** 2;\n };\n // Coarse scan, then golden-section refine over mired 20..1000 (50000 K .. 1000 K).\n let best = 20;\n let bd = Infinity;\n for (let m = 20; m <= 1000; m += 5) {\n const d = dist(m);\n if (d < bd) {\n bd = d;\n best = m;\n }\n }\n let a = Math.max(best - 5, 20);\n let b = Math.min(best + 5, 1000);\n const g = 0.61803398875;\n for (let i = 0; i < 60; i++) {\n const c = b - g * (b - a);\n const d = a + g * (b - a);\n if (dist(c) < dist(d)) b = d;\n else a = c;\n }\n const kelvin = 1e6 / ((a + b) / 2);\n const [lu, lv] = locusUv(kelvin);\n const [nu, nv] = locusNormal(kelvin);\n const duv = (u - lu) * nu + (v - lv) * nv;\n return { kelvin, tint: duv * TINT_SCALE };\n}\n\n// ---------------------------------------------------------------------------\n// Relative white balance for rendered (JPEG / PNG / WebP) sources\n// ---------------------------------------------------------------------------\n\n/** Rendered images carry no as-shot white: \"as shot\" is 6500 K / tint 0 and maps to the identity. */\nexport const AS_SHOT_KELVIN = 6500;\n/** Mireds per unit of the `temperature` slider (−100…100). */\nexport const MIRED_PER_TEMPERATURE_UNIT = 1;\nconst AS_SHOT_MIRED = 1e6 / AS_SHOT_KELVIN;\n\n/**\n * Maps the `temperature` slider (−100 cool … +100 warm) to the Kelvin white-balance setting:\n * one mired per unit around 6500 K, so 0 → 6500 K, +100 → ~18 570 K (a strong warming\n * correction), −100 → ~3 940 K (a strong cooling correction). Mireds are perceptually even,\n * so equal slider steps look like equal changes.\n */\nexport function temperatureToKelvin(temperature: number): number {\n return 1e6 / (AS_SHOT_MIRED - temperature * MIRED_PER_TEMPERATURE_UNIT);\n}\n\n/** Inverse of {@link temperatureToKelvin}. */\nexport function kelvinToTemperature(kelvin: number): number {\n return (AS_SHOT_MIRED - 1e6 / kelvin) / MIRED_PER_TEMPERATURE_UNIT;\n}\n\nconst AS_SHOT_WB_INV: Matrix3 = invertMatrix3(wbMatrix(tempTintToXy(AS_SHOT_KELVIN, 0))) ?? IDENTITY_MATRIX3;\n\n/**\n * White-balance matrix (linear sRGB) for the `temperature` / `tint` sliders, relative to\n * \"as shot\": the identity at 0 / 0. Normalised so neutral luminance is preserved (white\n * balance doesn't change exposure).\n */\nexport function whiteBalanceMatrix(temperature: number, tint = 0): Matrix3 {\n if (!temperature && !tint) return [...IDENTITY_MATRIX3];\n const set = wbMatrix(tempTintToXy(temperatureToKelvin(temperature), tint));\n const m = mulMatrix3(set, AS_SHOT_WB_INV);\n const g = applyMatrix3(m, [1, 1, 1]);\n const y = g[0] * LUMA_709[0] + g[1] * LUMA_709[1] + g[2] * LUMA_709[2];\n return mulMatrix3(m, diag3(1 / y, 1 / y, 1 / y));\n}\n\n/**\n * Neutral-point eyedropper: the `temperature` / `tint` that render the sampled sRGB colour\n * (0–255 per channel, as it appears in the unadjusted image) neutral. Results are clamped to\n * the slider range −100…100.\n */\nexport function whiteBalanceFromSample(\n rgb: readonly [number, number, number] | { r: number; g: number; b: number }\n): { temperature: number; tint: number } {\n const [r, g, b] = Array.isArray(rgb) ? rgb : [(rgb as any).r, (rgb as any).g, (rgb as any).b];\n const lin: [number, number, number] = [r, g, b].map((c) => srgbToLinear(Math.min(255, Math.max(0, c)) / 255)) as [\n number,\n number,\n number,\n ];\n if (lin[0] + lin[1] + lin[2] <= 1e-6) return { temperature: 0, tint: 0 };\n // Under relative WB the matrix is wb(set) · wb(asShot)⁻¹, so the sample is neutral when the\n // chosen source white is the chromaticity of wb(asShot)⁻¹ · sample.\n const src = linearSrgbToXy(applyMatrix3(AS_SHOT_WB_INV, lin));\n const { kelvin, tint } = xyToTempTint(src);\n const clamp = (v: number) => Math.max(-100, Math.min(100, v));\n return { temperature: clamp(kelvinToTemperature(kelvin)), tint: clamp(tint) };\n}\n\n// ---------------------------------------------------------------------------\n// Tone curve (display-referred, identity at neutral)\n// ---------------------------------------------------------------------------\n\n/** Number of intervals in a tone LUT (the table has `TONE_LUT_SIZE + 1` entries). */\nexport const TONE_LUT_SIZE = 4096;\nconst GREY = 0.18;\nconst GREY_P = Math.pow(GREY, 1 / 2.2);\n/** Linear value where the highlight shoulder starts when there is headroom above 1. */\nconst SHOULDER_KNEE = 0.6;\n\n/**\n * The process 2 tone curve for one linear input value in `[0, headroom]`:\n *\n * 1. A highlight shoulder (extended Reinhard with white point `headroom`) above a knee of 0.6\n * rolls values above 1 (after exposure / white balance) back into range instead of clipping.\n * With no headroom (`headroom = 1`) it is exactly the identity.\n * 2. In a gamma-2.2 perceptual domain, contrast is an S-curve anchored at 0, middle grey and 1\n * (as `ToneMap::display`), and brightness a midtone lift anchored at 0 and 1.\n *\n * `contrast` and `brightness` are −100…100; both 0 with `headroom = 1` is the identity.\n */\nexport function toneCurve(y: number, contrast: number, brightness: number, headroom = 1): number {\n if (!(y > 0)) return 0;\n const h = Math.max(1, headroom);\n let o = Math.min(y, h);\n if (h > 1 && o > SHOULDER_KNEE) {\n const span = 1 - SHOULDER_KNEE;\n const x = (o - SHOULDER_KNEE) / span;\n const X = (h - SHOULDER_KNEE) / span;\n o = SHOULDER_KNEE + span * ((x * (1 + x / (X * X))) / (1 + x));\n }\n if (contrast === 0 && brightness === 0) return Math.min(1, o);\n let p = Math.pow(Math.min(1, o), 1 / 2.2);\n const c = contrast / 100;\n if (c !== 0) {\n p += c * 0.4 * (p - GREY_P) * (1 - (2 * p - 1) ** 2);\n }\n const b = brightness / 100;\n if (b !== 0) {\n // Smooth midtone lift that leaves black and white in place (slope stays positive).\n p += b * 0.8 * p * (1 - p);\n }\n return Math.pow(Math.min(1, Math.max(0, p)), 2.2);\n}\n\n/** Tabulates {@link toneCurve} over `[0, headroom]` in {@link TONE_LUT_SIZE} steps. */\nexport function buildToneLut(contrast: number, brightness: number, headroom = 1): Float32Array {\n const h = Math.max(1, headroom);\n const lut = new Float32Array(TONE_LUT_SIZE + 1);\n for (let i = 0; i <= TONE_LUT_SIZE; i++) {\n lut[i] = toneCurve((h * i) / TONE_LUT_SIZE, contrast, brightness, h);\n }\n return lut;\n}\n\n/** Linearly interpolated lookup into a LUT from {@link buildToneLut}. */\nexport function sampleToneLut(lut: Float32Array, headroom: number, y: number): number {\n if (!(y > 0)) return lut[0];\n const n = lut.length - 1;\n const f = (y * n) / Math.max(1, headroom);\n if (f >= n) return lut[n];\n const i = f | 0;\n return lut[i] + (lut[i + 1] - lut[i]) * (f - i);\n}\n","import type { Annotation, CropBox, ImageEditorState, ImagePoint, ImageTransform } from './types';\n\n/*\n * Coordinate spaces\n * -----------------\n * The crop box and every annotation are stored in ONE canonical space: natural image\n * pixels of the current source image (origin top-left of the un-rotated, un-flipped,\n * un-cropped image). Rotation, flips, crop and the viewport fit/zoom/pan are pure view\n * transforms applied on top of that space by a single forward mapping:\n *\n * target = origin + scale * R(angle) * F(flipH, flipV) * (image - regionCenter)\n *\n * where `regionCenter` is the centre of the visible region (the crop box, or the whole\n * image while the crop tool is active). `projectImagePoint` / `unprojectCanvasPoint`\n * implement the mapping and its exact inverse, and `applyImageProjectionToContext`\n * applies the very same transform to a 2D context, so rendering, hit-testing, viewport\n * metrics, export and mask generation cannot drift apart.\n */\n\nexport type CropHandle = 'nw' | 'ne' | 'se' | 'sw' | 'n' | 's' | 'e' | 'w' | 'inside' | null;\n\nexport interface ViewportMetrics {\n vpWidth: number;\n vpHeight: number;\n centerX: number;\n centerY: number;\n baseW: number;\n baseH: number;\n fitScale: number;\n renderW: number;\n renderH: number;\n totalScale: number;\n imageRect: { x: number; y: number; width: number; height: number };\n cropRect: { x: number; y: number; width: number; height: number } | null;\n}\n\n/**\n * A complete description of the natural-image -> target mapping (see module comment).\n */\nexport interface ImageProjection {\n /** Visible region in natural image pixels (crop box or full image). */\n region: CropBox;\n /** Rotation in degrees, normalised to [0, 360). */\n angle: number;\n flipH: boolean;\n flipV: boolean;\n /** Uniform scale from image pixels to target pixels. */\n scale: number;\n /** Target-space point that the centre of `region` maps to. */\n originX: number;\n originY: number;\n}\n\n/** Normalises any rotation in degrees to [0, 360). */\nexport function normalizeAngle(degrees: number): number {\n const a = ((degrees % 360) + 360) % 360;\n return a === 360 ? 0 : a;\n}\n\nfunction cosSin(angle: number): [number, number] {\n // Exact values for quarter turns so 90deg steps never accumulate float noise.\n switch (angle) {\n case 0:\n return [1, 0];\n case 90:\n return [0, 1];\n case 180:\n return [-1, 0];\n case 270:\n return [0, -1];\n default: {\n const rad = (angle * Math.PI) / 180;\n return [Math.cos(rad), Math.sin(rad)];\n }\n }\n}\n\n/** Whether the rotation swaps the displayed width and height. */\nexport function isQuarterTurnSwapped(transform: Pick<ImageTransform, 'rotate'>): boolean {\n const angle = normalizeAngle(transform.rotate);\n return angle === 90 || angle === 270;\n}\n\n/** Size of a region once rotated (bounding box for arbitrary angles). */\nexport function getRotatedSize(\n width: number,\n height: number,\n transform: Pick<ImageTransform, 'rotate'>\n): { width: number; height: number } {\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n return {\n width: Math.abs(width * c) + Math.abs(height * s),\n height: Math.abs(width * s) + Math.abs(height * c),\n };\n}\n\n/** Maps a natural-image point to target space. */\nexport function projectImagePoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n let x = pt.x - (p.region.x + p.region.width / 2);\n let y = pt.y - (p.region.y + p.region.height / 2);\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n const rx = x * c - y * s;\n const ry = x * s + y * c;\n return { x: p.originX + rx * p.scale, y: p.originY + ry * p.scale };\n}\n\n/** Exact inverse of {@link projectImagePoint}. */\nexport function unprojectCanvasPoint(pt: ImagePoint, p: ImageProjection): ImagePoint {\n const [c, s] = cosSin(p.angle);\n const rx = (pt.x - p.originX) / p.scale;\n const ry = (pt.y - p.originY) / p.scale;\n let x = rx * c + ry * s;\n let y = -rx * s + ry * c;\n if (p.flipH) x = -x;\n if (p.flipV) y = -y;\n return { x: x + p.region.x + p.region.width / 2, y: y + p.region.y + p.region.height / 2 };\n}\n\n/**\n * Applies the projection to a 2D context so that subsequent drawing in natural image\n * pixels lands exactly where {@link projectImagePoint} says it does.\n */\nexport function applyImageProjectionToContext(\n ctx: Pick<CanvasRenderingContext2D, 'translate' | 'scale' | 'rotate'>,\n p: ImageProjection\n): void {\n ctx.translate(p.originX, p.originY);\n ctx.scale(p.scale, p.scale);\n if (p.angle !== 0) ctx.rotate((p.angle * Math.PI) / 180);\n if (p.flipH || p.flipV) ctx.scale(p.flipH ? -1 : 1, p.flipV ? -1 : 1);\n ctx.translate(-(p.region.x + p.region.width / 2), -(p.region.y + p.region.height / 2));\n}\n\n/** Projects an image-space rectangle and returns its target-space bounding box. */\nexport function projectImageRect(\n rect: CropBox,\n p: ImageProjection\n): { x: number; y: number; width: number; height: number } {\n const corners = [\n projectImagePoint({ x: rect.x, y: rect.y }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y }, p),\n projectImagePoint({ x: rect.x, y: rect.y + rect.height }, p),\n projectImagePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, p),\n ];\n const xs = corners.map((c) => c.x);\n const ys = corners.map((c) => c.y);\n const minX = Math.min(...xs);\n const minY = Math.min(...ys);\n return { x: minX, y: minY, width: Math.max(...xs) - minX, height: Math.max(...ys) - minY };\n}\n\n/** Clamps a crop box to the image bounds. */\nexport function clampCropToImage(crop: CropBox, width: number, height: number): CropBox {\n const x = Math.max(0, Math.min(width - 1, crop.x));\n const y = Math.max(0, Math.min(height - 1, crop.y));\n return {\n x,\n y,\n width: Math.max(1, Math.min(width - x, crop.width)),\n height: Math.max(1, Math.min(height - y, crop.height)),\n };\n}\n\nfunction resolveViewport(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping: boolean,\n cropOverride?: CropBox | null\n) {\n const { imageDimensions, crop: stateCrop, transform, zoom, pan, fit = 'margin' } = state;\n const naturalWidth = imageDimensions.width || 800;\n const naturalHeight = imageDimensions.height || 600;\n const full: CropBox = { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const activeCrop =\n cropOverride !== undefined && cropOverride !== null\n ? cropOverride\n : (stateCrop ?? (isActivelyCropping ? full : null));\n\n const region =\n !isActivelyCropping && activeCrop ? clampCropToImage(activeCrop, naturalWidth, naturalHeight) : full;\n const { width: baseW, height: baseH } = getRotatedSize(region.width, region.height, transform);\n\n const fitScale =\n fit === 'contain'\n ? Math.min(vpWidth / baseW, vpHeight / baseH)\n : Math.min((vpWidth * 0.85) / baseW, (vpHeight * 0.85) / baseH, 1);\n const centerX = vpWidth / 2 + pan.x;\n const centerY = vpHeight / 2 + pan.y;\n\n const projection: ImageProjection = {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: zoom * fitScale,\n originX: centerX,\n originY: centerY,\n };\n\n return { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection };\n}\n\n/**\n * Returns the natural-image -> canvas projection used by the interactive viewport.\n */\nexport function getViewportProjection(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ImageProjection {\n return resolveViewport(state, vpWidth, vpHeight, isActivelyCropping, cropOverride).projection;\n}\n\n/**\n * Calculates current viewport projection metrics for canvas <-> image coordinate mapping.\n * `cropRect` is the on-screen (rotated/flipped) rectangle of the crop box.\n */\nexport function getViewportMetrics(\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false,\n cropOverride?: CropBox | null\n): ViewportMetrics {\n const { zoom } = state;\n const { activeCrop, baseW, baseH, fitScale, centerX, centerY, projection } = resolveViewport(\n state,\n vpWidth,\n vpHeight,\n isActivelyCropping,\n cropOverride\n );\n\n const renderW = baseW * fitScale;\n const renderH = baseH * fitScale;\n\n const imageRect = {\n x: centerX - (renderW * zoom) / 2,\n y: centerY - (renderH * zoom) / 2,\n width: renderW * zoom,\n height: renderH * zoom,\n };\n\n const cropRect = activeCrop ? projectImageRect(activeCrop, projection) : null;\n\n return {\n vpWidth,\n vpHeight,\n centerX,\n centerY,\n baseW,\n baseH,\n fitScale,\n renderW,\n renderH,\n totalScale: zoom * fitScale,\n imageRect,\n cropRect,\n };\n}\n\n/**\n * Converts a point on the canvas (in internal canvas pixel space) to natural image pixel\n * space, accounting for zoom, pan, fit scale, crop offset, rotation and flips.\n */\nexport function canvasToImagePoint(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return unprojectCanvasPoint(canvasPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\n/**\n * Converts a point in natural image pixel space to internal canvas pixel space.\n * Exact inverse of {@link canvasToImagePoint}.\n */\nexport function imageToCanvasPoint(\n imgPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n isActivelyCropping = false\n): ImagePoint {\n return projectImagePoint(imgPt, getViewportProjection(state, vpWidth, vpHeight, isActivelyCropping));\n}\n\nconst HANDLE_VECTORS: Record<Exclude<CropHandle, 'inside' | null>, [number, number]> = {\n nw: [-1, -1],\n n: [0, -1],\n ne: [1, -1],\n e: [1, 0],\n se: [1, 1],\n s: [0, 1],\n sw: [-1, 1],\n w: [-1, 0],\n};\n\nfunction vectorToHandle(x: number, y: number): CropHandle {\n const vx = Math.abs(x) < 0.38 ? 0 : Math.sign(x);\n const vy = Math.abs(y) < 0.38 ? 0 : Math.sign(y);\n for (const [name, [hx, hy]] of Object.entries(HANDLE_VECTORS)) {\n if (hx === vx && hy === vy) return name as CropHandle;\n }\n return null;\n}\n\n/**\n * Converts a crop handle in screen orientation to the equivalent edge/corner of the crop\n * box in natural image space, or the reverse with `toScreen = true`.\n */\nexport function mapCropHandle(handle: CropHandle, transform: ImageTransform, toScreen = false): CropHandle {\n if (handle === null || handle === 'inside') return handle;\n const [hx, hy] = HANDLE_VECTORS[handle];\n const [c, s] = cosSin(normalizeAngle(transform.rotate));\n const fx = transform.flipH ? -1 : 1;\n const fy = transform.flipV ? -1 : 1;\n if (toScreen) {\n const x = hx * fx;\n const y = hy * fy;\n return vectorToHandle(x * c - y * s, x * s + y * c);\n }\n const x = hx * c + hy * s;\n const y = -hx * s + hy * c;\n return vectorToHandle(x * fx, y * fy);\n}\n\n/**\n * Hit-tests the crop box and its corner/edge handles given a canvas point.\n * The returned handle names the edge/corner of the crop box in natural image space\n * (what {@link calculateCropDrag} expects); use `getCropCursor(handle, state.transform)`\n * to obtain the matching on-screen cursor.\n */\nexport function hitTestCrop(\n canvasPt: ImagePoint,\n state: ImageEditorState,\n vpWidth: number,\n vpHeight: number,\n handleThreshold = 14,\n cropOverride?: CropBox | null\n): CropHandle {\n const metrics = getViewportMetrics(state, vpWidth, vpHeight, true, cropOverride);\n if (!metrics.cropRect) return null;\n\n const screenHandle = hitTestCropRect(canvasPt, metrics.cropRect, handleThreshold);\n return mapCropHandle(screenHandle, state.transform);\n}\n\nfunction hitTestCropRect(\n canvasPt: ImagePoint,\n rect: { x: number; y: number; width: number; height: number },\n handleThreshold: number\n): CropHandle {\n const { x, y, width: w, height: h } = rect;\n const px = canvasPt.x;\n const py = canvasPt.y;\n\n // 1. Corners (highest priority)\n const dist = (x1: number, y1: number, x2: number, y2: number) =>\n Math.hypot(x1 - x2, y1 - y2);\n\n if (dist(px, py, x, y) <= handleThreshold) return 'nw';\n if (dist(px, py, x + w, y) <= handleThreshold) return 'ne';\n if (dist(px, py, x + w, y + h) <= handleThreshold) return 'se';\n if (dist(px, py, x, y + h) <= handleThreshold) return 'sw';\n\n // 2. Edges\n const nearX = px >= x - handleThreshold && px <= x + w + handleThreshold;\n const nearY = py >= y - handleThreshold && py <= y + h + handleThreshold;\n\n if (nearX && Math.abs(py - y) <= handleThreshold) return 'n';\n if (nearX && Math.abs(py - (y + h)) <= handleThreshold) return 's';\n if (nearY && Math.abs(px - x) <= handleThreshold) return 'w';\n if (nearY && Math.abs(px - (x + w)) <= handleThreshold) return 'e';\n\n // 3. Inside crop region\n if (px >= x && px <= x + w && py >= y && py <= y + h) {\n return 'inside';\n }\n\n return null;\n}\n\n/**\n * Returns CSS cursor style corresponding to a crop handle. Pass the current transform when\n * the handle comes from {@link hitTestCrop} (image space) so the cursor matches the\n * on-screen orientation under rotation/flip.\n */\nexport function getCropCursor(handle: CropHandle, transform?: ImageTransform): string {\n const screenHandle = transform ? mapCropHandle(handle, transform, true) : handle;\n switch (screenHandle) {\n case 'nw':\n case 'se':\n return 'nwse-resize';\n case 'ne':\n case 'sw':\n return 'nesw-resize';\n case 'n':\n case 's':\n return 'ns-resize';\n case 'e':\n case 'w':\n return 'ew-resize';\n case 'inside':\n return 'move';\n default:\n return 'crosshair';\n }\n}\n\n/**\n * Calculates updated crop rectangle given a drag interaction.\n */\nexport function calculateCropDrag(\n startCrop: CropBox,\n handle: CropHandle,\n startImgPt: ImagePoint,\n currentImgPt: ImagePoint,\n imgWidth: number,\n imgHeight: number,\n aspectRatio?: number | null,\n transform?: ImageTransform\n): CropBox {\n // `aspectRatio` is the on-screen ratio; under a quarter turn it is inverted in image space.\n if (aspectRatio && transform && isQuarterTurnSwapped(transform)) {\n aspectRatio = 1 / aspectRatio;\n }\n const dx = currentImgPt.x - startImgPt.x;\n const dy = currentImgPt.y - startImgPt.y;\n\n // Whole crop box move\n if (handle === 'inside') {\n const maxX = Math.max(0, imgWidth - startCrop.width);\n const maxY = Math.max(0, imgHeight - startCrop.height);\n const newX = Math.max(0, Math.min(maxX, startCrop.x + dx));\n const newY = Math.max(0, Math.min(maxY, startCrop.y + dy));\n return {\n x: Math.round(newX),\n y: Math.round(newY),\n width: startCrop.width,\n height: startCrop.height,\n };\n }\n\n // Handle resizing\n let left = startCrop.x;\n let top = startCrop.y;\n let right = startCrop.x + startCrop.width;\n let bottom = startCrop.y + startCrop.height;\n\n const minSize = 20;\n\n if (handle === 'e' || handle === 'ne' || handle === 'se') {\n right = Math.min(imgWidth, Math.max(left + minSize, startCrop.x + startCrop.width + dx));\n }\n if (handle === 'w' || handle === 'nw' || handle === 'sw') {\n left = Math.max(0, Math.min(right - minSize, startCrop.x + dx));\n }\n if (handle === 's' || handle === 'se' || handle === 'sw') {\n bottom = Math.min(imgHeight, Math.max(top + minSize, startCrop.y + startCrop.height + dy));\n }\n if (handle === 'n' || handle === 'nw' || handle === 'ne') {\n top = Math.max(0, Math.min(bottom - minSize, startCrop.y + dy));\n }\n\n // Maintain aspect ratio if requested\n if (aspectRatio && aspectRatio > 0) {\n let currentW = right - left;\n let currentH = bottom - top;\n\n if (handle === 'e' || handle === 'w') {\n currentH = Math.min(imgHeight, currentW / aspectRatio);\n bottom = Math.min(imgHeight, top + currentH);\n } else if (handle === 'n' || handle === 's') {\n currentW = Math.min(imgWidth, currentH * aspectRatio);\n right = Math.min(imgWidth, left + currentW);\n } else {\n // Corner resizing\n const targetH = currentW / aspectRatio;\n if (top + targetH <= imgHeight) {\n bottom = top + targetH;\n } else {\n const targetW = currentH * aspectRatio;\n right = Math.min(imgWidth, left + targetW);\n }\n }\n }\n\n return {\n x: Math.round(left),\n y: Math.round(top),\n width: Math.round(Math.max(minSize, right - left)),\n height: Math.round(Math.max(minSize, bottom - top)),\n };\n}\n\n/**\n * Calculates bounding box of an annotation.\n */\nexport function getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n switch (ann.type) {\n case 'rect':\n case 'image':\n return {\n x: Math.min(ann.x, ann.x + ann.width),\n y: Math.min(ann.y, ann.y + ann.height),\n width: Math.abs(ann.width),\n height: Math.abs(ann.height),\n };\n case 'circle':\n return {\n x: ann.x - Math.abs(ann.radiusX),\n y: ann.y - Math.abs(ann.radiusY),\n width: Math.abs(ann.radiusX) * 2,\n height: Math.abs(ann.radiusY) * 2,\n };\n case 'arrow':\n case 'line': {\n const minX = Math.min(ann.x, ann.endX);\n const minY = Math.min(ann.y, ann.endY);\n return {\n x: minX,\n y: minY,\n width: Math.max(Math.abs(ann.endX - ann.x), 10),\n height: Math.max(Math.abs(ann.endY - ann.y), 10),\n };\n }\n case 'pen': {\n if (!ann.points.length) return { x: ann.x, y: ann.y, width: 0, height: 0 };\n let minX = ann.points[0].x;\n let maxX = ann.points[0].x;\n let minY = ann.points[0].y;\n let maxY = ann.points[0].y;\n for (const pt of ann.points) {\n if (pt.x < minX) minX = pt.x;\n if (pt.x > maxX) maxX = pt.x;\n if (pt.y < minY) minY = pt.y;\n if (pt.y > maxY) maxY = pt.y;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n case 'text':\n return {\n x: ann.x,\n y: ann.y,\n width: ann.text.length * ann.fontSize * 0.6,\n height: ann.fontSize * 1.2,\n };\n }\n}\n\n/**\n * Hit tests if a point in image space is within an annotation's bounds.\n */\nexport function hitTestAnnotation(imgPt: ImagePoint, ann: Annotation, padding = 10): boolean {\n const b = getAnnotationBounds(ann);\n return (\n imgPt.x >= b.x - padding &&\n imgPt.x <= b.x + b.width + padding &&\n imgPt.y >= b.y - padding &&\n imgPt.y <= b.y + b.height + padding\n );\n}\n\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\n\n/**\n * A selection: one 8-bit coverage value per image pixel (0 = not selected, 255 = fully\n * selected), row-major over the whole source image in natural pixels — the same space as the\n * crop box and annotations. Soft values are partial selection (feathered or anti-aliased edges).\n *\n * Treat a mask as immutable: every operation returns a new one, so history entries can share\n * them without copying.\n */\nexport interface SelectionMask {\n readonly width: number;\n readonly height: number;\n readonly data: Uint8Array;\n /** Bounding box of the non-zero coverage, or `null` when nothing is selected. */\n readonly bounds: CropBox | null;\n}\n\n/** How a new selection combines with the current one. */\nexport type SelectionMode = 'replace' | 'add' | 'subtract' | 'intersect';\n\nexport const SELECTION_MODES: readonly SelectionMode[] = ['replace', 'add', 'subtract', 'intersect'];\n\nexport function isSelectionMode(value: unknown): value is SelectionMode {\n return typeof value === 'string' && (SELECTION_MODES as readonly string[]).includes(value);\n}\n\n/** Parses `replace|add|subtract|intersect`; anything else is `replace`. */\nexport function parseSelectionMode(value: unknown): SelectionMode {\n return isSelectionMode(value) ? value : 'replace';\n}\n\n/** RGBA pixels, 8 bits per channel, straight alpha (an `ImageData` qualifies). */\nexport interface PixelBuffer {\n width: number;\n height: number;\n data: ArrayLike<number>;\n}\n\n/** Serialized selection: a run-length encoding of the 8-bit coverage. */\nexport interface SerializedSelection {\n width: number;\n height: number;\n /** `r1:` + base64 of (value byte, LEB128 run length) pairs. */\n rle: string;\n}\n\nconst MAX_SIDE = 1 << 15;\n\nfunction checkSize(width: number, height: number): void {\n if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1 || width > MAX_SIDE || height > MAX_SIDE) {\n throw new Error(`Invalid selection size ${width}×${height}`);\n }\n}\n\n/** Bounding box of non-zero bytes. */\nexport function computeSelectionBounds(width: number, height: number, data: ArrayLike<number>): CropBox | null {\n let x0 = width;\n let x1 = -1;\n let y0 = -1;\n let y1 = -1;\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let first = -1;\n for (let x = 0; x < width; x++) {\n if (data[row + x] !== 0) {\n first = x;\n break;\n }\n }\n if (first < 0) continue;\n let last = first;\n for (let x = width - 1; x > first; x--) {\n if (data[row + x] !== 0) {\n last = x;\n break;\n }\n }\n if (y0 < 0) y0 = y;\n y1 = y;\n if (first < x0) x0 = first;\n if (last > x1) x1 = last;\n }\n if (y0 < 0) return null;\n return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };\n}\n\n/** Wraps coverage bytes (taken over, not copied) as a selection, computing its bounds. */\nexport function createSelectionMask(width: number, height: number, data?: Uint8Array): SelectionMask {\n checkSize(width, height);\n const bytes = data ?? new Uint8Array(width * height);\n if (bytes.length !== width * height) throw new Error('Selection data does not match its size');\n return { width, height, data: bytes, bounds: data ? computeSelectionBounds(width, height, bytes) : null };\n}\n\n/** Select › All. */\nexport function fullSelection(width: number, height: number): SelectionMask {\n checkSize(width, height);\n return { width, height, data: new Uint8Array(width * height).fill(255), bounds: { x: 0, y: 0, width, height } };\n}\n\nexport function isSelectionEmpty(mask: SelectionMask | null | undefined): boolean {\n return !mask || mask.bounds === null;\n}\n\n/** Coverage at a pixel, 0..1 (0 outside the image). */\nexport function selectionValueAt(mask: SelectionMask, x: number, y: number): number {\n const xi = Math.floor(x);\n const yi = Math.floor(y);\n if (xi < 0 || yi < 0 || xi >= mask.width || yi >= mask.height) return 0;\n return mask.data[yi * mask.width + xi] / 255;\n}\n\n/** 8-bit value of a coverage (clamped and rounded; NaN → 0). */\nexport function coverageToByte(v: number): number {\n if (!(v > 0)) return 0;\n if (v >= 1) return 255;\n return Math.floor(v * 255 + 0.5);\n}\n\n/** A selection from 0..1 coverage. */\nexport function selectionFromCoverage(width: number, height: number, coverage: ArrayLike<number>): SelectionMask {\n const n = width * height;\n const out = new Uint8Array(n);\n for (let i = 0; i < n; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** The selection's coverage as 0..1 floats. */\nexport function selectionToCoverage(mask: SelectionMask): Float32Array {\n const out = new Float32Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = mask.data[i] / 255;\n return out;\n}\n\n/** Number of fully-or-partly selected pixels, weighted by coverage (Σ coverage). */\nexport function selectionArea(mask: SelectionMask | null | undefined): number {\n if (!mask || !mask.bounds) return 0;\n let sum = 0;\n const { x, y, width, height } = mask.bounds;\n for (let yy = y; yy < y + height; yy++) {\n const row = yy * mask.width;\n for (let xx = x; xx < x + width; xx++) sum += mask.data[row + xx];\n }\n return sum / 255;\n}\n\n/**\n * Combines `next` with `prev` by `mode` (8-bit, exact: combining a mask with itself is a no-op).\n * Add is the maximum, Subtract `max(prev − next, 0)`, Intersect the minimum. A missing `prev`\n * reads as nothing selected. Returns `null` when nothing remains selected.\n */\nexport function combineSelections(\n prev: SelectionMask | null | undefined,\n next: SelectionMask,\n mode: SelectionMode\n): SelectionMask | null {\n let result: SelectionMask;\n if (mode === 'replace' || (!prev && mode === 'add')) {\n result = next;\n } else if (!prev) {\n return null;\n } else {\n if (prev.width !== next.width || prev.height !== next.height) throw new Error('Selections differ in size');\n const a = prev.data;\n const b = next.data;\n const out = new Uint8Array(a.length);\n if (mode === 'add') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] : b[i];\n else if (mode === 'subtract') for (let i = 0; i < a.length; i++) out[i] = a[i] > b[i] ? a[i] - b[i] : 0;\n else for (let i = 0; i < a.length; i++) out[i] = a[i] < b[i] ? a[i] : b[i];\n result = createSelectionMask(prev.width, prev.height, out);\n }\n return result.bounds ? result : null;\n}\n\n/** Select › Inverse. */\nexport function invertSelection(mask: SelectionMask): SelectionMask {\n const out = new Uint8Array(mask.data.length);\n for (let i = 0; i < out.length; i++) out[i] = 255 - mask.data[i];\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Bilinear resample to another size (e.g. an AI mask returned at a different resolution). */\nexport function resizeSelection(mask: SelectionMask, width: number, height: number): SelectionMask {\n if (mask.width === width && mask.height === height) return mask;\n checkSize(width, height);\n const out = new Uint8Array(width * height);\n const sx = mask.width / width;\n const sy = mask.height / height;\n const src = mask.data;\n const mw = mask.width;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(mask.height - 1, Math.max(0, (y + 0.5) * sy - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(mask.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(mw - 1, Math.max(0, (x + 0.5) * sx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(mw - 1, x0 + 1);\n const tx = fx - x0;\n const top = src[y0 * mw + x0] * (1 - tx) + src[y0 * mw + x1] * tx;\n const bot = src[y1 * mw + x0] * (1 - tx) + src[y1 * mw + x1] * tx;\n out[y * width + x] = Math.round(top * (1 - ty) + bot * ty);\n }\n }\n return createSelectionMask(width, height, out);\n}\n\nexport type SelectionChannel = 'alpha' | 'luminance' | 'red' | 'green' | 'blue';\n\n/**\n * A selection from an image: by default its luminance (white = selected, the usual convention\n * of segmentation masks), or one channel. Partly transparent pixels count as black for\n * luminance, so a mask PNG with transparency works either way.\n */\nexport function selectionFromPixels(\n pixels: PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number } = {}\n): SelectionMask {\n const { width, height, data } = pixels;\n checkSize(width, height);\n if (data.length < width * height * 4) throw new Error('Pixel data is shorter than width × height × 4');\n const channel = options.channel ?? 'luminance';\n const out = new Uint8Array(width * height);\n const threshold = options.threshold;\n for (let i = 0, p = 0; i < out.length; i++, p += 4) {\n let v: number;\n switch (channel) {\n case 'alpha':\n v = data[p + 3];\n break;\n case 'red':\n v = data[p];\n break;\n case 'green':\n v = data[p + 1];\n break;\n case 'blue':\n v = data[p + 2];\n break;\n default:\n v = Math.round(((0.2126 * data[p] + 0.7152 * data[p + 1] + 0.0722 * data[p + 2]) * data[p + 3]) / 255);\n }\n if (options.invert) v = 255 - v;\n if (threshold !== undefined) v = v >= threshold ? 255 : 0;\n out[i] = v;\n }\n return createSelectionMask(width, height, out);\n}\n\n/** The selection as RGBA pixels: grey (white = selected, opaque) or white with coverage as alpha. */\nexport function selectionToPixels(mask: SelectionMask, as: 'gray' | 'alpha' = 'gray'): { width: number; height: number; data: Uint8ClampedArray } {\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n const v = mask.data[i];\n if (as === 'gray') {\n out[p] = out[p + 1] = out[p + 2] = v;\n out[p + 3] = 255;\n } else {\n out[p] = out[p + 1] = out[p + 2] = 255;\n out[p + 3] = v;\n }\n }\n return { width: mask.width, height: mask.height, data: out };\n}\n\n// ---------------------------------------------------------------------------\n// Run-length encoding\n// ---------------------------------------------------------------------------\n\nconst B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst B64_INDEX = (() => {\n const t = new Int16Array(128).fill(-1);\n for (let i = 0; i < B64.length; i++) t[B64.charCodeAt(i)] = i;\n return t;\n})();\n\nfunction toBase64(bytes: Uint8Array, length: number): string {\n const parts: string[] = [];\n let chunk = '';\n for (let i = 0; i < length; i += 3) {\n const a = bytes[i];\n const b = i + 1 < length ? bytes[i + 1] : 0;\n const c = i + 2 < length ? bytes[i + 2] : 0;\n chunk += B64[a >> 2] + B64[((a & 3) << 4) | (b >> 4)];\n chunk += i + 1 < length ? B64[((b & 15) << 2) | (c >> 6)] : '=';\n chunk += i + 2 < length ? B64[c & 63] : '=';\n if (chunk.length >= 8192) {\n parts.push(chunk);\n chunk = '';\n }\n }\n parts.push(chunk);\n return parts.join('');\n}\n\nfunction fromBase64(text: string): Uint8Array {\n if (text.length % 4 !== 0) throw new Error('Invalid base64 length');\n let pad = 0;\n if (text.endsWith('==')) pad = 2;\n else if (text.endsWith('=')) pad = 1;\n const out = new Uint8Array((text.length / 4) * 3 - pad);\n let o = 0;\n const digit = (i: number, k: number): number => {\n const ch = text.charCodeAt(i + k);\n if (ch === 61 /* = */ && i + 4 === text.length && k >= 4 - pad) return 0;\n const idx = ch < 128 ? B64_INDEX[ch] : -1;\n if (idx < 0) throw new Error('Invalid base64 character');\n return idx;\n };\n for (let i = 0; i < text.length; i += 4) {\n const n = (digit(i, 0) << 18) | (digit(i, 1) << 12) | (digit(i, 2) << 6) | digit(i, 3);\n if (o < out.length) out[o++] = (n >> 16) & 255;\n if (o < out.length) out[o++] = (n >> 8) & 255;\n if (o < out.length) out[o++] = n & 255;\n }\n return out;\n}\n\n/** Run-length encodes coverage bytes: `r1:` + base64 of (value, LEB128 run length) pairs. */\nexport function encodeSelectionRle(data: Uint8Array): string {\n let buf = new Uint8Array(64);\n let len = 0;\n const push = (b: number) => {\n if (len === buf.length) {\n const next = new Uint8Array(buf.length * 2);\n next.set(buf);\n buf = next;\n }\n buf[len++] = b;\n };\n let i = 0;\n while (i < data.length) {\n const v = data[i];\n let j = i + 1;\n while (j < data.length && data[j] === v) j++;\n push(v);\n let run = j - i;\n while (run >= 128) {\n push((run & 127) | 128);\n run = Math.floor(run / 128);\n }\n push(run);\n i = j;\n }\n return 'r1:' + toBase64(buf, len);\n}\n\n/** Decodes {@link encodeSelectionRle}; throws unless the runs cover exactly `length` bytes. */\nexport function decodeSelectionRle(rle: string, length: number): Uint8Array {\n if (typeof rle !== 'string' || !rle.startsWith('r1:')) throw new Error('Unknown selection encoding');\n const bytes = fromBase64(rle.slice(3));\n const out = new Uint8Array(length);\n let o = 0;\n let i = 0;\n while (i < bytes.length) {\n const v = bytes[i++];\n let run = 0;\n let scale = 1;\n for (;;) {\n if (i >= bytes.length) throw new Error('Truncated selection run');\n const b = bytes[i++];\n run += (b & 127) * scale;\n if (b < 128) break;\n scale *= 128;\n if (scale > 2 ** 35) throw new Error('Selection run too long');\n }\n if (run === 0 || o + run > length) throw new Error('Selection runs do not match its size');\n if (v !== 0) out.fill(v, o, o + run);\n o += run;\n }\n if (o !== length) throw new Error('Selection runs do not match its size');\n return out;\n}\n\nexport function serializeSelection(mask: SelectionMask): SerializedSelection {\n return { width: mask.width, height: mask.height, rle: encodeSelectionRle(mask.data) };\n}\n\n/** Restores a {@link SerializedSelection}; throws on damaged data. */\nexport function deserializeSelection(value: unknown): SelectionMask {\n if (!value || typeof value !== 'object') throw new Error('selection must be an object');\n const v = value as Record<string, unknown>;\n const width = v.width as number;\n const height = v.height as number;\n checkSize(width, height);\n return createSelectionMask(width, height, decodeSelectionRle(v.rle as string, width * height));\n}\n\n/** A short, stable hash of a string or bytes (FNV-1a, 32-bit, hex) for cache keys. */\nexport function hashMaskContent(content: string | Uint8Array): string {\n let h = 0x811c9dc5;\n if (typeof content === 'string') {\n for (let i = 0; i < content.length; i++) {\n h ^= content.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n } else {\n for (let i = 0; i < content.length; i++) {\n h ^= content[i];\n h = Math.imul(h, 0x01000193);\n }\n }\n return `${(h >>> 0).toString(16)}:${content.length}`;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs, photocraft/crates/algo/src/selection/distance.rs, photocraft/crates/algo/src/selection_blur.rs and photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport { createSelectionMask, coverageToByte, selectionToCoverage, type SelectionMask } from './mask';\nimport { gaussianBlur } from '../../raster/blur';\n\nexport { gaussianBlur, DIRECT_GAUSSIAN_MAX_SIGMA } from '../../raster/blur';\n\n/** Distance reported for every pixel when there is no seed at all. */\nexport const EMPTY_DISTANCE = 1e10;\n\n/**\n * Exact Euclidean distance from every pixel centre to the nearest seed pixel centre (row-major\n * `inside`, non-zero = seed): nearest seeds along columns, then the lower envelope of parabolas\n * along rows (Felzenszwalb & Huttenlocher). Linear in the pixel count.\n */\nexport function distanceTransform(inside: ArrayLike<number | boolean>, width: number, height: number): Float32Array {\n const n = width * height;\n const out = new Float32Array(n);\n let any = false;\n let all = true;\n for (let i = 0; i < n; i++) {\n if (inside[i]) any = true;\n else all = false;\n }\n if (!any) return out.fill(EMPTY_DISTANCE);\n if (all) return out;\n const NONE = 0xffffffff;\n // Vertical distance to the nearest seed in the same column.\n const vertical = new Uint32Array(n).fill(NONE);\n for (let x = 0; x < width; x++) {\n let nearest = -1;\n for (let y = 0; y < height; y++) {\n const i = y * width + x;\n if (inside[i]) nearest = y;\n if (nearest >= 0) vertical[i] = y - nearest;\n }\n nearest = -1;\n for (let y = height - 1; y >= 0; y--) {\n const i = y * width + x;\n if (vertical[i] === 0) nearest = y;\n if (nearest >= 0) vertical[i] = Math.min(vertical[i], nearest - y);\n }\n }\n const sites = new Int32Array(width);\n const starts = new Float64Array(width);\n for (let y = 0; y < height; y++) {\n const row = y * width;\n let count = 0;\n for (let q = 0; q < width; q++) {\n const dq = vertical[row + q];\n if (dq === NONE) continue;\n const fq = dq * dq;\n let start = -Infinity;\n while (count > 0) {\n const p = sites[count - 1];\n const dp = vertical[row + p];\n start = (fq - dp * dp) / (2 * (q - p)) + (q + p) * 0.5;\n if (start > starts[count - 1]) break;\n count--;\n }\n if (count === 0) start = -Infinity;\n sites[count] = q;\n starts[count] = start;\n count++;\n }\n if (count === 0) {\n out.fill(EMPTY_DISTANCE, row, row + width);\n continue;\n }\n let k = 0;\n for (let x = 0; x < width; x++) {\n while (k + 1 < count && starts[k + 1] < x) k++;\n const site = sites[k];\n const dx = x - site;\n const dv = vertical[row + site];\n out[row + x] = Math.sqrt(dx * dx + dv * dv);\n }\n }\n return out;\n}\n\nfunction thresholdInside(m: ArrayLike<number>, threshold = 0.5): Uint8Array {\n const out = new Uint8Array(m.length);\n for (let i = 0; i < m.length; i++) out[i] = m[i] >= threshold ? 1 : 0;\n return out;\n}\n\n/** Grows coverage (0..1) by `r` pixels: covered when the nearest selected centre is ≤ r away (fractional r → partial). */\nexport function expandCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const d = distanceTransform(thresholdInside(m), w, h);\n const out = new Float32Array(m.length);\n for (let i = 0; i < out.length; i++) out[i] = Math.max(m[i], Math.min(1, Math.max(0, r + 1 - d[i])));\n return out;\n}\n\n/** Shrinks coverage by `r` pixels (expands the inverse). */\nexport function contractCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inv = new Float32Array(m.length);\n for (let i = 0; i < inv.length; i++) inv[i] = 1 - m[i];\n const e = expandCoverage(inv, w, h, r);\n for (let i = 0; i < e.length; i++) e[i] = 1 - e[i];\n return e;\n}\n\n/**\n * Select › Modify › Border: a soft band `2r` wide centred on the edge. A pixel `D` away (centre\n * to centre) from the nearest pixel on the other side of the edge is selected `1 − (D − 1) / r`.\n */\nexport function borderCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const inside = thresholdInside(m);\n const outside = new Uint8Array(inside.length);\n for (let i = 0; i < inside.length; i++) outside[i] = inside[i] ? 0 : 1;\n const out = new Float32Array(m.length);\n if (!(r > 0)) return out;\n const toInside = distanceTransform(inside, w, h);\n const toOutside = distanceTransform(outside, w, h);\n for (let i = 0; i < out.length; i++) {\n const d = inside[i] ? toOutside[i] : toInside[i];\n out[i] = Math.min(1, Math.max(0, 1 - (d - 1) / r));\n }\n return out;\n}\n\n/** Box average of radius `r` along one line, edges replicated, via a running sum (O(1) per sample). */\nfunction boxLineReplicate(src: Float32Array, srcOff: number, stride: number, len: number, r: number, dst: Float32Array) {\n const at = (i: number) => src[srcOff + Math.min(len - 1, Math.max(0, i)) * stride];\n let sum = 0;\n for (let i = -r; i <= r; i++) sum += at(i);\n const k = 2 * r + 1;\n for (let i = 0; i < len; i++) {\n dst[i] = sum / k;\n sum += at(i + r + 1) - at(i - r);\n }\n}\n\n/**\n * Select › Modify › Smooth: box average of radius `round(r)` with replicated edges, then\n * re-thresholded at ½ — specks drop, corners round, straight edges stay hard.\n */\nexport function smoothCoverage(m: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const rr = Number.isFinite(r) ? Math.max(0, Math.round(r)) : 0;\n const src = Float32Array.from(m);\n const tmp = new Float32Array(src.length);\n const line = new Float32Array(Math.max(w, h));\n if (rr > 0) {\n for (let y = 0; y < h; y++) {\n boxLineReplicate(src, y * w, 1, w, rr, line);\n tmp.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n boxLineReplicate(tmp, x, w, h, rr, line);\n for (let y = 0; y < h; y++) src[y * w + x] = line[y];\n }\n }\n for (let i = 0; i < src.length; i++) src[i] = src[i] >= 0.5 ? 1 : 0;\n return src;\n}\n\n\n/** Select › Modify › Feather: Gaussian blur with σ = radius (as Photoshop), zero beyond the image. */\nexport function featherCoverage(m: ArrayLike<number>, w: number, h: number, radius: number): Float32Array {\n const out = gaussianBlur(m, w, h, Number.isFinite(radius) ? Math.max(0, radius) : 0, 'zero');\n for (let i = 0; i < out.length; i++) if (Math.abs(out[i]) < 1e-7) out[i] = 0;\n return out;\n}\n\n/**\n * Running max (or min) over a window of radius `r` along one line (edge-clipped), van Herk /\n * Gil–Werman: about three comparisons per sample whatever the radius.\n */\nfunction runningExtreme(src: Float32Array, off: number, stride: number, len: number, r: number, max: boolean, g: Float32Array, hb: Float32Array, dst: Float32Array) {\n const k = 2 * r + 1;\n const pick = max ? (a: number, b: number) => (a > b ? a : b) : (a: number, b: number) => (a < b ? a : b);\n const fill = max ? -Infinity : Infinity;\n // Padded line: r fills on each side.\n const plen = len + 2 * r;\n const at = (i: number) => (i < r || i >= r + len ? fill : src[off + (i - r) * stride]);\n for (let start = 0; start < plen; start += k) {\n const end = Math.min(plen, start + k);\n g[start] = at(start);\n for (let i = start + 1; i < end; i++) g[i] = pick(g[i - 1], at(i));\n hb[end - 1] = at(end - 1);\n for (let i = end - 2; i >= start; i--) hb[i] = pick(hb[i + 1], at(i));\n }\n for (let i = 0; i < len; i++) {\n // Window over padded indices i .. i + 2r.\n const a = hb[i];\n const b = g[i + 2 * r];\n dst[i] = pick(a, b);\n }\n}\n\n/** Grey dilation (`grow`) or erosion with a square of radius `r`, separable, O(1) per pixel. */\nexport function greyMorphology(src: ArrayLike<number>, w: number, h: number, r: number, grow: boolean): Float32Array {\n const out = Float32Array.from(src);\n if (r <= 0) return out;\n const n = Math.max(w, h);\n const g = new Float32Array(n + 2 * r);\n const hb = new Float32Array(n + 2 * r);\n const line = new Float32Array(n);\n for (let y = 0; y < h; y++) {\n runningExtreme(out, y * w, 1, w, r, grow, g, hb, line);\n out.set(line.subarray(0, w), y * w);\n }\n for (let x = 0; x < w; x++) {\n runningExtreme(out, x, w, h, r, grow, g, hb, line);\n for (let y = 0; y < h; y++) out[y * w + x] = line[y];\n }\n return out;\n}\n\n/**\n * Grows (positive) or shrinks (negative) soft coverage by a fractional distance: whole pixels\n * use one morphology, the remainder blends with the next pixel, so 0.3 px differs from 1 px.\n */\nexport function shiftCoverage(src: ArrayLike<number>, w: number, h: number, amount: number): Float32Array {\n const mag = Math.abs(amount);\n if (!Number.isFinite(mag) || mag < 1e-4) return Float32Array.from(src);\n const grow = amount > 0;\n const whole = Math.floor(mag);\n const frac = mag - whole;\n if (frac < 1e-4) return greyMorphology(src, w, h, whole, grow);\n const lo = whole === 0 ? Float32Array.from(src) : greyMorphology(src, w, h, whole, grow);\n const hi = greyMorphology(src, w, h, whole + 1, grow);\n for (let i = 0; i < lo.length; i++) lo[i] += (hi[i] - lo[i]) * frac;\n return lo;\n}\n\n// ---------------------------------------------------------------------------\n// Selection-level wrappers\n// ---------------------------------------------------------------------------\n\nexport type SelectionModifyOp = 'grow' | 'shrink' | 'feather' | 'border' | 'smooth';\n\nexport const SELECTION_MODIFY_OPS: readonly SelectionModifyOp[] = ['grow', 'shrink', 'feather', 'border', 'smooth'];\n\nfunction wrap(mask: SelectionMask, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(mask.width, mask.height, out);\n}\n\n/** Select › Modify (grow = Expand, shrink = Contract, feather, border, smooth) by `radius` pixels. */\nexport function modifySelection(mask: SelectionMask, op: SelectionModifyOp, radius: number): SelectionMask {\n const r = Number.isFinite(radius) ? Math.max(0, radius) : 0;\n if (r === 0 && op !== 'border') return mask;\n const m = selectionToCoverage(mask);\n const { width: w, height: h } = mask;\n switch (op) {\n case 'grow':\n return wrap(mask, expandCoverage(m, w, h, r));\n case 'shrink':\n return wrap(mask, contractCoverage(m, w, h, r));\n case 'feather':\n return wrap(mask, featherCoverage(m, w, h, r));\n case 'border':\n return wrap(mask, borderCoverage(m, w, h, r));\n case 'smooth':\n return wrap(mask, smoothCoverage(m, w, h, r));\n default:\n throw new Error(`Unknown selection modification \"${op as string}\"`);\n }\n}\n","// Portions ported from photocraft/crates/engine/src/layer_mask_props_cmds.rs and photocraft/crates/compose/src/masks.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentLayer, AdjustmentLayerMask } from '../types';\nimport { unprojectCanvasPoint, type ImageProjection } from '../geometry';\nimport {\n createSelectionMask,\n decodeSelectionRle,\n encodeSelectionRle,\n hashMaskContent,\n type SelectionMask,\n} from './mask';\nimport { gaussianBlur } from './modify';\n\n/** Properties of a layer mask that leave its pixels intact (Photoshop's Properties › Masks). */\nexport interface LayerMaskProps {\n /** 0..1 (default 1): at 0.5 the masked-out areas still get half of the adjustment. */\n density?: number;\n /** Gaussian feather σ in pixels, 0..1000 (default 0). */\n feather?: number;\n /** Disabled masks are kept but ignored (the layer applies everywhere). Default true. */\n enabled?: boolean;\n}\n\nconst MAX_SIDE = 1 << 15;\n\n/**\n * A clean layer mask from stored data, or `null` when it is unusable. Defaults are left out\n * (density 1, feather 0, enabled), so a plain mask serializes as `{width, height, rle}`. The\n * RLE is only decoded when rendering.\n */\nexport function normalizeLayerMask(raw: unknown): AdjustmentLayerMask | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, unknown>;\n const { width, height, rle } = r;\n if (\n typeof width !== 'number' ||\n typeof height !== 'number' ||\n !Number.isInteger(width) ||\n !Number.isInteger(height) ||\n width < 1 ||\n height < 1 ||\n width > MAX_SIDE ||\n height > MAX_SIDE ||\n typeof rle !== 'string' ||\n !rle.startsWith('r1:')\n ) {\n return null;\n }\n const out: AdjustmentLayerMask = { width, height, rle };\n if (typeof r.density === 'number' && Number.isFinite(r.density)) {\n const d = Math.min(1, Math.max(0, r.density));\n if (d !== 1) out.density = d;\n }\n if (typeof r.feather === 'number' && Number.isFinite(r.feather)) {\n const f = Math.min(1000, Math.max(0, r.feather));\n if (f !== 0) out.feather = f;\n }\n if (r.enabled === false) out.enabled = false;\n return out;\n}\n\n/** A layer mask holding the selection's pixels (Add Layer Mask › Reveal Selection). */\nexport function layerMaskFromSelection(selection: SelectionMask, props: LayerMaskProps = {}): AdjustmentLayerMask {\n return normalizeLayerMask({ width: selection.width, height: selection.height, rle: encodeSelectionRle(selection.data), ...props })!;\n}\n\n/** Applies property changes; `undefined` fields keep their value. */\nexport function updateLayerMaskProps(mask: AdjustmentLayerMask, props: LayerMaskProps): AdjustmentLayerMask {\n return normalizeLayerMask({\n ...mask,\n ...(props.density !== undefined ? { density: props.density } : {}),\n ...(props.feather !== undefined ? { feather: props.feather } : {}),\n ...(props.enabled !== undefined ? { enabled: props.enabled } : {}),\n })!;\n}\n\n// Small LRU caches: masks are stored as RLE strings and layers are deep-copied often.\nconst DECODE_LIMIT = 8;\nconst decoded = new Map<string, SelectionMask | null>();\nconst coverageCache = new Map<string, Float32Array | null>();\n\nfunction lru<T>(cache: Map<string, T>, key: string, make: () => T): T {\n if (cache.has(key)) {\n const v = cache.get(key)!;\n cache.delete(key);\n cache.set(key, v);\n return v;\n }\n const v = make();\n cache.set(key, v);\n if (cache.size > DECODE_LIMIT) cache.delete(cache.keys().next().value as string);\n return v;\n}\n\n/** The mask's pixels as a selection (Load Selection from mask), or `null` when the data is damaged. */\nexport function decodeLayerMask(mask: AdjustmentLayerMask): SelectionMask | null {\n const key = `${mask.width}x${mask.height}:${mask.rle}`;\n return lru(decoded, key, () => {\n try {\n return createSelectionMask(mask.width, mask.height, decodeSelectionRle(mask.rle, mask.width * mask.height));\n } catch {\n return null;\n }\n });\n}\n\n/** Cache key of everything about a mask that changes pixels. */\nexport function layerMaskKey(mask: AdjustmentLayerMask | undefined): string {\n if (!mask) return '';\n return `${mask.width}x${mask.height}:${hashMaskContent(mask.rle)}:${mask.density ?? 1}:${mask.feather ?? 0}:${mask.enabled !== false}`;\n}\n\n/**\n * The mask's effective coverage (0..1, mask resolution): feathered (σ = feather, borders\n * replicated), then density applied as `1 − density·(1 − v)`. `null` for a disabled or damaged\n * mask, meaning \"no mask\".\n */\nexport function layerMaskCoverage(mask: AdjustmentLayerMask): Float32Array | null {\n if (mask.enabled === false) return null;\n return lru(coverageCache, layerMaskKey(mask), () => {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n let v: Float32Array = new Float32Array(sel.data.length);\n for (let i = 0; i < v.length; i++) v[i] = sel.data[i] / 255;\n const feather = mask.feather ?? 0;\n if (feather > 0) v = gaussianBlur(v, mask.width, mask.height, feather, 'clamp');\n const density = mask.density ?? 1;\n if (density !== 1) for (let i = 0; i < v.length; i++) v[i] = 1 - density * (1 - v[i]);\n return v;\n });\n}\n\n/**\n * Resamples mask coverage (natural-image aligned, any resolution) into a target raster drawn\n * through `projection` (the base canvas of the renderer: crop, rotation, flips): each target\n * pixel centre is unprojected to natural pixels and sampled bilinearly.\n */\nexport function projectMaskCoverage(\n coverage: Float32Array,\n maskWidth: number,\n maskHeight: number,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Float32Array {\n const out = new Float32Array(targetWidth * targetHeight);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, projection);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, projection);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, projection);\n const kx = maskWidth / naturalWidth;\n const ky = maskHeight / naturalHeight;\n // Affine map target → mask coordinates (pixel centres at +0.5).\n const ux = (ox.x - o.x) * kx;\n const vx = (ox.y - o.y) * ky;\n const uy = (oy.x - o.x) * kx;\n const vy = (oy.y - o.y) * ky;\n const u0 = o.x * kx - 0.5;\n const v0 = o.y * ky - 0.5;\n const mw1 = maskWidth - 1;\n const mh1 = maskHeight - 1;\n for (let y = 0; y < targetHeight; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n const row = y * targetWidth;\n for (let x = 0; x < targetWidth; x++, u += ux, v += vx) {\n const fu = u < 0 ? 0 : u > mw1 ? mw1 : u;\n const fv = v < 0 ? 0 : v > mh1 ? mh1 : v;\n const x0 = fu | 0;\n const y0 = fv | 0;\n const x1 = x0 < mw1 ? x0 + 1 : x0;\n const y1 = y0 < mh1 ? y0 + 1 : y0;\n const tx = fu - x0;\n const ty = fv - y0;\n const a = coverage[y0 * maskWidth + x0] * (1 - tx) + coverage[y0 * maskWidth + x1] * tx;\n const b = coverage[y1 * maskWidth + x0] * (1 - tx) + coverage[y1 * maskWidth + x1] * tx;\n out[row + x] = a * (1 - ty) + b * ty;\n }\n }\n return out;\n}\n\n/**\n * Per-layer mask coverage for a render target, keyed by layer id; `undefined` when no layer has\n * an enabled, valid mask (so mask-free stacks take the exact same path as before).\n */\nexport function buildLayerMaskPlanes(\n layers: readonly AdjustmentLayer[] | undefined,\n naturalWidth: number,\n naturalHeight: number,\n projection: ImageProjection,\n targetWidth: number,\n targetHeight: number\n): Map<string, Float32Array> | undefined {\n if (!layers?.some((l) => l.mask && l.mask.enabled !== false)) return undefined;\n const planes = new Map<string, Float32Array>();\n for (const layer of layers) {\n if (!layer.mask) continue;\n const coverage = layerMaskCoverage(layer.mask);\n if (!coverage) continue;\n planes.set(\n layer.id,\n projectMaskCoverage(coverage, layer.mask.width, layer.mask.height, naturalWidth, naturalHeight, projection, targetWidth, targetHeight)\n );\n }\n return planes.size ? planes : undefined;\n}\n","// Portions ported from photocraft-main/crates/compose/src/adjust.rs, photocraft-main/crates/doc/src/adjust.rs and photocraft-main/crates/color/src/convert.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentLayerMask,\n AdjustmentLayerOf,\n AdjustmentParamsMap,\n BlackWhiteParams,\n ColorBalanceParams,\n ColorBalanceTone,\n CurvePoint,\n HueRange,\n HueSaturationParams,\n LevelsChannel,\n PhotoFilterParams,\n VibranceParams,\n} from './types';\nimport { type BlendMode, blendRgb, normalizeBlendMode, setLum } from './blend';\nimport { linearToSrgb, srgbToLinear } from './color';\nimport { layerMaskKey, normalizeLayerMask } from './selection/layer-mask';\n\n/**\n * Adjustment layers: Photoshop-matching evaluation on encoded sRGB colours in 0..1.\n *\n * Every layer is compiled once per parameter change into a per-pixel function (per-channel kinds\n * into three 4096-entry LUTs); {@link applyAdjustmentLayers} runs the stack on an ImageData.\n */\n\ntype Rgb = [number, number, number];\n\n/** Entries in a tone LUT (curves, levels, colour balance). */\nexport const ADJUSTMENT_LUT_SIZE = 4096;\n\nconst clamp01 = (v: number) => (v <= 0 ? 0 : v >= 1 ? 1 : v);\nconst finite = (v: unknown, fallback: number) => (typeof v === 'number' && Number.isFinite(v) ? v : fallback);\nconst clampTo = (v: unknown, lo: number, hi: number, fallback: number) => Math.min(hi, Math.max(lo, finite(v, fallback)));\nconst remEuclid = (a: number, b: number) => ((a % b) + b) % b;\n\n/** Linear interpolation into a 0..1-domain LUT. */\nexport function sampleLut(table: ArrayLike<number>, v: number): number {\n const n = table.length - 1;\n const x = clamp01(v) * n;\n const i = Math.floor(x);\n const j = Math.min(i + 1, n);\n const f = x - i;\n return table[i] * (1 - f) + table[j] * f;\n}\n\n// ---------------------------------------------------------------------------\n// Kinds, labels, defaults\n// ---------------------------------------------------------------------------\n\nexport interface AdjustmentKindInfo {\n kind: AdjustmentKind;\n label: string;\n}\n\nexport const ADJUSTMENT_KINDS: readonly AdjustmentKindInfo[] = [\n { kind: 'curves', label: 'Curves' },\n { kind: 'levels', label: 'Levels' },\n { kind: 'hueSaturation', label: 'Hue/Saturation' },\n { kind: 'vibrance', label: 'Vibrance' },\n { kind: 'colorBalance', label: 'Color Balance' },\n { kind: 'blackWhite', label: 'Black & White' },\n { kind: 'photoFilter', label: 'Photo Filter' },\n { kind: 'invert', label: 'Invert' },\n { kind: 'posterize', label: 'Posterize' },\n { kind: 'threshold', label: 'Threshold' },\n];\n\nconst KIND_LABELS = new Map(ADJUSTMENT_KINDS.map((k) => [k.kind, k.label]));\n\nexport function isAdjustmentKind(value: unknown): value is AdjustmentKind {\n return typeof value === 'string' && KIND_LABELS.has(value as AdjustmentKind);\n}\n\nexport function adjustmentKindLabel(kind: AdjustmentKind): string {\n return KIND_LABELS.get(kind) ?? kind;\n}\n\nexport const HUE_RANGE_LABELS = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'] as const;\n\n/** The neutral range `i` (0 reds … 5 magentas): fully on for 30° around its centre, fading over 30° each side. */\nexport function neutralHueRange(i: number): HueRange {\n const c = 60 * i;\n return { hue: 0, saturation: 0, lightness: 0, bounds: [c - 45, c - 15, c + 15, c + 45] };\n}\n\nexport const identityCurve = (): CurvePoint[] => [\n { input: 0, output: 0 },\n { input: 255, output: 255 },\n];\n\nexport const identityLevels = (): LevelsChannel => ({ inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 });\n\n/** Neutral parameters of every kind (fresh objects on every call). */\nexport function defaultAdjustmentParams<K extends AdjustmentKind>(kind: K): AdjustmentParamsMap[K] {\n const defaults: { [P in AdjustmentKind]: () => AdjustmentParamsMap[P] } = {\n curves: () => ({ master: identityCurve(), red: identityCurve(), green: identityCurve(), blue: identityCurve() }),\n levels: () => ({ master: identityLevels(), red: identityLevels(), green: identityLevels(), blue: identityLevels() }),\n hueSaturation: () => ({\n hue: 0,\n saturation: 0,\n lightness: 0,\n colorize: false,\n ranges: [0, 1, 2, 3, 4, 5].map(neutralHueRange),\n }),\n vibrance: () => ({ vibrance: 0, saturation: 0 }),\n colorBalance: () => ({ shadows: [0, 0, 0], midtones: [0, 0, 0], highlights: [0, 0, 0], preserveLuminosity: true }),\n blackWhite: () => ({ reds: 40, yellows: 60, greens: 40, cyans: 60, blues: 20, magentas: 80, tint: null }),\n invert: () => ({}) as Record<string, never>,\n posterize: () => ({ levels: 4 }),\n threshold: () => ({ level: 128 }),\n photoFilter: () => ({ color: '#ec8a00', density: 25, preserveLuminosity: true }),\n };\n return defaults[kind]() as AdjustmentParamsMap[K];\n}\n\nlet layerCounter = 0;\n\n/** A new layer of `kind` with neutral (or the given) parameters, enabled, opacity 1, normal blend. */\nexport function createAdjustmentLayer<K extends AdjustmentKind>(\n kind: K,\n options: {\n id?: string;\n name?: string;\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n mask?: AdjustmentLayerMask;\n } = {}\n): AdjustmentLayerOf<K> {\n const raw = {\n id: options.id ?? `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind,\n name: options.name,\n params: { ...defaultAdjustmentParams(kind), ...(options.params ?? {}) },\n enabled: options.enabled ?? true,\n opacity: options.opacity ?? 1,\n blend: options.blend ?? 'normal',\n mask: options.mask,\n };\n return normalizeAdjustmentLayer(raw) as AdjustmentLayerOf<K>;\n}\n\n// ---------------------------------------------------------------------------\n// Normalisation of stored layers\n// ---------------------------------------------------------------------------\n\nconst HEX = /^#[0-9a-f]{6}$/i;\n\nfunction normCurve(points: unknown): CurvePoint[] {\n if (!Array.isArray(points)) return identityCurve();\n const out = points\n .filter((p) => p && typeof p === 'object')\n .map((p) => ({ input: clampTo(p.input, 0, 255, 0), output: clampTo(p.output, 0, 255, 0) }))\n .sort((a, b) => a.input - b.input);\n return out.length >= 2 ? out : identityCurve();\n}\n\nfunction normLevels(ch: unknown): LevelsChannel {\n const c = (ch && typeof ch === 'object' ? ch : {}) as Partial<LevelsChannel>;\n return {\n inBlack: clampTo(c.inBlack, 0, 254, 0),\n inWhite: clampTo(c.inWhite, 1, 255, 255),\n gamma: clampTo(c.gamma, 0.1, 9.99, 1),\n outBlack: clampTo(c.outBlack, 0, 255, 0),\n outWhite: clampTo(c.outWhite, 0, 255, 255),\n };\n}\n\nfunction normTone(t: unknown): ColorBalanceTone {\n const a = Array.isArray(t) ? t : [];\n return [clampTo(a[0], -100, 100, 0), clampTo(a[1], -100, 100, 0), clampTo(a[2], -100, 100, 0)];\n}\n\nfunction normParams(kind: AdjustmentKind, raw: unknown): AdjustmentParamsMap[AdjustmentKind] {\n const p = (raw && typeof raw === 'object' ? raw : {}) as Record<string, any>;\n const d = defaultAdjustmentParams(kind) as Record<string, any>;\n switch (kind) {\n case 'curves':\n return { master: normCurve(p.master), red: normCurve(p.red), green: normCurve(p.green), blue: normCurve(p.blue) };\n case 'levels':\n return { master: normLevels(p.master), red: normLevels(p.red), green: normLevels(p.green), blue: normLevels(p.blue) };\n case 'hueSaturation': {\n const ranges: HueRange[] = [0, 1, 2, 3, 4, 5].map((i) => {\n const r = Array.isArray(p.ranges) && p.ranges[i] && typeof p.ranges[i] === 'object' ? p.ranges[i] : {};\n const n = neutralHueRange(i);\n const b = Array.isArray(r.bounds) && r.bounds.length === 4 && r.bounds.every((v: unknown) => Number.isFinite(v)) ? r.bounds : n.bounds;\n return {\n hue: clampTo(r.hue, -180, 180, 0),\n saturation: clampTo(r.saturation, -100, 100, 0),\n lightness: clampTo(r.lightness, -100, 100, 0),\n bounds: [b[0], b[1], b[2], b[3]] as [number, number, number, number],\n };\n });\n const colorize = p.colorize === true;\n return {\n hue: colorize ? clampTo(p.hue, 0, 360, 0) : clampTo(p.hue, -180, 180, 0),\n saturation: clampTo(p.saturation, -100, 100, 0),\n lightness: clampTo(p.lightness, -100, 100, 0),\n colorize,\n ranges,\n } satisfies HueSaturationParams;\n }\n case 'vibrance':\n return { vibrance: clampTo(p.vibrance, -100, 100, 0), saturation: clampTo(p.saturation, -100, 100, 0) } satisfies VibranceParams;\n case 'colorBalance':\n return {\n shadows: normTone(p.shadows),\n midtones: normTone(p.midtones),\n highlights: normTone(p.highlights),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies ColorBalanceParams;\n case 'blackWhite': {\n const out = {} as BlackWhiteParams;\n for (const k of ['reds', 'yellows', 'greens', 'cyans', 'blues', 'magentas'] as const) out[k] = clampTo(p[k], -200, 300, d[k]);\n out.tint = typeof p.tint === 'string' && HEX.test(p.tint) ? p.tint.toLowerCase() : null;\n return out;\n }\n case 'photoFilter':\n return {\n color: typeof p.color === 'string' && HEX.test(p.color) ? p.color.toLowerCase() : d.color,\n density: clampTo(p.density, 0, 100, d.density),\n preserveLuminosity: p.preserveLuminosity !== false,\n } satisfies PhotoFilterParams;\n case 'posterize':\n return { levels: Math.round(clampTo(p.levels, 2, 255, 4)) };\n case 'threshold':\n return { level: Math.round(clampTo(p.level, 1, 255, 128)) };\n case 'invert':\n return {} as Record<string, never>;\n }\n}\n\n/**\n * A clean, deep-copied layer from stored or hand-written data: missing fields get neutral\n * defaults, numbers are clamped to their ranges. `null` for an unknown kind.\n */\nexport function normalizeAdjustmentLayer(raw: unknown): AdjustmentLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n if (!isAdjustmentKind(r.kind)) return null;\n const layer: Record<string, unknown> = {\n id: typeof r.id === 'string' && r.id ? r.id : `adj_${Date.now().toString(36)}_${(++layerCounter).toString(36)}`,\n kind: r.kind,\n enabled: r.enabled !== false,\n opacity: clampTo(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n params: normParams(r.kind, r.params),\n };\n if (typeof r.name === 'string' && r.name) layer.name = r.name;\n // Only present when the layer has a usable mask, so mask-free layers serialize as before.\n const mask = normalizeLayerMask(r.mask);\n if (mask) layer.mask = mask;\n return layer as unknown as AdjustmentLayer;\n}\n\n/** Normalises a stored layer list (drops unknown kinds, de-duplicates ids). */\nexport function normalizeAdjustmentLayers(raw: unknown): AdjustmentLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const out: AdjustmentLayer[] = [];\n for (const item of raw) {\n const layer = normalizeAdjustmentLayer(item);\n if (!layer) continue;\n while (seen.has(layer.id)) layer.id = `${layer.id}_${(++layerCounter).toString(36)}`;\n seen.add(layer.id);\n out.push(layer);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Curves\n// ---------------------------------------------------------------------------\n\n/**\n * Photoshop curve: a natural cubic spline through the points (0..1 in and out; it may overshoot\n * between points, as Photoshop's does), constant outside the first / last point, clamped to\n * 0..1, sampled into a LUT. Fewer than two distinct points: the identity.\n */\nexport function curveLut(points: ReadonlyArray<{ input: number; output: number }>, size = ADJUSTMENT_LUT_SIZE): Float32Array {\n const pts = points\n .filter((p) => Number.isFinite(p.input) && Number.isFinite(p.output))\n .map((p) => [p.input, p.output] as [number, number])\n .sort((a, b) => a[0] - b[0]);\n const dedup: [number, number][] = [];\n for (const p of pts) {\n if (dedup.length && Math.abs(dedup[dedup.length - 1][0] - p[0]) < 1e-6) continue;\n dedup.push(p);\n }\n const out = new Float32Array(size);\n const n = dedup.length;\n if (n < 2) {\n for (let k = 0; k < size; k++) out[k] = k / (size - 1);\n return out;\n }\n const x = dedup.map((p) => p[0]);\n const y = dedup.map((p) => p[1]);\n // Second derivatives of the natural spline (tridiagonal solve).\n const m2 = new Float64Array(n);\n if (n > 2) {\n const c = new Float64Array(n);\n const d = new Float64Array(n);\n for (let i = 1; i < n - 1; i++) {\n const h0 = x[i] - x[i - 1];\n const h1 = x[i + 1] - x[i];\n const a = h0 / 6;\n const b = (h0 + h1) / 3;\n const cc = h1 / 6;\n const r = (y[i + 1] - y[i]) / h1 - (y[i] - y[i - 1]) / h0;\n const denom = b - a * c[i - 1];\n c[i] = cc / denom;\n d[i] = (r - a * d[i - 1]) / denom;\n }\n for (let i = n - 2; i >= 1; i--) m2[i] = d[i] - c[i] * m2[i + 1];\n }\n let seg = 0;\n for (let k = 0; k < size; k++) {\n const xv = k / (size - 1);\n if (xv <= x[0]) {\n out[k] = clamp01(y[0]);\n continue;\n }\n if (xv >= x[n - 1]) {\n out[k] = clamp01(y[n - 1]);\n continue;\n }\n while (seg < n - 2 && xv > x[seg + 1]) seg++;\n const x0 = x[seg], x1 = x[seg + 1];\n const h = x1 - x0;\n const a = (x1 - xv) / h;\n const b = (xv - x0) / h;\n const v = a * y[seg] + b * y[seg + 1] + ((a * a * a - a) * m2[seg] + (b * b * b - b) * m2[seg + 1]) * (h * h) / 6;\n out[k] = clamp01(v);\n }\n return out;\n}\n\n/** {@link curveLut} for Curves points in levels (0..255). */\nexport function curveLutFromLevels(points: readonly CurvePoint[]): Float32Array {\n return curveLut(points.map((p) => ({ input: p.input / 255, output: p.output / 255 })));\n}\n\n// ---------------------------------------------------------------------------\n// Levels\n// ---------------------------------------------------------------------------\n\n/** A Levels record with values in 0..1 (as `levelsValue` takes them). */\nexport interface UnitLevels {\n inBlack: number;\n inWhite: number;\n gamma: number;\n outBlack: number;\n outWhite: number;\n}\n\nconst toUnitLevels = (c: LevelsChannel): UnitLevels => ({\n inBlack: c.inBlack / 255,\n inWhite: c.inWhite / 255,\n gamma: c.gamma,\n outBlack: c.outBlack / 255,\n outWhite: c.outWhite / 255,\n});\n\n/**\n * One Levels record on `v` (0..1). With `quantum` (8-bit: 255) Photoshop works on whole levels:\n * the input is a level and the stretch is rounded (half up) before the gamma. A gamma above 1 has\n * Photoshop's soft shadow toe: a p-norm soft-min of the power curve and a line of slope ≈ 2^gamma.\n */\nexport function levelsValue(ch: UnitLevels, v: number, quantum?: number): number {\n const range = Math.max(1e-6, ch.inWhite - ch.inBlack);\n let u: number;\n if (quantum !== undefined && Number.isFinite(quantum) && quantum >= 1) {\n const q = quantum;\n const vq = Math.round(v * q) / q;\n u = Math.floor(clamp01((vq - ch.inBlack) / range) * q + 0.5 + 1e-3) / q;\n } else {\n u = clamp01((v - ch.inBlack) / range);\n }\n const g = Math.max(0.01, ch.gamma);\n let t: number;\n if (g > 1) {\n const power = Math.pow(u, 1 / g);\n const line = 0.93 * Math.pow(2, g) * u;\n if (power <= 1e-6 || line <= 1e-6) t = Math.min(power, line);\n else {\n const p = 10;\n t = Math.pow(Math.pow(power, -p) + Math.pow(line, -p), -1 / p);\n }\n } else {\n t = Math.pow(u, 1 / g);\n }\n return ch.outBlack + t * (ch.outWhite - ch.outBlack);\n}\n\n// ---------------------------------------------------------------------------\n// HSL and Hue/Saturation\n// ---------------------------------------------------------------------------\n\n/** RGB (0..1) → HSL with hue in turns (0..1). */\nexport function rgbToHsl(c: readonly number[]): [number, number, number] {\n const max = Math.max(c[0], c[1], c[2]);\n const min = Math.min(c[0], c[1], c[2]);\n const l = (max + min) / 2;\n if (Math.abs(max - min) < 1e-7) return [0, 0, l];\n const d = max - min;\n const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);\n let h: number;\n if (max === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (max === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return [h / 6, s, l];\n}\n\nexport function hslToRgb(h: number, s: number, l: number): Rgb {\n if (s <= 0) return [l, l, l];\n const q = l < 0.5 ? l * (1 + s) : l + s - l * s;\n const p = 2 * l - q;\n const f = (t: number) => {\n t = remEuclid(t, 1);\n if (t < 1 / 6) return p + (q - p) * 6 * t;\n if (t < 0.5) return q;\n if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;\n return p;\n };\n return [f(h + 1 / 3), f(h), f(h - 1 / 3)];\n}\n\n/** How much a pixel of hue `deg` belongs to `range` (0..1): a trapezoid that may wrap past 0/360. */\nexport function hueRangeWeight(range: Pick<HueRange, 'bounds'>, deg: number): number {\n const [a, b0, c0, d0] = range.bounds;\n const rel = (x: number) => (Number.isFinite(x) && Number.isFinite(a) ? remEuclid(x - a, 360) : 0);\n const b = rel(b0);\n const c = Math.max(rel(c0), b);\n const d = Math.max(rel(d0), c);\n const h = rel(deg);\n if (h <= b) return b <= 0 ? 1 : h / b;\n if (h <= c) return 1;\n if (h <= d) return d <= c ? 1 : (d - h) / (d - c);\n return 0;\n}\n\nconst isNeutralRange = (r: HueRange) => r.hue === 0 && r.saturation === 0 && r.lightness === 0;\n\n/** Range edits as three hue-indexed tables (hue shift in degrees, saturation and lightness as fractions). */\nexport function hueRangeTables(ranges: readonly HueRange[]): [Float32Array, Float32Array, Float32Array] {\n const n = ADJUSTMENT_LUT_SIZE;\n const t: [Float32Array, Float32Array, Float32Array] = [new Float32Array(n), new Float32Array(n), new Float32Array(n)];\n const active = ranges.filter((r) => !isNeutralRange(r));\n for (let k = 0; k < n; k++) {\n const deg = (k / (n - 1)) * 360;\n for (const r of active) {\n const w = hueRangeWeight(r, deg);\n t[0][k] += w * r.hue;\n t[1][k] += (w * r.saturation) / 100;\n t[2][k] += (w * r.lightness) / 100;\n }\n }\n return t;\n}\n\n/**\n * One Hue/Saturation evaluation: hue shift in degrees, saturation and lightness in -1..1.\n * Lightness first, then hue and saturation (raising divides by 1 − s, lowering multiplies by\n * 1 + s). Colorize sets hue and saturation at the lightened lightness.\n */\nexport function hueSaturation(c: readonly number[], hue: number, s: number, l: number, colorize: boolean): Rgb {\n const lighten = (v: number) => (l > 0 ? v + (1 - v) * l : v * (1 + l));\n if (colorize) {\n const ll = rgbToHsl(c)[2];\n return hslToRgb(remEuclid(hue, 360) / 360, Math.min(1, Math.abs(s)), lighten(ll));\n }\n const [hh0, ss0, ll] = rgbToHsl([lighten(c[0]), lighten(c[1]), lighten(c[2])]);\n const hh = remEuclid(hh0 + hue / 360, 1);\n const ss = Math.min(1, Math.max(0, s > 0 ? ss0 / Math.max(1 / 512, 1 - s) : ss0 * (1 + s)));\n return hslToRgb(hh, ss, ll);\n}\n\n/** A colour range's Lightness: towards the brightest channel when raised, the darkest when lowered. */\nexport function rangeLightness(c: readonly number[], l: number): Rgb {\n const k = Math.min(1, Math.max(-1, l));\n const target = k > 0 ? Math.max(c[0], c[1], c[2]) : Math.min(c[0], c[1], c[2]);\n const a = Math.abs(k);\n return [c[0] + (target - c[0]) * a, c[1] + (target - c[1]) * a, c[2] + (target - c[2]) * a];\n}\n\n// ---------------------------------------------------------------------------\n// Vibrance\n// ---------------------------------------------------------------------------\n\n/** Grey Vibrance › Saturation desaturates to (linear light; blue has no weight). */\nexport const VIBRANCE_GREY: readonly [number, number] = [0.288, 0.712];\nconst VIBRANCE_BOOST = [0.5001, 0.0887, 0.4374, 0.7573, -0.8113, 0.5154, 2.8026] as const;\n\nfunction vibranceSkin(h: number): number {\n if (h <= 30) return 1;\n if (h < 45) return (45 - h) / 15;\n if (h < 300) return 0;\n return (h - 300) / 60;\n}\n\nfunction hsvHue(c: readonly number[], mx: number, mn: number): number {\n const d = mx - mn;\n let h: number;\n if (mx === c[0]) h = remEuclid((c[1] - c[2]) / d, 6);\n else if (mx === c[1]) h = (c[2] - c[0]) / d + 2;\n else h = (c[0] - c[1]) / d + 4;\n return h * 60;\n}\n\n/** The Vibrance slider (`v` in -1..1) on a linear-light pixel: an HSV change that keeps the hue. */\nexport function vibranceLinear(c: Rgb, v: number): Rgb {\n const mx = Math.max(c[0], c[1], c[2]);\n const mn = Math.min(c[0], c[1], c[2]);\n if (mx <= 0 || mx - mn <= 1e-9) return c;\n const t = 1 - mn / mx;\n const shape = t * (1 - t) * (2 - t + t * t) * mx * Math.max(0, 1 - mx);\n let t2: number, v2: number;\n if (v < 0) {\n const a = -v;\n t2 = t * (1 - a / 4) * (1 - a + (a * t * (1 + t)) / 2);\n v2 = mx - a * shape;\n } else {\n const [c0, c1, p, q, r0, ra, rt] = VIBRANCE_BOOST;\n const w = vibranceSkin(hsvHue(c, mx, mn));\n const a = v * (1 - w) + 0.64 * Math.pow(v, 1.6) * w;\n const boost =\n (c0 * a + c1 * a * a) * Math.pow(t, p) * Math.pow(1 - t, q) * Math.exp((r0 + ra * a + rt * (1 - t)) * Math.max(0, 1 - mx));\n t2 = Math.min(1, t * (1 + boost));\n v2 = mx + (v / 4) * shape;\n }\n const m2 = v2 * (1 - t2);\n const k = (v2 - m2) / (mx - mn);\n return [m2 + (c[0] - mn) * k, m2 + (c[1] - mn) * k, m2 + (c[2] - mn) * k];\n}\n\n/** Photoshop's Vibrance on one encoded sRGB pixel (`v`, `s`: the sliders / 100). */\nexport function vibrancePixel(c: readonly number[], v: number, s: number): Rgb {\n let l: Rgb = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n if (v !== 0) l = vibranceLinear(l, v);\n if (s !== 0) {\n const g = VIBRANCE_GREY[0] * l[0] + VIBRANCE_GREY[1] * l[1];\n l = l.map((x) => Math.min(1, Math.max(0, g + (1 + s) * (x - g)))) as Rgb;\n }\n return l.map((x) => linearToSrgb(clamp01(x))) as Rgb;\n}\n\n// ---------------------------------------------------------------------------\n// Color Balance\n// ---------------------------------------------------------------------------\n\n/** How far one Color Balance slider at ±100 moves the black or white point. */\nconst BALANCE_LEVELS = 0.39;\n\n/**\n * Color Balance on channel `ch` (0 red, 1 green, 2 blue) of `v` (0..1); `tones` are the\n * shadows, midtones and highlights slider triples (-100..100). Highlights, shadows, then midtones.\n */\nexport function colorBalanceChannel(\n tones: readonly [readonly number[], readonly number[], readonly number[]],\n preserveLuminosity: boolean,\n ch: number,\n v: number\n): number {\n const [shadows, midtones, highlights] = tones;\n const black = (x: number, b: number) => Math.max(0, (x - b) / (1 - b));\n const white = (x: number, w: number) => Math.min(1, x / w);\n let out = clamp01(v);\n for (const [tone, sliders] of [\n [2, highlights],\n [0, shadows],\n [1, midtones],\n ] as const) {\n const a = [0, 1, 2].map((i) => Math.min(1, Math.max(-1, sliders[i] / 100)));\n if (preserveLuminosity) {\n const own = a[ch] ?? 0;\n const lo = Math.min(a[0], a[1], a[2]);\n const hi = Math.max(a[0], a[1], a[2]);\n if (tone === 0) out = black(out, BALANCE_LEVELS * (hi - own));\n else if (tone === 1) out = Math.pow(out, Math.pow(2, (hi + lo) / 2 - own));\n else out = white(out, 1 - BALANCE_LEVELS * (own - lo));\n continue;\n }\n const s = a[ch];\n if (s === 0) continue;\n if (tone === 1) out = Math.pow(out, Math.pow(2, -s));\n else if (tone === 0 && s < 0) out = Math.pow(black(out, BALANCE_LEVELS * -s), Math.pow(2, -s / 2));\n else if (tone === 2 && s > 0) out = Math.pow(white(out, 1 - BALANCE_LEVELS * s), Math.pow(2, -s / 2));\n else out = Math.pow(out, Math.pow(2, -s / 2));\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Black & White, Photo Filter, Posterize, Threshold\n// ---------------------------------------------------------------------------\n\n/**\n * Black & White grey: the smallest channel, plus the secondary part (middle − smallest) and the\n * primary part (largest − middle) weighted by the sliders (percent: reds, yellows, greens, cyans,\n * blues, magentas).\n */\nexport function blackWhiteGray(c: readonly number[], weights: readonly number[]): number {\n const [r, g, b] = c;\n const max = Math.max(r, g, b);\n const min = Math.min(r, g, b);\n const mid = r + g + b - max - min;\n const primary = r >= g && r >= b ? 0 : g >= b ? 2 : 4;\n const secondary = b <= r && b <= g ? 1 : r <= g ? 3 : 5;\n return clamp01(min + ((mid - min) * weights[secondary]) / 100 + ((max - mid) * weights[primary]) / 100);\n}\n\n/** D50 white (ICC PCS). */\nexport const D50 = [0.96422, 1.0, 0.82521] as const;\n/** Linear sRGB → D50 XYZ (Bradford-adapted, as in the ICC sRGB profile). */\nconst SRGB_TO_XYZ_D50 = [\n [0.436074, 0.385064, 0.14308],\n [0.222504, 0.716878, 0.060618],\n [0.013932, 0.097104, 0.714173],\n] as const;\n/** D50 XYZ → linear sRGB. */\nexport const XYZ_D50_TO_SRGB = [\n [3.133856, -1.616867, -0.490615],\n [-0.978768, 1.916142, 0.033454],\n [0.071945, -0.228991, 1.405243],\n] as const;\n\n/**\n * Photo Filter's linear-light RGB matrix (row-major 3×3): the pixel's D50 XYZ, relative to white,\n * multiplied by the filter colour's mixed in by `density` (0..1). `normalizeY` keeps luminance Y.\n */\nexport function photoFilterMatrix(color: readonly number[], density: number, normalizeY = false): number[][] {\n const d = Number.isFinite(density) ? clamp01(density) : 0;\n const lin = [0, 1, 2].map((i) => srgbToLinear(clamp01(color[i])));\n const xyz = SRGB_TO_XYZ_D50.map((r) => r[0] * lin[0] + r[1] * lin[1] + r[2] * lin[2]);\n let s = [0, 1, 2].map((i) => 1 - d + (d * xyz[i]) / D50[i]);\n if (normalizeY && s[1] > 1e-6) {\n const y = s[1];\n s = s.map((v) => v / y);\n }\n return [0, 1, 2].map((r) =>\n [0, 1, 2].map((c) => XYZ_D50_TO_SRGB[r][0] * s[0] * SRGB_TO_XYZ_D50[0][c] + XYZ_D50_TO_SRGB[r][1] * s[1] * SRGB_TO_XYZ_D50[1][c] + XYZ_D50_TO_SRGB[r][2] * s[2] * SRGB_TO_XYZ_D50[2][c])\n );\n}\n\n/** Photoshop posterize: `levels` equal input bins over 0..255, outputs `floor(k·255 / (levels − 1))`. */\nexport function posterizeValue(v: number, levels: number): number {\n const n = Math.min(255, Math.max(2, Math.round(levels)));\n const x = Math.round(clamp01(v) * 255);\n const bin = Math.min(n - 1, Math.floor((x * n) / 256));\n return Math.floor((bin * 255) / (n - 1)) / 255;\n}\n\n/** Rec.601 luma (Photoshop's grey for Threshold). */\nexport const rgbToGray = (c: readonly number[]) => 0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2];\n\n/** `#rrggbb` → 0..1 RGB (black for anything else). */\nexport function hexToRgb(hex: string): Rgb {\n if (!HEX.test(hex)) return [0, 0, 0];\n const n = parseInt(hex.slice(1), 16);\n return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];\n}\n\n// ---------------------------------------------------------------------------\n// Compilation and evaluation\n// ---------------------------------------------------------------------------\n\n/** Mutates an encoded sRGB colour (0..1) in place. */\nexport type PixelOp = (c: Rgb) => void;\n\nfunction lutOp(luts: [Float32Array, Float32Array, Float32Array]): PixelOp {\n const [lr, lg, lb] = luts;\n return (c) => {\n c[0] = sampleLut(lr, c[0]);\n c[1] = sampleLut(lg, c[1]);\n c[2] = sampleLut(lb, c[2]);\n };\n}\n\nconst isIdentityCurve = (pts: readonly CurvePoint[]) =>\n pts.length === 2 && pts[0].input === 0 && pts[0].output === 0 && pts[1].input === 255 && pts[1].output === 255;\n\nconst isIdentityLevels = (c: LevelsChannel) =>\n c.inBlack === 0 && c.inWhite === 255 && c.gamma === 1 && c.outBlack === 0 && c.outWhite === 255;\n\n/** Quantum Levels works in (8-bit documents: whole levels). */\nconst LEVELS_QUANTUM = 255;\n\n/**\n * Per-channel LUTs of a Curves or Levels layer: each channel's record first, then the master.\n * `null` for any other kind.\n */\nexport function toneLuts(layer: AdjustmentLayer): [Float32Array, Float32Array, Float32Array] | null {\n const n = ADJUSTMENT_LUT_SIZE;\n if (layer.kind === 'curves') {\n const m = curveLutFromLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((pts) => {\n const ch = curveLutFromLevels(pts);\n for (let k = 0; k < n; k++) ch[k] = sampleLut(m, ch[k]);\n return ch;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n if (layer.kind === 'levels') {\n const m = toUnitLevels(layer.params.master);\n return [layer.params.red, layer.params.green, layer.params.blue].map((rec) => {\n const c = toUnitLevels(rec);\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = levelsValue(m, levelsValue(c, k / (n - 1), LEVELS_QUANTUM), LEVELS_QUANTUM);\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n }\n return null;\n}\n\n/**\n * Compiles a layer into a pixel function, or `null` when it is the identity (neutral parameters).\n * Pure: depends on `kind` and `params` only.\n */\nexport function compileAdjustmentLayer(layer: AdjustmentLayer): PixelOp | null {\n switch (layer.kind) {\n case 'curves': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityCurve)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'levels': {\n const p = layer.params;\n if ([p.master, p.red, p.green, p.blue].every(isIdentityLevels)) return null;\n return lutOp(toneLuts(layer)!);\n }\n case 'hueSaturation': {\n const p = layer.params;\n const rangesActive = !p.colorize && p.ranges.some((r) => !isNeutralRange(r));\n if (!p.colorize && !rangesActive && p.hue === 0 && p.saturation === 0 && p.lightness === 0) return null;\n const tables = rangesActive ? hueRangeTables(p.ranges) : null;\n const sat = p.saturation / 100;\n const light = Math.min(1, Math.max(-1, p.lightness / 100));\n return (c) => {\n let dh = 0, ds = 0, dl = 0;\n if (tables) {\n const h0 = rgbToHsl(c)[0];\n dh = sampleLut(tables[0], h0);\n ds = sampleLut(tables[1], h0);\n dl = sampleLut(tables[2], h0);\n }\n const out = rangeLightness(hueSaturation(c, p.hue + dh, Math.min(1, Math.max(-1, sat + ds)), light, p.colorize), dl);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'vibrance': {\n const v = layer.params.vibrance / 100;\n const s = layer.params.saturation / 100;\n if (v === 0 && s === 0) return null;\n return (c) => {\n const out = vibrancePixel(c, v, s);\n c[0] = out[0];\n c[1] = out[1];\n c[2] = out[2];\n };\n }\n case 'colorBalance': {\n const p = layer.params;\n const tones = [p.shadows, p.midtones, p.highlights] as const;\n if (tones.every((t) => t[0] === 0 && t[1] === 0 && t[2] === 0)) return null;\n const n = ADJUSTMENT_LUT_SIZE;\n const luts = [0, 1, 2].map((ch) => {\n const out = new Float32Array(n);\n for (let k = 0; k < n; k++) out[k] = colorBalanceChannel(tones, p.preserveLuminosity, ch, k / (n - 1));\n return out;\n }) as [Float32Array, Float32Array, Float32Array];\n return lutOp(luts);\n }\n case 'blackWhite': {\n const p = layer.params;\n const weights = [p.reds, p.yellows, p.greens, p.cyans, p.blues, p.magentas];\n const tint = p.tint ? hexToRgb(p.tint) : null;\n return (c) => {\n const g = blackWhiteGray(c, weights);\n if (tint) {\n const t = setLum(tint, g);\n c[0] = clamp01(t[0]);\n c[1] = clamp01(t[1]);\n c[2] = clamp01(t[2]);\n } else {\n c[0] = c[1] = c[2] = g;\n }\n };\n }\n case 'photoFilter': {\n const p = layer.params;\n if (p.density <= 0) return null;\n const m = photoFilterMatrix(hexToRgb(p.color), p.density / 100, false);\n const preserve = p.preserveLuminosity;\n return (c) => {\n const l = [srgbToLinear(Math.max(0, c[0])), srgbToLinear(Math.max(0, c[1])), srgbToLinear(Math.max(0, c[2]))];\n let f: Rgb = [0, 1, 2].map((r) => linearToSrgb(Math.max(0, m[r][0] * l[0] + m[r][1] * l[1] + m[r][2] * l[2]))) as Rgb;\n if (preserve) {\n const target = 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];\n f = setLum(f, target);\n }\n c[0] = clamp01(f[0]);\n c[1] = clamp01(f[1]);\n c[2] = clamp01(f[2]);\n };\n }\n case 'invert':\n return (c) => {\n c[0] = 1 - c[0];\n c[1] = 1 - c[1];\n c[2] = 1 - c[2];\n };\n case 'posterize': {\n const levels = layer.params.levels;\n return (c) => {\n c[0] = posterizeValue(c[0], levels);\n c[1] = posterizeValue(c[1], levels);\n c[2] = posterizeValue(c[2], levels);\n };\n }\n case 'threshold': {\n const t = Math.round(layer.params.level);\n return (c) => {\n const v = Math.round(rgbToGray(c) * 255) >= t ? 1 : 0;\n c[0] = c[1] = c[2] = v;\n };\n }\n }\n return null;\n}\n\n// Compiled layers keyed by kind + params, so a stack is compiled once per parameter change.\nconst COMPILE_CACHE_LIMIT = 64;\nconst compileCache = new Map<string, PixelOp | null>();\n\nfunction compiledFor(layer: AdjustmentLayer): PixelOp | null {\n const key = `${layer.kind}:${JSON.stringify(layer.params)}`;\n if (compileCache.has(key)) {\n const op = compileCache.get(key)!;\n // Refresh recency.\n compileCache.delete(key);\n compileCache.set(key, op);\n return op;\n }\n const op = compileAdjustmentLayer(layer);\n compileCache.set(key, op);\n if (compileCache.size > COMPILE_CACHE_LIMIT) compileCache.delete(compileCache.keys().next().value as string);\n return op;\n}\n\n/** Layers that change pixels: enabled, visible, not the identity. */\nexport function activeAdjustmentLayers(layers: readonly AdjustmentLayer[] | undefined): AdjustmentLayer[] {\n if (!layers?.length) return [];\n return layers.filter((l) => l.enabled !== false && (l.opacity ?? 1) > 0 && compiledFor(l) !== null);\n}\n\n/**\n * Evaluates one colour (encoded sRGB, 0..1) through a layer stack: each layer adjusts the result\n * of the layers below and is blended over it with its blend mode and opacity (an opaque\n * backdrop, so `(1 − α)·Cb + α·B(Cb, Cs)`).\n */\nexport function evaluateAdjustmentLayers(color: readonly number[], layers: readonly AdjustmentLayer[]): Rgb {\n const c: Rgb = [color[0], color[1], color[2]];\n for (const layer of layers) applyLayer(c, layer, compiledFor(layer));\n return c;\n}\n\nfunction applyLayer(c: Rgb, layer: AdjustmentLayer, op: PixelOp | null, coverage = 1): void {\n if (!op || layer.enabled === false) return;\n const alpha = Math.min(1, Math.max(0, layer.opacity ?? 1)) * coverage;\n if (alpha <= 0) return;\n const blend = layer.blend ?? 'normal';\n if (blend === 'normal' && alpha >= 1) {\n op(c);\n return;\n }\n const cb0 = c[0], cb1 = c[1], cb2 = c[2];\n op(c);\n const b = blend === 'normal' ? c : blendRgb(blend, [cb0, cb1, cb2], c);\n c[0] = (1 - alpha) * cb0 + alpha * b[0];\n c[1] = (1 - alpha) * cb1 + alpha * b[1];\n c[2] = (1 - alpha) * cb2 + alpha * b[2];\n}\n\n/**\n * Applies an adjustment-layer stack to 8-bit straight RGBA pixels in place. Each pixel is\n * decoded once, run through every layer in float, and encoded once. Transparent pixels are\n * left alone; alpha is never changed.\n *\n * `masks` (layer id → 0..1 coverage per pixel of `imageData`, see `buildLayerMaskPlanes`)\n * scales each masked layer's opacity per pixel: coverage 0 leaves the pixel as the layers below\n * made it, 1 applies the layer fully.\n */\nexport function applyAdjustmentLayers(\n imageData: { data: Uint8ClampedArray },\n layers: readonly AdjustmentLayer[] | undefined,\n masks?: ReadonlyMap<string, ArrayLike<number>>\n): void {\n const active = activeAdjustmentLayers(layers);\n if (!active.length) return;\n const ops = active.map((l) => compiledFor(l));\n const planes = active.map((l) => masks?.get(l.id));\n const data = imageData.data;\n const len = data.length;\n const c: Rgb = [0, 0, 0];\n for (let i = 0; i < len; i += 4) {\n if (data[i + 3] === 0) continue;\n c[0] = data[i] / 255;\n c[1] = data[i + 1] / 255;\n c[2] = data[i + 2] / 255;\n for (let k = 0; k < active.length; k++) {\n const plane = planes[k];\n if (plane) {\n const cov = plane[i >> 2];\n if (cov > 0) applyLayer(c, active[k], ops[k], cov >= 1 ? 1 : cov);\n } else {\n applyLayer(c, active[k], ops[k]);\n }\n }\n data[i] = Math.round(clamp01(c[0]) * 255);\n data[i + 1] = Math.round(clamp01(c[1]) * 255);\n data[i + 2] = Math.round(clamp01(c[2]) * 255);\n }\n}\n\n/** Stable cache key of a layer stack's pixel effect (for renderer caches). */\nexport function adjustmentLayersKey(layers: readonly AdjustmentLayer[] | undefined): string {\n if (!layers?.length) return '';\n return JSON.stringify(\n layers.map((l) => {\n const key: unknown[] = [l.kind, l.enabled !== false, l.opacity ?? 1, l.blend ?? 'normal', l.params];\n if (l.mask) key.push(layerMaskKey(l.mask));\n return key;\n })\n );\n}\n","import type { DepthMaskOptions, ImageAdjustments } from './types';\nimport {\n LUMA_709,\n LATEST_PROCESS_VERSION,\n TONE_LUT_SIZE,\n buildToneLut,\n linearToSrgb,\n normalizeProcessVersion,\n srgbToLinear,\n whiteBalanceMatrix,\n} from './color';\nimport { applyAdjustmentLayers } from './adjustment-layers';\n\nfunction clamp(value: number, min = 0, max = 255): number {\n return Math.max(min, Math.min(max, value));\n}\n\n/**\n * Applies color and tone adjustments (brightness, contrast, saturation, exposure, temperature, tint,\n * opacity, then any adjustment `layers`, then blur) directly to the pixel data of an ImageData buffer.\n *\n * `adjustments.process` selects the rendering: `2` is the linear-light pipeline\n * ({@link applyProcess2Adjustments}); anything else (including a missing value, as in edits saved\n * before process versions existed) is the frozen legacy rendering ({@link applyLegacyAdjustments}).\n */\nexport function applyAdjustments(\n imageData: ImageData,\n adjustments: ImageAdjustments,\n options: { layerMasks?: ReadonlyMap<string, ArrayLike<number>> } = {}\n): void {\n if (normalizeProcessVersion(adjustments.process) === LATEST_PROCESS_VERSION) {\n applyProcess2Adjustments(imageData, adjustments);\n } else {\n applyLegacyAdjustments(imageData, adjustments);\n }\n\n // Adjustment layers, bottom to top, on the encoded result (no-op without layers).\n // Layer masks (see `buildLayerMaskPlanes`) are aligned with `imageData`.\n applyAdjustmentLayers(imageData, adjustments.layers, options.layerMasks);\n\n // Apply blur if requested\n if (adjustments.blur > 0) {\n applyFastBoxBlur(imageData, Math.min(Math.round(adjustments.blur), 40));\n }\n}\n\n/**\n * Process 1: the original 8-bit gamma-space rendering, clamping after every step. Frozen so\n * that edits saved without a `process` look exactly as they always have. `tint` is ignored.\n */\nexport function applyLegacyAdjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const { brightness, contrast, saturation, exposure, temperature, opacity } = adjustments;\n const data = imageData.data;\n const len = data.length;\n\n const hasBrightness = brightness !== 0;\n const hasContrast = contrast !== 0;\n const hasSaturation = saturation !== 0;\n const hasExposure = exposure !== 0;\n const hasTemperature = temperature !== 0;\n const hasOpacity = opacity < 1;\n\n // Pre-calculate contrast factor\n const contrastFactor = hasContrast\n ? (259 * (contrast + 255)) / (255 * (259 - contrast))\n : 1;\n\n // Pre-calculate exposure multiplier\n const exposureMultiplier = hasExposure ? Math.pow(2, exposure / 50) : 1;\n\n // Pre-calculate saturation multiplier\n const satMultiplier = hasSaturation ? 1 + saturation / 100 : 1;\n\n // Pre-calculate temperature offsets\n const tempOffset = hasTemperature ? temperature * 0.8 : 0;\n\n if (hasBrightness || hasContrast || hasSaturation || hasExposure || hasTemperature || hasOpacity) {\n for (let i = 0; i < len; i += 4) {\n let r = data[i];\n let g = data[i + 1];\n let b = data[i + 2];\n let a = data[i + 3];\n\n // Exposure\n if (hasExposure) {\n r = clamp(r * exposureMultiplier);\n g = clamp(g * exposureMultiplier);\n b = clamp(b * exposureMultiplier);\n }\n\n // Brightness\n if (hasBrightness) {\n const bOffset = brightness * 2.55;\n r = clamp(r + bOffset);\n g = clamp(g + bOffset);\n b = clamp(b + bOffset);\n }\n\n // Contrast\n if (hasContrast) {\n r = clamp(contrastFactor * (r - 128) + 128);\n g = clamp(contrastFactor * (g - 128) + 128);\n b = clamp(contrastFactor * (b - 128) + 128);\n }\n\n // Saturation\n if (hasSaturation) {\n const luminance = 0.2989 * r + 0.587 * g + 0.114 * b;\n r = clamp(luminance + (r - luminance) * satMultiplier);\n g = clamp(luminance + (g - luminance) * satMultiplier);\n b = clamp(luminance + (b - luminance) * satMultiplier);\n }\n\n // Temperature / Warmth\n if (hasTemperature) {\n r = clamp(r + tempOffset);\n b = clamp(b - tempOffset);\n }\n\n // Opacity\n if (hasOpacity) {\n a = clamp(a * opacity);\n }\n\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n data[i + 3] = a;\n }\n }\n}\n\n/** sRGB byte → linear light (exact; shared by every process 2 render). */\nconst DECODE_LUT: Float32Array = (() => {\n const lut = new Float32Array(256);\n for (let i = 0; i < 256; i++) lut[i] = srgbToLinear(i / 255);\n return lut;\n})();\n\n/** Intervals of the linear → sRGB byte encode table over [0, 1]. */\nconst ENCODE_LUT_SIZE = 65535;\nlet encodeLut: Uint8Array | null = null;\nfunction getEncodeLut(): Uint8Array {\n if (!encodeLut) {\n encodeLut = new Uint8Array(ENCODE_LUT_SIZE + 1);\n for (let i = 0; i <= ENCODE_LUT_SIZE; i++) {\n encodeLut[i] = Math.round(linearToSrgb(i / ENCODE_LUT_SIZE) * 255);\n }\n }\n return encodeLut;\n}\n\ninterface Process2Plan {\n key: string;\n /** Exposure gain folded into the white-balance matrix (row-major). */\n m: Float64Array;\n /** Per-channel tone LUTs over [0, headroom[c]]. */\n luts: [Float32Array, Float32Array, Float32Array];\n /** LUT index scale per channel: TONE_LUT_SIZE / headroom[c]. */\n scale: [number, number, number];\n saturation: number;\n}\n\nlet cachedPlan: Process2Plan | null = null;\n\n/**\n * Precomputes everything per parameter change: the 3×3 colour matrix (exposure gain × white\n * balance) and one tone LUT per channel whose domain is that channel's largest possible value,\n * so the highlight shoulder absorbs it instead of clipping.\n */\nfunction getProcess2Plan(adjustments: ImageAdjustments): Process2Plan {\n const exposure = adjustments.exposure || 0;\n const temperature = adjustments.temperature || 0;\n const tint = adjustments.tint || 0;\n const contrast = adjustments.contrast || 0;\n const brightness = adjustments.brightness || 0;\n const saturation = 1 + (adjustments.saturation || 0) / 100;\n const key = `${exposure},${temperature},${tint},${contrast},${brightness},${saturation}`;\n if (cachedPlan && cachedPlan.key === key) return cachedPlan;\n\n const gain = Math.pow(2, exposure / 50);\n const wb = whiteBalanceMatrix(temperature, tint);\n const m = new Float64Array(9);\n for (let i = 0; i < 9; i++) m[i] = wb[i] * gain;\n const luts = [] as unknown as [Float32Array, Float32Array, Float32Array];\n const scale = [0, 0, 0] as [number, number, number];\n for (let c = 0; c < 3; c++) {\n // Largest value this channel can reach from inputs in [0, 1].\n let reach = 0;\n for (let k = 0; k < 3; k++) reach += Math.max(0, m[c * 3 + k]);\n const headroom = Math.max(1, reach);\n luts[c] = buildToneLut(contrast, brightness, headroom);\n scale[c] = TONE_LUT_SIZE / headroom;\n }\n cachedPlan = { key, m, luts, scale, saturation };\n return cachedPlan;\n}\n\n/**\n * Process 2: decode sRGB to linear float once; apply exposure (gain) and white balance (a\n * Bradford matrix relative to as-shot) in linear light; a per-channel tone LUT for the\n * highlight shoulder, contrast and brightness; luminance-preserving saturation in linear light;\n * then encode back to sRGB through a LUT. Nothing is clamped between stages, only at the final\n * encode. Neutral settings are the identity.\n */\nexport function applyProcess2Adjustments(imageData: { data: Uint8ClampedArray }, adjustments: ImageAdjustments): void {\n const data = imageData.data;\n const len = data.length;\n const opacity = adjustments.opacity ?? 1;\n const hasOpacity = opacity < 1;\n const hasColor =\n !!adjustments.brightness ||\n !!adjustments.contrast ||\n !!adjustments.saturation ||\n !!adjustments.exposure ||\n !!adjustments.temperature ||\n !!adjustments.tint;\n\n if (!hasColor) {\n if (hasOpacity) {\n for (let i = 3; i < len; i += 4) data[i] = clamp(data[i] * opacity);\n }\n return;\n }\n\n const plan = getProcess2Plan(adjustments);\n const dec = DECODE_LUT;\n const enc = getEncodeLut();\n const { m, luts, scale, saturation } = plan;\n const m0 = m[0], m1 = m[1], m2 = m[2], m3 = m[3], m4 = m[4], m5 = m[5], m6 = m[6], m7 = m[7], m8 = m[8];\n const lutR = luts[0], lutG = luts[1], lutB = luts[2];\n const sR = scale[0], sG = scale[1], sB = scale[2];\n const N = TONE_LUT_SIZE;\n const hasSaturation = saturation !== 1;\n const wr = LUMA_709[0], wg = LUMA_709[1], wb = LUMA_709[2];\n\n for (let i = 0; i < len; i += 4) {\n const r0 = dec[data[i]];\n const g0 = dec[data[i + 1]];\n const b0 = dec[data[i + 2]];\n\n // Exposure × white balance (linear light, unclamped).\n let r = m0 * r0 + m1 * g0 + m2 * b0;\n let g = m3 * r0 + m4 * g0 + m5 * b0;\n let b = m6 * r0 + m7 * g0 + m8 * b0;\n\n // Tone LUTs (linear interpolation).\n let f: number;\n let k: number;\n if (r <= 0) r = lutR[0];\n else if ((f = r * sR) >= N) r = lutR[N];\n else { k = f | 0; r = lutR[k] + (lutR[k + 1] - lutR[k]) * (f - k); }\n if (g <= 0) g = lutG[0];\n else if ((f = g * sG) >= N) g = lutG[N];\n else { k = f | 0; g = lutG[k] + (lutG[k + 1] - lutG[k]) * (f - k); }\n if (b <= 0) b = lutB[0];\n else if ((f = b * sB) >= N) b = lutB[N];\n else { k = f | 0; b = lutB[k] + (lutB[k + 1] - lutB[k]) * (f - k); }\n\n // Luminance-preserving saturation in linear light.\n if (hasSaturation) {\n const y = wr * r + wg * g + wb * b;\n r = y + (r - y) * saturation;\n g = y + (g - y) * saturation;\n b = y + (b - y) * saturation;\n }\n\n // Encode (the only clamp).\n data[i] = enc[r <= 0 ? 0 : r >= 1 ? ENCODE_LUT_SIZE : (r * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 1] = enc[g <= 0 ? 0 : g >= 1 ? ENCODE_LUT_SIZE : (g * ENCODE_LUT_SIZE + 0.5) | 0];\n data[i + 2] = enc[b <= 0 ? 0 : b >= 1 ? ENCODE_LUT_SIZE : (b * ENCODE_LUT_SIZE + 0.5) | 0];\n if (hasOpacity) data[i + 3] = clamp(data[i + 3] * opacity);\n }\n}\n\n/**\n * Fast separable box blur running horizontally and vertically (`adjustments.blur` and the `blur`\n * layer filter): `radius` pixels each side, edges clamped, rounded after each pass.\n */\nexport function applyBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n applyFastBoxBlur(imageData, radius);\n}\n\nfunction applyFastBoxBlur(imageData: { width: number; height: number; data: Uint8ClampedArray }, radius: number): void {\n if (radius < 1) return;\n const w = imageData.width;\n const h = imageData.height;\n const src = imageData.data;\n const temp = new Uint8ClampedArray(src.length);\n\n // Horizontal blur pass\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const px = Math.min(w - 1, Math.max(0, x + k));\n const idx = (y * w + px) * 4;\n r += src[idx];\n g += src[idx + 1];\n b += src[idx + 2];\n a += src[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n temp[dstIdx] = Math.round(r / count);\n temp[dstIdx + 1] = Math.round(g / count);\n temp[dstIdx + 2] = Math.round(b / count);\n temp[dstIdx + 3] = Math.round(a / count);\n }\n }\n\n // Vertical blur pass back into src\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) {\n let r = 0, g = 0, b = 0, a = 0;\n let count = 0;\n for (let k = -radius; k <= radius; k++) {\n const py = Math.min(h - 1, Math.max(0, y + k));\n const idx = (py * w + x) * 4;\n r += temp[idx];\n g += temp[idx + 1];\n b += temp[idx + 2];\n a += temp[idx + 3];\n count++;\n }\n const dstIdx = (y * w + x) * 4;\n src[dstIdx] = Math.round(r / count);\n src[dstIdx + 1] = Math.round(g / count);\n src[dstIdx + 2] = Math.round(b / count);\n src[dstIdx + 3] = Math.round(a / count);\n }\n }\n}\n\n/**\n * Blends a depth/alpha mask into the main image data based on Gleamforge depth masking.\n * Supports threshold range with softness gradation, brightness mode, and optional inversion.\n */\nexport function applyDepthMask(\n mainImageData: ImageData,\n maskImageData: ImageData,\n options: DepthMaskOptions\n): void {\n const { mode, depthRange, softness, invert } = options;\n const mainData = mainImageData.data;\n const maskData = maskImageData.data;\n const len = Math.min(mainData.length, maskData.length);\n const effectiveSoftness = Math.max(1, softness || 10);\n const minDepth = depthRange[0];\n const maxDepth = depthRange[1];\n\n for (let i = 0; i < len; i += 4) {\n let maskValue = maskData[i]; // Use red channel of mask (or grayscale)\n if (invert) {\n maskValue = 255 - maskValue;\n }\n\n let alphaValue: number;\n\n if (mode === 'threshold') {\n if (maskValue >= maxDepth + effectiveSoftness) {\n alphaValue = 0;\n } else if (maskValue > maxDepth) {\n alphaValue = Math.round(((maxDepth + effectiveSoftness - maskValue) / effectiveSoftness) * 255);\n } else if (maskValue >= minDepth) {\n alphaValue = 255;\n } else if (maskValue < minDepth - effectiveSoftness) {\n alphaValue = 0;\n } else {\n alphaValue = Math.round(((maskValue - (minDepth - effectiveSoftness)) / effectiveSoftness) * 255);\n }\n } else {\n // Brightness mode\n alphaValue = clamp(maskValue + minDepth);\n }\n\n // Multiply existing alpha with mask alpha\n mainData[i + 3] = clamp((mainData[i + 3] * alphaValue) / 255);\n }\n}\n\n/**\n * Removes alpha channel by setting all alpha bytes to 255 (completely opaque).\n * From Gleamforge image-filters.ts.\n */\nexport function removeAlpha(imageData: ImageData): void {\n const data = imageData.data;\n const len = data.length;\n for (let i = 3; i < len; i += 4) {\n data[i] = 255;\n }\n}\n","// Portions ported from photocraft-main/crates/algo/src/histogram.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Display bins of a histogram. */\nexport const HISTOGRAM_BINS = 256;\n\nexport interface ImageHistogram {\n /** 256-bin counts per channel. */\n red: Uint32Array;\n green: Uint32Array;\n blue: Uint32Array;\n /** 256-bin counts of Rec.709 luma `round(0.2126 R + 0.7152 G + 0.0722 B)` of the encoded values. */\n luminance: Uint32Array;\n /** Visible pixels counted (alpha > 0). Each counts once, partially transparent ones included. */\n samples: number;\n /** Fully transparent pixels, excluded from every bin. */\n transparent: number;\n /** Per channel (R, G, B): visible pixels at exactly 0. */\n shadows: [number, number, number];\n /** Per channel (R, G, B): visible pixels at exactly 255. */\n highlights: [number, number, number];\n /** Largest bin count over R, G, B and luminance (for scaling a plot). */\n max: number;\n}\n\ninterface PixelSource {\n data: ArrayLike<number>;\n width?: number;\n height?: number;\n}\n\n/**\n * Per-channel and luminance histograms of straight 8-bit RGBA pixels (an `ImageData` or anything\n * with `data`), with clipping counts at the exact endpoints. Transparent pixels cannot add\n * phantom peaks: they are counted separately and excluded.\n */\nexport function computeHistogram(imageData: PixelSource): ImageHistogram {\n const red = new Uint32Array(HISTOGRAM_BINS);\n const green = new Uint32Array(HISTOGRAM_BINS);\n const blue = new Uint32Array(HISTOGRAM_BINS);\n const luminance = new Uint32Array(HISTOGRAM_BINS);\n const shadows: [number, number, number] = [0, 0, 0];\n const highlights: [number, number, number] = [0, 0, 0];\n let samples = 0;\n let transparent = 0;\n const data = imageData.data;\n const len = data.length - (data.length % 4);\n for (let i = 0; i < len; i += 4) {\n if (!(data[i + 3] > 0)) {\n transparent++;\n continue;\n }\n samples++;\n const r = clampByte(data[i]);\n const g = clampByte(data[i + 1]);\n const b = clampByte(data[i + 2]);\n red[r]++;\n green[g]++;\n blue[b]++;\n luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) shadows[0]++;\n if (g === 0) shadows[1]++;\n if (b === 0) shadows[2]++;\n if (r === 255) highlights[0]++;\n if (g === 255) highlights[1]++;\n if (b === 255) highlights[2]++;\n }\n let max = 0;\n for (const bins of [red, green, blue, luminance]) {\n for (let k = 0; k < HISTOGRAM_BINS; k++) if (bins[k] > max) max = bins[k];\n }\n return { red, green, blue, luminance, samples, transparent, shadows, highlights, max };\n}\n\nfunction clampByte(v: number): number {\n return v <= 0 ? 0 : v >= 255 ? 255 : Math.round(v);\n}\n\nexport type HistogramChannel = 'red' | 'green' | 'blue' | 'luminance';\n\n/**\n * An SVG path (`M … Z`) of one channel scaled into a `width` × `height` box, bins left to right,\n * counts up from the bottom. `scale: 'sqrt'` (default) keeps small populations visible.\n */\nexport function histogramPath(\n histogram: ImageHistogram,\n channel: HistogramChannel,\n width: number,\n height: number,\n options: { scale?: 'linear' | 'sqrt'; max?: number } = {}\n): string {\n const bins = histogram[channel];\n const sqrt = (options.scale ?? 'sqrt') === 'sqrt';\n const f = (v: number) => (sqrt ? Math.sqrt(v) : v);\n const top = f(options.max ?? histogram.max) || 1;\n const dx = width / (HISTOGRAM_BINS - 1);\n let d = `M0 ${height}`;\n for (let k = 0; k < HISTOGRAM_BINS; k++) {\n const y = height - (f(bins[k]) / top) * height;\n d += ` L${round2(k * dx)} ${round2(y)}`;\n }\n return `${d} L${round2(width)} ${height} Z`;\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { SelectionMask } from './mask';\n\n/**\n * The selection's outline at `threshold` (0..255, default 128) as axis-aligned segments on pixel\n * boundaries, `[x0, y0, x1, y1, …]` in natural image pixels, with collinear neighbours merged.\n * This is what marching ants are drawn from. Linear in the pixel count.\n */\nexport function selectionOutline(mask: SelectionMask, threshold = 128): Float32Array {\n const { width: w, height: h, data } = mask;\n const segs: number[] = [];\n if (!mask.bounds) return new Float32Array(0);\n const b = mask.bounds;\n const inside = (x: number, y: number) => x >= 0 && y >= 0 && x < w && y < h && data[y * w + x] >= threshold;\n // Horizontal boundaries: the line y separates rows y − 1 and y.\n for (let y = b.y; y <= b.y + b.height; y++) {\n let start = -1;\n for (let x = b.x; x <= b.x + b.width; x++) {\n const edge = x < b.x + b.width && inside(x, y - 1) !== inside(x, y);\n if (edge && start < 0) start = x;\n else if (!edge && start >= 0) {\n segs.push(start, y, x, y);\n start = -1;\n }\n }\n }\n // Vertical boundaries: the line x separates columns x − 1 and x.\n for (let x = b.x; x <= b.x + b.width; x++) {\n let start = -1;\n for (let y = b.y; y <= b.y + b.height; y++) {\n const edge = y < b.y + b.height && inside(x - 1, y) !== inside(x, y);\n if (edge && start < 0) start = y;\n else if (!edge && start >= 0) {\n segs.push(x, start, x, y);\n start = -1;\n }\n }\n }\n return Float32Array.from(segs);\n}\n\nexport interface QuickMaskStyle {\n /** Overlay colour, `[r, g, b]` 0..255 (default Photoshop's rubylith red). */\n color?: readonly [number, number, number];\n /** 0..1 (default 0.5): opacity over fully unselected pixels. */\n opacity?: number;\n}\n\n/**\n * Quick Mask view: RGBA pixels (natural image size) tinting the *unselected* area, so the\n * selection shows through clear, as Photoshop's rubylith overlay.\n */\nexport function quickMaskPixels(mask: SelectionMask, style: QuickMaskStyle = {}): Uint8ClampedArray {\n const [r, g, b] = style.color ?? [255, 0, 0];\n const k = Math.min(1, Math.max(0, style.opacity ?? 0.5));\n const out = new Uint8ClampedArray(mask.data.length * 4);\n for (let i = 0, p = 0; i < mask.data.length; i++, p += 4) {\n out[p] = r;\n out[p + 1] = g;\n out[p + 2] = b;\n out[p + 3] = Math.round((255 - mask.data[i]) * k);\n }\n return out;\n}\n","/**\n * The image editor's layer stack: pixel, adjustment, group and annotation layers in one ordered\n * tree (bottom to top), all in document space (natural pixels of the document).\n *\n * Layer objects are immutable. Every change builds new objects along the path from the root to\n * the changed layer and shares everything else, so undo history, the compositor's caches and\n * serialization can compare layers by identity.\n */\n\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n Annotation,\n CropBox,\n DepthMaskOptions,\n} from '../types';\nimport type { PixelSurface } from '../../raster/surface';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n type GroupBlendMode,\n type LayerLocation as RasterLayerLocation,\n type LayerLocks,\n type RasterLayerBase,\n type RasterMask as BaseRasterMask,\n type RasterPixelLayer,\n} from '../../raster/layers';\n\n/** Blend modes of a layer; groups also accept `pass-through`. */\nexport type LayerBlendMode = GroupBlendMode;\n\nexport type { LayerLocks };\n\n/**\n * A layer mask (see the raster module's `RasterMask`) whose parametric `source` is a depth map:\n * a depth mask is generated from it instead of painted, and its surface is unused.\n */\nexport type RasterMask = BaseRasterMask<DepthMaskOptions>;\n\nexport interface LayerBase extends RasterLayerBase {\n mask?: RasterMask;\n}\n\n/** The base sliders of the old document-level adjustments, as a filter on one layer. */\nexport interface DevelopSettings {\n brightness: number;\n contrast: number;\n saturation: number;\n exposure: number;\n temperature: number;\n tint?: number;\n /** Rendering process (see `ImageAdjustments.process`). */\n process?: 1 | 2;\n}\n\nexport type DevelopFilter = { id: string; kind: 'develop'; enabled: boolean } & DevelopSettings;\n/** Box blur of `radius` pixels (the old `adjustments.blur`), clamped at the canvas edges. */\nexport interface BlurFilter {\n id: string;\n kind: 'blur';\n enabled: boolean;\n radius: number;\n}\nexport type LayerFilter = DevelopFilter | BlurFilter;\nexport type LayerFilterKind = LayerFilter['kind'];\n\nexport interface PixelLayer extends LayerBase, Omit<RasterPixelLayer, keyof RasterLayerBase> {\n /** Live filters, applied bottom (first) to top before the mask and blend. */\n filters?: LayerFilter[];\n /**\n * `'document'`: the pixels are the document's source image, unmodified (the Background of an\n * opened image). Such a layer's `surface` is a placeholder; read its pixels through the\n * controller. Painting it drops the flag.\n */\n source?: 'document';\n}\n\nexport type AdjustmentLayerNode<K extends AdjustmentKind = AdjustmentKind> = {\n [P in K]: LayerBase & { type: 'adjustment'; kind: P; params: AdjustmentParamsMap[P] };\n}[K];\n\nexport interface GroupLayer extends Omit<LayerBase, 'blend'> {\n type: 'group';\n blend: LayerBlendMode;\n /** Bottom to top. */\n children: ImageLayer[];\n /** UI only. */\n collapsed?: boolean;\n}\n\n/** One annotation (pen, shape, arrow, text, image) as a layer, drawn by the annotation renderer. */\nexport interface AnnotationLayer extends LayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\nexport type ImageLayer = PixelLayer | AdjustmentLayerNode | GroupLayer | AnnotationLayer;\nexport type ImageLayerType = ImageLayer['type'];\n\n/** Which part of the active layer painting targets. */\nexport type LayerTarget = 'pixels' | 'mask';\n\n// ---------------------------------------------------------------------------\n// Ids\n// ---------------------------------------------------------------------------\n\nlet idCounter = 0;\n\n/** A fresh id with `prefix` (`layer`, `flt`, …). */\nexport function newLayerId(prefix = 'layer'): string {\n return `${prefix}_${Date.now().toString(36)}_${(++idCounter).toString(36)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Tree helpers (pure)\n// ---------------------------------------------------------------------------\n\nexport type LayerLocation = RasterLayerLocation<ImageLayer>;\n\nexport { flattenLayers, findLayer, mapLayer, insertLayer, isLayerLocked };\n\n/** The top-level and nested pixel layers. */\nexport function pixelLayers(layers: readonly ImageLayer[]): PixelLayer[] {\n return flattenLayers(layers).filter((l): l is PixelLayer => l.type === 'pixel');\n}\n\n/** The develop filter of a pixel layer, if any. */\nexport function developFilter(layer: PixelLayer): DevelopFilter | undefined {\n return layer.filters?.find((f): f is DevelopFilter => f.kind === 'develop');\n}\n\nexport function blurFilter(layer: PixelLayer): BlurFilter | undefined {\n return layer.filters?.find((f): f is BlurFilter => f.kind === 'blur');\n}\n\n/** True for filters that change pixels (enabled, non-neutral). */\nexport function isActiveFilter(f: LayerFilter): boolean {\n if (!f.enabled) return false;\n if (f.kind === 'blur') return Math.min(Math.round(f.radius), 40) >= 1;\n return Boolean(f.brightness || f.contrast || f.saturation || f.exposure || f.temperature || f.tint);\n}\n\n/** True when a filter needs neighbouring pixels (it can't run tile by tile). */\nexport function isSpatialFilter(f: LayerFilter): boolean {\n return f.kind === 'blur';\n}\n\n/** Document-space bounds of a pixel layer's stored tiles, or `null`. */\nexport function pixelLayerBounds(layer: PixelLayer, surface: PixelSurface = layer.surface): CropBox | null {\n const b = surface.bounds;\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height };\n}\n\n/** Human label of a layer kind. */\nexport function layerTypeLabel(type: ImageLayerType): string {\n return type === 'pixel' ? 'Pixel layer' : type === 'adjustment' ? 'Adjustment layer' : type === 'group' ? 'Group' : 'Annotation';\n}\n","/**\n * The image editor's compositor: the raster engine (`@pixerate/editor/raster`) with the photo\n * layer kinds plugged in. Adjustment layers filter what is below them (a run of adjacent ones is\n * one float pass with one rounding at the end, exactly as `applyAdjustmentLayers` does, so a\n * document migrated from adjustment layers renders identically); annotation layers are drawn by\n * the host; pixel layers can carry develop and blur filters; masks can be depth masks.\n */\n\nimport { applyAdjustmentLayers } from '../adjustment-layers';\nimport { applyBoxBlur, applyLegacyAdjustments, applyProcess2Adjustments } from '../filters';\nimport { LATEST_PROCESS_VERSION, normalizeProcessVersion } from '../color';\nimport type { BlendMode } from '../blend';\nimport type { AdjustmentLayer, Annotation, CropBox, DepthMaskOptions, ImageAdjustments } from '../types';\nimport {\n RasterCompositor,\n type CompositeCallOptions,\n type CompositeStats,\n type CompositorOptions,\n type CompositorProfile,\n type PixelFilterSpec,\n type RasterSources,\n} from '../../raster/compositor';\nimport type { PixelSurface } from '../../raster/surface';\nimport { isActiveFilter, isSpatialFilter, type AdjustmentLayerNode, type ImageLayer, type LayerFilter, type PixelLayer } from './model';\n\nexport { blendTile, changedTiles } from '../../raster/compositor';\nexport type { CompositeCallOptions, CompositeStats, CompositorOptions };\n\n/** What the compositor needs from its host besides the layers. */\nexport interface CompositorSources extends RasterSources {\n /** The pixels of a pixel layer (resolves layers whose pixels are the document source). */\n surfaceOf?(layer: PixelLayer): PixelSurface;\n /**\n * A parametric mask's depth map as RGBA at canvas size (the red channel is the depth), or\n * `null` while it is unavailable (the mask then reveals everything).\n */\n depthMap?(source: DepthMaskOptions): { width: number; height: number; data: ArrayLike<number> } | null;\n /** Draws one annotation into RGBA for `rect` (canvas hosts only); `null` when unavailable. */\n rasterizeAnnotation?(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null;\n}\n\nconst legacyViewCache = new WeakMap<AdjustmentLayerNode, AdjustmentLayer>();\n\n/** The `AdjustmentLayer` shape `applyAdjustmentLayers` evaluates, for a node (memoised). */\nexport function adjustmentNodeAsLayer(node: AdjustmentLayerNode): AdjustmentLayer {\n let view = legacyViewCache.get(node);\n if (!view) {\n view = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n } as AdjustmentLayer;\n legacyViewCache.set(node, view);\n }\n return view;\n}\n\n/** Develop settings as `ImageAdjustments` for the process functions (opacity is document-level). */\nfunction developAdjustments(f: Extract<LayerFilter, { kind: 'develop' }>): ImageAdjustments {\n return {\n brightness: f.brightness,\n contrast: f.contrast,\n saturation: f.saturation,\n exposure: f.exposure,\n temperature: f.temperature,\n tint: f.tint ?? 0,\n blur: 0,\n opacity: 1,\n process: f.process,\n };\n}\n\n/** Runs per-pixel filters over RGBA `data` in place. */\nfunction applyPixelFilters(data: Uint8ClampedArray, filters: readonly LayerFilter[]): void {\n for (const f of filters) {\n if (!isActiveFilter(f) || f.kind !== 'develop') continue;\n const adj = developAdjustments(f);\n if (normalizeProcessVersion(adj.process) === LATEST_PROCESS_VERSION) applyProcess2Adjustments({ data }, adj);\n else applyLegacyAdjustments({ data }, adj);\n }\n}\n\n/** Evaluates a depth mask (see `applyDepthMask`) into one coverage byte per depth sample. */\nexport function depthMaskAlpha(depth: number, options: DepthMaskOptions): number {\n const { mode, depthRange, softness, invert } = options;\n let v = invert ? 255 - depth : depth;\n const soft = Math.max(1, softness || 10);\n const [lo, hi] = depthRange;\n if (mode === 'threshold') {\n if (v >= hi + soft) return 0;\n if (v > hi) return Math.round(((hi + soft - v) / soft) * 255);\n if (v >= lo) return 255;\n if (v < lo - soft) return 0;\n return Math.round(((v - (lo - soft)) / soft) * 255);\n }\n v += lo;\n return v < 0 ? 0 : v > 255 ? 255 : v;\n}\n\nconst filterSpecCache = new WeakMap<object, PixelFilterSpec | null>();\n\n/** A pixel layer's active filters for the engine (memoised per filter list). */\nfunction pixelFilterSpec(layer: PixelLayer): PixelFilterSpec | null {\n const list = layer.filters;\n if (!list?.length) return null;\n if (filterSpecCache.has(list)) return filterSpecCache.get(list)!;\n const filters = list.filter(isActiveFilter);\n let spec: PixelFilterSpec | null = null;\n if (filters.length) {\n const spatial = filters.some(isSpatialFilter);\n spec = {\n key: JSON.stringify(filters),\n spatial,\n apply: (data, width, height) => {\n for (const f of filters) {\n if (f.kind === 'develop') applyPixelFilters(data, [f]);\n else if (f.kind === 'blur') applyBoxBlur({ width, height, data }, Math.min(Math.round(f.radius), 40));\n }\n },\n };\n }\n filterSpecCache.set(list, spec);\n return spec;\n}\n\nconst PHOTO_PROFILE: CompositorProfile<ImageLayer, CompositorSources> = {\n isFilterLayer: (l) => l.type === 'adjustment',\n applyFilterRun: (layers, acc, _r, masks) => {\n const views = (layers as AdjustmentLayerNode[]).map(adjustmentNodeAsLayer);\n applyAdjustmentLayers({ data: acc }, views, masks);\n },\n contentTile: (l, r, sources) => (l.type === 'annotation' ? (sources.rasterizeAnnotation?.(l.annotation, r) ?? null) : null),\n /** Annotation layers draw with their annotation's blend mode unless the layer sets one. */\n contentBlend: (l) => (l.type === 'annotation' && l.blend === 'normal' && l.annotation.blend ? l.annotation.blend : (l.blend as BlendMode)),\n layerKey: (l) =>\n l.type === 'pixel' ? [l.filters ?? null, l.source ?? null] : l.type === 'adjustment' ? [l.kind, l.params] : l.type === 'annotation' ? l.annotation : undefined,\n pixelFilters: (l) => (l.type === 'pixel' ? pixelFilterSpec(l) : null),\n maskSourcePlane: (source, W, H, sources) => {\n const opts = source as DepthMaskOptions;\n const depth = sources.depthMap?.(opts) ?? null;\n if (!depth || depth.width !== W || depth.height !== H) return null;\n const v = new Float32Array(W * H);\n for (let i = 0; i < v.length; i++) v[i] = depthMaskAlpha(depth.data[i * 4], opts) / 255;\n return v;\n },\n};\n\n/** The per-tile compositor for image editor layer stacks (see the raster module for the caches). */\nexport class LayerCompositor extends RasterCompositor<ImageLayer, CompositorSources> {\n constructor(options: CompositorOptions = {}) {\n super(PHOTO_PROFILE, options);\n }\n}\n","import { LATEST_PROCESS_VERSION } from '../color';\nimport type { DepthMaskOptions, ImageAdjustments, ImageTransform } from '../types';\n\n/** Neutral base adjustments, latest process (re-exported as `DEFAULT_ADJUSTMENTS`). */\nexport const DEFAULT_ADJUSTMENTS_VALUES: ImageAdjustments = {\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n blur: 0,\n opacity: 1,\n process: LATEST_PROCESS_VERSION,\n};\n\nexport const DEFAULT_TRANSFORM_VALUES: ImageTransform = {\n rotate: 0,\n flipH: false,\n flipV: false,\n};\n\nexport const DEFAULT_DEPTH_MASK_VALUES: DepthMaskOptions = {\n enabled: false,\n maskSource: null,\n mode: 'threshold',\n depthRange: [0, 255],\n softness: 10,\n invert: false,\n};\n","/**\n * Between the old document-level model (schema v1 / v2: base sliders, adjustment layers, blur,\n * depth mask and annotations) and the layer stack:\n *\n * - `layersFromLegacy` lifts a v1 / v2 state into layers: the source becomes the Background\n * pixel layer carrying the base sliders as its `develop` filter and the depth mask as its\n * parametric mask; adjustment layers go above it, annotations on top in their order. Blur\n * becomes a `blur` filter on the Background, or `legacyOutputBlur` when adjustment layers\n * exist (blur used to run after them).\n * - `legacyView` reads the stack back as `adjustments` / `depthMask` / `annotations` (the\n * compatibility views on the editor state) and says whether the stack still has exactly the\n * legacy shape, in which case it serializes as v1 / v2 byte for byte.\n */\n\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport { normalizeAdjustmentLayers } from '../adjustment-layers';\nimport { normalizeProcessVersion } from '../color';\nimport { decodeLayerMask, normalizeLayerMask } from '../selection/layer-mask';\nimport { encodeSelectionRle, resizeSelection } from '../selection/mask';\nimport type {\n AdjustmentLayer,\n AdjustmentLayerMask,\n Annotation,\n DepthMaskOptions,\n ImageAdjustments,\n} from '../types';\nimport {\n blurFilter,\n developFilter,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type ImageLayer,\n type LayerFilter,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { emptySurface, readRegion, surfaceFromPixels, type MaskSurface } from '../../raster/surface';\n\nexport const BACKGROUND_LAYER_ID = 'background';\nexport const BACKGROUND_LAYER_NAME = 'Background';\nexport const DEVELOP_FILTER_ID = 'develop';\nexport const BLUR_FILTER_ID = 'blur';\n\n/** The old document-level parts of the editor state. */\nexport interface LegacyParts {\n adjustments: ImageAdjustments;\n depthMask: DepthMaskOptions;\n annotations: Annotation[];\n}\n\n/** The stack as stored at document level next to the layers. */\nexport interface LayerStack {\n layers: ImageLayer[];\n /** Export alpha (the old `adjustments.opacity`), 0..1. */\n outputOpacity: number;\n /** Migrated blur that must run after the adjustment layers (never created by new edits). */\n legacyOutputBlur?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Masks\n// ---------------------------------------------------------------------------\n\n/**\n * Stored `r1:` encodings of masks lifted from v1 / v2, by surface, so an unchanged mask writes\n * back byte for byte (and a mask of another size than the document keeps its size).\n */\nconst legacyMaskMemo = new WeakMap<MaskSurface, { width: number; height: number; rle: string }>();\n\n/** A raster mask from an old adjustment-layer mask, resampled to the document if needed. */\nexport function rasterMaskFromLegacy(mask: AdjustmentLayerMask, width: number, height: number): RasterMask | null {\n const sel = decodeLayerMask(mask);\n if (!sel) return null;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const surface = surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0);\n legacyMaskMemo.set(surface, { width: mask.width, height: mask.height, rle: mask.rle });\n const out: RasterMask = { surface, defaultValue: 0 };\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\n/**\n * True when an adjustment layer's mask came from a v1 / v2 mask of another size than the canvas\n * (the old renderer resampled it per render; the stack holds a canvas-sized copy).\n */\nexport function hasResampledLegacyMask(layers: readonly ImageLayer[], width: number, height: number): boolean {\n for (const l of layers) {\n if (l.type !== 'adjustment' || !l.mask) continue;\n const memo = legacyMaskMemo.get(l.mask.surface);\n if (memo && (memo.width !== width || memo.height !== height)) return true;\n }\n return false;\n}\n\n/** The old mask shape of a raster mask (its pixels over the canvas, run-length encoded). */\nexport function legacyMaskOf(mask: RasterMask, width: number, height: number): AdjustmentLayerMask {\n let memo = legacyMaskMemo.get(mask.surface);\n if (!memo) {\n const data = readRegion(mask.surface, { x: 0, y: 0, width, height });\n memo = { width, height, rle: encodeSelectionRle(new Uint8Array(data.buffer, data.byteOffset, data.length)) };\n legacyMaskMemo.set(mask.surface, memo);\n }\n return normalizeLayerMask({ ...memo, density: mask.density, feather: mask.feather, enabled: mask.enabled })!;\n}\n\n// ---------------------------------------------------------------------------\n// Legacy → layers\n// ---------------------------------------------------------------------------\n\nfunction sameJson(a: unknown, b: unknown): boolean {\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/** True when the depth mask settings are the defaults (no mask at all). */\nexport function isDefaultDepthMask(mask: DepthMaskOptions | undefined): boolean {\n if (!mask) return true;\n return sameJson({ ...DEFAULT_DEPTH_MASK_VALUES, ...mask, depthRange: [...(mask.depthRange ?? [])] }, DEFAULT_DEPTH_MASK_VALUES);\n}\n\nexport function developFromAdjustments(adjustments: ImageAdjustments): DevelopFilter {\n return {\n id: DEVELOP_FILTER_ID,\n kind: 'develop',\n enabled: true,\n brightness: adjustments.brightness ?? 0,\n contrast: adjustments.contrast ?? 0,\n saturation: adjustments.saturation ?? 0,\n exposure: adjustments.exposure ?? 0,\n temperature: adjustments.temperature ?? 0,\n tint: adjustments.tint ?? 0,\n process: normalizeProcessVersion(adjustments.process),\n };\n}\n\n/** The default name of an annotation layer. */\nexport function annotationLayerName(annotation: Annotation): string {\n switch (annotation.type) {\n case 'pen':\n return 'Pen';\n case 'rect':\n return 'Rectangle';\n case 'circle':\n return 'Ellipse';\n case 'arrow':\n return 'Arrow';\n case 'line':\n return 'Line';\n case 'text':\n return annotation.text ? annotation.text.slice(0, 32) : 'Text';\n case 'image':\n return 'Image';\n }\n}\n\nexport function annotationLayerFor(annotation: Annotation): AnnotationLayer {\n return {\n type: 'annotation',\n id: annotation.id,\n name: annotationLayerName(annotation),\n visible: true,\n opacity: 1,\n blend: 'normal',\n annotation,\n };\n}\n\nexport function adjustmentNodeFromLegacy(layer: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const node = {\n type: 'adjustment',\n id: layer.id,\n name: layer.name ?? '',\n visible: layer.enabled !== false,\n opacity: layer.opacity ?? 1,\n blend: layer.blend ?? 'normal',\n kind: layer.kind,\n params: JSON.parse(JSON.stringify(layer.params)),\n } as AdjustmentLayerNode;\n if (layer.mask) {\n const mask = rasterMaskFromLegacy(layer.mask, width, height);\n if (mask) node.mask = mask;\n }\n return node;\n}\n\n/** The Background layer of an opened image (its pixels are the document source). */\nexport function backgroundLayer(filters: LayerFilter[] = [], mask?: RasterMask): PixelLayer {\n const layer: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: emptySurface(4),\n offset: { x: 0, y: 0 },\n source: 'document',\n filters,\n };\n if (mask) layer.mask = mask;\n return layer;\n}\n\n/** A parametric depth mask on a layer. */\nexport function depthRasterMask(source: DepthMaskOptions): RasterMask {\n const out: RasterMask = {\n surface: emptySurface(1, 255),\n defaultValue: 255,\n source: { ...source, depthRange: [...source.depthRange] as [number, number] },\n };\n if (!source.enabled) out.enabled = false;\n return out;\n}\n\n/** Lifts a v1 / v2 state into a layer stack (see the module comment). */\nexport function layersFromLegacy(parts: Partial<LegacyParts>, width: number, height: number): LayerStack {\n // As `normalizeStoredAdjustments`: stored values without `process` keep the legacy rendering.\n const stored = parts.adjustments;\n const adjustments = (\n stored ? { ...DEFAULT_ADJUSTMENTS_VALUES, ...stored, process: normalizeProcessVersion(stored.process) } : { ...DEFAULT_ADJUSTMENTS_VALUES }\n ) as ImageAdjustments;\n const adjLayers = normalizeAdjustmentLayers(adjustments.layers);\n const filters: LayerFilter[] = [developFromAdjustments(adjustments)];\n const blur = typeof adjustments.blur === 'number' && adjustments.blur > 0 ? adjustments.blur : 0;\n let legacyOutputBlur: number | undefined;\n if (blur > 0) {\n if (adjLayers.length) legacyOutputBlur = blur;\n else filters.push({ id: BLUR_FILTER_ID, kind: 'blur', enabled: true, radius: blur });\n }\n const depth = parts.depthMask;\n const mask = depth && !isDefaultDepthMask(depth) ? depthRasterMask({ ...DEFAULT_DEPTH_MASK_VALUES, ...depth }) : undefined;\n const layers: ImageLayer[] = [backgroundLayer(filters, mask)];\n for (const l of adjLayers) layers.push(adjustmentNodeFromLegacy(l, width, height));\n for (const a of parts.annotations ?? []) layers.push(annotationLayerFor(JSON.parse(JSON.stringify(a))));\n const out: LayerStack = { layers, outputOpacity: typeof adjustments.opacity === 'number' ? adjustments.opacity : 1 };\n if (legacyOutputBlur !== undefined) out.legacyOutputBlur = legacyOutputBlur;\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Layers → legacy views\n// ---------------------------------------------------------------------------\n\n/** The old `AdjustmentLayer` shape of a node, keys in the order `normalizeAdjustmentLayer` writes. */\nexport function legacyAdjustmentLayer(node: AdjustmentLayerNode, width: number, height: number): AdjustmentLayer {\n const out: Record<string, unknown> = {\n id: node.id,\n kind: node.kind,\n enabled: node.visible,\n opacity: node.opacity,\n blend: node.blend,\n params: node.params,\n };\n if (node.name) out.name = node.name;\n if (node.mask && !node.mask.source) out.mask = legacyMaskOf(node.mask, width, height);\n return out as unknown as AdjustmentLayer;\n}\n\n/** The pixel layer whose develop filter the legacy API edits: the active one, else the Background. */\nexport function developTarget(layers: readonly ImageLayer[], activeLayerId: string | null): PixelLayer | null {\n const pixels: PixelLayer[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'pixel') pixels.push(l);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (activeLayerId) {\n const active = pixels.find((l) => l.id === activeLayerId);\n if (active) return active;\n }\n return pixels.find((l) => l.source === 'document') ?? pixels.find((l) => l.id === BACKGROUND_LAYER_ID) ?? pixels[0] ?? null;\n}\n\nexport interface LegacyViewResult extends LegacyParts {\n /**\n * True when the stack has exactly the shape a v1 / v2 document lifts to (an unpainted,\n * unmoved Background with develop / blur filters and an optional depth mask, then adjustment\n * layers, then plain annotation layers), so it can be written as v1 / v2.\n */\n exact: boolean;\n}\n\nconst isPlain = (l: ImageLayer) =>\n l.visible && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask && !l.locks;\n\n/** Reads the stack back as the old document-level parts (see `LegacyViewResult`). */\nexport function legacyView(stack: LayerStack, width: number, height: number, activeLayerId: string | null = null): LegacyViewResult {\n const { layers } = stack;\n const target = developTarget(layers, activeLayerId);\n const develop = target ? developFilter(target) : undefined;\n const blur = stack.legacyOutputBlur ?? (target ? blurFilter(target)?.radius : undefined) ?? 0;\n const adjustments: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS_VALUES,\n ...(develop\n ? {\n brightness: develop.brightness,\n contrast: develop.contrast,\n saturation: develop.saturation,\n exposure: develop.exposure,\n temperature: develop.temperature,\n tint: develop.tint ?? 0,\n }\n : {}),\n blur,\n opacity: stack.outputOpacity,\n process: develop ? normalizeProcessVersion(develop.process) : DEFAULT_ADJUSTMENTS_VALUES.process,\n };\n const adjNodes: AdjustmentLayerNode[] = [];\n const annotations: Annotation[] = [];\n const walk = (list: readonly ImageLayer[]) => {\n for (const l of list) {\n if (l.type === 'adjustment') adjNodes.push(l);\n else if (l.type === 'annotation') annotations.push(l.annotation);\n else if (l.type === 'group') walk(l.children);\n }\n };\n walk(layers);\n if (adjNodes.length) adjustments.layers = adjNodes.map((n) => legacyAdjustmentLayer(n, width, height));\n const depthSource = target?.mask?.source;\n const depthMask: DepthMaskOptions = depthSource\n ? { ...depthSource, depthRange: [...depthSource.depthRange] as [number, number] }\n : { ...DEFAULT_DEPTH_MASK_VALUES, depthRange: [...DEFAULT_DEPTH_MASK_VALUES.depthRange] as [number, number] };\n return { adjustments, depthMask, annotations, exact: isLegacyShape(stack) };\n}\n\n/** See `LegacyViewResult.exact`. */\nexport function isLegacyShape(stack: LayerStack): boolean {\n const { layers } = stack;\n const bg = layers[0];\n if (!bg || bg.type !== 'pixel' || bg.source !== 'document') return false;\n if (bg.id !== BACKGROUND_LAYER_ID || bg.name !== BACKGROUND_LAYER_NAME) return false;\n if (!bg.visible || bg.opacity !== 1 || bg.blend !== 'normal' || bg.clipped || bg.locks) return false;\n if ((bg.fillOpacity ?? 1) !== 1 || bg.offset.x !== 0 || bg.offset.y !== 0) return false;\n const filters = bg.filters ?? [];\n if (filters.length < 1 || filters.length > 2) return false;\n const [develop, blur] = filters;\n if (develop.kind !== 'develop' || !develop.enabled || develop.id !== DEVELOP_FILTER_ID) return false;\n if (blur && (blur.kind !== 'blur' || !blur.enabled || blur.id !== BLUR_FILTER_ID || !(blur.radius > 0))) return false;\n if (bg.mask) {\n const m = bg.mask;\n if (!m.source || m.density !== undefined || m.feather !== undefined) return false;\n if ((m.enabled !== false) !== m.source.enabled) return false;\n }\n let i = 1;\n let adjustments = 0;\n for (; i < layers.length && layers[i].type === 'adjustment'; i++) {\n const l = layers[i] as AdjustmentLayerNode;\n if (l.clipped || l.locks || l.mask?.source) return false;\n adjustments++;\n }\n for (; i < layers.length; i++) {\n const l = layers[i];\n if (l.type !== 'annotation' || !isPlain(l) || l.id !== l.annotation.id || l.name !== annotationLayerName(l.annotation)) return false;\n }\n if (blur && adjustments) return false;\n if (stack.legacyOutputBlur !== undefined && (!adjustments || !(stack.legacyOutputBlur > 0))) return false;\n return true;\n}\n","/**\n * Pure helpers between the compositor and the renderer: the output stage (projection to the\n * cropped, rotated frame, output opacity and the migrated blur), the routing rule that keeps\n * old-shape documents on the exact legacy path where document-space compositing cannot match it,\n * and per-tile histograms. No DOM.\n */\n\nimport { applyBoxBlur } from '../filters';\nimport { normalizeAngle, unprojectCanvasPoint, getRotatedSize, type ImageProjection } from '../geometry';\nimport { HISTOGRAM_BINS, type ImageHistogram } from '../histogram';\nimport type { CropBox, ImageTransform } from '../types';\nimport { blurFilter, type AnnotationLayer, type ImageLayer, type PixelLayer } from './model';\nimport { hasResampledLegacyMask, isLegacyShape, type LayerStack } from './migrate';\nimport { LayerCompositor, type CompositorSources } from './compositor';\n\n/** True for 0, 90, 180 and 270 degrees. */\nexport function isQuarterTurn(rotate: number): boolean {\n return normalizeAngle(rotate) % 90 === 0;\n}\n\nfunction isIntegerRect(r: CropBox): boolean {\n return Number.isInteger(r.x) && Number.isInteger(r.y) && Number.isInteger(r.width) && Number.isInteger(r.height);\n}\n\n/** The projection that draws `region` into a `baseW × baseH` buffer at scale 1 (the renderer's base canvas). */\nexport function baseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/**\n * Projects `src` (RGBA covering `srcRect` in document space) into the base frame of `region`\n * under a quarter-turn `transform`: an exact pixel permutation (crop, flips, 90° steps), the\n * same mapping the canvas draws with. Returns the base buffer and its size.\n */\nexport function projectQuarterTurn(\n src: ArrayLike<number>,\n srcRect: CropBox,\n region: CropBox,\n transform: ImageTransform\n): { width: number; height: number; data: Uint8ClampedArray } {\n const rotated = getRotatedSize(region.width, region.height, transform);\n const w = Math.max(1, Math.round(rotated.width));\n const h = Math.max(1, Math.round(rotated.height));\n const out = new Uint8ClampedArray(w * h * 4);\n const p = baseProjection(region, transform, w, h);\n const o = unprojectCanvasPoint({ x: 0.5, y: 0.5 }, p);\n const ox = unprojectCanvasPoint({ x: 1.5, y: 0.5 }, p);\n const oy = unprojectCanvasPoint({ x: 0.5, y: 1.5 }, p);\n const ux = Math.round(ox.x - o.x);\n const vx = Math.round(ox.y - o.y);\n const uy = Math.round(oy.x - o.x);\n const vy = Math.round(oy.y - o.y);\n const u0 = Math.floor(o.x) - srcRect.x;\n const v0 = Math.floor(o.y) - srcRect.y;\n const sw = srcRect.width;\n const sh = srcRect.height;\n for (let y = 0; y < h; y++) {\n let u = u0 + y * uy;\n let v = v0 + y * vy;\n for (let x = 0; x < w; x++, u += ux, v += vx) {\n if (u < 0 || v < 0 || u >= sw || v >= sh) continue;\n const s = (v * sw + u) * 4;\n const d = (y * w + x) * 4;\n out[d] = src[s];\n out[d + 1] = src[s + 1];\n out[d + 2] = src[s + 2];\n out[d + 3] = src[s + 3];\n }\n }\n return { width: w, height: h, data: out };\n}\n\n/** Output opacity as the old base adjustments applied it (`clamp(a · opacity)`), then the migrated blur. */\nexport function applyOutputStage(img: { width: number; height: number; data: Uint8ClampedArray }, opacity: number, legacyOutputBlur?: number): void {\n if (opacity < 1) {\n const d = img.data;\n for (let i = 3; i < d.length; i += 4) d[i] = d[i] * opacity;\n }\n if (legacyOutputBlur && legacyOutputBlur > 0) applyBoxBlur(img, Math.min(Math.round(legacyOutputBlur), 40));\n}\n\nexport interface RouteInput {\n stack: LayerStack;\n region: CropBox;\n canvas: { width: number; height: number };\n transform: ImageTransform;\n /** Whether the source image is fully opaque. */\n sourceOpaque: boolean;\n}\n\n/**\n * Whether a document must render through the legacy base path to look exactly as before. Only\n * documents that still have the v1 / v2 shape qualify, and only where compositing in document\n * space cannot reproduce the old order of operations:\n *\n * - a rotation that is not a quarter turn, or a crop with fractional coordinates (the old path\n * resampled before adjusting);\n * - an enabled depth mask (applied after the blur and resampled with the canvas);\n * - output opacity below 1 (it rounded alpha before the depth mask);\n * - a Background blur filter with a crop or a 90° / 270° turn (the old blur clamped at the crop\n * edge and ran on the rotated buffer);\n * - a migrated output blur over a source with transparency (canvas premultiplication would round\n * colours before blurring);\n * - an adjustment-layer mask stored at another size than the canvas (it was resampled through\n * the projection on every render).\n */\nexport function needsLegacyBase(input: RouteInput): boolean {\n const { stack, region, canvas, transform } = input;\n if (!isLegacyShape(stack)) return false;\n const bg = stack.layers[0] as PixelLayer;\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) return true;\n if (bg.mask?.source?.enabled && bg.mask.source.maskSource) return true;\n if (stack.outputOpacity < 1) return true;\n const blur = blurFilter(bg);\n if (blur && blur.enabled && blur.radius > 0) {\n const full = region.x === 0 && region.y === 0 && region.width === canvas.width && region.height === canvas.height;\n const angle = normalizeAngle(transform.rotate);\n if (!full || angle === 90 || angle === 270) return true;\n }\n if ((stack.legacyOutputBlur ?? 0) > 0 && !input.sourceOpaque) return true;\n if (hasResampledLegacyMask(stack.layers, canvas.width, canvas.height)) return true;\n return false;\n}\n\nconst isPlainAnnotation = (l: ImageLayer): l is AnnotationLayer =>\n l.type === 'annotation' && l.opacity === 1 && l.blend === 'normal' && !l.clipped && !l.mask;\n\n/**\n * Splits the stack into what the compositor rasterizes and the annotation layers at the top that\n * draw as crisp vectors over the result (as annotations always did): the trailing run of plain\n * top-level annotation layers (hidden ones are skipped).\n */\nexport function splitOverlayAnnotations(layers: readonly ImageLayer[]): { composite: ImageLayer[]; overlay: AnnotationLayer[] } {\n let i = layers.length;\n while (i > 0) {\n const l = layers[i - 1];\n if (l.type === 'annotation' && (!l.visible || isPlainAnnotation(l))) i--;\n else break;\n }\n return {\n composite: layers.slice(0, i) as ImageLayer[],\n overlay: (layers.slice(i) as AnnotationLayer[]).filter((l) => l.visible),\n };\n}\n\n/** The integer rectangle covering `region`. */\nexport function coveringRect(region: CropBox): CropBox {\n const x = Math.floor(region.x);\n const y = Math.floor(region.y);\n return { x, y, width: Math.ceil(region.x + region.width) - x, height: Math.ceil(region.y + region.height) - y };\n}\n\n/**\n * The base image of a layered document without a canvas: the stack composited over `region`\n * (integer), projected under a quarter-turn `transform`, then output opacity and the migrated\n * blur. Annotation layers need a canvas and are skipped unless `sources` can rasterize them.\n * This is what the renderer's base canvas holds for such documents.\n */\nexport function renderLayeredBase(\n stack: LayerStack,\n region: CropBox,\n transform: ImageTransform,\n sources: CompositorSources,\n compositor = new LayerCompositor()\n): { width: number; height: number; data: Uint8ClampedArray } {\n if (!isQuarterTurn(transform.rotate) || !isIntegerRect(region)) {\n throw new Error('renderLayeredBase needs an integer region and a quarter-turn rotation.');\n }\n const composite = compositor.composite(stack.layers, region, sources);\n const out = projectQuarterTurn(composite, region, region, transform);\n applyOutputStage(out, stack.outputOpacity, stack.legacyOutputBlur);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Histograms\n// ---------------------------------------------------------------------------\n\n/** An empty histogram. */\nexport function emptyHistogram(): ImageHistogram {\n return {\n red: new Uint32Array(HISTOGRAM_BINS),\n green: new Uint32Array(HISTOGRAM_BINS),\n blue: new Uint32Array(HISTOGRAM_BINS),\n luminance: new Uint32Array(HISTOGRAM_BINS),\n samples: 0,\n transparent: 0,\n shadows: [0, 0, 0],\n highlights: [0, 0, 0],\n max: 0,\n };\n}\n\n/** Adds the pixels of `rect` (in tile-local coordinates of a `stride`-wide RGBA buffer) to `h`. */\nexport function addToHistogram(h: ImageHistogram, data: ArrayLike<number>, stride: number, rect: CropBox): void {\n for (let y = rect.y; y < rect.y + rect.height; y++) {\n let i = (y * stride + rect.x) * 4;\n for (let x = 0; x < rect.width; x++, i += 4) {\n if (!(data[i + 3] > 0)) {\n h.transparent++;\n continue;\n }\n h.samples++;\n const r = data[i];\n const g = data[i + 1];\n const b = data[i + 2];\n h.red[r]++;\n h.green[g]++;\n h.blue[b]++;\n h.luminance[Math.round(0.2126 * r + 0.7152 * g + 0.0722 * b)]++;\n if (r === 0) h.shadows[0]++;\n if (g === 0) h.shadows[1]++;\n if (b === 0) h.shadows[2]++;\n if (r === 255) h.highlights[0]++;\n if (g === 255) h.highlights[1]++;\n if (b === 255) h.highlights[2]++;\n }\n }\n}\n\n/** Sums histograms (and recomputes `max`). */\nexport function sumHistograms(parts: Iterable<ImageHistogram>): ImageHistogram {\n const out = emptyHistogram();\n for (const p of parts) {\n for (let i = 0; i < HISTOGRAM_BINS; i++) {\n out.red[i] += p.red[i];\n out.green[i] += p.green[i];\n out.blue[i] += p.blue[i];\n out.luminance[i] += p.luminance[i];\n }\n out.samples += p.samples;\n out.transparent += p.transparent;\n for (let c = 0; c < 3; c++) {\n out.shadows[c] += p.shadows[c];\n out.highlights[c] += p.highlights[c];\n }\n }\n let max = 0;\n for (let i = 0; i < HISTOGRAM_BINS; i++) max = Math.max(max, out.red[i], out.green[i], out.blue[i], out.luminance[i]);\n out.max = max;\n return out;\n}\n","import type {\n Annotation,\n CropBox,\n ImagePoint,\n ExportOptions,\n ImageEditorFit,\n ImageEditorState,\n ImageTransform,\n PenAnnotation,\n RectAnnotation,\n CircleAnnotation,\n ArrowAnnotation,\n LineAnnotation,\n TextAnnotation,\n ImageAnnotation,\n} from './types';\nimport { applyAdjustments, applyDepthMask } from './filters';\nimport { adjustmentLayersKey } from './adjustment-layers';\nimport { canvasCompositeOperation, blendImageData, normalizeBlendMode } from './blend';\nimport { computeHistogram, type ImageHistogram } from './histogram';\nimport { normalizeProcessVersion } from './color';\nimport { buildLayerMaskPlanes } from './selection/layer-mask';\nimport { selectionOutline, quickMaskPixels } from './selection/overlay';\nimport type { SelectionMask } from './selection/mask';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport { flattenLayers, type ImageLayer, type PixelLayer } from './layers/model';\nimport {\n addToHistogram,\n applyOutputStage,\n baseProjection,\n coveringRect,\n emptyHistogram,\n needsLegacyBase,\n splitOverlayAnnotations,\n sumHistograms,\n} from './layers/render';\nimport { TILE, tileKey, type PixelSurface, type TileKey } from '../raster/surface';\nimport { isLegacyShape as isLegacyStack } from './layers/migrate';\nimport {\n applyImageProjectionToContext,\n clampCropToImage,\n getAnnotationBounds,\n getRotatedSize,\n getViewportMetrics,\n getViewportProjection,\n normalizeAngle,\n type ImageProjection,\n type ViewportMetrics,\n} from './geometry';\n\nexport interface RenderOptions {\n viewportWidth?: number;\n viewportHeight?: number;\n showCropOverlay?: boolean;\n showSelectionOverlay?: boolean;\n interactive?: boolean;\n draftAnnotation?: Annotation | null;\n draftCrop?: CropBox | null;\n /** Viewport fit mode: 'contain' scales to full viewport bounds; 'margin' scales to 85% with 1.0 cap (default). */\n fit?: ImageEditorFit;\n /**\n * How the active selection is shown: marching ants (default when there is a selection),\n * a Quick Mask tint over the unselected area (default when `state.quickMask`), or not at all.\n */\n selectionDisplay?: 'ants' | 'quickMask' | 'none';\n /** Dash offset of the marching ants in screen pixels (animate it to make them march). */\n antsOffset?: number;\n /** A selection being drawn (e.g. a lasso), shown as a dashed outline in natural pixels. */\n draftSelectionPath?: ImagePoint[] | null;\n /** Document rectangles that are the only change since the last render (strokes); recomposites only there. */\n dirty?: CropBox[];\n /** The layer being edited: tiles keep a checkpoint below it so strokes and drags on it start there. */\n checkpointLayerId?: string | null;\n /** The paint tools' cursor: a circle of `diameter` px at (`x`, `y`) in natural pixels. */\n brushCursor?: { x: number; y: number; diameter: number } | null;\n}\n\n/**\n * Image Cache to avoid refetching HTMLImageElement objects repeatedly during re-renders.\n */\nclass ImageCache {\n private cache = new Map<string, HTMLImageElement>();\n\n public async get(src: string): Promise<HTMLImageElement> {\n const existing = this.cache.get(src);\n if (existing && existing.complete) {\n return existing;\n }\n\n return new Promise((resolve, reject) => {\n if (typeof window === 'undefined' || typeof Image === 'undefined') {\n reject(new Error('Image loading is only supported in browser/DOM environments'));\n return;\n }\n const img = new Image();\n img.crossOrigin = 'anonymous';\n img.onload = () => {\n this.cache.set(src, img);\n resolve(img);\n };\n img.onerror = (e) => reject(new Error(`Failed to load image from: ${src}`));\n img.src = src;\n\n // In JSDOM or test environments where Image resource loading is inactive, resolve promptly\n if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {\n setTimeout(() => {\n try {\n Object.defineProperty(img, 'naturalWidth', { value: 800, configurable: true });\n Object.defineProperty(img, 'naturalHeight', { value: 600, configurable: true });\n Object.defineProperty(img, 'complete', { value: true, configurable: true });\n } catch {\n // Ignore definition errors in restrictive environments\n }\n this.cache.set(src, img);\n resolve(img);\n }, 10);\n }\n });\n }\n\n public set(src: string, img: HTMLImageElement): void {\n this.cache.set(src, img);\n }\n\n /** The loaded image for `src`, or `undefined` while it is not loaded yet. */\n public peek(src: string): HTMLImageElement | undefined {\n const img = this.cache.get(src);\n return img && img.complete ? img : undefined;\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\nexport const globalImageCache = new ImageCache();\n\n/**\n * Renders an image with all its transformations, filters, depth masks, and annotations\n * to a target HTMLCanvasElement.\n */\nexport class ImageEditorRenderer {\n public imageCache = globalImageCache;\n private cachedBaseCanvas: HTMLCanvasElement | null = null;\n private cachedBaseKey = '';\n /**\n * Histogram of the last viewport base render (after adjustments and layers, before\n * annotations); `null` until a render with pixel access has happened.\n */\n public previewHistogram: ImageHistogram | null = null;\n /** Called whenever the viewport base image is re-rendered with the new histogram. */\n public onPreviewHistogram?: (histogram: ImageHistogram | null) => void;\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 0;\n /**\n * The viewport's compositor. It only ever composites the viewport's stack, so its tile cache can\n * trust dirty rectangles; exports use `exportCompositor`.\n */\n public compositor = new LayerCompositor();\n private exportCompositor = new LayerCompositor({ maxCacheBytes: 64 * 1024 * 1024 });\n /** How the host resolves layer pixels (set by the controller). */\n public layerSources?: LayerSourcesHost;\n /**\n * Debug / test switch: `'legacy'` forces the old base path for documents that still have the\n * v1 / v2 shape, `'layers'` forces the compositor. Default: chosen per document.\n */\n public baseRoute: 'auto' | 'legacy' | 'layers' = 'auto';\n /** Which path the last base render took. */\n public lastBaseRoute: 'legacy' | 'layers' | null = null;\n private comp: CompositeCanvas | null = null;\n private depthMaps = new Map<string, { width: number; height: number; data: Uint8ClampedArray }>();\n private assetVersion = 0;\n private scratch: HTMLCanvasElement | null = null;\n private cachedPostCanvas: HTMLCanvasElement | null = null;\n private cachedPostKey = '';\n private cachedPostHistogram: ImageHistogram | null = null;\n\n /**\n * Helper to create a canvas element safely in browser/JSDOM.\n */\n public createCanvas(width: number, height: number): HTMLCanvasElement {\n if (typeof document !== 'undefined' && document.createElement) {\n const c = document.createElement('canvas');\n c.width = Math.max(1, Math.round(width));\n c.height = Math.max(1, Math.round(height));\n return c;\n }\n // Fallback for non-DOM / headless\n return {\n width: Math.max(1, Math.round(width)),\n height: Math.max(1, Math.round(height)),\n getContext: () => null,\n toDataURL: () => '',\n toBlob: () => {},\n } as unknown as HTMLCanvasElement;\n }\n\n /**\n * Renders the editor state onto a canvas (viewport or preview).\n */\n public async renderToCanvas(\n targetCanvas: HTMLCanvasElement,\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: RenderOptions = {}\n ): Promise<void> {\n const ctx = targetCanvas.getContext('2d');\n if (!ctx) return;\n\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const fit = options.fit ?? state.fit ?? 'margin';\n // Geometry helpers read `imageDimensions`; make sure they see the real source size so the\n // viewport drawn here and the hit-testing done by the UI share one projection.\n const geoState: ImageEditorState = {\n ...state,\n fit,\n imageDimensions:\n state.imageDimensions.width === naturalWidth && state.imageDimensions.height === naturalHeight\n ? state.imageDimensions\n : { width: naturalWidth, height: naturalHeight },\n };\n\n const { crop, transform, adjustments, depthMask, annotations, selectedAnnotationId, zoom, pan, isOriginalCompared } = state;\n\n // Viewport dimensions\n const vpWidth = options.viewportWidth || targetCanvas.width;\n const vpHeight = options.viewportHeight || targetCanvas.height;\n\n targetCanvas.width = vpWidth;\n targetCanvas.height = vpHeight;\n\n ctx.clearRect(0, 0, vpWidth, vpHeight);\n\n // If in compare mode, render original image directly and exit\n if (isOriginalCompared) {\n this.drawFittedImage(ctx, img, naturalWidth, naturalHeight, vpWidth, vpHeight, zoom, pan, fit);\n return;\n }\n\n // When actively in crop tool or overlay requested, show full image so user can adjust the crop box\n const isActivelyCropping = Boolean(options.showCropOverlay || state.activeTool === 'crop');\n const projection = getViewportProjection(geoState, vpWidth, vpHeight, isActivelyCropping);\n const { region } = projection;\n const rotated = getRotatedSize(region.width, region.height, transform);\n\n const route = this.chooseRoute(state, region, naturalWidth, naturalHeight);\n let overlay: Annotation[] = annotations;\n if (route === 'legacy') {\n // 1. Offscreen canvas for Base Image + Transform (Flip/Rotate) + Crop + Adjustments + Depth mask\n const baseKey = [\n this.getSourceId(img),\n state.sourceUrl,\n `${naturalWidth}x${naturalHeight}`,\n `${region.x},${region.y},${region.width},${region.height}`,\n `${projection.angle},${transform.flipH},${transform.flipV}`,\n `${adjustments.brightness},${adjustments.contrast},${adjustments.saturation},${adjustments.exposure},${adjustments.temperature},${adjustments.tint ?? 0},${adjustments.blur},${adjustments.opacity},p${normalizeProcessVersion(adjustments.process)}`,\n adjustmentLayersKey(adjustments.layers),\n `${depthMask.enabled},${depthMask.maskSource},${depthMask.depthRange?.[0]}-${depthMask.depthRange?.[1]},${depthMask.softness},${depthMask.invert},${depthMask.mode}`,\n ].join('|');\n\n let baseCanvas = this.cachedBaseCanvas;\n if (!baseCanvas || this.cachedBaseKey !== baseKey) {\n let histogram: ImageHistogram | null = null;\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state, (pixels) => {\n histogram = computeHistogram(pixels);\n });\n this.cachedBaseCanvas = baseCanvas;\n this.cachedBaseKey = baseKey;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n // 2. Draw the base canvas into the viewport (centred, fitted, zoomed, panned)\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(baseCanvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n } else {\n // 1–2. The layer stack, composited per tile in document space, drawn through the projection.\n const split = splitOverlayAnnotations(state.layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const comp = this.updateComposite(split.composite, region, naturalWidth, naturalHeight, options);\n const base = this.layeredBase(comp, state, region, transform);\n if (base) {\n // Drawn exactly as the legacy base canvas is, so both paths look the same at any zoom.\n const drawW = rotated.width * projection.scale;\n const drawH = rotated.height * projection.scale;\n ctx.drawImage(base.canvas, projection.originX - drawW / 2, projection.originY - drawH / 2, drawW, drawH);\n this.publishHistogram(base.histogram ?? comp?.histogram() ?? null);\n }\n }\n\n // 3. Annotations live in natural image space: draw them through the same projection,\n // clipped to the visible (cropped) region.\n if (overlay.length > 0 || options.draftAnnotation) {\n ctx.save();\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n if (options.showSelectionOverlay && ann.id === selectedAnnotationId) {\n this.drawAnnotationSelection(ctx, ann);\n }\n }\n // A selected annotation drawn inside the composite still gets its handles.\n if (options.showSelectionOverlay && selectedAnnotationId && !overlay.some((a) => a.id === selectedAnnotationId)) {\n const sel = annotations.find((a) => a.id === selectedAnnotationId);\n if (sel) this.drawAnnotationSelection(ctx, sel);\n }\n\n // Render active live draft annotation\n if (options.draftAnnotation) {\n await this.drawAnnotation(ctx, options.draftAnnotation);\n }\n\n ctx.restore();\n }\n\n // 4. Selection: marching ants or Quick Mask, through the same projection.\n const display = options.selectionDisplay ?? (state.quickMask ? 'quickMask' : 'ants');\n if ((state.selection?.bounds && display !== 'none') || options.draftSelectionPath?.length) {\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n if (state.selection?.bounds && display === 'quickMask') this.drawQuickMask(ctx, state.selection);\n else if (state.selection?.bounds && display === 'ants') this.drawAnts(ctx, state.selection, projection.scale, options.antsOffset ?? 0);\n if (options.draftSelectionPath?.length) this.drawDraftPath(ctx, options.draftSelectionPath, projection.scale);\n } catch {\n // Overlays are best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // Brush cursor: the tip outline, one screen pixel wide, white over black.\n if (options.brushCursor) {\n const b = options.brushCursor;\n ctx.save();\n try {\n applyImageProjectionToContext(ctx, projection);\n const px = 1 / (projection.scale || 1);\n ctx.beginPath();\n ctx.arc(b.x, b.y, Math.max(px, b.diameter / 2), 0, Math.PI * 2);\n ctx.lineWidth = 2 * px;\n ctx.strokeStyle = 'rgba(0,0,0,0.6)';\n ctx.stroke();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n } catch {\n // Best effort (headless / mocked contexts).\n }\n ctx.restore();\n }\n\n // 5. Draw Crop Grid Overlay if crop overlay is requested\n if (isActivelyCropping) {\n const activeCrop =\n options.draftCrop !== undefined && options.draftCrop !== null\n ? options.draftCrop\n : (crop ?? { x: 0, y: 0, width: naturalWidth, height: naturalHeight });\n this.drawCropOverlay(ctx, getViewportMetrics(geoState, vpWidth, vpHeight, true, activeCrop));\n }\n }\n\n /**\n * Draws `region` of the source image with rotation/flip applied, then pixel adjustments\n * and the depth mask (which receives the identical transform so it stays aligned).\n */\n private async renderBaseImage(\n img: CanvasImageSource,\n naturalWidth: number,\n naturalHeight: number,\n region: CropBox,\n transform: ImageTransform,\n state: Pick<ImageEditorState, 'adjustments' | 'depthMask'>,\n onPixels?: (pixels: ImageData) => void\n ): Promise<HTMLCanvasElement> {\n const { adjustments, depthMask } = state;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const baseCanvas = this.createCanvas(baseW, baseH);\n const baseCtx = baseCanvas.getContext('2d');\n if (!baseCtx) return baseCanvas;\n\n const projection = getBaseProjection(region, transform, baseW, baseH);\n\n baseCtx.save();\n applyImageProjectionToContext(baseCtx, projection);\n baseCtx.drawImage(\n img,\n region.x, region.y, region.width, region.height,\n region.x, region.y, region.width, region.height\n );\n baseCtx.restore();\n\n const imageData = baseCtx.getImageData(0, 0, baseW, baseH);\n if (imageData) {\n // Layer masks live in natural pixels: resample them through the same projection.\n const layerMasks = buildLayerMaskPlanes(adjustments.layers, naturalWidth, naturalHeight, projection, baseW, baseH);\n applyAdjustments(imageData, adjustments, layerMasks ? { layerMasks } : {});\n\n if (depthMask.enabled && depthMask.maskSource) {\n try {\n const maskImg = await this.imageCache.get(depthMask.maskSource);\n const maskSize = getSourceSize(maskImg, naturalWidth, naturalHeight);\n const sx = maskSize.width / naturalWidth;\n const sy = maskSize.height / naturalHeight;\n const maskCanvas = this.createCanvas(baseW, baseH);\n const maskCtx = maskCanvas.getContext('2d');\n if (maskCtx) {\n // The depth map covers the whole source image; scale it to natural pixels and\n // apply the exact same rotate/flip/crop projection as the image itself.\n maskCtx.save();\n applyImageProjectionToContext(maskCtx, projection);\n maskCtx.drawImage(\n maskImg,\n region.x * sx, region.y * sy, region.width * sx, region.height * sy,\n region.x, region.y, region.width, region.height\n );\n maskCtx.restore();\n const maskData = maskCtx.getImageData(0, 0, baseW, baseH);\n if (maskData) {\n applyDepthMask(imageData, maskData, depthMask);\n }\n }\n } catch {\n // Mask failed to load; continue without crashing\n }\n }\n\n onPixels?.(imageData);\n baseCtx.putImageData(imageData, 0, 0);\n }\n\n return baseCanvas;\n }\n\n // --- Layered documents -------------------------------------------------------\n\n /** Which base path renders `state` (see `needsLegacyBase`). */\n private chooseRoute(state: ImageEditorState, region: CropBox, naturalWidth: number, naturalHeight: number): 'legacy' | 'layers' {\n let route: 'legacy' | 'layers';\n if (!state.layers?.length) route = 'legacy';\n else if (this.baseRoute === 'layers') route = 'layers';\n else {\n const stack = { layers: state.layers, outputOpacity: state.outputOpacity ?? 1, legacyOutputBlur: state.legacyOutputBlur };\n const shaped = this.baseRoute === 'legacy' && isLegacyStack(stack);\n route =\n shaped ||\n needsLegacyBase({\n stack,\n region,\n canvas: { width: naturalWidth, height: naturalHeight },\n transform: state.transform,\n sourceOpaque: this.layerSources?.sourceIsOpaque() ?? true,\n })\n ? 'legacy'\n : 'layers';\n }\n this.lastBaseRoute = route;\n return route;\n }\n\n /** Compositor sources for a `width × height` canvas: layer pixels, depth maps and annotation rasterizing. */\n public compositorSources(width: number, height: number): CompositorSources {\n const host = this.layerSources;\n return {\n width,\n height,\n surfaceOf: host ? (l: PixelLayer) => host.surfaceOf(l) : undefined,\n depthMap: (source) => {\n if (!source.enabled || !source.maskSource) return null;\n return this.depthMaps.get(`${source.maskSource}|${width}x${height}`) ?? null;\n },\n rasterizeAnnotation: (annotation, rect) => this.rasterizeAnnotation(annotation, rect),\n version: `${host?.version() ?? ''}:${this.assetVersion}`,\n };\n }\n\n /** Loads what the compositor reads synchronously: depth maps (at canvas size) and annotation images. */\n /** Loads depth maps and annotation images the compositor reads synchronously. */\n public async prepareLayerAssets(layers: readonly ImageLayer[], width: number, height: number): Promise<void> {\n for (const l of flattenLayers(layers)) {\n const source = l.mask?.source;\n if (source?.enabled && source.maskSource) {\n const key = `${source.maskSource}|${width}x${height}`;\n if (this.depthMaps.has(key)) continue;\n try {\n const maskImg = await this.imageCache.get(source.maskSource);\n const canvas = this.createCanvas(width, height);\n const mctx = canvas.getContext('2d');\n const data = mctx ? (mctx.drawImage(maskImg, 0, 0, width, height), mctx.getImageData?.(0, 0, width, height)) : null;\n if (data) {\n this.depthMaps.set(key, { width, height, data: data.data });\n this.assetVersion++;\n }\n } catch {\n // A depth map that fails to load reveals the layer, as before.\n }\n }\n if (l.type === 'annotation' && l.annotation.type === 'image' && !this.imageCache.peek(l.annotation.src)) {\n try {\n await this.imageCache.get(l.annotation.src);\n this.assetVersion++;\n } catch {\n // Skipped, as a watermark that fails to load always was.\n }\n }\n }\n }\n\n /** Whether annotation layers can be rasterized here (a 2D canvas with pixel access). */\n public canRasterize(): boolean {\n const ctx = this.createCanvas(1, 1).getContext('2d');\n return Boolean(ctx && typeof ctx.getImageData === 'function');\n }\n\n /** One annotation drawn into a tile-sized RGBA buffer at `rect` (document space), or `null` without a canvas. */\n private rasterizeAnnotation(annotation: Annotation, rect: CropBox): Uint8ClampedArray | null {\n if (!this.scratch) this.scratch = this.createCanvas(rect.width, rect.height);\n const canvas = this.scratch;\n if (canvas.width !== rect.width || canvas.height !== rect.height) {\n canvas.width = rect.width;\n canvas.height = rect.height;\n }\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n try {\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.clearRect(0, 0, rect.width, rect.height);\n ctx.setTransform(1, 0, 0, 1, -rect.x, -rect.y);\n const img = annotation.type === 'image' ? this.imageCache.peek(annotation.src) ?? null : null;\n this.drawAnnotationShape(ctx, annotation, img);\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n return new Uint8ClampedArray(ctx.getImageData(0, 0, rect.width, rect.height).data);\n } catch {\n return null;\n }\n }\n\n private putTile(ctx: CanvasRenderingContext2D, data: Uint8ClampedArray, tx: number, ty: number, rect: CropBox): void {\n const ox = tx * TILE - rect.x;\n const oy = ty * TILE - rect.y;\n const x0 = Math.max(0, -ox);\n const y0 = Math.max(0, -oy);\n const x1 = Math.min(TILE, rect.width - ox);\n const y1 = Math.min(TILE, rect.height - oy);\n if (x1 <= x0 || y1 <= y0) return;\n const img = ctx.createImageData(TILE, TILE);\n img.data.set(data);\n ctx.putImageData(img, ox, oy, x0, y0, x1 - x0, y1 - y0);\n }\n\n /**\n * Brings the viewport's composite canvas (document space, covering `region`) up to date,\n * uploading only the tiles that changed since the last render.\n */\n private updateComposite(\n layers: readonly ImageLayer[],\n region: CropBox,\n width: number,\n height: number,\n options: Pick<RenderOptions, 'dirty' | 'checkpointLayerId'> = {}\n ): CompositeCanvas | null {\n const rect = coveringRect(region);\n let comp = this.comp;\n let fresh = false;\n if (!comp || comp.rect.x !== rect.x || comp.rect.y !== rect.y || comp.rect.width !== rect.width || comp.rect.height !== rect.height) {\n const canvas = this.createCanvas(rect.width, rect.height);\n comp = this.comp = new CompositeCanvas(canvas, rect);\n fresh = true;\n }\n const ctx = comp.canvas.getContext('2d');\n if (!ctx || typeof ctx.createImageData !== 'function') return null;\n let changed = false;\n this.compositor.compositeTiles(layers, rect, this.compositorSources(width, height), (tx, ty, data) => {\n const key = tileKey(tx, ty);\n if (comp!.tiles.get(key) === data) return;\n comp!.tiles.set(key, data);\n comp!.histograms.delete(key);\n this.putTile(ctx, data, tx, ty, rect);\n changed = true;\n }, { checkpointLayerId: options.checkpointLayerId, dirty: fresh ? undefined : options.dirty });\n if (changed) comp.version++;\n comp.region = region;\n return comp;\n }\n\n /**\n * The base canvas of a layered document: the composite drawn through the base projection (crop,\n * rotation, flips at scale 1), then output opacity and the migrated blur, which run in the output\n * frame. Cached until the composite or those settings change.\n */\n private layeredBase(\n comp: CompositeCanvas | null,\n state: ImageEditorState,\n region: CropBox,\n transform: ImageTransform\n ): { canvas: HTMLCanvasElement; histogram: ImageHistogram | null } | null {\n if (!comp) return null;\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n const key = `${comp.version}|${region.x},${region.y},${region.width},${region.height}|${normalizeAngle(transform.rotate)},${transform.flipH},${transform.flipV}|${opacity}|${blur}`;\n if (this.cachedPostCanvas && this.cachedPostKey === key) return { canvas: this.cachedPostCanvas, histogram: this.cachedPostHistogram };\n const canvas =\n this.cachedPostCanvas && this.cachedPostCanvas.width === baseW && this.cachedPostCanvas.height === baseH\n ? this.cachedPostCanvas\n : this.createCanvas(baseW, baseH);\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n ctx.clearRect(0, 0, baseW, baseH);\n ctx.save();\n applyImageProjectionToContext(ctx, baseProjection(region, transform, baseW, baseH));\n ctx.beginPath();\n ctx.rect(region.x, region.y, region.width, region.height);\n ctx.clip();\n ctx.drawImage(comp.canvas, comp.rect.x, comp.rect.y);\n ctx.restore();\n const histogram = this.applyPost(ctx, baseW, baseH, state);\n this.cachedPostCanvas = canvas;\n this.cachedPostKey = key;\n this.cachedPostHistogram = histogram;\n return { canvas, histogram };\n }\n\n /** Output opacity then the migrated blur, in place on a base canvas; returns its histogram. */\n private applyPost(ctx: CanvasRenderingContext2D, w: number, h: number, state: ImageEditorState): ImageHistogram | null {\n const opacity = state.outputOpacity ?? 1;\n const blur = state.legacyOutputBlur ?? 0;\n if (!(opacity < 1) && !(blur > 0)) return null;\n const data = ctx.getImageData?.(0, 0, w, h);\n if (!data) return null;\n applyOutputStage(data, opacity, blur);\n ctx.putImageData(data, 0, 0);\n return computeHistogram(data);\n }\n\n private publishHistogram(histogram: ImageHistogram | null): void {\n if (histogram === this.previewHistogram) return;\n this.previewHistogram = histogram;\n this.onPreviewHistogram?.(histogram);\n }\n\n private quickMaskCache = new WeakMap<SelectionMask, HTMLCanvasElement>();\n private outlineCache = new WeakMap<SelectionMask, Float32Array>();\n\n /** Rubylith over the unselected area, drawn in natural image space. */\n private drawQuickMask(ctx: CanvasRenderingContext2D, selection: SelectionMask): void {\n let canvas = this.quickMaskCache.get(selection);\n if (!canvas) {\n canvas = this.createCanvas(selection.width, selection.height);\n const mctx = canvas.getContext('2d');\n if (!mctx || typeof mctx.createImageData !== 'function') return;\n const img = mctx.createImageData(selection.width, selection.height);\n img.data.set(quickMaskPixels(selection));\n mctx.putImageData(img, 0, 0);\n this.quickMaskCache.set(selection, canvas);\n }\n ctx.drawImage(canvas, 0, 0, selection.width, selection.height);\n }\n\n /** Marching ants: a white line under a black dashed one, one screen pixel wide. */\n private drawAnts(ctx: CanvasRenderingContext2D, selection: SelectionMask, scale: number, offset: number): void {\n let segs = this.outlineCache.get(selection);\n if (!segs) {\n segs = selectionOutline(selection);\n this.outlineCache.set(selection, segs);\n }\n if (!segs.length) return;\n const px = 1 / (scale || 1);\n ctx.beginPath();\n for (let i = 0; i < segs.length; i += 4) {\n ctx.moveTo(segs[i], segs[i + 1]);\n ctx.lineTo(segs[i + 2], segs[i + 3]);\n }\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') {\n ctx.setLineDash([4 * px, 4 * px]);\n ctx.lineDashOffset = -offset * px;\n }\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private drawDraftPath(ctx: CanvasRenderingContext2D, points: ImagePoint[], scale: number): void {\n const px = 1 / (scale || 1);\n ctx.beginPath();\n points.forEach((p, i) => (i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y)));\n ctx.closePath();\n ctx.lineWidth = px;\n ctx.strokeStyle = '#ffffff';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([4 * px, 4 * px]);\n ctx.strokeStyle = '#000000';\n ctx.stroke();\n if (typeof ctx.setLineDash === 'function') ctx.setLineDash([]);\n }\n\n private getSourceId(img: CanvasImageSource): number {\n const key = img as unknown as object;\n let id = this.sourceIds.get(key);\n if (id === undefined) {\n id = ++this.nextSourceId;\n this.sourceIds.set(key, id);\n }\n return id;\n }\n\n /**\n * Draws an individual annotation onto the canvas.\n */\n public async drawAnnotation(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n const blend = normalizeBlendMode(ann.blend);\n if (blend === 'normal') {\n await this.drawAnnotationContent(ctx, ann);\n return;\n }\n const op = canvasCompositeOperation(blend);\n if (op) {\n ctx.save();\n ctx.globalCompositeOperation = op;\n await this.drawAnnotationContent(ctx, ann);\n ctx.restore();\n return;\n }\n if (!(await this.drawAnnotationBlended(ctx, ann))) {\n // No pixel access (e.g. a tainted or headless canvas): draw it normally.\n await this.drawAnnotationContent(ctx, ann);\n }\n }\n\n /**\n * Scalar fallback for blend modes the canvas cannot composite (Photoshop soft light, linear /\n * vivid / pin light, hard mix, subtract, divide, darker / lighter colour, linear burn / dodge):\n * draws the annotation on a same-size layer with the same transform, blends it into a copy of\n * the target's pixels with `blendImageData`, and copies the result back inside the current clip.\n */\n private async drawAnnotationBlended(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<boolean> {\n const canvas = ctx.canvas as HTMLCanvasElement | undefined;\n if (!canvas || typeof ctx.getTransform !== 'function' || typeof ctx.getImageData !== 'function') return false;\n const w = canvas.width;\n const h = canvas.height;\n const layer = this.createCanvas(w, h);\n const lctx = layer.getContext('2d');\n if (!lctx) return false;\n try {\n lctx.setTransform(ctx.getTransform());\n lctx.globalAlpha = ctx.globalAlpha;\n await this.drawAnnotationContent(lctx, ann);\n const backdrop = ctx.getImageData(0, 0, w, h);\n const source = lctx.getImageData(0, 0, w, h);\n if (!backdrop || !source) return false;\n blendImageData(backdrop, source, normalizeBlendMode(ann.blend));\n lctx.setTransform(1, 0, 0, 1, 0, 0);\n lctx.putImageData(backdrop, 0, 0);\n } catch {\n return false;\n }\n ctx.save();\n ctx.setTransform(1, 0, 0, 1, 0, 0);\n ctx.globalAlpha = 1;\n ctx.globalCompositeOperation = 'copy';\n ctx.drawImage(layer, 0, 0);\n ctx.restore();\n return true;\n }\n\n private async drawAnnotationContent(ctx: CanvasRenderingContext2D, ann: Annotation): Promise<void> {\n let img: CanvasImageSource | null = null;\n if (ann.type === 'image') {\n try {\n img = await this.imageCache.get(ann.src);\n } catch {\n // Skip if watermark image failed to load\n }\n }\n this.drawAnnotationShape(ctx, ann, img);\n }\n\n /** Draws an annotation synchronously; an image annotation draws `img` (skipped when `null`). */\n public drawAnnotationShape(ctx: CanvasRenderingContext2D, ann: Annotation, img: CanvasImageSource | null): void {\n ctx.save();\n if (ann.opacity !== undefined) {\n ctx.globalAlpha = ann.opacity;\n }\n\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n const pen = ann as PenAnnotation;\n if (pen.points.length > 0) {\n ctx.beginPath();\n ctx.strokeStyle = pen.strokeColor;\n ctx.lineWidth = pen.strokeWidth;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n ctx.moveTo(pen.points[0].x, pen.points[0].y);\n for (let i = 1; i < pen.points.length; i++) {\n ctx.lineTo(pen.points[i].x, pen.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n\n case 'rect': {\n const rect = ann as RectAnnotation;\n ctx.strokeStyle = rect.strokeColor;\n ctx.lineWidth = rect.strokeWidth;\n if (rect.fillColor) {\n ctx.fillStyle = rect.fillColor;\n ctx.fillRect(rect.x, rect.y, rect.width, rect.height);\n }\n if (rect.strokeWidth > 0) {\n ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);\n }\n break;\n }\n\n case 'circle': {\n const circle = ann as CircleAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = circle.strokeColor;\n ctx.lineWidth = circle.strokeWidth;\n ctx.ellipse(circle.x, circle.y, Math.abs(circle.radiusX), Math.abs(circle.radiusY), 0, 0, 2 * Math.PI);\n if (circle.fillColor) {\n ctx.fillStyle = circle.fillColor;\n ctx.fill();\n }\n if (circle.strokeWidth > 0) {\n ctx.stroke();\n }\n break;\n }\n\n case 'arrow': {\n const arrow = ann as ArrowAnnotation;\n ctx.strokeStyle = arrow.strokeColor;\n ctx.fillStyle = arrow.strokeColor;\n ctx.lineWidth = arrow.strokeWidth;\n\n // Line body\n ctx.beginPath();\n ctx.moveTo(arrow.x, arrow.y);\n ctx.lineTo(arrow.endX, arrow.endY);\n ctx.stroke();\n\n // Arrow head\n const angle = Math.atan2(arrow.endY - arrow.y, arrow.endX - arrow.x);\n const headLen = Math.max(12, arrow.strokeWidth * 3);\n ctx.beginPath();\n ctx.moveTo(arrow.endX, arrow.endY);\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle - Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle - Math.PI / 6)\n );\n ctx.lineTo(\n arrow.endX - headLen * Math.cos(angle + Math.PI / 6),\n arrow.endY - headLen * Math.sin(angle + Math.PI / 6)\n );\n ctx.closePath();\n ctx.fill();\n break;\n }\n\n case 'line': {\n const line = ann as LineAnnotation;\n ctx.beginPath();\n ctx.strokeStyle = line.strokeColor;\n ctx.lineWidth = line.strokeWidth;\n ctx.moveTo(line.x, line.y);\n ctx.lineTo(line.endX, line.endY);\n ctx.stroke();\n break;\n }\n\n case 'text': {\n const textAnn = ann as TextAnnotation;\n ctx.font = `${textAnn.fontWeight || 'normal'} ${textAnn.fontSize}px ${textAnn.fontFamily || 'sans-serif'}`;\n ctx.fillStyle = textAnn.color;\n ctx.textBaseline = 'top';\n ctx.fillText(textAnn.text, textAnn.x, textAnn.y);\n break;\n }\n\n case 'image': {\n const imgAnn = ann as ImageAnnotation;\n if (img) ctx.drawImage(img, imgAnn.x, imgAnn.y, imgAnn.width, imgAnn.height);\n break;\n }\n }\n\n ctx.restore();\n }\n\n /**\n * Draws selection box around active annotation.\n */\n private drawAnnotationSelection(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.strokeStyle = '#3b82f6'; // Tailwind blue-500\n ctx.lineWidth = 1.5;\n ctx.setLineDash([4, 4]);\n\n const bounds = getAnnotationBounds(ann);\n const pad = 4;\n ctx.strokeRect(bounds.x - pad, bounds.y - pad, bounds.width + pad * 2, bounds.height + pad * 2);\n\n // Draw handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#3b82f6';\n ctx.setLineDash([]);\n const handleSize = 6;\n const corners = [\n { x: bounds.x - pad, y: bounds.y - pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y - pad },\n { x: bounds.x - pad, y: bounds.y + bounds.height + pad },\n { x: bounds.x + bounds.width + pad, y: bounds.y + bounds.height + pad },\n ];\n for (const c of corners) {\n ctx.fillRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(c.x - handleSize / 2, c.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n /** @deprecated Use the standalone `getAnnotationBounds` from the geometry module. */\n public getAnnotationBounds(ann: Annotation): { x: number; y: number; width: number; height: number } {\n return getAnnotationBounds(ann);\n }\n\n /**\n * Draws a rule-of-thirds crop grid overlay. All rectangles come from\n * `getViewportMetrics`, i.e. the crop box projected through rotation/flip.\n */\n private drawCropOverlay(ctx: CanvasRenderingContext2D, metrics: ViewportMetrics): void {\n const { imageRect, cropRect } = metrics;\n if (!cropRect) return;\n\n const imgL = imageRect.x;\n const imgT = imageRect.y;\n const imgR = imageRect.x + imageRect.width;\n const imgB = imageRect.y + imageRect.height;\n const startX = cropRect.x;\n const startY = cropRect.y;\n const cropBoxW = cropRect.width;\n const cropBoxH = cropRect.height;\n\n ctx.save();\n\n // Dark semi-transparent scrim outside crop box (within the image)\n ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n // Top\n ctx.fillRect(imgL, imgT, imageRect.width, Math.max(0, startY - imgT));\n // Bottom\n ctx.fillRect(imgL, startY + cropBoxH, imageRect.width, Math.max(0, imgB - (startY + cropBoxH)));\n // Left\n ctx.fillRect(imgL, startY, Math.max(0, startX - imgL), cropBoxH);\n // Right\n ctx.fillRect(startX + cropBoxW, startY, Math.max(0, imgR - (startX + cropBoxW)), cropBoxH);\n\n // Crop border\n ctx.strokeStyle = '#ffffff';\n ctx.lineWidth = 2;\n ctx.strokeRect(startX, startY, cropBoxW, cropBoxH);\n\n // Rule of thirds lines\n ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)';\n ctx.lineWidth = 1;\n ctx.beginPath();\n // Vertical lines\n ctx.moveTo(startX + cropBoxW / 3, startY);\n ctx.lineTo(startX + cropBoxW / 3, startY + cropBoxH);\n ctx.moveTo(startX + (cropBoxW * 2) / 3, startY);\n ctx.lineTo(startX + (cropBoxW * 2) / 3, startY + cropBoxH);\n // Horizontal lines\n ctx.moveTo(startX, startY + cropBoxH / 3);\n ctx.lineTo(startX + cropBoxW, startY + cropBoxH / 3);\n ctx.moveTo(startX, startY + (cropBoxH * 2) / 3);\n ctx.lineTo(startX + cropBoxW, startY + (cropBoxH * 2) / 3);\n ctx.stroke();\n\n // Corner and edge handles\n ctx.fillStyle = '#ffffff';\n ctx.strokeStyle = '#0284c7';\n ctx.lineWidth = 1.5;\n const handleSize = 10;\n const handles = [\n // Corners\n { x: startX, y: startY },\n { x: startX + cropBoxW, y: startY },\n { x: startX, y: startY + cropBoxH },\n { x: startX + cropBoxW, y: startY + cropBoxH },\n // Edges\n { x: startX + cropBoxW / 2, y: startY },\n { x: startX + cropBoxW / 2, y: startY + cropBoxH },\n { x: startX, y: startY + cropBoxH / 2 },\n { x: startX + cropBoxW, y: startY + cropBoxH / 2 },\n ];\n for (const h of handles) {\n ctx.fillRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n ctx.strokeRect(h.x - handleSize / 2, h.y - handleSize / 2, handleSize, handleSize);\n }\n\n ctx.restore();\n }\n\n private drawFittedImage(\n ctx: CanvasRenderingContext2D,\n img: CanvasImageSource,\n imgW: number,\n imgH: number,\n vpW: number,\n vpH: number,\n zoom: number,\n pan: { x: number; y: number },\n fit: ImageEditorFit = 'margin'\n ): void {\n ctx.save();\n ctx.translate(vpW / 2 + pan.x, vpH / 2 + pan.y);\n ctx.scale(zoom, zoom);\n const fitScale =\n fit === 'contain'\n ? Math.min(vpW / imgW, vpH / imgH)\n : Math.min((vpW * 0.85) / imgW, (vpH * 0.85) / imgH, 1);\n const renderW = imgW * fitScale;\n const renderH = imgH * fitScale;\n ctx.drawImage(img, -renderW / 2, -renderH / 2, renderW, renderH);\n ctx.restore();\n }\n\n /**\n * Performs an export render at exact pixel dimensions without zoom/pan viewport offsets.\n */\n public async renderExport(\n imageSource: CanvasImageSource | string,\n state: ImageEditorState,\n options: ExportOptions = {}\n ): Promise<HTMLCanvasElement> {\n let img: CanvasImageSource;\n if (typeof imageSource === 'string') {\n img = await this.imageCache.get(imageSource);\n } else {\n img = imageSource;\n }\n\n const { width: naturalWidth, height: naturalHeight } = getSourceSize(img);\n const { crop, transform, annotations } = state;\n\n const region = crop\n ? clampCropToImage(crop, naturalWidth, naturalHeight)\n : { x: 0, y: 0, width: naturalWidth, height: naturalHeight };\n const rotated = getRotatedSize(region.width, region.height, transform);\n const baseW = Math.max(1, Math.round(rotated.width));\n const baseH = Math.max(1, Math.round(rotated.height));\n\n const exportW = options.width || baseW;\n const exportH = options.height || baseH;\n\n const exportCanvas = this.createCanvas(exportW, exportH);\n const ctx = exportCanvas.getContext('2d');\n if (!ctx) return exportCanvas;\n\n let overlay: Annotation[] = annotations;\n let baseCanvas: HTMLCanvasElement;\n // A render of chosen layers (`layerIds`) always composites them; the legacy views describe the whole stack.\n if (!options.layerIds && this.chooseRoute(state, region, naturalWidth, naturalHeight) === 'legacy') {\n baseCanvas = await this.renderBaseImage(img, naturalWidth, naturalHeight, region, transform, state);\n } else {\n const layers = options.includeAnnotations === false ? removeAnnotationLayers(state.layers) : state.layers;\n const split = splitOverlayAnnotations(layers);\n overlay = split.overlay.map((l) => l.annotation);\n await this.prepareLayerAssets(split.composite, naturalWidth, naturalHeight);\n const rect = coveringRect(region);\n const comp = this.createCanvas(rect.width, rect.height);\n const cctx = comp.getContext('2d');\n if (cctx && typeof cctx.createImageData === 'function') {\n this.exportCompositor.compositeTiles(split.composite, rect, this.compositorSources(naturalWidth, naturalHeight), (tx, ty, data) => {\n this.putTile(cctx, data, tx, ty, rect);\n });\n }\n baseCanvas = this.createCanvas(baseW, baseH);\n const bctx = baseCanvas.getContext('2d');\n if (bctx) {\n bctx.save();\n applyImageProjectionToContext(bctx, baseProjection(region, transform, baseW, baseH));\n bctx.beginPath();\n bctx.rect(region.x, region.y, region.width, region.height);\n bctx.clip();\n bctx.drawImage(comp, rect.x, rect.y);\n bctx.restore();\n this.applyPost(bctx, baseW, baseH, state);\n }\n }\n\n // Scale to target export dimensions\n ctx.drawImage(baseCanvas, 0, 0, baseW, baseH, 0, 0, exportW, exportH);\n\n // Draw annotations (natural image space) through the same rotate/flip/crop projection\n if (options.includeAnnotations !== false && overlay.length > 0) {\n ctx.save();\n ctx.scale(exportW / baseW, exportH / baseH);\n applyImageProjectionToContext(ctx, getBaseProjection(region, transform, baseW, baseH));\n for (const ann of overlay) {\n await this.drawAnnotation(ctx, ann);\n }\n ctx.restore();\n }\n\n return exportCanvas;\n }\n\n /**\n * Generates a strictly binary mask canvas (white = area to regenerate, black = keep) in\n * natural image pixel space of the current source image — the same space the crop box\n * and annotations are stored in. Pair it with `toSourceDataURL()` for AI inpainting.\n */\n public async renderMask(\n state: ImageEditorState,\n options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }\n ): Promise<HTMLCanvasElement> {\n const naturalWidth = Math.max(1, Math.round(state.imageDimensions.width));\n const naturalHeight = Math.max(1, Math.round(state.imageDimensions.height));\n const maskCanvas = this.createCanvas(naturalWidth, naturalHeight);\n const ctx = maskCanvas.getContext('2d');\n if (!ctx) return maskCanvas;\n\n // Background: unmasked (black)\n ctx.fillStyle = MASK_BLACK;\n ctx.fillRect(0, 0, naturalWidth, naturalHeight);\n\n const useCrop = options?.useCrop ?? true;\n const useAnnotations = options?.useAnnotations ?? true;\n\n // Crop box mask (if active)\n if (useCrop && state.crop) {\n ctx.fillStyle = MASK_WHITE;\n ctx.fillRect(state.crop.x, state.crop.y, state.crop.width, state.crop.height);\n }\n\n // Annotation masks (drawn in solid white, never in annotation colours)\n if (useAnnotations && state.annotations.length > 0) {\n for (const ann of state.annotations) {\n this.drawAnnotationMask(ctx, ann);\n }\n }\n\n // Anti-aliased edges produce grey pixels: threshold to pure black/white.\n const imageData = ctx.getImageData?.(0, 0, naturalWidth, naturalHeight);\n // The active selection (same natural space) is added to the mask.\n const selection =\n (options?.useSelection ?? true) &&\n state.selection?.bounds &&\n state.selection.width === naturalWidth &&\n state.selection.height === naturalHeight\n ? state.selection\n : null;\n if (imageData) {\n const d = imageData.data;\n for (let i = 0; i < d.length; i += 4) {\n const v = d[i] >= 128 || (selection !== null && selection.data[i >> 2] >= 128) ? 255 : 0;\n d[i] = v;\n d[i + 1] = v;\n d[i + 2] = v;\n d[i + 3] = 255;\n }\n ctx.putImageData(imageData, 0, 0);\n }\n\n return maskCanvas;\n }\n\n private drawAnnotationMask(ctx: CanvasRenderingContext2D, ann: Annotation): void {\n ctx.save();\n ctx.globalAlpha = 1;\n ctx.fillStyle = MASK_WHITE;\n ctx.strokeStyle = MASK_WHITE;\n ctx.lineCap = 'round';\n ctx.lineJoin = 'round';\n if (ann.rotation) {\n ctx.translate(ann.x, ann.y);\n ctx.rotate((ann.rotation * Math.PI) / 180);\n ctx.translate(-ann.x, -ann.y);\n }\n\n switch (ann.type) {\n case 'pen': {\n if (ann.points.length > 0) {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.points[0].x, ann.points[0].y);\n for (let i = 1; i < ann.points.length; i++) {\n ctx.lineTo(ann.points[i].x, ann.points[i].y);\n }\n ctx.stroke();\n }\n break;\n }\n case 'circle': {\n ctx.beginPath();\n ctx.ellipse(ann.x, ann.y, Math.abs(ann.radiusX), Math.abs(ann.radiusY), 0, 0, Math.PI * 2);\n ctx.fill();\n break;\n }\n case 'arrow':\n case 'line': {\n ctx.beginPath();\n ctx.lineWidth = Math.max(ann.strokeWidth || 0, 8);\n ctx.moveTo(ann.x, ann.y);\n ctx.lineTo(ann.endX, ann.endY);\n ctx.stroke();\n break;\n }\n case 'rect':\n case 'image':\n case 'text': {\n const b = getAnnotationBounds(ann);\n ctx.fillRect(b.x, b.y, b.width, b.height);\n break;\n }\n }\n ctx.restore();\n }\n}\n\nconst MASK_WHITE = '#ffffff';\nconst MASK_BLACK = '#000000';\n\nfunction getSourceSize(img: CanvasImageSource, fallbackW = 800, fallbackH = 600): { width: number; height: number } {\n const anyImg = img as any;\n return {\n width: anyImg.naturalWidth || anyImg.videoWidth || anyImg.width || fallbackW,\n height: anyImg.naturalHeight || anyImg.videoHeight || anyImg.height || fallbackH,\n };\n}\n\nfunction getBaseProjection(region: CropBox, transform: ImageTransform, baseW: number, baseH: number): ImageProjection {\n return {\n region,\n angle: normalizeAngle(transform.rotate),\n flipH: transform.flipH,\n flipV: transform.flipV,\n scale: 1,\n originX: baseW / 2,\n originY: baseH / 2,\n };\n}\n\n/** What the renderer asks its host (the controller) about layer pixels. */\nexport interface LayerSourcesHost {\n surfaceOf(layer: PixelLayer): PixelSurface;\n sourceIsOpaque(): boolean;\n version(): string;\n}\n\n/** The viewport's composite canvas and what each of its tiles holds. */\nclass CompositeCanvas {\n readonly tiles = new Map<TileKey, Uint8ClampedArray>();\n readonly histograms = new Map<TileKey, ImageHistogram>();\n version = 0;\n region: CropBox;\n constructor(readonly canvas: HTMLCanvasElement, readonly rect: CropBox) {\n this.region = rect;\n }\n\n /** Histogram of the composite over the region (per-tile parts are cached). */\n histogram(): ImageHistogram | null {\n if (!this.tiles.size) return null;\n const r = coveringRect(this.region);\n const parts: ImageHistogram[] = [];\n for (const [key, data] of this.tiles) {\n let h = this.histograms.get(key);\n if (!h) {\n const tx = (key % 65536) - 32768;\n const ty = Math.floor(key / 65536) - 32768;\n const x0 = Math.max(r.x, tx * TILE);\n const y0 = Math.max(r.y, ty * TILE);\n const x1 = Math.min(r.x + r.width, tx * TILE + TILE);\n const y1 = Math.min(r.y + r.height, ty * TILE + TILE);\n h = emptyHistogram();\n if (x1 > x0 && y1 > y0) addToHistogram(h, data, TILE, { x: x0 - tx * TILE, y: y0 - ty * TILE, width: x1 - x0, height: y1 - y0 });\n this.histograms.set(key, h);\n }\n parts.push(h);\n }\n return sumHistograms(parts);\n }\n}\n\nfunction removeAnnotationLayers(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: removeAnnotationLayers(l.children) } : l);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox, ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type SelectionMask } from './mask';\n\nexport interface ShapeSelectionOptions {\n /** Partial coverage along the edge (default true). */\n antiAlias?: boolean;\n}\n\ninterface Edge {\n top: number;\n bottom: number;\n ax: number;\n ay: number;\n bx: number;\n by: number;\n}\n\n/**\n * Even-odd polygon coverage over a `width × height` image: exact horizontal coverage on 4\n * sub-rows per pixel row (1 without anti-aliasing, where a pixel is in when its centre is).\n * Each sub-row only tests the edges that cross it (an active edge list over edges sorted by\n * their top), so the cost is about pixels + edges × rows they span.\n */\nexport function polygonCoverage(\n points: readonly ImagePoint[],\n width: number,\n height: number,\n antiAlias = true\n): Float32Array {\n const m = new Float32Array(width * height);\n if (points.length < 3 || width <= 0 || height <= 0) return m;\n const sub = antiAlias ? 4 : 1;\n const edges: Edge[] = [];\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n if (![a.x, a.y, b.x, b.y].every(Number.isFinite)) continue;\n if (a.y < b.y) edges.push({ top: a.y, bottom: b.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n else if (b.y < a.y) edges.push({ top: b.y, bottom: a.y, ax: a.x, ay: a.y, bx: b.x, by: b.y });\n }\n edges.sort((p, q) => p.top - q.top);\n let next = 0;\n let active: Edge[] = [];\n const xs: number[] = [];\n for (let py = 0; py < height; py++) {\n const row = py * width;\n for (let s = 0; s < sub; s++) {\n const y = py + (s + 0.5) / sub;\n while (next < edges.length && edges[next].top <= y) active.push(edges[next++]);\n if (active.length) active = active.filter((e) => e.bottom > y);\n if (!active.length) continue;\n xs.length = 0;\n for (const e of active) {\n if (e.top <= y) xs.push(e.ax + ((y - e.ay) / (e.by - e.ay)) * (e.bx - e.ax));\n }\n xs.sort((a, b) => a - b);\n for (let k = 0; k + 1 < xs.length; k += 2) {\n const x0 = Math.max(0, xs[k]);\n const x1 = Math.min(width, xs[k + 1]);\n if (x1 <= x0) continue;\n const c0 = Math.floor(x0);\n const c1 = Math.min(width, Math.ceil(x1));\n for (let cx = c0; cx < c1; cx++) {\n let cov: number;\n if (antiAlias) cov = Math.min(cx + 1, x1) - Math.max(cx, x0);\n else {\n const c = cx + 0.5;\n cov = c >= x0 && c < x1 ? 1 : 0;\n }\n if (cov > 0) m[row + cx] += cov / sub;\n }\n }\n }\n }\n for (let i = 0; i < m.length; i++) if (m[i] > 1) m[i] = 1;\n return m;\n}\n\nfunction toMask(width: number, height: number, coverage: Float32Array): SelectionMask {\n const out = new Uint8Array(coverage.length);\n for (let i = 0; i < out.length; i++) out[i] = coverageToByte(coverage[i]);\n return createSelectionMask(width, height, out);\n}\n\n/** Lasso / polygonal lasso: an even-odd polygon in natural image pixels. */\nexport function polygonSelection(\n width: number,\n height: number,\n points: readonly ImagePoint[],\n options: ShapeSelectionOptions = {}\n): SelectionMask {\n return toMask(width, height, polygonCoverage(points, width, height, options.antiAlias ?? true));\n}\n\n/** Normalises a rectangle with a negative width or height. */\nexport function normalizeRect(rect: CropBox): CropBox {\n const x = rect.width < 0 ? rect.x + rect.width : rect.x;\n const y = rect.height < 0 ? rect.y + rect.height : rect.y;\n return { x, y, width: Math.abs(rect.width), height: Math.abs(rect.height) };\n}\n\n/** Rectangular marquee. Fractional edges get partial coverage when anti-aliased. */\nexport function rectSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n const r = normalizeRect(rect);\n const pts = [\n { x: r.x, y: r.y },\n { x: r.x + r.width, y: r.y },\n { x: r.x + r.width, y: r.y + r.height },\n { x: r.x, y: r.y + r.height },\n ];\n return polygonSelection(width, height, pts, options);\n}\n\n/** The ellipse inscribed in `rect` as a polygon fine enough that its chords stay within 0.05 px. */\nexport function ellipsePoints(rect: CropBox): ImagePoint[] {\n const r = normalizeRect(rect);\n const rx = r.width / 2;\n const ry = r.height / 2;\n const cx = r.x + rx;\n const cy = r.y + ry;\n const rmax = Math.max(rx, ry, 0.5);\n // Sagitta of a chord with angle step θ is r(1 − cos(θ/2)) ≈ rθ²/8 ≤ 0.05.\n const step = Math.sqrt(0.4 / rmax);\n const n = Math.min(4096, Math.max(16, Math.ceil((2 * Math.PI) / step)));\n const pts: ImagePoint[] = [];\n for (let i = 0; i < n; i++) {\n const t = (i / n) * 2 * Math.PI;\n pts.push({ x: cx + rx * Math.cos(t), y: cy + ry * Math.sin(t) });\n }\n return pts;\n}\n\n/** Elliptical marquee inscribed in `rect`. */\nexport function ellipseSelection(width: number, height: number, rect: CropBox, options: ShapeSelectionOptions = {}): SelectionMask {\n return polygonSelection(width, height, ellipsePoints(rect), options);\n}\n","// Portions ported from photocraft/crates/algo/src/selection.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { ImagePoint } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\n\n/** How the magic wand measures colour difference. */\nexport type ColorDistance = 'rgb' | 'perceptual';\n\nexport interface MagicWandOptions {\n /**\n * 0..255. `rgb`: the largest per-channel difference in levels (alpha included), as\n * Photoshop's wand. `perceptual`: CIE76 ΔE*ab, with 255 ≙ ΔE 100. Default 32.\n */\n tolerance?: number;\n /** Only the 4-connected region around the seed (default true); otherwise every similar pixel. */\n contiguous?: boolean;\n /** Soft one-pixel fringe on the edge (default true). */\n antiAlias?: boolean;\n /** Default `rgb`. */\n distance?: ColorDistance;\n}\n\nfunction checkPixels(p: PixelBuffer): number {\n const n = p.width * p.height;\n if (!Number.isInteger(p.width) || !Number.isInteger(p.height) || p.width < 1 || p.height < 1) {\n throw new Error('Invalid pixel buffer size');\n }\n if (p.data.length < n * 4) throw new Error('Pixel data is shorter than width × height × 4');\n return n;\n}\n\nconst SRGB_TO_LINEAR = (() => {\n const t = new Float32Array(256);\n for (let i = 0; i < 256; i++) {\n const v = i / 255;\n t[i] = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n }\n return t;\n})();\n\nconst labF = (t: number) => (t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116);\n\n/** CIE Lab (D65) of an 8-bit sRGB colour. */\nexport function srgb8ToLab(r: number, g: number, b: number): [number, number, number] {\n const rl = SRGB_TO_LINEAR[r & 255];\n const gl = SRGB_TO_LINEAR[g & 255];\n const bl = SRGB_TO_LINEAR[b & 255];\n const x = (0.4124564 * rl + 0.3575761 * gl + 0.1804375 * bl) / 0.95047;\n const y = 0.2126729 * rl + 0.7151522 * gl + 0.072175 * bl;\n const z = (0.0193339 * rl + 0.119192 * gl + 0.9503041 * bl) / 1.08883;\n const fx = labF(x);\n const fy = labF(y);\n const fz = labF(z);\n return [116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)];\n}\n\n/** `similar(i)` for the wand: does pixel `i` match the seed colour within the tolerance? */\nfunction similarity(pixels: PixelBuffer, seed: number, tolerance: number, distance: ColorDistance): (i: number) => boolean {\n const d = pixels.data;\n const tr = d[seed * 4];\n const tg = d[seed * 4 + 1];\n const tb = d[seed * 4 + 2];\n const ta = d[seed * 4 + 3];\n const tol = Math.max(0, Math.floor(tolerance + 1e-3));\n if (distance === 'perceptual') {\n const [tl, taa, tbb] = srgb8ToLab(tr, tg, tb);\n const de = (Math.max(0, tolerance) * 100) / 255;\n const de2 = de * de + 1e-6;\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n // Copying a selection can leave RGB under zero alpha; hidden colours must not split a\n // transparent region.\n if (ta === 0 && a === 0) return true;\n if (Math.abs(a - ta) > tol) return false;\n const [l, aa, bb] = srgb8ToLab(d[p], d[p + 1], d[p + 2]);\n return (l - tl) ** 2 + (aa - taa) ** 2 + (bb - tbb) ** 2 <= de2;\n };\n }\n return (i) => {\n const p = i * 4;\n const a = d[p + 3];\n if (ta === 0 && a === 0) return true;\n return (\n Math.abs(d[p] - tr) <= tol &&\n Math.abs(d[p + 1] - tg) <= tol &&\n Math.abs(d[p + 2] - tb) <= tol &&\n Math.abs(a - ta) <= tol\n );\n };\n}\n\n/**\n * Softens hard edges: an edge pixel (one whose 3×3 neighbourhood differs from it) becomes\n * `½ + ½·avg` if selected, `½·avg` if not (neighbours outside the image count as the pixel).\n * Only the box `x0..x1 × y0..y1` (exclusive ends) is visited.\n */\nfunction antialiasMarks(marks: Uint8Array, w: number, h: number, x0: number, y0: number, x1: number, y1: number): Uint8Array {\n const out = marks.slice();\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const i = y * w + x;\n const v = marks[i];\n let sum = 0;\n let edge = false;\n for (let dy = -1; dy <= 1; dy++) {\n const ny = y + dy;\n for (let dx = -1; dx <= 1; dx++) {\n const nx = x + dx;\n const n = nx < 0 || ny < 0 || nx >= w || ny >= h ? v : marks[ny * w + nx];\n if (n !== v) edge = true;\n sum += n;\n }\n }\n if (edge) {\n const avg = sum / (9 * 255);\n out[i] = Math.round((v > 127 ? 0.5 + 0.5 * avg : 0.5 * avg) * 255);\n }\n }\n }\n return out;\n}\n\n/**\n * Magic wand: pixels similar to the seed pixel. Contiguous mode is a scanline span fill with an\n * explicit stack (fill a run, then scan the rows above and below it for new runs), so it is\n * linear in the pixels visited and never recurses. An empty selection when the seed is outside.\n */\nexport function magicWand(pixels: PixelBuffer, seed: ImagePoint, options: MagicWandOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h } = pixels;\n const sx = Math.floor(seed.x);\n const sy = Math.floor(seed.y);\n if (!(sx >= 0 && sy >= 0 && sx < w && sy < h)) return createSelectionMask(w, h);\n const similar = similarity(pixels, sy * w + sx, options.tolerance ?? 32, options.distance ?? 'rgb');\n const marks = new Uint8Array(n);\n let bx0 = w;\n let by0 = h;\n let bx1 = -1;\n let by1 = -1;\n if (options.contiguous ?? true) {\n // Span fill. Each stack entry is a seed pixel index.\n const stack: number[] = [sy * w + sx];\n while (stack.length) {\n const idx = stack.pop()!;\n if (marks[idx]) continue;\n const y = (idx / w) | 0;\n const row = y * w;\n let l = idx - row;\n let r = l;\n while (l > 0 && !marks[row + l - 1] && similar(row + l - 1)) l--;\n while (r + 1 < w && !marks[row + r + 1] && similar(row + r + 1)) r++;\n marks.fill(255, row + l, row + r + 1);\n if (l < bx0) bx0 = l;\n if (r > bx1) bx1 = r;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n for (const ny of [y - 1, y + 1]) {\n if (ny < 0 || ny >= h) continue;\n const nrow = ny * w;\n let inRun = false;\n for (let nx = l; nx <= r; nx++) {\n const ok = !marks[nrow + nx] && similar(nrow + nx);\n if (ok && !inRun) stack.push(nrow + nx);\n inRun = ok;\n }\n }\n }\n } else {\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) {\n if (similar(row + x)) {\n marks[row + x] = 255;\n if (x < bx0) bx0 = x;\n if (x > bx1) bx1 = x;\n if (y < by0) by0 = y;\n if (y > by1) by1 = y;\n }\n }\n }\n }\n if (bx1 < 0) return createSelectionMask(w, h);\n if (!(options.antiAlias ?? true)) return createSelectionMask(w, h, marks);\n return createSelectionMask(\n w,\n h,\n antialiasMarks(marks, w, h, Math.max(0, bx0 - 1), Math.max(0, by0 - 1), Math.min(w, bx1 + 2), Math.min(h, by1 + 2))\n );\n}\n\n/** One Color Range sample: a colour (0..255 levels or `#rrggbb`) and, optionally, where it was picked. */\nexport interface ColorRangeSample {\n color: readonly [number, number, number] | string;\n at?: ImagePoint;\n}\n\nexport interface ColorRangeOptions {\n /** 0..200 levels (default 40): coverage falls off linearly to 0 at `fuzziness + 1` levels. */\n fuzziness?: number;\n /**\n * Localized Color Clusters: a sample with a position also fades with distance from it,\n * reaching 0 at this many pixels.\n */\n localized?: number;\n}\n\nfunction sampleColor(c: ColorRangeSample['color']): [number, number, number] {\n if (typeof c === 'string') {\n const m = /^#?([0-9a-f]{6})$/i.exec(c.trim());\n if (!m) throw new Error(`Invalid colour \"${c}\"`);\n const v = parseInt(m[1], 16);\n return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n }\n return [c[0], c[1], c[2]];\n}\n\n/**\n * Select › Color Range › Sampled Colors: each pixel takes its best match over the samples; a\n * match falls off linearly with the largest per-channel difference, `1 − d / (fuzziness + 1)`.\n * Transparent pixels are never selected.\n */\nexport function colorRange(pixels: PixelBuffer, samples: readonly ColorRangeSample[], options: ColorRangeOptions = {}): SelectionMask {\n const n = checkPixels(pixels);\n const { width: w, height: h, data } = pixels;\n const f = Math.max(0, options.fuzziness ?? 40) + 1;\n const parsed = samples.map((s) => ({ c: sampleColor(s.color), at: s.at }));\n const radius = options.localized !== undefined ? Math.max(1, options.localized) : undefined;\n const out = new Uint8Array(n);\n if (!parsed.length) return createSelectionMask(w, h, out);\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (data[p + 3] <= 0) continue;\n const x = i % w;\n const y = (i - x) / w;\n let best = 0;\n for (const s of parsed) {\n const d = Math.max(Math.abs(data[p] - s.c[0]), Math.abs(data[p + 1] - s.c[1]), Math.abs(data[p + 2] - s.c[2]));\n let k = Math.min(1, Math.max(0, 1 - d / f));\n if (k > 0 && radius !== undefined && s.at) {\n const dist = Math.hypot(x - s.at.x, y - s.at.y);\n k *= Math.min(1, Math.max(0, 1 - dist / radius));\n }\n if (k > best) best = k;\n }\n out[i] = coverageToByte(best);\n }\n return createSelectionMask(w, h, out);\n}\n\nexport type ToneRangePreset = 'shadows' | 'midtones' | 'highlights';\n\n/** Luma bounds (0..255, Rec. 601) of the tonal presets. */\nexport const TONE_RANGE_PRESETS: Record<ToneRangePreset, [number, number]> = {\n shadows: [0, 65],\n midtones: [105, 150],\n highlights: [190, 255],\n};\n\n/**\n * Select › Color Range › Shadows / Midtones / Highlights: pixels whose luma lies in `lo..hi` are\n * selected; outside, selection falls off linearly to 0 over `falloff` levels.\n */\nexport function toneRange(\n pixels: PixelBuffer,\n range: ToneRangePreset | readonly [number, number],\n falloff = 20\n): SelectionMask {\n const n = checkPixels(pixels);\n const [lo, hi] = typeof range === 'string' ? TONE_RANGE_PRESETS[range] : range;\n const ramp = Math.max(1e-3, falloff);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const l = Math.min(255, Math.max(0, 0.299 * d[p] + 0.587 * d[p + 1] + 0.114 * d[p + 2]));\n const outside = l < lo ? lo - l : l > hi ? l - hi : 0;\n out[i] = coverageToByte(1 - outside / ramp);\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n\n/**\n * Select › Color Range › Reds … Magentas (`center` 0, 60, …, 300°): closeness of hue (full at\n * the centre, none 60° away) × chroma × (2 − max), so neutrals are never selected.\n */\nexport function hueRange(pixels: PixelBuffer, center: number): SelectionMask {\n const n = checkPixels(pixels);\n const out = new Uint8Array(n);\n const d = pixels.data;\n for (let i = 0; i < n; i++) {\n const p = i * 4;\n if (d[p + 3] <= 0) continue;\n const r = d[p] / 255;\n const g = d[p + 1] / 255;\n const b = d[p + 2] / 255;\n const mx = Math.max(r, g, b);\n const mn = Math.min(r, g, b);\n const chroma = mx - mn;\n if (chroma <= 0) continue;\n let hue: number;\n if (mx === r) hue = (((g - b) / chroma) % 6 + 6) % 6;\n else if (mx === g) hue = (b - r) / chroma + 2;\n else hue = (r - g) / chroma + 4;\n hue *= 60;\n let dh = (((hue - center) % 360) + 360) % 360;\n dh = Math.min(dh, 360 - dh);\n out[i] = coverageToByte(Math.min(1, Math.max(0, 1 - dh / 60)) * chroma * (2 - mx));\n }\n return createSelectionMask(pixels.width, pixels.height, out);\n}\n","// Portions ported from photocraft/crates/algo/src/matting.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { CropBox } from '../types';\nimport { createSelectionMask, coverageToByte, type PixelBuffer, type SelectionMask } from './mask';\nimport { distanceTransform, gaussianBlur, greyMorphology, shiftCoverage } from './modify';\n\n/**\n * Refine Edge / Select and Mask parameters (Photoshop's units). Refinement is edge-aware: within\n * `radius` pixels of the mask's edge the coverage is re-estimated from the image with a colour\n * guided filter (He et al.), so a coarse or misaligned mask (e.g. an AI segmentation) follows\n * the real edges of the image.\n */\nexport interface RefineEdgeParams {\n /** Edge-detection band radius in pixels (0 = no edge detection). */\n radius?: number;\n /** Narrow the band where the image edge is sharp (hair stays soft, hard edges stay hard). */\n smartRadius?: boolean;\n /** 0..100: rounds jagged edges. */\n smooth?: number;\n /** Pixels of Gaussian softening. */\n feather?: number;\n /** 0..100: hardens soft edges. */\n contrast?: number;\n /** −100..100: moves the edge out (positive) or in; 100 % is `max(radius, 4)` pixels. */\n shiftEdge?: number;\n}\n\nexport const DEFAULT_REFINE_EDGE_PARAMS: Required<RefineEdgeParams> = {\n radius: 0,\n smartRadius: false,\n smooth: 0,\n feather: 0,\n contrast: 0,\n shiftEdge: 0,\n};\n\n/** Guided-filter regulariser (colour guide, samples in 0..1). */\nexport const GUIDED_FILTER_EPS = 1e-4;\n\nconst fin = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\n/** Clamped, complete parameters. */\nexport function normalizeRefineEdgeParams(p: RefineEdgeParams = {}): Required<RefineEdgeParams> {\n return {\n radius: Math.min(250, Math.max(0, fin(p.radius, 0))),\n smartRadius: p.smartRadius === true,\n smooth: Math.min(100, Math.max(0, fin(p.smooth, 0))),\n feather: Math.min(1000, Math.max(0, fin(p.feather, 0))),\n contrast: Math.min(100, Math.max(0, fin(p.contrast, 0))),\n shiftEdge: Math.min(100, Math.max(-100, fin(p.shiftEdge, 0))),\n };\n}\n\nconst gfRadius = (p: Required<RefineEdgeParams>) => Math.min(64, Math.max(1, Math.ceil(p.radius)));\nconst smoothSigma = (p: Required<RefineEdgeParams>) => (p.smooth / 100) * 4;\nconst featherSigma = (p: Required<RefineEdgeParams>) => p.feather / 2;\nfunction shiftAmount(p: Required<RefineEdgeParams>): number {\n const px = (p.shiftEdge / 100) * Math.max(p.radius, 4);\n return Number.isFinite(px) ? Math.min(64, Math.max(-64, px)) : 0;\n}\n\n/** Pixels around the mask's content that refinement can read or write. */\nexport function refineEdgeHalo(params: RefineEdgeParams = {}): number {\n const p = normalizeRefineEdgeParams(params);\n const g = gfRadius(p);\n const edge = p.radius > 0 ? Math.max(Math.ceil(p.radius), 2 * g + (p.smartRadius ? g + 1 : 0)) : 0;\n return (\n edge +\n Math.ceil(3 * smoothSigma(p)) +\n Math.ceil(3 * featherSigma(p)) +\n Math.ceil(Math.abs(shiftAmount(p))) +\n 2\n );\n}\n\n/** Box mean of radius `r` (window clipped to the image, divided by its pixel count). O(1) per pixel. */\nexport function boxMean(src: ArrayLike<number>, w: number, h: number, r: number): Float32Array {\n const tmp = new Float32Array(w * h);\n const pre = new Float64Array(Math.max(w, h) + 1);\n for (let y = 0; y < h; y++) {\n const row = y * w;\n for (let x = 0; x < w; x++) pre[x + 1] = pre[x] + src[row + x];\n for (let x = 0; x < w; x++) {\n const lo = Math.max(0, x - r);\n const hi = Math.min(w, x + r + 1);\n tmp[row + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n const out = new Float32Array(w * h);\n for (let x = 0; x < w; x++) {\n for (let y = 0; y < h; y++) pre[y + 1] = pre[y] + tmp[y * w + x];\n for (let y = 0; y < h; y++) {\n const lo = Math.max(0, y - r);\n const hi = Math.min(h, y + r + 1);\n out[y * w + x] = (pre[hi] - pre[lo]) / (hi - lo);\n }\n }\n return out;\n}\n\n/**\n * Guided filter with a colour guide (He et al., Eq. 19–21: `a_k = (Σ_k + εU)⁻¹ cov_k(I, p)`):\n * filters `p` so that it is locally a linear function of the guide's colour, which makes the\n * output's edges follow the guide's edges. `guide` is three planes of 0..1 values.\n */\nexport function guidedFilterColor(\n guide: readonly [Float32Array, Float32Array, Float32Array],\n p: Float32Array,\n w: number,\n h: number,\n r: number,\n eps = GUIDED_FILTER_EPS\n): Float32Array {\n const n = w * h;\n const [c0, c1, c2] = guide;\n const prod = (a: ArrayLike<number>, b: ArrayLike<number>) => {\n const o = new Float32Array(n);\n for (let i = 0; i < n; i++) o[i] = a[i] * b[i];\n return o;\n };\n const m0 = boxMean(c0, w, h, r);\n const m1 = boxMean(c1, w, h, r);\n const m2 = boxMean(c2, w, h, r);\n const mp = boxMean(p, w, h, r);\n const mip0 = boxMean(prod(c0, p), w, h, r);\n const mip1 = boxMean(prod(c1, p), w, h, r);\n const mip2 = boxMean(prod(c2, p), w, h, r);\n const s00 = boxMean(prod(c0, c0), w, h, r);\n const s01 = boxMean(prod(c0, c1), w, h, r);\n const s02 = boxMean(prod(c0, c2), w, h, r);\n const s11 = boxMean(prod(c1, c1), w, h, r);\n const s12 = boxMean(prod(c1, c2), w, h, r);\n const s22 = boxMean(prod(c2, c2), w, h, r);\n const a0 = new Float32Array(n);\n const a1 = new Float32Array(n);\n const a2 = new Float32Array(n);\n const b = new Float32Array(n);\n for (let i = 0; i < n; i++) {\n const u0 = m0[i], u1 = m1[i], u2 = m2[i], up = mp[i];\n const rr = s00[i] - u0 * u0 + eps;\n const rg = s01[i] - u0 * u1;\n const rb = s02[i] - u0 * u2;\n const gg = s11[i] - u1 * u1 + eps;\n const gb = s12[i] - u1 * u2;\n const bb = s22[i] - u2 * u2 + eps;\n const v0 = mip0[i] - u0 * up;\n const v1 = mip1[i] - u1 * up;\n const v2 = mip2[i] - u2 * up;\n // Inverse of the symmetric 3×3 matrix.\n const i00 = gg * bb - gb * gb;\n const i01 = gb * rb - rg * bb;\n const i02 = rg * gb - gg * rb;\n const i11 = rr * bb - rb * rb;\n const i12 = rb * rg - rr * gb;\n const i22 = rr * gg - rg * rg;\n const det = rr * i00 + rg * i01 + rb * i02;\n let x0 = 0, x1 = 0, x2 = 0;\n if (Math.abs(det) >= 1e-30) {\n x0 = (i00 * v0 + i01 * v1 + i02 * v2) / det;\n x1 = (i01 * v0 + i11 * v1 + i12 * v2) / det;\n x2 = (i02 * v0 + i12 * v1 + i22 * v2) / det;\n }\n a0[i] = x0;\n a1[i] = x1;\n a2[i] = x2;\n b[i] = up - x0 * u0 - x1 * u1 - x2 * u2;\n }\n const ma0 = boxMean(a0, w, h, r);\n const ma1 = boxMean(a1, w, h, r);\n const ma2 = boxMean(a2, w, h, r);\n const mb = boxMean(b, w, h, r);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = ma0[i] * c0[i] + ma1[i] * c1[i] + ma2[i] * c2[i] + mb[i];\n return out;\n}\n\n/**\n * Per-pixel transition width (pixels) of the guide's luminance: local range over local maximum\n * gradient in a window of radius `r` (a sharp step gives about 2, a ramp of width L about L).\n */\nexport function edgeWidth(guide: readonly [Float32Array, Float32Array, Float32Array], w: number, h: number, r: number): Float32Array {\n const n = w * h;\n const y = new Float32Array(n);\n for (let i = 0; i < n; i++) y[i] = 0.299 * guide[0][i] + 0.587 * guide[1][i] + 0.114 * guide[2][i];\n const g = new Float32Array(n);\n for (let yy = 0; yy < h; yy++) {\n for (let xx = 0; xx < w; xx++) {\n const gx = (y[yy * w + Math.min(w - 1, xx + 1)] - y[yy * w + Math.max(0, xx - 1)]) / 2;\n const gy = (y[Math.min(h - 1, yy + 1) * w + xx] - y[Math.max(0, yy - 1) * w + xx]) / 2;\n g[yy * w + xx] = Math.sqrt(gx * gx + gy * gy);\n }\n }\n const gm = greyMorphology(g, w, h, r, true);\n const hi = greyMorphology(y, w, h, r, true);\n const lo = greyMorphology(y, w, h, r, false);\n const out = new Float32Array(n);\n for (let i = 0; i < n; i++) out[i] = (hi[i] - lo[i]) / Math.max(gm[i], 1e-4);\n return out;\n}\n\n/**\n * Refines soft coverage `m` (`w × h`, 0..1) with `guide` (three 0..1 planes, required when\n * `radius > 0`): edge-aware re-estimation in a band around the edge, then smooth, feather,\n * contrast and shift edge, in that order.\n */\nexport function refineCoverage(\n guide: readonly [Float32Array, Float32Array, Float32Array] | null,\n m: Float32Array,\n w: number,\n h: number,\n params: RefineEdgeParams = {}\n): Float32Array {\n const p = normalizeRefineEdgeParams(params);\n let a: Float32Array = Float32Array.from(m);\n if (p.radius > 0 && guide) {\n const n = w * h;\n const boundary = new Uint8Array(n);\n let any = false;\n for (let y = 0; y < h; y++) {\n for (let x = 0; x < w; x++) {\n const i = y * w + x;\n const v = m[i] >= 0.5;\n const diff =\n (x > 0 && m[i - 1] >= 0.5 !== v) ||\n (x + 1 < w && m[i + 1] >= 0.5 !== v) ||\n (y > 0 && m[i - w] >= 0.5 !== v) ||\n (y + 1 < h && m[i + w] >= 0.5 !== v);\n if (diff) {\n boundary[i] = 1;\n any = true;\n }\n }\n }\n if (any) {\n const dist = distanceTransform(boundary, w, h);\n const r = gfRadius(p);\n const q = guidedFilterColor(guide, m, w, h, r);\n const widths = p.smartRadius ? edgeWidth(guide, w, h, r) : null;\n for (let i = 0; i < n; i++) {\n const reff = widths ? Math.min(Math.max(p.radius, 1.5), Math.max(1.5, widths[i])) : p.radius;\n const wgt = Math.min(1, Math.max(0, reff + 1 - dist[i]));\n if (wgt > 0) {\n const qi = Math.min(1, Math.max(0, q[i]));\n a[i] = m[i] + (qi - m[i]) * wgt;\n }\n }\n }\n }\n const ss = smoothSigma(p);\n if (ss >= 0.3) {\n a = gaussianBlur(a, w, h, ss, 'clamp');\n const gain = 1 + ss;\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n a = gaussianBlur(a, w, h, featherSigma(p), 'clamp');\n if (p.contrast > 0) {\n const gain = 1 / (1 - (0.99 * p.contrast) / 100);\n for (let i = 0; i < a.length; i++) a[i] = Math.min(1, Math.max(0, (a[i] - 0.5) * gain + 0.5));\n }\n const amount = shiftAmount(p);\n if (Math.abs(amount) >= 1e-4) a = shiftCoverage(a, w, h, amount);\n return a;\n}\n\n/** Three 0..1 planes of a window of an RGBA buffer. */\nexport function guidePlanes(pixels: PixelBuffer, win?: CropBox): [Float32Array, Float32Array, Float32Array] {\n const box = win ?? { x: 0, y: 0, width: pixels.width, height: pixels.height };\n const n = box.width * box.height;\n const r = new Float32Array(n);\n const g = new Float32Array(n);\n const b = new Float32Array(n);\n const d = pixels.data;\n for (let y = 0; y < box.height; y++) {\n for (let x = 0; x < box.width; x++) {\n const i = y * box.width + x;\n const p = ((box.y + y) * pixels.width + box.x + x) * 4;\n r[i] = d[p] / 255;\n g[i] = d[p + 1] / 255;\n b[i] = d[p + 2] / 255;\n }\n }\n return [r, g, b];\n}\n\n/**\n * Refine Edge on a selection (or a coarse AI mask loaded as one) against `pixels` (the image,\n * same size). Work is confined to the mask's bounds grown by {@link refineEdgeHalo}, so a small\n * selection on a large image stays cheap.\n */\nexport function refineSelection(pixels: PixelBuffer | null, mask: SelectionMask, params: RefineEdgeParams = {}): SelectionMask {\n const p = normalizeRefineEdgeParams(params);\n if (pixels && (pixels.width !== mask.width || pixels.height !== mask.height)) {\n throw new Error('Refine Edge needs pixels the size of the selection');\n }\n if (!mask.bounds) return mask;\n const halo = refineEdgeHalo(p);\n // The guided filter reads 2r beyond the band; the window must hold that too.\n const x0 = Math.max(0, mask.bounds.x - halo);\n const y0 = Math.max(0, mask.bounds.y - halo);\n const x1 = Math.min(mask.width, mask.bounds.x + mask.bounds.width + halo);\n const y1 = Math.min(mask.height, mask.bounds.y + mask.bounds.height + halo);\n const ww = x1 - x0;\n const wh = y1 - y0;\n const m = new Float32Array(ww * wh);\n for (let y = 0; y < wh; y++) {\n const src = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) m[y * ww + x] = mask.data[src + x] / 255;\n }\n const guide = pixels && p.radius > 0 ? guidePlanes(pixels, { x: x0, y: y0, width: ww, height: wh }) : null;\n const refined = refineCoverage(guide, m, ww, wh, p);\n const out = new Uint8Array(mask.width * mask.height);\n for (let y = 0; y < wh; y++) {\n const dst = (y0 + y) * mask.width + x0;\n for (let x = 0; x < ww; x++) out[dst + x] = coverageToByte(refined[y * ww + x]);\n }\n return createSelectionMask(mask.width, mask.height, out);\n}\n","/**\n * Schema version 3 of the image editor document: the layer tree, with pixel layers and painted\n * masks stored as PNG assets (cropped to their content and placed by position), plus the\n * document-level crop, transform, output opacity, migrated blur and selection.\n *\n * Encoding is synchronous and exact (see `png.ts`). Each surface's encoding is memoised, so\n * saving an unchanged layer again costs nothing.\n */\n\nimport { normalizeAdjustmentLayer } from '../adjustment-layers';\nimport { isBlendMode, normalizeBlendMode } from '../blend';\nimport { normalizeProcessVersion } from '../color';\nimport type { Annotation, CropBox, DepthMaskOptions, ImageTransform } from '../types';\nimport type { SerializedSelection } from '../selection/mask';\nimport { DEFAULT_DEPTH_MASK_VALUES } from './defaults';\nimport {\n newLayerId,\n type AdjustmentLayerNode,\n type GroupLayer,\n type ImageLayer,\n type LayerBase,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\nimport { bytesToBase64, decodePng, parseDataUrl, pngDataUrl } from '../../raster/png';\nimport { contentBounds, emptySurface, readRegion, surfaceFromPixels, type TiledSurface } from '../../raster/surface';\n\nexport const LAYERED_SCHEMA_VERSION = 3 as const;\n\n/** A stored raster mask. Pixels: `asset` (grey PNG) placed at `x`, `y`; elsewhere `defaultValue`. */\nexport interface SerializedRasterMask {\n defaultValue: 0 | 255;\n asset?: string;\n x?: number;\n y?: number;\n density?: number;\n feather?: number;\n enabled?: boolean;\n source?: DepthMaskOptions;\n}\n\ninterface SerializedLayerBase {\n id: string;\n name: string;\n visible: boolean;\n opacity: number;\n blend: string;\n clipped?: boolean;\n mask?: SerializedRasterMask;\n locks?: LayerLocks;\n}\n\nexport interface SerializedPixelLayer extends SerializedLayerBase {\n type: 'pixel';\n /** Asset id of the pixels (RGBA PNG) placed at `x`, `y` in layer space; missing when empty. */\n asset?: string;\n x?: number;\n y?: number;\n offset: { x: number; y: number };\n fillOpacity?: number;\n filters?: LayerFilter[];\n /** `'document'`: the pixels are the document's source image (no asset is stored). */\n source?: 'document';\n}\n\nexport interface SerializedAdjustmentLayer extends SerializedLayerBase {\n type: 'adjustment';\n kind: string;\n params: unknown;\n}\n\nexport interface SerializedGroupLayer extends SerializedLayerBase {\n type: 'group';\n children: SerializedLayer[];\n collapsed?: boolean;\n}\n\nexport interface SerializedAnnotationLayer extends SerializedLayerBase {\n type: 'annotation';\n annotation: Annotation;\n}\n\nexport type SerializedLayer = SerializedPixelLayer | SerializedAdjustmentLayer | SerializedGroupLayer | SerializedAnnotationLayer;\n\n/** A version 3 document (layers). */\nexport interface SerializedLayeredImageEditorState {\n version: 3;\n /** Canvas size in pixels. */\n width: number;\n height: number;\n /** Bottom to top. */\n layers: SerializedLayer[];\n /** Asset id → PNG data URL (or an external reference, see `ImageEditorAssetStore`). */\n assets: Record<string, string>;\n activeLayerId: string | null;\n crop: CropBox | null;\n transform: ImageTransform;\n /** Export alpha, 0..1. */\n opacity: number;\n /** Blur migrated from a v1 / v2 document with adjustment layers (runs after the layers). */\n legacyOutputBlur?: number;\n selection?: SerializedSelection;\n}\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\ninterface EncodedSurface {\n url: string;\n x: number;\n y: number;\n}\n\nconst encodedCache = new WeakMap<TiledSurface, EncodedSurface | null>();\n\n/** The surface's content (non-default pixels) as a PNG data URL and its position, or `null`. */\nexport function encodeSurface(surface: TiledSurface): EncodedSurface | null {\n if (encodedCache.has(surface)) return encodedCache.get(surface)!;\n const b = contentBounds(surface);\n let out: EncodedSurface | null = null;\n if (b) {\n const data = readRegion(surface, b);\n out = { url: pngDataUrl(b.width, b.height, data, surface.channels), x: b.x, y: b.y };\n }\n encodedCache.set(surface, out);\n return out;\n}\n\nexport interface SerializeContext {\n /** Adds an asset and returns its id. */\n addAsset(url: string, hint: string): string;\n}\n\nfunction baseFields(layer: ImageLayer, ctx: SerializeContext): SerializedLayerBase {\n const out: SerializedLayerBase = {\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend: layer.blend,\n };\n if (layer.clipped) out.clipped = true;\n if (layer.mask) out.mask = serializeMask(layer.mask, `${layer.id}-mask`, ctx);\n if (layer.locks && Object.values(layer.locks).some(Boolean)) out.locks = { ...layer.locks };\n return out;\n}\n\nfunction serializeMask(mask: RasterMask, hint: string, ctx: SerializeContext): SerializedRasterMask {\n const out: SerializedRasterMask = { defaultValue: mask.defaultValue };\n if (!mask.source) {\n const enc = encodeSurface(mask.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, hint);\n out.x = enc.x;\n out.y = enc.y;\n }\n } else {\n out.source = { ...mask.source, depthRange: [...mask.source.depthRange] as [number, number] };\n }\n if (mask.density !== undefined && mask.density !== 1) out.density = mask.density;\n if (mask.feather) out.feather = mask.feather;\n if (mask.enabled === false) out.enabled = false;\n return out;\n}\n\nexport function serializeLayer(layer: ImageLayer, ctx: SerializeContext): SerializedLayer {\n const base = baseFields(layer, ctx);\n switch (layer.type) {\n case 'pixel': {\n const out: SerializedPixelLayer = { ...base, type: 'pixel', offset: { ...layer.offset } };\n if (layer.source === 'document') out.source = 'document';\n else {\n const enc = encodeSurface(layer.surface);\n if (enc) {\n out.asset = ctx.addAsset(enc.url, layer.id);\n out.x = enc.x;\n out.y = enc.y;\n }\n }\n if (layer.fillOpacity !== undefined && layer.fillOpacity !== 1) out.fillOpacity = layer.fillOpacity;\n if (layer.filters?.length) out.filters = JSON.parse(JSON.stringify(layer.filters));\n return out;\n }\n case 'adjustment':\n return { ...base, type: 'adjustment', kind: layer.kind, params: JSON.parse(JSON.stringify(layer.params)) };\n case 'group': {\n const out: SerializedGroupLayer = { ...base, type: 'group', children: layer.children.map((c) => serializeLayer(c, ctx)) };\n if (layer.collapsed) out.collapsed = true;\n return out;\n }\n case 'annotation':\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify(layer.annotation)) };\n }\n}\n\n/** Serializes a layer tree with inline assets (asset ids are derived from layer ids). */\nexport function serializeLayers(layers: readonly ImageLayer[]): { layers: SerializedLayer[]; assets: Record<string, string> } {\n const assets: Record<string, string> = {};\n const ctx: SerializeContext = {\n addAsset(url, hint) {\n let id = hint;\n for (let n = 2; id in assets && assets[id] !== url; n++) id = `${hint}-${n}`;\n assets[id] = url;\n return id;\n },\n };\n return { layers: layers.map((l) => serializeLayer(l, ctx)), assets };\n}\n\n// ---------------------------------------------------------------------------\n// External assets\n// ---------------------------------------------------------------------------\n\n/**\n * Where a host keeps layer pixels outside the JSON (`toJSON({ assets: 'external', store })`):\n * `put` stores a PNG and returns a reference (a URL, a key) that `get` resolves later.\n */\nexport interface ImageEditorAssetStore {\n put(blob: Blob, info: { id: string; width: number; height: number }): Promise<string>;\n get(ref: string): Promise<Blob>;\n}\n\nconst externalRefs = new WeakMap<ImageEditorAssetStore, Map<string, string>>();\n\n/**\n * Serializes a layer tree with its assets in `store`: each PNG is uploaded once per store (by\n * content), and `assets` maps asset ids to the store's references.\n */\nexport async function serializeLayersExternal(\n layers: readonly ImageLayer[],\n store: ImageEditorAssetStore\n): Promise<{ layers: SerializedLayer[]; assets: Record<string, string> }> {\n const inline = serializeLayers(layers);\n let refs = externalRefs.get(store);\n if (!refs) {\n refs = new Map();\n externalRefs.set(store, refs);\n }\n const assets: Record<string, string> = {};\n for (const [id, url] of Object.entries(inline.assets)) {\n let ref = refs.get(url);\n if (!ref) {\n const parsed = parseDataUrl(url)!;\n const png = decodePngHeader(parsed.bytes);\n ref = await store.put(new Blob([parsed.bytes as BlobPart], { type: 'image/png' }), { id, width: png.width, height: png.height });\n if (typeof ref !== 'string' || !ref) throw new Error(`The asset store returned no reference for \"${id}\".`);\n refs.set(url, ref);\n }\n assets[id] = ref;\n }\n return { layers: inline.layers, assets };\n}\n\n/** Width and height from a PNG's header. */\nfunction decodePngHeader(bytes: Uint8Array): { width: number; height: number } {\n const u32 = (o: number) => ((bytes[o] << 24) | (bytes[o + 1] << 16) | (bytes[o + 2] << 8) | bytes[o + 3]) >>> 0;\n return { width: u32(16), height: u32(20) };\n}\n\n/**\n * Turns external asset references back into data URLs through `store.get` (data URLs pass through),\n * so the document loads like an inline one. References that fail are reported and left out.\n */\nexport async function resolveExternalAssets(\n assets: Record<string, string>,\n store: ImageEditorAssetStore | undefined,\n problems: string[]\n): Promise<Record<string, string>> {\n const out: Record<string, string> = {};\n for (const [id, ref] of Object.entries(assets)) {\n if (ref.startsWith('data:')) {\n out[id] = ref;\n continue;\n }\n if (!store) {\n problems.push(`Asset \"${id}\" is external (${ref}) and no asset store was given.`);\n continue;\n }\n try {\n const blob = await store.get(ref);\n const bytes = new Uint8Array(await blob.arrayBuffer());\n out[id] = `data:image/png;base64,${bytesToBase64(bytes)}`;\n } catch (err) {\n problems.push(`Asset \"${id}\" (${ref}) could not be fetched: ${(err as Error).message}`);\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nexport interface DeserializeContext {\n assets: Record<string, string>;\n /** Decodes an asset (PNG data URL by default). Throw for an unusable asset. */\n decodeAsset?(ref: string, channels: 1 | 4): { width: number; height: number; data: ArrayLike<number> };\n /** Problems found while reading (unknown kinds, broken assets); the rest still loads. */\n problems: string[];\n}\n\n/** Decodes a PNG data URL into pixels of `channels` bytes each. */\nexport function decodePngDataUrl(url: string, channels: 1 | 4): { width: number; height: number; data: Uint8ClampedArray } {\n const parsed = parseDataUrl(url);\n if (!parsed) throw new Error('Asset is not a data URL.');\n const png = decodePng(parsed.bytes, { keepGray: channels === 1 });\n if (png.channels === channels) return png;\n if (channels === 1) {\n // An RGBA PNG used as a mask: take its luminance (alpha-weighted).\n const out = new Uint8ClampedArray(png.width * png.height);\n for (let i = 0, p = 0; p < out.length; i += 4, p++) {\n out[p] = Math.round(((0.299 * png.data[i] + 0.587 * png.data[i + 1] + 0.114 * png.data[i + 2]) * png.data[i + 3]) / 255);\n }\n return { width: png.width, height: png.height, data: out };\n }\n const out = new Uint8ClampedArray(png.width * png.height * 4);\n for (let p = 0; p < png.width * png.height; p++) {\n out[p * 4] = out[p * 4 + 1] = out[p * 4 + 2] = png.data[p];\n out[p * 4 + 3] = 255;\n }\n return { width: png.width, height: png.height, data: out };\n}\n\nconst num = (v: unknown, lo: number, hi: number, fallback: number) =>\n typeof v === 'number' && Number.isFinite(v) ? Math.min(hi, Math.max(lo, v)) : fallback;\n\nfunction readSurface(ref: unknown, x: unknown, y: unknown, channels: 1 | 4, defaultValue: number, ctx: DeserializeContext, what: string): TiledSurface {\n if (typeof ref !== 'string' || !ref) return emptySurface(channels, defaultValue);\n const url = ctx.assets[ref];\n if (typeof url !== 'string') {\n ctx.problems.push(`${what}: asset \"${ref}\" is missing`);\n return emptySurface(channels, defaultValue);\n }\n try {\n const px = (ctx.decodeAsset ?? decodePngDataUrl)(url, channels);\n return surfaceFromPixels(px, channels, Math.round(num(x, -1e7, 1e7, 0)), Math.round(num(y, -1e7, 1e7, 0)), defaultValue);\n } catch (err) {\n ctx.problems.push(`${what}: asset \"${ref}\" could not be decoded (${(err as Error).message})`);\n return emptySurface(channels, defaultValue);\n }\n}\n\nfunction readMask(raw: unknown, ctx: DeserializeContext, what: string): RasterMask | undefined {\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, unknown>;\n const defaultValue: 0 | 255 = r.defaultValue === 255 ? 255 : 0;\n const mask: RasterMask = {\n surface: r.source ? emptySurface(1, 255) : readSurface(r.asset, r.x, r.y, 1, defaultValue, ctx, `${what} mask`),\n defaultValue: r.source ? 255 : defaultValue,\n };\n if (typeof r.density === 'number' && r.density !== 1) mask.density = num(r.density, 0, 1, 1);\n if (typeof r.feather === 'number' && r.feather > 0) mask.feather = num(r.feather, 0, 1000, 0);\n if (r.enabled === false) mask.enabled = false;\n if (r.source && typeof r.source === 'object') {\n const s = { ...DEFAULT_DEPTH_MASK_VALUES, ...(r.source as Partial<DepthMaskOptions>) };\n mask.source = { ...s, depthRange: [...s.depthRange] as [number, number] };\n }\n return mask;\n}\n\nfunction readFilters(raw: unknown): LayerFilter[] | undefined {\n if (!Array.isArray(raw)) return undefined;\n const out: LayerFilter[] = [];\n for (const f of raw) {\n if (!f || typeof f !== 'object') continue;\n const r = f as Record<string, unknown>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId('flt');\n const enabled = r.enabled !== false;\n if (r.kind === 'blur') out.push({ id, kind: 'blur', enabled, radius: num(r.radius, 0, 50, 0) });\n else if (r.kind === 'develop') {\n out.push({\n id,\n kind: 'develop',\n enabled,\n brightness: num(r.brightness, -100, 100, 0),\n contrast: num(r.contrast, -100, 100, 0),\n saturation: num(r.saturation, -100, 100, 0),\n exposure: num(r.exposure, -100, 100, 0),\n temperature: num(r.temperature, -100, 100, 0),\n tint: num(r.tint, -100, 100, 0),\n process: normalizeProcessVersion(r.process),\n });\n }\n }\n return out.length ? out : undefined;\n}\n\nexport function deserializeLayer(raw: unknown, ctx: DeserializeContext): ImageLayer | null {\n if (!raw || typeof raw !== 'object') return null;\n const r = raw as Record<string, any>;\n const id = typeof r.id === 'string' && r.id ? r.id : newLayerId();\n const base: LayerBase = {\n id,\n name: typeof r.name === 'string' ? r.name : '',\n visible: r.visible !== false,\n opacity: num(r.opacity, 0, 1, 1),\n blend: normalizeBlendMode(r.blend),\n };\n if (r.clipped === true) base.clipped = true;\n const mask = readMask(r.mask, ctx, `Layer \"${base.name || id}\"`);\n if (mask) base.mask = mask;\n if (r.locks && typeof r.locks === 'object') {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (r.locks[k] === true) locks[k] = true;\n if (Object.keys(locks).length) base.locks = locks;\n }\n switch (r.type) {\n case 'pixel': {\n const layer: PixelLayer = {\n ...base,\n type: 'pixel',\n surface: r.source === 'document' ? emptySurface(4) : readSurface(r.asset, r.x, r.y, 4, 0, ctx, `Layer \"${base.name || id}\"`),\n offset: { x: Math.round(num(r.offset?.x, -1e7, 1e7, 0)), y: Math.round(num(r.offset?.y, -1e7, 1e7, 0)) },\n };\n if (r.source === 'document') layer.source = 'document';\n if (typeof r.fillOpacity === 'number' && r.fillOpacity !== 1) layer.fillOpacity = num(r.fillOpacity, 0, 1, 1);\n const filters = readFilters(r.filters);\n if (filters) layer.filters = filters;\n return layer;\n }\n case 'adjustment': {\n const norm = normalizeAdjustmentLayer({ id, kind: r.kind, params: r.params });\n if (!norm) {\n ctx.problems.push(`Layer \"${base.name || id}\": unknown adjustment kind \"${r.kind}\"`);\n return null;\n }\n return { ...base, type: 'adjustment', kind: norm.kind, params: norm.params } as AdjustmentLayerNode;\n }\n case 'group': {\n const children = Array.isArray(r.children) ? r.children.map((c: unknown) => deserializeLayer(c, ctx)).filter(Boolean) : [];\n const blend = r.blend === 'pass-through' ? 'pass-through' : isBlendMode(r.blend) ? r.blend : 'pass-through';\n const group: GroupLayer = { ...base, type: 'group', blend, children: children as ImageLayer[] };\n if (r.collapsed === true) group.collapsed = true;\n return group;\n }\n case 'annotation': {\n const a = r.annotation;\n if (!a || typeof a !== 'object' || typeof a.type !== 'string') {\n ctx.problems.push(`Layer \"${base.name || id}\": annotation is missing`);\n return null;\n }\n return { ...base, type: 'annotation', annotation: JSON.parse(JSON.stringify({ ...a, id: typeof a.id === 'string' ? a.id : id })) };\n }\n default:\n ctx.problems.push(`Layer \"${base.name || id}\": unknown layer type \"${r.type}\"`);\n return null;\n }\n}\n\n/** Reads a stored layer tree (layers that cannot be read are dropped and reported). */\nexport function deserializeLayers(raw: unknown, ctx: DeserializeContext): ImageLayer[] {\n if (!Array.isArray(raw)) return [];\n const seen = new Set<string>();\n const dedupe = (list: ImageLayer[]): ImageLayer[] =>\n list.map((l) => {\n let next = l;\n if (seen.has(l.id)) next = { ...l, id: newLayerId() } as ImageLayer;\n seen.add(next.id);\n if (next.type === 'group') next = { ...next, children: dedupe(next.children) };\n return next;\n });\n return dedupe(raw.map((l) => deserializeLayer(l, ctx)).filter(Boolean) as ImageLayer[]);\n}\n","/**\n * Layer tree operations as pure functions: each validates its arguments against the stack and\n * throws a `LayerOperationError` that says what is wrong before changing anything, and returns a\n * new stack that shares every untouched layer. The controller wraps them with undo history.\n */\n\nimport type { BlendMode } from '../blend';\nimport { isBlendMode } from '../blend';\nimport type { CropBox } from '../types';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isLayerLocked,\n mapLayer,\n newLayerId,\n type GroupLayer,\n type ImageLayer,\n type LayerBlendMode,\n type LayerFilter,\n type LayerLocks,\n type PixelLayer,\n type RasterMask,\n} from './model';\n\n/** A refused layer operation; the message says why and how to fix it. */\nexport class LayerOperationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'LayerOperationError';\n }\n}\n\nconst fail = (message: string): never => {\n throw new LayerOperationError(message);\n};\n\n/** Where a layer goes: above or below a layer (in its group), or at a position in a group. */\nexport interface LayerPlacement {\n /** Directly above this layer, in the same group. */\n aboveId?: string;\n /** Directly below this layer, in the same group. */\n belowId?: string;\n /** Inside this group (at `index`, default the top). */\n parentId?: string;\n /** Index in the parent's children (or the top level), 0 = bottom. */\n index?: number;\n}\n\n/** The layer with `id`, or a `LayerOperationError` listing the ids that exist. */\nexport function requireLayer(layers: readonly ImageLayer[], id: string) {\n const loc = findLayer(layers, id);\n if (!loc) {\n const ids = flattenLayers(layers).map((l) => l.id);\n fail(`Unknown layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'The document has no layers.'} Call list_layers for ids.`);\n }\n return loc!;\n}\n\n/** Refuses when the layer is locked for `what` (and always for `all`). */\nexport function requireUnlocked(layer: ImageLayer, what: keyof LayerLocks, action: string): void {\n if (isLayerLocked(layer, 'all')) fail(`Layer \"${layer.name || layer.id}\" is locked (all); unlock it before you ${action}.`);\n if (what !== 'all' && isLayerLocked(layer, what)) fail(`Layer \"${layer.name || layer.id}\" has its ${what} locked; unlock it before you ${action}.`);\n}\n\n/** Inserts `layer` at `placement` (default: the top level at `defaultIndex`, or the top). */\nexport function placeLayer(layers: readonly ImageLayer[], layer: ImageLayer, placement: LayerPlacement = {}, defaultIndex?: number): ImageLayer[] {\n const given = [placement.aboveId, placement.belowId, placement.parentId].filter((v) => v !== undefined).length;\n if (given > 1) fail('Give only one of aboveId, belowId and parentId.');\n if (placement.aboveId || placement.belowId) {\n const ref = requireLayer(layers, (placement.aboveId ?? placement.belowId)!);\n return insertLayer(layers, layer, ref.parent?.id ?? null, ref.index + (placement.aboveId ? 1 : 0));\n }\n if (placement.parentId) {\n const ref = requireLayer(layers, placement.parentId);\n if (ref.layer.type !== 'group') fail(`Layer \"${placement.parentId}\" is not a group; parentId must be a group id.`);\n return insertLayer(layers, layer, ref.layer.id, placement.index);\n }\n return insertLayer(layers, layer, null, placement.index ?? defaultIndex);\n}\n\n/** Removes a layer (refused when it is locked). */\nexport function removeLayer(layers: readonly ImageLayer[], id: string): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'delete it');\n return mapLayer(layers, id, () => null);\n}\n\n/** Moves a layer to `placement`. A group cannot go inside itself. Returns the new stack. */\nexport function moveLayer(layers: readonly ImageLayer[], id: string, placement: LayerPlacement): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'move it');\n const target = placement.aboveId ?? placement.belowId ?? placement.parentId;\n if (target === id) fail('A layer cannot be placed relative to itself.');\n if (target && loc.layer.type === 'group' && flattenLayers(loc.layer.children).some((l) => l.id === target)) {\n fail(`Group \"${id}\" cannot be moved into its own child \"${target}\".`);\n }\n if (target) requireLayer(layers, target);\n if (!target && placement.index === undefined) fail('Give aboveId, belowId, parentId or index.');\n const without = mapLayer(layers, id, () => null);\n return placeLayer(without, loc.layer, placement);\n}\n\n/** Every id in a subtree gets a fresh one (for duplicates). */\nexport function withNewIds<T extends ImageLayer>(layer: T): T {\n const next = { ...layer, id: newLayerId(layer.type === 'adjustment' ? 'adj' : layer.type) } as T;\n if (next.type === 'group') (next as GroupLayer).children = (next as GroupLayer).children.map(withNewIds);\n if (next.type === 'annotation') next.annotation = { ...next.annotation, id: next.id };\n return next;\n}\n\n/** Duplicates a layer (and its children) directly above it. Returns the stack and the copy's id. */\nexport function duplicateLayer(layers: readonly ImageLayer[], id: string, name?: string): { layers: ImageLayer[]; id: string } {\n const loc = requireLayer(layers, id);\n const copy = withNewIds(loc.layer);\n copy.name = name ?? `${loc.layer.name || 'Layer'} copy`;\n delete copy.locks;\n return { layers: insertLayer(layers, copy, loc.parent?.id ?? null, loc.index + 1), id: copy.id };\n}\n\n/** Properties `setLayerProperties` changes. */\nexport interface LayerProperties {\n name?: string;\n visible?: boolean;\n /** 0..1 */\n opacity?: number;\n /** 0..1, pixel layers only. */\n fillOpacity?: number;\n blend?: LayerBlendMode;\n clipped?: boolean;\n locks?: LayerLocks;\n /** Groups only (UI state). */\n collapsed?: boolean;\n}\n\n/** Validates and applies property changes to one layer. */\nexport function setLayerProperties(layers: readonly ImageLayer[], id: string, props: LayerProperties): ImageLayer[] {\n const loc = requireLayer(layers, id);\n const layer = loc.layer;\n const onlyUnlocking = Object.keys(props).every((k) => k === 'locks' || k === 'visible' || k === 'collapsed');\n if (!onlyUnlocking) requireUnlocked(layer, 'all', 'change its properties');\n const next = { ...layer } as ImageLayer;\n if (props.name !== undefined) {\n if (typeof props.name !== 'string' || !props.name.trim()) fail('name must be a non-empty string.');\n next.name = props.name.trim();\n }\n if (props.visible !== undefined) next.visible = Boolean(props.visible);\n if (props.opacity !== undefined) {\n if (!(typeof props.opacity === 'number' && props.opacity >= 0 && props.opacity <= 1)) fail('opacity must be a number from 0 to 1.');\n next.opacity = props.opacity;\n }\n if (props.fillOpacity !== undefined) {\n if (layer.type !== 'pixel') fail(`fillOpacity applies to pixel layers; \"${id}\" is a ${layer.type} layer.`);\n if (!(typeof props.fillOpacity === 'number' && props.fillOpacity >= 0 && props.fillOpacity <= 1)) fail('fillOpacity must be a number from 0 to 1.');\n const p = next as PixelLayer;\n if (props.fillOpacity === 1) delete p.fillOpacity;\n else p.fillOpacity = props.fillOpacity;\n }\n if (props.blend !== undefined) {\n if (props.blend === 'pass-through') {\n if (layer.type !== 'group') fail('pass-through is only for groups.');\n } else if (!isBlendMode(props.blend)) fail(`Unknown blend mode \"${props.blend}\".`);\n (next as { blend: LayerBlendMode }).blend = props.blend;\n }\n if (props.clipped !== undefined) {\n if (props.clipped && loc.index === 0) fail('The bottom layer of a group or the stack has nothing below to clip to.');\n if (props.clipped) next.clipped = true;\n else delete next.clipped;\n }\n if (props.locks !== undefined) {\n const locks: LayerLocks = {};\n for (const k of ['pixels', 'transparency', 'position', 'all'] as const) if (props.locks[k]) locks[k] = true;\n if (Object.keys(locks).length) next.locks = locks;\n else delete next.locks;\n }\n if (props.collapsed !== undefined) {\n if (layer.type !== 'group') fail('collapsed applies to groups.');\n const g = next as GroupLayer;\n if (props.collapsed) g.collapsed = true;\n else delete g.collapsed;\n }\n return mapLayer(layers, id, () => next);\n}\n\n/**\n * Puts layers (siblings in one group or the top level) into a new group at the position of the\n * topmost one, keeping their order. Returns the stack and the group id.\n */\nexport function groupLayers(layers: readonly ImageLayer[], ids: string[], name = 'Group'): { layers: ImageLayer[]; id: string } {\n if (!ids.length) fail('Give at least one layer id to group.');\n const locs = ids.map((id) => requireLayer(layers, id));\n const parent = locs[0].parent?.id ?? null;\n if (locs.some((l) => (l.parent?.id ?? null) !== parent)) fail('Layers to group must share one parent; move them next to each other first.');\n for (const l of locs) requireUnlocked(l.layer, 'all', 'group it');\n const ordered = [...new Set(locs)].sort((a, b) => a.index - b.index);\n const top = ordered[ordered.length - 1].index;\n const below = ordered.filter((l) => l.index < top).length;\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name,\n visible: true,\n opacity: 1,\n blend: 'pass-through',\n children: ordered.map((l) => {\n const c = { ...l.layer } as ImageLayer;\n return c;\n }),\n };\n // The bottom child cannot be clipped to anything inside the new group.\n if (group.children[0]?.clipped) {\n const c = { ...group.children[0] };\n delete c.clipped;\n group.children[0] = c;\n }\n let out = layers as ImageLayer[];\n for (const l of ordered) out = mapLayer(out, l.layer.id, () => null);\n return { layers: insertLayer(out, group, parent, top - below), id: group.id };\n}\n\n/** Replaces a group by its children (the group's own opacity, blend and mask are dropped). */\nexport function ungroupLayer(layers: readonly ImageLayer[], id: string): { layers: ImageLayer[]; ids: string[] } {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'group') fail(`Layer \"${id}\" is not a group.`);\n requireUnlocked(loc.layer, 'all', 'ungroup it');\n const group = loc.layer as GroupLayer;\n let out = mapLayer(layers, id, () => null);\n group.children.forEach((c, i) => {\n out = insertLayer(out, c, loc.parent?.id ?? null, loc.index + i);\n });\n return { layers: out, ids: group.children.map((c) => c.id) };\n}\n\n/** Sets a pixel layer's offset (refused when its position is locked). */\nexport function offsetLayer(layers: readonly ImageLayer[], id: string, offset: { x: number; y: number }): ImageLayer[] {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Only pixel layers move; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'position', 'move it');\n if (!Number.isFinite(offset.x) || !Number.isFinite(offset.y)) fail('x and y must be numbers.');\n const layer = loc.layer as PixelLayer;\n return mapLayer(layers, id, () => ({ ...layer, offset: { x: Math.round(offset.x), y: Math.round(offset.y) } }));\n}\n\n/** Sets or removes (`null`) a layer's mask. */\nexport function setLayerMask(layers: readonly ImageLayer[], id: string, mask: RasterMask | null): ImageLayer[] {\n const loc = requireLayer(layers, id);\n requireUnlocked(loc.layer, 'all', 'change its mask');\n return mapLayer(layers, id, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n}\n\n// ---------------------------------------------------------------------------\n// Filters\n// ---------------------------------------------------------------------------\n\nconst DEVELOP_FIELDS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/** A clean filter from loose input (for tools and the UI). */\nexport function normalizeFilter(raw: Partial<LayerFilter> & { kind: LayerFilter['kind'] }, id = newLayerId('flt')): LayerFilter {\n const enabled = raw.enabled !== false;\n if (raw.kind === 'blur') {\n const r = (raw as { radius?: unknown }).radius;\n if (r !== undefined && !(typeof r === 'number' && r >= 0 && r <= 50)) fail('blur radius must be a number from 0 to 50 px.');\n return { id, kind: 'blur', enabled, radius: typeof r === 'number' ? r : 2 };\n }\n if (raw.kind !== 'develop') fail(`Unknown filter kind \"${(raw as { kind: string }).kind}\". Use develop or blur.`);\n const out = { id, kind: 'develop', enabled, brightness: 0, contrast: 0, saturation: 0, exposure: 0, temperature: 0, tint: 0, process: 2 } as LayerFilter & { kind: 'develop' };\n for (const k of DEVELOP_FIELDS) {\n const v = (raw as Record<string, unknown>)[k];\n if (v === undefined) continue;\n if (!(typeof v === 'number' && v >= -100 && v <= 100)) fail(`${k} must be a number from -100 to 100.`);\n out[k] = v as number;\n }\n const p = (raw as { process?: unknown }).process;\n if (p !== undefined) {\n if (p !== 1 && p !== 2) fail('process must be 1 or 2.');\n out.process = p as 1 | 2;\n }\n return out;\n}\n\nfunction requirePixelLayer(layers: readonly ImageLayer[], id: string): PixelLayer {\n const loc = requireLayer(layers, id);\n if (loc.layer.type !== 'pixel') fail(`Filters belong to pixel layers; \"${id}\" is a ${loc.layer.type} layer.`);\n requireUnlocked(loc.layer, 'all', 'change its filters');\n return loc.layer as PixelLayer;\n}\n\n/** Adds a filter on top of a pixel layer's filters. A layer has at most one filter of each kind. */\nexport function addLayerFilter(layers: readonly ImageLayer[], id: string, filter: LayerFilter): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (layer.filters?.some((f) => f.kind === filter.kind)) fail(`Layer \"${id}\" already has a ${filter.kind} filter; change it with update_layer_filter.`);\n return mapLayer(layers, id, () => ({ ...layer, filters: [...(layer.filters ?? []), filter] }));\n}\n\n/** Changes one filter of a pixel layer (`changes` merged and validated). */\nexport function updateLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string, changes: Record<string, unknown>): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n const i = (layer.filters ?? []).findIndex((f) => f.id === filterId);\n if (i === -1) fail(`Layer \"${id}\" has no filter \"${filterId}\". Filters: ${(layer.filters ?? []).map((f) => f.id).join(', ') || 'none'}.`);\n const current = layer.filters![i];\n if ('kind' in changes && changes.kind !== current.kind) fail('A filter kind cannot change; remove it and add another.');\n const merged = normalizeFilter({ ...current, ...changes, kind: current.kind } as LayerFilter, current.id);\n const filters = [...layer.filters!];\n filters[i] = merged;\n return mapLayer(layers, id, () => ({ ...layer, filters }));\n}\n\nexport function removeLayerFilter(layers: readonly ImageLayer[], id: string, filterId: string): ImageLayer[] {\n const layer = requirePixelLayer(layers, id);\n if (!(layer.filters ?? []).some((f) => f.id === filterId)) fail(`Layer \"${id}\" has no filter \"${filterId}\".`);\n const filters = layer.filters!.filter((f) => f.id !== filterId);\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return mapLayer(layers, id, () => next);\n}\n\n/** A blend mode check for tools. */\nexport function parseBlend(value: unknown, allowPassThrough = false): BlendMode | 'pass-through' | undefined {\n if (value === undefined) return undefined;\n if (value === 'pass-through' && allowPassThrough) return value;\n if (!isBlendMode(value)) fail(`Unknown blend mode \"${String(value)}\".`);\n return value as BlendMode;\n}\n\n/** The union of two rectangles (either may be null). */\nexport function unionRect(a: CropBox | null, b: CropBox | null): CropBox | null {\n if (!a) return b;\n if (!b) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n return { x, y, width: Math.max(a.x + a.width, b.x + b.width) - x, height: Math.max(a.y + a.height, b.y + b.height) - y };\n}\n","// Portions ported from photocraft/crates/psd/src/io.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** A malformed or unsupported PSD; the message says what was wrong and where. */\nexport class PsdError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PsdError';\n }\n}\n\n/** Big-endian reader over a byte range that never reads past its end (it throws `PsdError`). */\nexport class PsdReader {\n pos: number;\n\n constructor(\n readonly data: Uint8Array,\n private readonly start = 0,\n readonly end = data.length\n ) {\n this.pos = start;\n }\n\n get remaining(): number {\n return this.end - this.pos;\n }\n\n get offset(): number {\n return this.pos - this.start;\n }\n\n private need(n: number, what: string): void {\n if (n < 0 || this.pos + n > this.end) throw new PsdError(`Unexpected end of data reading ${what} at byte ${this.pos}.`);\n }\n\n u8(): number {\n this.need(1, 'a byte');\n return this.data[this.pos++];\n }\n\n i8(): number {\n const v = this.u8();\n return v > 127 ? v - 256 : v;\n }\n\n u16(): number {\n this.need(2, 'a 16-bit value');\n const v = (this.data[this.pos] << 8) | this.data[this.pos + 1];\n this.pos += 2;\n return v;\n }\n\n i16(): number {\n const v = this.u16();\n return v > 32767 ? v - 65536 : v;\n }\n\n u32(): number {\n this.need(4, 'a 32-bit value');\n const d = this.data;\n const p = this.pos;\n this.pos += 4;\n return ((d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]) >>> 0;\n }\n\n i32(): number {\n return this.u32() | 0;\n }\n\n u64(): number {\n const hi = this.u32();\n const lo = this.u32();\n const v = hi * 2 ** 32 + lo;\n if (!Number.isSafeInteger(v)) throw new PsdError('A 64-bit length is too large.');\n return v;\n }\n\n i64(): number {\n const hi = this.i32();\n const lo = this.u32();\n return hi * 2 ** 32 + lo;\n }\n\n f32(): number {\n this.need(4, 'a float');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 4).getFloat32(0, false);\n this.pos += 4;\n return v;\n }\n\n f64(): number {\n this.need(8, 'a double');\n const v = new DataView(this.data.buffer, this.data.byteOffset + this.pos, 8).getFloat64(0, false);\n this.pos += 8;\n return v;\n }\n\n /** A length field: 8 bytes in PSB where the format says so, else 4. */\n len(long: boolean): number {\n return long ? this.u64() : this.u32();\n }\n\n bytes(n: number): Uint8Array {\n this.need(n, `${n} bytes`);\n const out = this.data.subarray(this.pos, this.pos + n);\n this.pos += n;\n return out;\n }\n\n ascii(n: number): string {\n return String.fromCharCode(...this.bytes(n));\n }\n\n skip(n: number): void {\n this.need(n, `${n} bytes`);\n this.pos += n;\n }\n\n /** A reader over the next `n` bytes (and skips them here). */\n sub(n: number): PsdReader {\n this.need(n, `a ${n}-byte section`);\n const r = new PsdReader(this.data, this.pos, this.pos + n);\n this.pos += n;\n return r;\n }\n\n peek(n: number): Uint8Array {\n return this.data.subarray(this.pos, Math.min(this.end, this.pos + n));\n }\n\n rest(): Uint8Array {\n const out = this.data.subarray(this.pos, this.end);\n this.pos = this.end;\n return out;\n }\n\n /** Refuses counts that could not fit in what is left (each item at least `size` bytes). */\n checkCount(n: number, size: number, what: string): void {\n if (n * size > this.remaining) throw new PsdError(`${what}: count ${n} does not fit in the remaining ${this.remaining} bytes.`);\n }\n\n /** Pascal string padded to a multiple of `align` (length byte included). */\n pascal(align: number): Uint8Array {\n const n = this.u8();\n const s = this.bytes(n);\n const pad = (align - ((1 + n) % align)) % align;\n this.skip(Math.min(pad, this.remaining));\n return s;\n }\n\n /** Photoshop Unicode string: u32 code-unit count, UTF-16BE units (trailing NULs dropped). */\n unicode(): string {\n const n = this.u32();\n this.checkCount(n, 2, 'Unicode string');\n let s = '';\n for (let i = 0; i < n; i++) s += String.fromCharCode(this.u16());\n return s.replace(/\\u0000+$/, '');\n }\n}\n\n/** A growable big-endian writer. */\nexport class PsdWriter {\n private buf = new Uint8Array(1 << 16);\n length = 0;\n\n private ensure(n: number): void {\n if (this.length + n <= this.buf.length) return;\n let size = this.buf.length * 2;\n while (size < this.length + n) size *= 2;\n const next = new Uint8Array(size);\n next.set(this.buf.subarray(0, this.length));\n this.buf = next;\n }\n\n u8(v: number): void {\n this.ensure(1);\n this.buf[this.length++] = v & 255;\n }\n\n u16(v: number): void {\n this.ensure(2);\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i16(v: number): void {\n this.u16(v < 0 ? v + 65536 : v);\n }\n\n u32(v: number): void {\n this.ensure(4);\n this.buf[this.length++] = (v >>> 24) & 255;\n this.buf[this.length++] = (v >>> 16) & 255;\n this.buf[this.length++] = (v >>> 8) & 255;\n this.buf[this.length++] = v & 255;\n }\n\n i32(v: number): void {\n this.u32(v >>> 0);\n }\n\n u64(v: number): void {\n this.u32(Math.floor(v / 2 ** 32));\n this.u32(v >>> 0);\n }\n\n f64(v: number): void {\n this.ensure(8);\n new DataView(this.buf.buffer, this.length, 8).setFloat64(0, v, false);\n this.length += 8;\n }\n\n bytes(b: ArrayLike<number>): void {\n this.ensure(b.length);\n this.buf.set(b as Uint8Array, this.length);\n this.length += b.length;\n }\n\n ascii(s: string): void {\n for (let i = 0; i < s.length; i++) this.u8(s.charCodeAt(i));\n }\n\n zeros(n: number): void {\n this.ensure(n);\n this.buf.fill(0, this.length, this.length + n);\n this.length += n;\n }\n\n /** Reserves a length field; `endLen` fills it with the bytes written since. */\n beginLen(long: boolean): { at: number; long: boolean } {\n const at = this.length;\n if (long) this.u64(0);\n else this.u32(0);\n return { at, long };\n }\n\n endLen(mark: { at: number; long: boolean }): number {\n const n = this.length - mark.at - (mark.long ? 8 : 4);\n const save = this.length;\n this.length = mark.at;\n if (mark.long) this.u64(n);\n else this.u32(n);\n this.length = save;\n return n;\n }\n\n /** Pascal string padded to a multiple of `align`. */\n pascal(s: Uint8Array, align: number): void {\n const n = Math.min(255, s.length);\n this.u8(n);\n this.bytes(s.subarray(0, n));\n this.zeros((align - ((1 + n) % align)) % align);\n }\n\n unicode(s: string, nul = false): void {\n const n = s.length + (nul ? 1 : 0);\n this.u32(n);\n for (let i = 0; i < s.length; i++) this.u16(s.charCodeAt(i));\n if (nul) this.u16(0);\n }\n\n result(): Uint8Array {\n return this.buf.slice(0, this.length);\n }\n}\n\n/** A legacy (Pascal) name: UTF-8 when valid, else Latin-1. */\nexport function decodeLegacyName(b: Uint8Array): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(b);\n } catch {\n return String.fromCharCode(...b);\n }\n}\n\n/** Encodes a name for a legacy Pascal field: non-ASCII characters become `?`. */\nexport function encodeLegacyName(s: string): Uint8Array {\n const out: number[] = [];\n for (const ch of s) {\n const c = ch.codePointAt(0)!;\n out.push(c >= 32 && c < 127 ? c : 63);\n if (out.length >= 255) break;\n }\n return Uint8Array.from(out);\n}\n","// Portions ported from photocraft/crates/psd/src/descriptor.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Photoshop ActionDescriptors (the structured values inside `vibA`, `blwh` and other blocks):\n * read and write. Keys and class ids are 4-character codes, or longer strings.\n */\n\nimport { PsdError, PsdReader, PsdWriter } from './binary';\n\nconst MAX_DEPTH = 64;\n/** Ids that are 4 characters long but stored as strings, never codes. */\nconst STRING_IDS = new Set(['warp', 'list', 'time']);\n\nexport interface DescClass {\n name: string;\n classId: string;\n}\n\nexport type ReferenceItem =\n | { type: 'prop'; class: DescClass; key: string }\n | { type: 'Clss'; class: DescClass }\n | { type: 'Enmr'; class: DescClass; typeId: string; value: string }\n | { type: 'rele'; class: DescClass; offset: number }\n | { type: 'Idnt'; class: DescClass; id: number }\n | { type: 'indx'; class: DescClass; index: number }\n | { type: 'name'; class: DescClass; name: string };\n\nexport type DescValue =\n | { type: 'obj '; items: ReferenceItem[] }\n | { type: 'Objc' | 'GlbO'; value: Descriptor }\n | { type: 'VlLs'; items: DescValue[] }\n | { type: 'doub'; value: number }\n | { type: 'UntF'; unit: string; value: number }\n | { type: 'UnFl'; unit: string; values: number[] }\n | { type: 'TEXT'; value: string }\n | { type: 'enum'; typeId: string; value: string }\n | { type: 'long'; value: number }\n | { type: 'comp'; value: number }\n | { type: 'bool'; value: boolean }\n | { type: 'type' | 'GlbC'; class: DescClass }\n | { type: 'alis' | 'Pth ' | 'tdta'; data: Uint8Array }\n | { type: 'ObAr'; prefix: number; body: Descriptor };\n\nexport interface Descriptor {\n name: string;\n classId: string;\n items: Array<[string, DescValue]>;\n}\n\nexport function descriptor(classId: string, items: Array<[string, DescValue]> = []): Descriptor {\n return { name: '', classId, items };\n}\n\nexport function descGet(d: Descriptor, key: string): DescValue | undefined {\n return d.items.find(([k]) => k === key)?.[1];\n}\n\n/** A number value (long, double or unit float), or `undefined`. */\nexport function descNumber(d: Descriptor, key: string): number | undefined {\n const v = descGet(d, key);\n if (!v) return undefined;\n if (v.type === 'long' || v.type === 'doub' || v.type === 'UntF' || v.type === 'comp') return v.value;\n return undefined;\n}\n\nexport function descBool(d: Descriptor, key: string): boolean | undefined {\n const v = descGet(d, key);\n return v?.type === 'bool' ? v.value : undefined;\n}\n\nexport function descObject(d: Descriptor, key: string): Descriptor | undefined {\n const v = descGet(d, key);\n return v && (v.type === 'Objc' || v.type === 'GlbO') ? v.value : undefined;\n}\n\nfunction readId(r: PsdReader): string {\n const n = r.u32();\n return n === 0 ? r.ascii(4) : r.ascii(n);\n}\n\nfunction writeId(w: PsdWriter, id: string): void {\n if (id.length === 4 && !STRING_IDS.has(id)) {\n w.u32(0);\n w.ascii(id);\n } else {\n w.u32(id.length);\n w.ascii(id);\n }\n}\n\nfunction readClass(r: PsdReader): DescClass {\n return { name: r.unicode(), classId: readId(r) };\n}\n\nfunction writeClass(w: PsdWriter, c: DescClass): void {\n w.unicode(c.name);\n writeId(w, c.classId);\n}\n\nfunction readReferenceItem(r: PsdReader): ReferenceItem {\n const type = r.ascii(4);\n switch (type) {\n case 'prop':\n return { type, class: readClass(r), key: readId(r) };\n case 'Clss':\n return { type, class: readClass(r) };\n case 'Enmr':\n return { type, class: readClass(r), typeId: readId(r), value: readId(r) };\n case 'rele':\n return { type, class: readClass(r), offset: r.i32() };\n case 'Idnt':\n return { type, class: readClass(r), id: r.u32() };\n case 'indx':\n return { type, class: readClass(r), index: r.u32() };\n case 'name':\n return { type, class: readClass(r), name: r.unicode() };\n default:\n throw new PsdError(`Unsupported descriptor reference item \"${type}\".`);\n }\n}\n\nfunction writeReferenceItem(w: PsdWriter, it: ReferenceItem): void {\n w.ascii(it.type);\n writeClass(w, it.class);\n switch (it.type) {\n case 'prop':\n writeId(w, it.key);\n break;\n case 'Enmr':\n writeId(w, it.typeId);\n writeId(w, it.value);\n break;\n case 'rele':\n w.i32(it.offset);\n break;\n case 'Idnt':\n w.u32(it.id);\n break;\n case 'indx':\n w.u32(it.index);\n break;\n case 'name':\n w.unicode(it.name);\n break;\n }\n}\n\nfunction readValue(r: PsdReader, depth: number): DescValue {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const type = r.ascii(4);\n switch (type) {\n case 'obj ': {\n const n = r.u32();\n r.checkCount(n, 8, 'Descriptor reference');\n const items: ReferenceItem[] = [];\n for (let i = 0; i < n; i++) items.push(readReferenceItem(r));\n return { type, items };\n }\n case 'Objc':\n case 'GlbO':\n return { type, value: readDescriptorBody(r, depth + 1) };\n case 'VlLs': {\n const n = r.u32();\n r.checkCount(n, 4, 'Descriptor list');\n const items: DescValue[] = [];\n for (let i = 0; i < n; i++) items.push(readValue(r, depth + 1));\n return { type, items };\n }\n case 'doub':\n return { type, value: r.f64() };\n case 'UntF':\n return { type, unit: r.ascii(4), value: r.f64() };\n case 'UnFl': {\n const unit = r.ascii(4);\n const n = r.u32();\n r.checkCount(n, 8, 'Unit float list');\n const values: number[] = [];\n for (let i = 0; i < n; i++) values.push(r.f64());\n return { type, unit, values };\n }\n case 'TEXT':\n return { type, value: r.unicode() };\n case 'enum':\n return { type, typeId: readId(r), value: readId(r) };\n case 'long':\n return { type, value: r.i32() };\n case 'comp':\n return { type, value: r.i64() };\n case 'bool':\n return { type, value: r.u8() !== 0 };\n case 'type':\n case 'GlbC':\n return { type, class: readClass(r) };\n case 'alis':\n case 'Pth ':\n case 'tdta':\n return { type, data: r.bytes(r.u32()) };\n case 'ObAr':\n return { type, prefix: r.u32(), body: readDescriptorBody(r, depth + 1) };\n default:\n throw new PsdError(`Unsupported descriptor value type \"${type}\".`);\n }\n}\n\nfunction writeValue(w: PsdWriter, v: DescValue): void {\n w.ascii(v.type);\n switch (v.type) {\n case 'obj ':\n w.u32(v.items.length);\n for (const it of v.items) writeReferenceItem(w, it);\n break;\n case 'Objc':\n case 'GlbO':\n writeDescriptorBody(w, v.value);\n break;\n case 'VlLs':\n w.u32(v.items.length);\n for (const it of v.items) writeValue(w, it);\n break;\n case 'doub':\n w.f64(v.value);\n break;\n case 'UntF':\n w.ascii(v.unit);\n w.f64(v.value);\n break;\n case 'UnFl':\n w.ascii(v.unit);\n w.u32(v.values.length);\n for (const x of v.values) w.f64(x);\n break;\n case 'TEXT':\n w.unicode(v.value, true);\n break;\n case 'enum':\n writeId(w, v.typeId);\n writeId(w, v.value);\n break;\n case 'long':\n w.i32(v.value);\n break;\n case 'comp':\n w.u32(Math.floor(v.value / 2 ** 32) >>> 0);\n w.u32(v.value >>> 0);\n break;\n case 'bool':\n w.u8(v.value ? 1 : 0);\n break;\n case 'type':\n case 'GlbC':\n writeClass(w, v.class);\n break;\n case 'alis':\n case 'Pth ':\n case 'tdta':\n w.u32(v.data.length);\n w.bytes(v.data);\n break;\n case 'ObAr':\n w.u32(v.prefix);\n writeDescriptorBody(w, v.body);\n break;\n }\n}\n\nfunction readDescriptorBody(r: PsdReader, depth: number): Descriptor {\n if (depth > MAX_DEPTH) throw new PsdError('Descriptor nesting is too deep.');\n const name = r.unicode();\n const classId = readId(r);\n const n = r.u32();\n r.checkCount(n, 10, 'Descriptor items');\n const items: Array<[string, DescValue]> = [];\n for (let i = 0; i < n; i++) {\n const key = readId(r);\n items.push([key, readValue(r, depth + 1)]);\n }\n return { name, classId, items };\n}\n\nfunction writeDescriptorBody(w: PsdWriter, d: Descriptor): void {\n w.unicode(d.name, true);\n writeId(w, d.classId);\n w.u32(d.items.length);\n for (const [k, v] of d.items) {\n writeId(w, k);\n writeValue(w, v);\n }\n}\n\n/** A versioned descriptor (u32 version 16, then the descriptor) at the reader's position. */\nexport function readVersionedDescriptor(r: PsdReader): Descriptor {\n const version = r.u32();\n if (version !== 16) throw new PsdError(`Descriptor version ${version}, expected 16.`);\n return readDescriptorBody(r, 0);\n}\n\nexport function writeVersionedDescriptor(w: PsdWriter, d: Descriptor): void {\n w.u32(16);\n writeDescriptorBody(w, d);\n}\n\n/** A versioned descriptor from the start of `bytes` (trailing bytes are ignored). */\nexport function parseVersionedDescriptor(bytes: Uint8Array): Descriptor {\n return readVersionedDescriptor(new PsdReader(bytes));\n}\n\nexport function versionedDescriptorBytes(d: Descriptor): Uint8Array {\n const w = new PsdWriter();\n writeVersionedDescriptor(w, d);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/adjust_map.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD adjustment-layer blocks ↔ our ten adjustment kinds. Parameters use our units (levels\n * 0..255, degrees, percent), which are also Photoshop's, so most fields copy straight across.\n */\n\nimport { D50, XYZ_D50_TO_SRGB, defaultAdjustmentParams, hexToRgb, neutralHueRange } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport type {\n AdjustmentKind,\n AdjustmentParamsMap,\n BlackWhiteParams,\n CurvePoint,\n CurvesParams,\n HueRange,\n LevelsChannel,\n} from '../types';\nimport { PsdReader, PsdWriter } from './binary';\nimport {\n type Descriptor,\n type DescValue,\n descBool,\n descNumber,\n descObject,\n descriptor,\n parseVersionedDescriptor,\n versionedDescriptorBytes,\n} from './descriptor';\n\n/** Every adjustment block key Photoshop writes. */\nexport const ADJUSTMENT_KEYS = ['levl', 'curv', 'hue2', 'brit', 'nvrt', 'thrs', 'post', 'expA', 'vibA', 'blnc', 'mixr', 'grdm', 'phfl', 'selc', 'blwh', 'clrL'];\n\n/** Keys we read and write. */\nexport const SUPPORTED_ADJUSTMENT_KEYS: Record<string, AdjustmentKind> = {\n levl: 'levels',\n curv: 'curves',\n hue2: 'hueSaturation',\n vibA: 'vibrance',\n blnc: 'colorBalance',\n blwh: 'blackWhite',\n nvrt: 'invert',\n post: 'posterize',\n thrs: 'threshold',\n phfl: 'photoFilter',\n};\n\nconst LABELS: Record<string, string> = {\n levl: 'Levels',\n curv: 'Curves',\n hue2: 'Hue/Saturation',\n brit: 'Brightness/Contrast',\n nvrt: 'Invert',\n thrs: 'Threshold',\n post: 'Posterize',\n expA: 'Exposure',\n vibA: 'Vibrance',\n blnc: 'Color Balance',\n mixr: 'Channel Mixer',\n grdm: 'Gradient Map',\n phfl: 'Photo Filter',\n selc: 'Selective Color',\n blwh: 'Black & White',\n clrL: 'Color Lookup',\n};\n\nexport function adjustmentLabel(key: string): string {\n return LABELS[key] ?? 'adjustment';\n}\n\nexport type ParsedAdjustment = {\n [K in AdjustmentKind]: { kind: K; params: AdjustmentParamsMap[K]; approximated?: string };\n}[AdjustmentKind];\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\nconst toHex = (rgb: readonly number[]) =>\n '#' + rgb.map((v) => Math.round(clamp(v, 0, 1) * 255).toString(16).padStart(2, '0')).join('');\n\n/** The canonical form of four range sliders: increasing, within one turn, centre in -30..330. */\nexport function canonicalBounds(b: readonly number[]): [number, number, number, number] {\n if (b.some((v) => !Number.isFinite(v))) return [-45, -15, 15, 45];\n const rem = (v: number) => ((v % 360) + 360) % 360;\n const out = [b[0], b[0], b[0], b[0]];\n for (let k = 1; k < 4; k++) out[k] = Math.min(out[k - 1] + rem(b[k] - out[k - 1]), out[0] + 359);\n const centre = (out[1] + out[2]) / 2;\n const shift = rem(centre + 30) - 30 - centre;\n return out.map((v) => v + shift) as [number, number, number, number];\n}\n\nfunction levelsRecord(r: PsdReader): LevelsChannel {\n const inBlack = r.u16();\n const inWhite = r.u16();\n const outBlack = r.u16();\n const outWhite = r.u16();\n const gamma = r.u16() / 100;\n return {\n inBlack: clamp(inBlack, 0, 255),\n inWhite: clamp(inWhite, 0, 255),\n gamma: clamp(gamma || 1, 0.1, 9.99),\n outBlack: clamp(outBlack, 0, 255),\n outWhite: clamp(outWhite, 0, 255),\n };\n}\n\n/** A pencil (256-entry map) curve as points every 15 levels. */\nfunction curveFromMap(map: Uint8Array): CurvePoint[] {\n const pts: CurvePoint[] = [];\n for (let i = 0; i <= 255; i += 15) pts.push({ input: i, output: map[i] });\n return pts;\n}\n\nfunction parseCurves(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const pencil = r.u8() !== 0;\n const version = r.u16();\n if (version !== 1 && version !== 4) return null;\n const bits = r.u32();\n const params = defaultAdjustmentParams('curves');\n const order: Array<keyof CurvesParams> = ['master', 'red', 'green', 'blue'];\n for (let bit = 0; bit < 32; bit++) {\n if (!(bits & (1 << bit))) continue;\n let pts: CurvePoint[];\n if (pencil) {\n pts = curveFromMap(r.bytes(256));\n } else {\n const n = r.u16();\n r.checkCount(n, 4, 'Curve points');\n pts = [];\n for (let i = 0; i < n; i++) {\n const output = r.u16();\n const input = r.u16();\n pts.push({ input: clamp(input, 0, 255), output: clamp(output, 0, 255) });\n }\n pts.sort((a, b) => a.input - b.input);\n }\n if (bit < 4 && pts.length >= 2) params[order[bit]] = pts;\n }\n return { kind: 'curves', params, approximated: pencil ? 'its pencil curve is approximated by points' : undefined };\n}\n\nfunction parseHueSaturation(d: Uint8Array): ParsedAdjustment {\n const r = new PsdReader(d);\n r.u16();\n const colorize = r.u8() !== 0;\n r.u8();\n const triple = () => [r.i16(), r.i16(), r.i16()];\n const col = triple();\n const master = triple();\n const ranges: HueRange[] = [];\n for (let i = 0; i < 6; i++) {\n if (r.remaining < 14) {\n ranges.push(neutralHueRange(i));\n continue;\n }\n const b = [r.u16(), r.u16(), r.u16(), r.u16()];\n const [hue, saturation, lightness] = triple();\n ranges.push({ hue, saturation, lightness, bounds: canonicalBounds(b) });\n }\n const [hue, saturation, lightness] = colorize ? col : master;\n return { kind: 'hueSaturation', params: { hue, saturation, lightness, colorize, ranges } };\n}\n\nconst BW_KEYS: Array<[keyof Omit<BlackWhiteParams, 'tint'>, string]> = [\n ['reds', 'Rd '],\n ['yellows', 'Yllw'],\n ['greens', 'Grn '],\n ['cyans', 'Cyn '],\n ['blues', 'Bl '],\n ['magentas', 'Mgnt'],\n];\nconst BW_TINT = [225 / 255, 211 / 255, 179 / 255];\n\nfunction descRgb(d: Descriptor | undefined): number[] | null {\n if (!d) return null;\n if (descNumber(d, 'redFloat') !== undefined) {\n return [descNumber(d, 'redFloat')!, descNumber(d, 'greenFloat') ?? 0, descNumber(d, 'blueFloat') ?? 0].map((v) => clamp(v, 0, 1));\n }\n const r = descNumber(d, 'Rd ');\n const g = descNumber(d, 'Grn ');\n const b = descNumber(d, 'Bl ');\n if (r === undefined || g === undefined || b === undefined) return null;\n return [r, g, b].map((v) => clamp(v / 255, 0, 1));\n}\n\nfunction parseBlackWhite(d: Uint8Array): ParsedAdjustment {\n const desc = parseVersionedDescriptor(d);\n const params = defaultAdjustmentParams('blackWhite');\n for (const [field, key] of BW_KEYS) {\n const v = descNumber(desc, key);\n if (v !== undefined) params[field] = clamp(v, -200, 300);\n }\n params.tint = descBool(desc, 'useTint') ? toHex(descRgb(descObject(desc, 'tintColor')) ?? BW_TINT) : null;\n return { kind: 'blackWhite', params };\n}\n\nfunction labToRgb(l: number, a: number, b: number): number[] {\n const fy = (l + 16) / 116;\n const fx = fy + a / 500;\n const fz = fy - b / 200;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const xyz = [f(fx) * D50[0], f(fy) * D50[1], f(fz) * D50[2]];\n return XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n}\n\n/** A PSD colour structure (space id + four u16) as 0..1 RGB, or `null` for spaces we cannot read. */\nexport function decodePsdColor(space: number, c: readonly number[]): number[] | null {\n const n = (v: number) => v / 65535;\n switch (space) {\n case 0:\n return [n(c[0]), n(c[1]), n(c[2])];\n case 1: {\n const h = n(c[0]) * 360;\n const s = n(c[1]);\n const v = n(c[2]);\n const k = (m: number) => (m + h / 60) % 6;\n const f = (m: number) => v - v * s * Math.max(0, Math.min(k(m), 4 - k(m), 1));\n return [f(5), f(3), f(1)];\n }\n case 2: {\n // Stored inverted: 65535 is no ink. A naive conversion; CMYK profiles are not read.\n const [cy, m, y, k] = c.map(n);\n return [cy * k, m * k, y * k];\n }\n case 7: {\n const signed = (v: number) => (v << 16) >> 16;\n return labToRgb(Math.min(c[0], 10000) / 100, signed(c[1]) / 100, signed(c[2]) / 100);\n }\n case 8: {\n const v = 1 - Math.min(c[0], 10000) / 10000;\n return [v, v, v];\n }\n default:\n return null;\n }\n}\n\nfunction parsePhotoFilter(d: Uint8Array): ParsedAdjustment | null {\n const r = new PsdReader(d);\n const version = r.u16();\n let rgb: number[] | null;\n if (version === 2) {\n const space = r.u16();\n const c = [r.u16(), r.u16(), r.u16(), r.u16()];\n rgb = [0, 2, 7, 8].includes(space) ? decodePsdColor(space, c) : null;\n } else if (version === 3) {\n const rel = [r.u32(), r.u32(), r.u32()].map((v) => v / 32768);\n if (rel.some((v) => v > 2)) return null;\n const xyz = [rel[0] * D50[0], rel[1], rel[2] * D50[2]];\n rgb = XYZ_D50_TO_SRGB.map((row) => linearToSrgb(clamp(row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2], 0, 1)));\n } else {\n return null;\n }\n if (!rgb) return null;\n const density = clamp(r.u32(), 0, 100);\n const preserveLuminosity = r.u8() !== 0;\n return { kind: 'photoFilter', params: { color: toHex(rgb), density, preserveLuminosity } };\n}\n\n/**\n * Our parameters for an adjustment block, or `null` when the key is not one of our kinds or the\n * block cannot be read.\n */\nexport function parseAdjustmentBlock(key: string, data: Uint8Array): ParsedAdjustment | null {\n try {\n const r = new PsdReader(data);\n switch (key) {\n case 'nvrt':\n return { kind: 'invert', params: {} as Record<string, never> };\n case 'thrs':\n return { kind: 'threshold', params: { level: clamp(r.u16(), 1, 255) } };\n case 'post':\n return { kind: 'posterize', params: { levels: clamp(r.u16(), 2, 255) } };\n case 'levl': {\n r.u16();\n const master = levelsRecord(r);\n const red = levelsRecord(r);\n const green = levelsRecord(r);\n const blue = levelsRecord(r);\n return { kind: 'levels', params: { master, red, green, blue } };\n }\n case 'curv':\n return parseCurves(data);\n case 'hue2':\n return parseHueSaturation(data);\n case 'vibA': {\n const desc = parseVersionedDescriptor(data);\n const get = (k: string) => clamp(descNumber(desc, k) ?? 0, -100, 100);\n return { kind: 'vibrance', params: { vibrance: get('vibrance'), saturation: get('Strt') } };\n }\n case 'blnc': {\n const tone = () => [r.i16(), r.i16(), r.i16()].map((v) => clamp(v, -100, 100)) as [number, number, number];\n const shadows = tone();\n const midtones = tone();\n const highlights = tone();\n return { kind: 'colorBalance', params: { shadows, midtones, highlights, preserveLuminosity: r.u8() !== 0 } };\n }\n case 'blwh':\n return parseBlackWhite(data);\n case 'phfl':\n return parsePhotoFilter(data);\n default:\n return null;\n }\n } catch {\n return null;\n }\n}\n\nconst q = (v: number) => Math.round(clamp(v, 0, 255));\nconst i16 = (v: number) => Math.round(clamp(v, -32768, 32767));\n\nfunction writeLevels(w: PsdWriter, c: LevelsChannel): void {\n w.u16(q(c.inBlack));\n w.u16(q(c.inWhite));\n w.u16(q(c.outBlack));\n w.u16(q(c.outWhite));\n w.u16(Math.round(clamp(c.gamma * 100, 1, 999)));\n}\n\nconst long = (value: number): DescValue => ({ type: 'long', value: i16(value) });\n\n/** The PSD block (key and data) for one of our adjustment layers. */\nexport function writeAdjustmentBlock<K extends AdjustmentKind>(kind: K, params: AdjustmentParamsMap[K]): { key: string; data: Uint8Array } {\n const w = new PsdWriter();\n switch (kind) {\n case 'invert':\n return { key: 'nvrt', data: new Uint8Array(0) };\n case 'threshold': {\n const p = params as AdjustmentParamsMap['threshold'];\n w.u16(Math.max(1, q(p.level)));\n w.u16(0);\n return { key: 'thrs', data: w.result() };\n }\n case 'posterize': {\n const p = params as AdjustmentParamsMap['posterize'];\n w.u16(Math.round(clamp(p.levels, 2, 255)));\n w.u16(0);\n return { key: 'post', data: w.result() };\n }\n case 'levels': {\n const p = params as AdjustmentParamsMap['levels'];\n w.u16(2);\n for (const c of [p.master, p.red, p.green, p.blue]) writeLevels(w, c);\n const ident: LevelsChannel = { inBlack: 0, inWhite: 255, gamma: 1, outBlack: 0, outWhite: 255 };\n for (let i = 4; i < 29; i++) writeLevels(w, ident);\n return { key: 'levl', data: w.result() };\n }\n case 'curves': {\n const p = params as AdjustmentParamsMap['curves'];\n w.u8(0);\n w.u16(1);\n w.u32(0b1111);\n for (const c of [p.master, p.red, p.green, p.blue]) {\n const pts = c.slice(0, 19);\n w.u16(pts.length);\n for (const pt of pts) {\n w.u16(q(pt.output));\n w.u16(q(pt.input));\n }\n }\n return { key: 'curv', data: w.result() };\n }\n case 'hueSaturation': {\n const p = params as AdjustmentParamsMap['hueSaturation'];\n w.u16(2);\n w.u8(p.colorize ? 1 : 0);\n w.u8(0);\n for (let k = 0; k < 2; k++) for (const x of [p.hue, p.saturation, p.lightness]) w.i16(i16(x));\n for (let i = 0; i < 6; i++) {\n const r = p.ranges[i] ?? neutralHueRange(i);\n for (const b of r.bounds) w.u16(Math.round(((b % 360) + 360) % 360) % 360);\n for (const x of [r.hue, r.saturation, r.lightness]) w.i16(i16(x));\n }\n return { key: 'hue2', data: w.result() };\n }\n case 'vibrance': {\n const p = params as AdjustmentParamsMap['vibrance'];\n const d = descriptor('null', [\n ['vibrance', long(clamp(p.vibrance, -100, 100))],\n ['Strt', long(clamp(p.saturation, -100, 100))],\n ]);\n return { key: 'vibA', data: versionedDescriptorBytes(d) };\n }\n case 'colorBalance': {\n const p = params as AdjustmentParamsMap['colorBalance'];\n for (const x of [...p.shadows, ...p.midtones, ...p.highlights]) w.i16(i16(clamp(x, -100, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.u8(0);\n return { key: 'blnc', data: w.result() };\n }\n case 'blackWhite': {\n const p = params as AdjustmentParamsMap['blackWhite'];\n const tint = p.tint ? hexToRgb(p.tint) : BW_TINT;\n const dbl = (v: number): DescValue => ({ type: 'doub', value: clamp(v, 0, 1) * 255 });\n const items: Array<[string, DescValue]> = BW_KEYS.map(([field, key]) => [key, long(clamp(p[field], -200, 300))]);\n items.push(\n ['useTint', { type: 'bool', value: p.tint !== null }],\n ['tintColor', { type: 'Objc', value: descriptor('RGBC', [['Rd ', dbl(tint[0])], ['Grn ', dbl(tint[1])], ['Bl ', dbl(tint[2])]]) }],\n ['bwPresetKind', { type: 'long', value: 1 }],\n ['blackAndWhitePresetFileName', { type: 'TEXT', value: '' }]\n );\n return { key: 'blwh', data: versionedDescriptorBytes(descriptor('null', items)) };\n }\n case 'photoFilter': {\n const p = params as AdjustmentParamsMap['photoFilter'];\n const rgb = hexToRgb(p.color);\n w.u16(2);\n w.u16(0);\n for (const x of rgb) w.u16(Math.round(clamp(x, 0, 1) * 65535));\n w.u16(0);\n w.u32(Math.round(clamp(p.density, 0, 100)));\n w.u8(p.preserveLuminosity ? 1 : 0);\n w.zeros(3);\n return { key: 'phfl', data: w.result() };\n }\n }\n throw new Error(`Unknown adjustment kind \"${kind}\".`);\n}\n","// Portions ported from photocraft/crates/psd/src/compression.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Channel and image data compression: raw, RLE (PackBits), ZIP and ZIP with prediction. */\n\nimport { ZlibError, deflate, inflate } from '../../raster/zlib';\nimport { PsdError, PsdReader } from './binary';\n\nexport type PsdCompression = 0 | 1 | 2 | 3;\nexport const COMPRESSION_NAMES = ['raw', 'rle', 'zip', 'zip-prediction'] as const;\n\n/** Largest decoded channel set accepted from one file (bytes). */\nexport const MAX_DECODED_BYTES = 2 ** 31;\n\nexport function rowBytes(width: number, depth: number): number {\n return depth === 1 ? Math.ceil(width / 8) : width * Math.max(1, depth >> 3);\n}\n\nexport interface PlaneLayout {\n planes: number;\n width: number;\n height: number;\n depth: number;\n /** PSB: RLE row counts are 4 bytes. */\n psb: boolean;\n}\n\nfunction decodedLength(l: PlaneLayout): number {\n const total = l.planes * l.height * rowBytes(l.width, l.depth);\n if (total > MAX_DECODED_BYTES) throw new PsdError(`Decoded channel data of ${total} bytes is larger than the ${MAX_DECODED_BYTES}-byte limit.`);\n return total;\n}\n\n/** Worst-case PackBits size of `n` bytes. */\nexport function packbitsBound(n: number): number {\n return n + Math.ceil(n / 128);\n}\n\n/** PackBits: writes `src` encoded into `out` at `at` (room for `packbitsBound`); returns the new offset. */\nexport function packbitsEncode(src: Uint8Array, out: Uint8Array, at: number): number {\n const n = src.length;\n let i = 0;\n while (i < n) {\n let run = 1;\n while (i + run < n && run < 128 && src[i + run] === src[i]) run++;\n if (run >= 2) {\n out[at++] = (257 - run) & 255;\n out[at++] = src[i];\n i += run;\n continue;\n }\n const start = i;\n let len = 0;\n while (i < n && len < 128) {\n if (i + 2 < n && src[i] === src[i + 1] && src[i] === src[i + 2]) break;\n i++;\n len++;\n }\n out[at++] = len - 1;\n out.set(src.subarray(start, start + len), at);\n at += len;\n }\n return at;\n}\n\n/** PackBits: decodes exactly `expected` bytes from `src` into `out` at `at`; returns the new offset. */\nexport function packbitsDecodeInto(src: Uint8Array, expected: number, out: Uint8Array, at: number): number {\n const target = at + expected;\n let i = 0;\n while (at < target) {\n if (i >= src.length) throw new PsdError('PackBits row ended early.');\n const h = src[i++];\n if (h < 128) {\n const len = h + 1;\n if (i + len > src.length) throw new PsdError('PackBits literal is truncated.');\n if (at + len > target) throw new PsdError('PackBits literal overflows its row.');\n out.set(src.subarray(i, i + len), at);\n at += len;\n i += len;\n } else if (h !== 128) {\n const len = 257 - h;\n if (i >= src.length) throw new PsdError('PackBits run is truncated.');\n const b = src[i++];\n if (at + len > target) throw new PsdError('PackBits run overflows its row.');\n out.fill(b, at, at + len);\n at += len;\n }\n }\n return at;\n}\n\nfunction inflateChecked(data: Uint8Array, expected: number): Uint8Array {\n let out: Uint8Array;\n try {\n out = inflate(data, { maxOutput: expected + (1 << 20), sizeHint: expected, verify: false });\n } catch (e) {\n throw new PsdError(`ZIP channel data: ${e instanceof ZlibError || e instanceof Error ? e.message : String(e)}`);\n }\n if (out.length < expected) throw new PsdError(`ZIP stream produced ${out.length} of ${expected} bytes.`);\n return out.length === expected ? out : out.subarray(0, expected);\n}\n\nfunction unpredict(buf: Uint8Array, l: PlaneLayout): void {\n const rb = rowBytes(l.width, l.depth);\n if (rb === 0) return;\n const w = l.width;\n const scratch = l.depth === 32 ? new Uint8Array(rb) : null;\n for (let o = 0; o + rb <= buf.length; o += rb) {\n if (l.depth === 1 || l.depth === 8) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n } else if (l.depth === 16) {\n for (let i = 1; i < w; i++) {\n const a = (buf[o + 2 * i] << 8) | buf[o + 2 * i + 1];\n const b = (buf[o + 2 * i - 2] << 8) | buf[o + 2 * i - 1];\n const v = (a + b) & 0xffff;\n buf[o + 2 * i] = v >> 8;\n buf[o + 2 * i + 1] = v & 255;\n }\n } else if (l.depth === 32) {\n for (let i = 1; i < rb; i++) buf[o + i] = (buf[o + i] + buf[o + i - 1]) & 255;\n scratch!.set(buf.subarray(o, o + rb));\n for (let i = 0; i < w; i++) for (let k = 0; k < 4; k++) buf[o + 4 * i + k] = scratch![k * w + i];\n } else {\n throw new PsdError(`ZIP prediction is not defined for depth ${l.depth}.`);\n }\n }\n}\n\nfunction decodeRle(r: PsdReader, l: PlaneLayout, total: number): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n r.checkCount(rows, l.psb ? 4 : 2, 'RLE row counts');\n const counts = new Array<number>(rows);\n for (let i = 0; i < rows; i++) counts[i] = l.psb ? r.u32() : r.u16();\n if (total > r.remaining * 130) throw new PsdError('RLE data is too short for its declared size.');\n const out = new Uint8Array(total);\n let at = 0;\n for (const c of counts) at = packbitsDecodeInto(r.bytes(c), rb, out, at);\n return out;\n}\n\n/** Decodes planar channel data (`planes` planes of `height` rows each). */\nexport function decodePlanes(compression: number, data: Uint8Array, l: PlaneLayout): Uint8Array {\n const total = decodedLength(l);\n switch (compression) {\n case 0:\n if (data.length < total) throw new PsdError(`Raw channel data has ${data.length} of ${total} bytes.`);\n return data.slice(0, total);\n case 1:\n return decodeRle(new PsdReader(data), l, total);\n case 2:\n return inflateChecked(data, total).slice();\n case 3: {\n const v = inflateChecked(data, total).slice();\n unpredict(v, l);\n return v;\n }\n default:\n throw new PsdError(`Unsupported compression method ${compression}.`);\n }\n}\n\n/** RLE-encodes planar data: row counts (u16, or u32 in PSB), then the packed rows. */\nexport function encodeRle(decoded: Uint8Array, l: PlaneLayout): Uint8Array {\n const rows = l.planes * l.height;\n const rb = rowBytes(l.width, l.depth);\n const cs = l.psb ? 4 : 2;\n const packed = new Uint8Array(rows * packbitsBound(rb));\n let at = 0;\n const counts = new Uint8Array(rows * cs);\n for (let row = 0; row < rows; row++) {\n const start = at;\n at = packbitsEncode(decoded.subarray(row * rb, (row + 1) * rb), packed, at);\n const n = at - start;\n if (cs === 2) {\n if (n > 0xffff) throw new PsdError('An RLE row is longer than 65535 bytes; write a PSB instead.');\n counts[row * 2] = n >> 8;\n counts[row * 2 + 1] = n & 255;\n } else {\n counts[row * 4] = n >>> 24;\n counts[row * 4 + 1] = (n >>> 16) & 255;\n counts[row * 4 + 2] = (n >>> 8) & 255;\n counts[row * 4 + 3] = n & 255;\n }\n }\n const out = new Uint8Array(counts.length + at);\n out.set(counts);\n out.set(packed.subarray(0, at), counts.length);\n return out;\n}\n\n/** ZIP without prediction (used by tests to build fixtures). */\nexport function encodeZip(decoded: Uint8Array): Uint8Array {\n return deflate(decoded);\n}\n","// Portions ported from photocraft/crates/psd/src/{header,file,layer,tagged,tree,resources,image_data}.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * The PSD / PSB container: header, colour mode data, image resources, layer and mask info\n * (records, channel data, tagged blocks) and the merged image. Channel data stays encoded\n * until a caller decodes it, so inspecting a file is cheap.\n */\n\nimport { PsdError, PsdReader, PsdWriter, decodeLegacyName } from './binary';\nimport { decodePlanes, rowBytes } from './compression';\n\nexport const PSD_MAX_DIMENSION = 30_000;\nexport const PSB_MAX_DIMENSION = 300_000;\nconst MAX_CHANNELS = 56;\nconst MAX_LAYER_CHANNELS = 64;\n\nexport const COLOR_MODES: Record<number, string> = {\n 0: 'bitmap',\n 1: 'grayscale',\n 2: 'indexed',\n 3: 'rgb',\n 4: 'cmyk',\n 7: 'multichannel',\n 8: 'duotone',\n 9: 'lab',\n};\n\n/** Tagged block keys whose length is 8 bytes in a PSB. */\nconst PSB_LONG_KEYS = new Set([\n 'LMsk', 'Lr16', 'Lr32', 'Layr', 'Mt16', 'Mt32', 'Mtrn', 'Alph', 'FMsk', 'lnk2', 'FEid', 'FXid', 'PxSD',\n 'lnk3', 'lnkE', 'pths', 'extd', 'extn', 'FELS', 'cinf', 'artd',\n]);\n\nexport const CHANNEL_ALPHA = -1;\nexport const CHANNEL_USER_MASK = -2;\nexport const CHANNEL_REAL_USER_MASK = -3;\n\nexport const LAYER_FLAG_TRANSPARENCY_PROTECTED = 1;\nexport const LAYER_FLAG_HIDDEN = 2;\nexport const MASK_FLAG_DISABLED = 2;\nexport const MASK_FLAG_PARAMETERS = 16;\n\n/** Section divider (`lsct`) kinds. */\nexport const SECTION_OTHER = 0;\nexport const SECTION_OPEN = 1;\nexport const SECTION_CLOSED = 2;\nexport const SECTION_DIVIDER = 3;\n\nexport interface PsdHeader {\n psb: boolean;\n channels: number;\n height: number;\n width: number;\n depth: number;\n colorMode: number;\n}\n\nexport interface PsdRect {\n top: number;\n left: number;\n bottom: number;\n right: number;\n}\n\nexport interface PsdChannel {\n id: number;\n /** `null` for a zero-length channel. */\n compression: number | null;\n /** Encoded bytes, without the compression field. */\n data: Uint8Array;\n}\n\nexport interface PsdLayerMask {\n rect: PsdRect;\n defaultColor: number;\n flags: number;\n userDensity?: number;\n userFeather?: number;\n vectorDensity?: number;\n vectorFeather?: number;\n /** The real (vector-combined) user mask, when present. */\n real?: { flags: number; background: number; rect: PsdRect };\n}\n\nexport interface PsdTaggedBlock {\n key: string;\n data: Uint8Array;\n}\n\nexport interface PsdLayerRecord {\n rect: PsdRect;\n channels: PsdChannel[];\n blendKey: string;\n opacity: number;\n clipping: number;\n flags: number;\n mask?: PsdLayerMask;\n /** The legacy (Pascal) name; `layerName` prefers `luni`. */\n legacyName: string;\n blocks: PsdTaggedBlock[];\n}\n\nexport interface PsdResource {\n id: number;\n name: string;\n data: Uint8Array;\n}\n\nexport interface PsdFile {\n header: PsdHeader;\n colorModeData: Uint8Array;\n resources: PsdResource[];\n /** Bottom-most first, as stored. */\n layers: PsdLayerRecord[];\n /** The layer count was negative: the merged image's first extra channel is its alpha. */\n mergedAlpha: boolean;\n globalBlocks: PsdTaggedBlock[];\n imageCompression: number;\n imageData: Uint8Array;\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\nfunction readRect(r: PsdReader): PsdRect {\n return { top: r.i32(), left: r.i32(), bottom: r.i32(), right: r.i32() };\n}\n\nexport function rectSize(rect: PsdRect): { width: number; height: number } {\n const width = rect.right - rect.left;\n const height = rect.bottom - rect.top;\n if (width < 0 || height < 0) throw new PsdError('A layer rectangle has a negative size.');\n if (width > PSB_MAX_DIMENSION || height > PSB_MAX_DIMENSION) throw new PsdError('A layer is larger than 300000 pixels.');\n return { width, height };\n}\n\nfunction readHeader(r: PsdReader): PsdHeader {\n if (r.ascii(4) !== '8BPS') throw new PsdError('Not a PSD file: the signature is not \"8BPS\".');\n const version = r.u16();\n if (version !== 1 && version !== 2) throw new PsdError(`Unsupported PSD version ${version} (1 is PSD, 2 is PSB).`);\n r.skip(6);\n const h: PsdHeader = { psb: version === 2, channels: r.u16(), height: r.u32(), width: r.u32(), depth: r.u16(), colorMode: r.u16() };\n if (h.channels < 1 || h.channels > MAX_CHANNELS) throw new PsdError(`Channel count ${h.channels} is outside 1 to ${MAX_CHANNELS}.`);\n if (h.width < 1 || h.height < 1) throw new PsdError(`Image size ${h.width} × ${h.height} must be at least 1 × 1.`);\n const max = h.psb ? PSB_MAX_DIMENSION : PSD_MAX_DIMENSION;\n if (h.width > max || h.height > max) throw new PsdError(`Image size ${h.width} × ${h.height} exceeds ${max} pixels.`);\n if (![1, 8, 16, 32].includes(h.depth)) throw new PsdError(`Unsupported bit depth ${h.depth}.`);\n return h;\n}\n\nfunction readResources(r: PsdReader): PsdResource[] {\n const s = r.sub(r.u32());\n const out: PsdResource[] = [];\n while (s.remaining >= 12) {\n s.ascii(4); // 8BIM, or another vendor signature\n const id = s.u16();\n const name = decodeLegacyName(s.pascal(2));\n const len = s.u32();\n const data = s.bytes(len);\n if (len % 2 && s.remaining) s.skip(1);\n out.push({ id, name, data });\n }\n return out;\n}\n\nconst isSig = (r: PsdReader, at = 0) => {\n const b = r.data.subarray(r.pos + at, r.pos + at + 4);\n if (b.length < 4) return false;\n const s = String.fromCharCode(b[0], b[1], b[2], b[3]);\n return s === '8BIM' || s === '8B64';\n};\n\n/** Tagged blocks until the end of `r` (or something that is not a block). */\nfunction readBlocks(r: PsdReader, psb: boolean, alignment: number): PsdTaggedBlock[] {\n const out: PsdTaggedBlock[] = [];\n while (r.remaining >= 12 && isSig(r)) {\n r.skip(4);\n const key = r.ascii(4);\n const len = r.len(psb && PSB_LONG_KEYS.has(key));\n const data = r.bytes(len);\n // Writers disagree on padding: take the gap up to the next signature, else the default.\n let pad = -1;\n for (let k = 0; k <= 3; k++) {\n if (k === r.remaining || (k < r.remaining && isSig(r, k))) {\n pad = k;\n break;\n }\n }\n if (pad < 0) {\n const def = (alignment - (len % alignment)) % alignment;\n pad = r.peek(def).every((b) => b === 0) && r.remaining >= def ? def : 0;\n }\n r.skip(pad);\n out.push({ key, data });\n }\n return out;\n}\n\nfunction maskParameterBytes(pf: number): number {\n return (pf & 1 ? 1 : 0) + (pf & 2 ? 8 : 0) + (pf & 4 ? 1 : 0) + (pf & 8 ? 8 : 0);\n}\n\n/** Photoshop writes the real mask before the parameters; the spec says after. Guess which. */\nfunction realBeforeParameters(d: Uint8Array): boolean {\n const fits = (at: number) => {\n if (at >= d.length) return false;\n const end = at + 1 + maskParameterBytes(d[at]);\n return d.length >= end && d.length <= end + 3;\n };\n if (fits(18)) return false;\n if (!fits(36)) return false;\n const known = (pf: number) => (pf & 0xf0) === 0;\n const bgOk = (at: number) => at + 1 < d.length && (d[at + 1] === 0 || d[at + 1] === 255);\n const photoshop = bgOk(18) && known(d[36]);\n const realAt = 19 + maskParameterBytes(d[18]);\n const spec = known(d[18]) && d.length >= realAt + 18 && d.length <= realAt + 21 && bgOk(realAt);\n return photoshop || !spec;\n}\n\nfunction parseMask(d: Uint8Array): PsdLayerMask | undefined {\n try {\n const r = new PsdReader(d);\n const m: PsdLayerMask = { rect: readRect(r), defaultColor: r.u8(), flags: r.u8() };\n const readReal = () => (r.remaining >= 18 ? { flags: r.u8(), background: r.u8(), rect: readRect(r) } : undefined);\n const params = (m.flags & MASK_FLAG_PARAMETERS) !== 0;\n const realFirst = params && realBeforeParameters(d);\n if (realFirst) m.real = readReal();\n if (params) {\n const pf = r.u8();\n if (pf & 1) m.userDensity = r.u8();\n if (pf & 2) m.userFeather = r.f64();\n if (pf & 4) m.vectorDensity = r.u8();\n if (pf & 8) m.vectorFeather = r.f64();\n }\n if (!realFirst) m.real = readReal();\n return m;\n } catch {\n return undefined;\n }\n}\n\nfunction readLayerRecord(r: PsdReader, psb: boolean): { record: PsdLayerRecord; lengths: number[] } {\n const rect = readRect(r);\n const n = r.u16();\n if (n > MAX_LAYER_CHANNELS) throw new PsdError(`A layer has ${n} channels (at most ${MAX_LAYER_CHANNELS}).`);\n const channels: PsdChannel[] = [];\n const lengths: number[] = [];\n for (let i = 0; i < n; i++) {\n channels.push({ id: r.i16(), compression: null, data: new Uint8Array(0) });\n lengths.push(r.len(psb));\n }\n if (r.ascii(4) !== '8BIM') throw new PsdError('A layer record has no \"8BIM\" blend-mode signature.');\n const blendKey = r.ascii(4);\n const opacity = r.u8();\n const clipping = r.u8();\n const flags = r.u8();\n r.u8();\n const x = r.sub(r.u32());\n const maskBytes = x.bytes(x.u32());\n const mask = maskBytes.length ? parseMask(maskBytes) : undefined;\n x.skip(x.u32()); // blending ranges\n const legacyName = decodeLegacyName(x.pascal(4));\n const blocks = readBlocks(x, psb, 2);\n return { record: { rect, channels, blendKey, opacity, clipping, flags, mask, legacyName, blocks }, lengths };\n}\n\nfunction readLayerInfo(data: Uint8Array, psb: boolean): { layers: PsdLayerRecord[]; mergedAlpha: boolean } {\n const r = new PsdReader(data);\n const count = r.i16();\n const n = Math.abs(count);\n r.checkCount(n, 34, 'Layer records');\n const read = [];\n for (let i = 0; i < n; i++) read.push(readLayerRecord(r, psb));\n for (const { record, lengths } of read) {\n record.channels.forEach((ch, i) => {\n const len = lengths[i];\n if (len === 0) return;\n if (len === 1) throw new PsdError('A channel has a data length of 1.');\n ch.compression = r.u16();\n ch.data = r.bytes(len - 2);\n });\n }\n return { layers: read.map((x) => x.record), mergedAlpha: count < 0 };\n}\n\n/** Parses a PSD or PSB. Throws `PsdError` with what was wrong. */\nexport function readPsd(bytes: Uint8Array): PsdFile {\n const r = new PsdReader(bytes);\n const header = readHeader(r);\n const { psb } = header;\n const colorModeData = r.bytes(r.u32());\n const resources = readResources(r);\n const s = r.sub(r.len(psb));\n let layers: PsdLayerRecord[] = [];\n let mergedAlpha = false;\n let globalBlocks: PsdTaggedBlock[] = [];\n if (s.remaining) {\n const liLen = s.len(psb);\n if (liLen > 0) ({ layers, mergedAlpha } = readLayerInfo(s.bytes(liLen), psb));\n if (s.remaining >= 4) {\n s.skip(s.u32()); // global layer mask\n globalBlocks = readBlocks(s, psb, 4);\n // 16- and 32-bit files keep their layers in an Lr16 / Lr32 block.\n const key = header.depth === 16 ? 'Lr16' : header.depth === 32 ? 'Lr32' : null;\n const block = globalBlocks.find((b) => (key ? b.key === key : !layers.length && ['Layr', 'Lr16', 'Lr32'].includes(b.key)));\n if (block) {\n ({ layers, mergedAlpha } = readLayerInfo(block.data, psb));\n globalBlocks = globalBlocks.filter((b) => b !== block);\n }\n }\n }\n const imageCompression = r.remaining >= 2 ? r.u16() : 0;\n return { header, colorModeData, resources, layers, mergedAlpha, globalBlocks, imageCompression, imageData: r.rest() };\n}\n\n// ---------------------------------------------------------------------------\n// Queries\n// ---------------------------------------------------------------------------\n\nexport function layerBlock(rec: PsdLayerRecord, key: string): Uint8Array | undefined {\n return rec.blocks.find((b) => b.key === key)?.data;\n}\n\nexport function layerName(rec: PsdLayerRecord): string {\n const luni = layerBlock(rec, 'luni');\n if (luni) {\n try {\n return new PsdReader(luni).unicode();\n } catch {\n // fall back to the legacy name\n }\n }\n return rec.legacyName;\n}\n\nexport interface SectionDivider {\n kind: number;\n blendKey?: string;\n}\n\nexport function sectionDivider(rec: PsdLayerRecord): SectionDivider | undefined {\n const d = layerBlock(rec, 'lsct') ?? layerBlock(rec, 'lsdk');\n if (!d || d.length < 4) return undefined;\n const r = new PsdReader(d);\n const kind = r.u32();\n if (r.remaining >= 8 && r.ascii(4) === '8BIM') return { kind, blendKey: r.ascii(4) };\n return { kind };\n}\n\nexport function u32Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 4 ? new PsdReader(d).u32() : undefined;\n}\n\nexport function u8Block(rec: PsdLayerRecord, key: string): number | undefined {\n const d = layerBlock(rec, key);\n return d && d.length >= 1 ? d[0] : undefined;\n}\n\nexport interface PsdTreeNode {\n /** Index into `PsdFile.layers` (the folder record for a group). */\n index: number;\n children?: PsdTreeNode[];\n}\n\n/** The layer tree, bottom-most first (the stored order, as in our model), from the `lsct` dividers. */\nexport function layerTree(file: PsdFile): PsdTreeNode[] {\n const stack: PsdTreeNode[][] = [[]];\n file.layers.forEach((rec, index) => {\n const kind = sectionDivider(rec)?.kind ?? SECTION_OTHER;\n if (kind === SECTION_DIVIDER) {\n stack.push([]);\n } else if (kind === SECTION_OPEN || kind === SECTION_CLOSED) {\n const children = stack.length > 1 ? stack.pop()! : [];\n stack[stack.length - 1].push({ index, children });\n } else {\n stack[stack.length - 1].push({ index });\n }\n });\n while (stack.length > 1) {\n const children = stack.pop()!;\n stack[stack.length - 1].push(...children);\n }\n return stack[0];\n}\n\nexport function channelRect(rec: PsdLayerRecord, id: number): PsdRect {\n if (id === CHANNEL_USER_MASK && rec.mask) return rec.mask.rect;\n if (id === CHANNEL_REAL_USER_MASK && rec.mask) return rec.mask.real?.rect ?? rec.mask.rect;\n return rec.rect;\n}\n\n/** One layer channel decoded to `depth`-bit planar bytes, or `undefined` if the layer has none. */\nexport function decodeLayerChannel(file: PsdFile, rec: PsdLayerRecord, id: number): Uint8Array | undefined {\n const ch = rec.channels.find((c) => c.id === id);\n if (!ch) return undefined;\n const { width, height } = rectSize(channelRect(rec, id));\n if (ch.compression === null) {\n if (width * height === 0) return new Uint8Array(0);\n throw new PsdError(`Channel ${id} of layer \"${layerName(rec)}\" has no data.`);\n }\n return decodePlanes(ch.compression, ch.data, { planes: 1, width, height, depth: file.header.depth, psb: file.header.psb });\n}\n\n/** The merged image's channels decoded, planar. */\nexport function decodeMergedImage(file: PsdFile): Uint8Array {\n const { channels, width, height, depth, psb } = file.header;\n return decodePlanes(file.imageCompression, file.imageData, { planes: channels, width, height, depth, psb });\n}\n\nexport function resource(file: PsdFile, id: number): Uint8Array | undefined {\n return file.resources.find((r) => r.id === id)?.data;\n}\n\n/** Resolution (1005) in pixels per inch, or `undefined`. */\nexport function resolution(file: PsdFile): number | undefined {\n const d = resource(file, 1005);\n if (!d || d.length < 4) return undefined;\n return new PsdReader(d).u32() / 65536;\n}\n\nexport { rowBytes };\n\n// ---------------------------------------------------------------------------\n// Writing\n// ---------------------------------------------------------------------------\n\nfunction writeRect(w: PsdWriter, r: PsdRect): void {\n w.i32(r.top);\n w.i32(r.left);\n w.i32(r.bottom);\n w.i32(r.right);\n}\n\nfunction writeBlocks(w: PsdWriter, blocks: PsdTaggedBlock[], psb: boolean, alignment: number): void {\n for (const b of blocks) {\n const long = psb && PSB_LONG_KEYS.has(b.key);\n w.ascii(long ? '8B64' : '8BIM');\n w.ascii(b.key);\n // Pad inside the declared length, as Photoshop does, so readers that skip by length stay aligned.\n const pad = (alignment - (b.data.length % alignment)) % alignment;\n if (long) w.u64(b.data.length + pad);\n else w.u32(b.data.length + pad);\n w.bytes(b.data);\n w.zeros(pad);\n }\n}\n\nfunction writeMask(w: PsdWriter, m: PsdLayerMask): void {\n writeRect(w, m.rect);\n w.u8(m.defaultColor);\n const pf = (m.userDensity !== undefined ? 1 : 0) | (m.userFeather !== undefined ? 2 : 0);\n w.u8(pf ? m.flags | MASK_FLAG_PARAMETERS : m.flags & ~MASK_FLAG_PARAMETERS);\n if (pf) {\n w.u8(pf);\n if (m.userDensity !== undefined) w.u8(m.userDensity);\n if (m.userFeather !== undefined) w.f64(m.userFeather);\n }\n // Pad to the 20-byte minimum the spec asks for.\n const len = 18 + (pf ? 1 + maskParameterBytes(pf) : 0);\n w.zeros(Math.max(0, 20 - len) + ((4 - (Math.max(20, len) % 4)) % 4));\n}\n\nfunction writeLayerRecord(w: PsdWriter, rec: PsdLayerRecord, psb: boolean, nameBytes: Uint8Array): void {\n writeRect(w, rec.rect);\n w.u16(rec.channels.length);\n for (const c of rec.channels) {\n w.i16(c.id);\n const len = c.compression === null ? 0 : 2 + c.data.length;\n if (psb) w.u64(len);\n else w.u32(len);\n }\n if (rec.blendKey.length !== 4) throw new PsdError(`Blend key \"${rec.blendKey}\" must be 4 characters.`);\n w.ascii('8BIM');\n w.ascii(rec.blendKey);\n w.u8(rec.opacity);\n w.u8(rec.clipping);\n w.u8(rec.flags);\n w.u8(0);\n const extra = w.beginLen(false);\n const mask = w.beginLen(false);\n if (rec.mask) writeMask(w, rec.mask);\n w.endLen(mask);\n // Blending ranges: composite gray plus one per colour channel, all full.\n w.u32(40);\n for (let i = 0; i < 5; i++) w.bytes([0, 0, 255, 255, 0, 0, 255, 255]);\n w.pascal(nameBytes, 4);\n writeBlocks(w, rec.blocks, psb, 2);\n w.endLen(extra);\n}\n\n/** Serializes a file. Channel data must already be encoded; `encodeName` writes legacy (Pascal) names. */\nexport function writePsd(file: PsdFile, encodeName: (s: string) => Uint8Array): Uint8Array {\n const { header } = file;\n const { psb } = header;\n const w = new PsdWriter();\n w.ascii('8BPS');\n w.u16(psb ? 2 : 1);\n w.zeros(6);\n w.u16(header.channels);\n w.u32(header.height);\n w.u32(header.width);\n w.u16(header.depth);\n w.u16(header.colorMode);\n w.u32(file.colorModeData.length);\n w.bytes(file.colorModeData);\n const res = w.beginLen(false);\n for (const r of file.resources) {\n w.ascii('8BIM');\n w.u16(r.id);\n w.pascal(encodeName(r.name), 2);\n w.u32(r.data.length);\n w.bytes(r.data);\n if (r.data.length % 2) w.u8(0);\n }\n w.endLen(res);\n const lm = w.beginLen(psb);\n if (file.layers.length || file.globalBlocks.length) {\n const li = w.beginLen(psb);\n if (file.layers.length) {\n const start = w.length;\n w.i16(file.mergedAlpha ? -file.layers.length : file.layers.length);\n for (const rec of file.layers) writeLayerRecord(w, rec, psb, encodeName(rec.legacyName));\n for (const rec of file.layers) {\n for (const c of rec.channels) {\n if (c.compression === null) continue;\n w.u16(c.compression);\n w.bytes(c.data);\n }\n }\n if ((w.length - start) % 2) w.u8(0);\n }\n w.endLen(li);\n w.u32(0); // no global layer mask\n writeBlocks(w, file.globalBlocks, psb, 4);\n }\n w.endLen(lm);\n w.u16(file.imageCompression);\n w.bytes(file.imageData);\n return w.result();\n}\n","// Portions ported from photocraft/crates/io/src/pixels.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/** Planar PSD channels (any colour mode, 1 / 8 / 16 / 32 bits) → 8-bit sRGB RGBA, and back. */\n\nimport { D50, XYZ_D50_TO_SRGB } from '../adjustment-layers';\nimport { linearToSrgb } from '../color';\nimport { PsdError } from './binary';\n\nexport const MODE_BITMAP = 0;\nexport const MODE_GRAYSCALE = 1;\nexport const MODE_INDEXED = 2;\nexport const MODE_RGB = 3;\nexport const MODE_CMYK = 4;\nexport const MODE_MULTICHANNEL = 7;\nexport const MODE_DUOTONE = 8;\nexport const MODE_LAB = 9;\n\n/** Colour channels of a mode (alpha and spot channels come after them). */\nexport function colorChannelCount(mode: number, headerChannels: number): number {\n switch (mode) {\n case MODE_RGB:\n case MODE_LAB:\n return 3;\n case MODE_CMYK:\n return 4;\n case MODE_MULTICHANNEL:\n return Math.min(3, headerChannels);\n default:\n return 1;\n }\n}\n\nconst clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);\n\n/** A plane's sample at `i` as 0..1 (`gamma`: 32-bit samples are linear light, so encode them). */\nfunction sampler(plane: Uint8Array | undefined, depth: number, width: number, encode: boolean, fill: number): (i: number) => number {\n if (!plane) return () => fill;\n switch (depth) {\n case 8:\n return (i) => plane[i] / 255;\n case 16:\n return (i) => ((plane[2 * i] << 8) | plane[2 * i + 1]) / 65535;\n case 32: {\n const view = new DataView(plane.buffer, plane.byteOffset, plane.byteLength);\n return (i) => {\n const v = clamp01(view.getFloat32(4 * i, false) || 0);\n return encode ? linearToSrgb(v) : v;\n };\n }\n case 1: {\n const rb = Math.ceil(width / 8);\n // Bitmap: a set bit is black.\n return (i) => {\n const x = i % width;\n const y = (i - x) / width;\n return (plane[y * rb + (x >> 3)] >> (7 - (x & 7))) & 1 ? 0 : 1;\n };\n }\n default:\n throw new PsdError(`Unsupported bit depth ${depth}.`);\n }\n}\n\nfunction labToSrgb(l: number, a: number, b: number): [number, number, number] {\n const fy = (l + 16) / 116;\n const f = (t: number) => (t > 6 / 29 ? t ** 3 : 3 * (6 / 29) ** 2 * (t - 4 / 29));\n const x = f(fy + a / 500) * D50[0];\n const y = f(fy) * D50[1];\n const z = f(fy - b / 200) * D50[2];\n const m = XYZ_D50_TO_SRGB;\n return [\n linearToSrgb(clamp01(m[0][0] * x + m[0][1] * y + m[0][2] * z)),\n linearToSrgb(clamp01(m[1][0] * x + m[1][1] * y + m[1][2] * z)),\n linearToSrgb(clamp01(m[2][0] * x + m[2][1] * y + m[2][2] * z)),\n ];\n}\n\nexport interface ChannelSet {\n width: number;\n height: number;\n depth: number;\n mode: number;\n /** Colour planes in mode order (missing planes read as 0, or no ink for CMYK). */\n color: Array<Uint8Array | undefined>;\n /** Missing: opaque. */\n alpha?: Uint8Array;\n /** Indexed: 768-byte palette (all reds, all greens, all blues). */\n palette?: Uint8Array;\n}\n\n/** Interleaved 8-bit sRGB RGBA. */\nexport function toRgba8(set: ChannelSet): Uint8ClampedArray {\n const { width, height, depth, mode } = set;\n const n = width * height;\n const out = new Uint8ClampedArray(n * 4);\n const linear = depth === 32;\n const isLab = mode === MODE_LAB;\n const cmyk = mode === MODE_CMYK;\n // CMYK planes store 1 for no ink; a missing plane is no ink too.\n const planes = set.color.map((p) => sampler(p, depth, width, linear && !isLab && !cmyk, cmyk ? 1 : 0));\n const alpha = sampler(set.alpha, depth, width, false, 1);\n const fast8 = depth === 8 && mode === MODE_RGB && set.color.every(Boolean);\n if (fast8) {\n const [r, g, b] = set.color as Uint8Array[];\n const a = set.alpha;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n out[o] = r[i];\n out[o + 1] = g[i];\n out[o + 2] = b[i];\n out[o + 3] = a ? a[i] : 255;\n }\n return out;\n }\n const palette = set.palette;\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n let r: number;\n let g: number;\n let b: number;\n switch (mode) {\n case MODE_RGB:\n case MODE_MULTICHANNEL:\n r = planes[0](i);\n g = planes[1] ? planes[1](i) : r;\n b = planes[2] ? planes[2](i) : r;\n break;\n case MODE_CMYK: {\n const k = planes[3] ? planes[3](i) : 1;\n r = planes[0](i) * k;\n g = planes[1](i) * k;\n b = planes[2](i) * k;\n break;\n }\n case MODE_LAB:\n [r, g, b] = labToSrgb(planes[0](i) * 100, planes[1](i) * 255 - 128, planes[2](i) * 255 - 128);\n break;\n case MODE_INDEXED: {\n const idx = Math.round(planes[0](i) * 255);\n if (palette && palette.length >= 768) {\n out[o] = palette[idx];\n out[o + 1] = palette[256 + idx];\n out[o + 2] = palette[512 + idx];\n out[o + 3] = Math.round(alpha(i) * 255);\n continue;\n }\n r = g = b = idx / 255;\n break;\n }\n default:\n r = g = b = planes[0](i);\n }\n out[o] = Math.round(r * 255);\n out[o + 1] = Math.round(g * 255);\n out[o + 2] = Math.round(b * 255);\n out[o + 3] = Math.round(alpha(i) * 255);\n }\n return out;\n}\n\n/** An 8-bit coverage plane (masks) from any depth. */\nexport function toMask8(plane: Uint8Array, width: number, height: number, depth: number): Uint8ClampedArray {\n if (depth === 8) return new Uint8ClampedArray(plane.buffer, plane.byteOffset, width * height);\n const s = sampler(plane, depth, width, false, 0);\n const out = new Uint8ClampedArray(width * height);\n for (let i = 0; i < out.length; i++) out[i] = Math.round(s(i) * 255);\n return out;\n}\n\n/** Splits interleaved RGBA into planes `[r, g, b, a]` (8-bit). */\nexport function rgbaToPlanes(data: ArrayLike<number>, n: number): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {\n const planes: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] = [new Uint8Array(n), new Uint8Array(n), new Uint8Array(n), new Uint8Array(n)];\n for (let i = 0, o = 0; i < n; i++, o += 4) {\n planes[0][i] = data[o];\n planes[1][i] = data[o + 1];\n planes[2][i] = data[o + 2];\n planes[3][i] = data[o + 3];\n }\n return planes;\n}\n\n/**\n * Photoshop stores a transparent merged image matted over white (colour × alpha + white × (1 −\n * alpha)). `unmatteWhite` recovers the colour in place; `matteWhite` applies the matte.\n */\nexport function unmatteWhite(data: Uint8ClampedArray): void {\n for (let i = 0; i < data.length; i += 4) {\n const a = data[i + 3];\n if (a === 255 || a === 0) continue;\n const k = 255 - a;\n for (let c = 0; c < 3; c++) data[i + c] = Math.round(((data[i + c] - k) * 255) / a);\n }\n}\n\nexport function matteWhite(data: ArrayLike<number>): Uint8ClampedArray {\n const out = Uint8ClampedArray.from(data);\n for (let i = 0; i < out.length; i += 4) {\n const a = out[i + 3];\n if (a === 255) continue;\n for (let c = 0; c < 3; c++) out[i + c] = Math.round((out[i + c] * a + 255 * (255 - a)) / 255);\n }\n return out;\n}\n","// Portions ported from photocraft/crates/io/src/psd_import.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * PSD / PSB → our layer tree. Everything the model holds maps directly; everything else comes in\n * as close as it can (type, smart-object, fill and shape layers as their stored pixels) with a\n * warning naming the layers.\n */\n\nimport type { BlendMode } from '../blend';\nimport { emptySurface, surfaceFromPixels } from '../../raster/surface';\nimport type { GroupLayer, ImageLayer, LayerBlendMode, LayerLocks, PixelLayer, RasterMask } from '../layers/model';\nimport { ADJUSTMENT_KEYS, adjustmentLabel, parseAdjustmentBlock } from './adjust';\nimport { PsdError } from './binary';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_REAL_USER_MASK,\n CHANNEL_USER_MASK,\n COLOR_MODES,\n LAYER_FLAG_HIDDEN,\n MASK_FLAG_DISABLED,\n SECTION_CLOSED,\n type PsdFile,\n type PsdLayerRecord,\n type PsdRect,\n type PsdTreeNode,\n decodeLayerChannel,\n decodeMergedImage,\n layerName,\n layerTree,\n readPsd,\n rectSize,\n resolution,\n resource,\n rowBytes,\n sectionDivider,\n u32Block,\n u8Block,\n} from './file';\nimport { MODE_INDEXED, colorChannelCount, toMask8, toRgba8, unmatteWhite } from './pixels';\n\n/** PSD blend keys → our blend modes (`diss` is the one we lack). */\nexport const PSD_BLEND_KEYS: Record<string, LayerBlendMode> = {\n pass: 'pass-through',\n norm: 'normal',\n dark: 'darken',\n 'mul ': 'multiply',\n idiv: 'color-burn',\n lbrn: 'linear-burn',\n dkCl: 'darker-color',\n lite: 'lighten',\n scrn: 'screen',\n 'div ': 'color-dodge',\n lddg: 'linear-dodge',\n lgCl: 'lighter-color',\n over: 'overlay',\n sLit: 'soft-light',\n hLit: 'hard-light',\n vLit: 'vivid-light',\n lLit: 'linear-light',\n pLit: 'pin-light',\n hMix: 'hard-mix',\n diff: 'difference',\n smud: 'exclusion',\n fsub: 'subtract',\n fdiv: 'divide',\n 'hue ': 'hue',\n 'sat ': 'saturation',\n colr: 'color',\n 'lum ': 'luminosity',\n};\n\nexport const BLEND_TO_PSD: Record<LayerBlendMode, string> = Object.fromEntries(\n Object.entries(PSD_BLEND_KEYS).map(([k, v]) => [v, k])\n) as Record<LayerBlendMode, string>;\n\nconst FILL_KEYS = ['SoCo', 'GdFl', 'PtFl'];\nconst TYPE_KEYS = ['TySh', 'tySh'];\nconst SMART_KEYS = ['SoLd', 'SoLE', 'PlLd'];\nconst VECTOR_KEYS = ['vmsk', 'vsms'];\nconst EFFECT_KEYS = ['lfx2', 'lfxs', 'lmfx'];\n\n/** What happened to one PSD layer on import. */\nexport interface PsdLayerSummary {\n name: string;\n /** Our layer type, or `null` when the layer was dropped. */\n type: 'pixel' | 'adjustment' | 'group' | null;\n /** `mapped`: held exactly; `approximated`: imported with losses; `dropped`: left out. */\n status: 'mapped' | 'approximated' | 'dropped';\n /** What Photoshop calls it (`pixel`, `type`, `smart-object`, `fill`, `shape`, `Levels` …). */\n psdKind: string;\n /** Why it is not `mapped`. */\n notes: string[];\n /** Our layer id (when imported). */\n id?: string;\n depth: number;\n}\n\nexport interface PsdImportResult {\n width: number;\n height: number;\n /** Bottom to top, as in our model. */\n layers: ImageLayer[];\n /** One sentence per problem, naming the affected layers. */\n warnings: string[];\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n /** Pixels per inch, when the file says. */\n resolution?: number;\n info: { psb: boolean; depth: number; colorMode: string; channels: number };\n}\n\nexport interface PsdImportOptions {\n /** Makes layer ids (defaults to `psd-<n>`). */\n newId?: (hint: string) => string;\n /** Skip decoding pixels (inspection only). */\n skipPixels?: boolean;\n}\n\n/** A PSD byte (0..255) as the whole percent Photoshop shows, 0..1. */\nconst percent = (v: number) => Math.round((v / 255) * 100) / 100;\n\nclass Warnings {\n private groups = new Map<string, string[]>();\n readonly extra: string[] = [];\n\n add(message: string, layer: string): void {\n const list = this.groups.get(message) ?? [];\n if (!list.includes(layer)) list.push(layer);\n this.groups.set(message, list);\n }\n\n list(): string[] {\n const out = [...this.extra];\n for (const [message, layers] of this.groups) {\n const names = layers.map((n) => `\"${n}\"`);\n const shown = names.length > 6 ? `${names.slice(0, 6).join(', ')} and ${names.length - 6} more` : names.join(', ');\n out.push(`${message} (${layers.length === 1 ? 'layer' : 'layers'} ${shown}).`);\n }\n return out;\n }\n}\n\nfunction psdKind(rec: PsdLayerRecord): string {\n if (rec.blocks.some((b) => TYPE_KEYS.includes(b.key))) return 'type';\n if (rec.blocks.some((b) => SMART_KEYS.includes(b.key))) return 'smart-object';\n const fill = rec.blocks.some((b) => FILL_KEYS.includes(b.key));\n const vector = rec.blocks.some((b) => VECTOR_KEYS.includes(b.key) || b.key === 'vstk');\n if (fill && vector) return 'shape';\n if (fill) return 'fill';\n const adj = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adj) return adjustmentLabel(adj.key);\n return 'pixel';\n}\n\nfunction locksOf(rec: PsdLayerRecord): LayerLocks | undefined {\n const v = u32Block(rec, 'lspf');\n if (!v) return undefined;\n const locks: LayerLocks = {};\n if (v & 0x80000000) locks.all = true;\n if (v & 1) locks.transparency = true;\n if (v & 2) locks.pixels = true;\n if (v & 4) locks.position = true;\n return Object.keys(locks).length ? locks : undefined;\n}\n\ninterface Ctx {\n file: PsdFile;\n warn: Warnings;\n newId: (hint: string) => string;\n skipPixels: boolean;\n /** Every PSD layer, bottom first, a group before its children. */\n summary: PsdLayerSummary[];\n colorCount: number;\n palette?: Uint8Array;\n}\n\nfunction decodeOr(ctx: Ctx, rec: PsdLayerRecord, id: number, name: string): Uint8Array | undefined {\n try {\n return decodeLayerChannel(ctx.file, rec, id);\n } catch (e) {\n ctx.warn.add(`A channel could not be decoded (${e instanceof Error ? e.message : String(e)}) and the layer is treated as empty`, name);\n return undefined;\n }\n}\n\n/** A rectangle's size, or 0 × 0 (with a warning) for a malformed one. */\nfunction safeSize(ctx: Ctx, rect: PsdRect, name: string): { width: number; height: number } {\n try {\n return rectSize(rect);\n } catch (e) {\n ctx.warn.add(`A malformed rectangle (${e instanceof Error ? e.message : String(e)}) left the layer or its mask empty`, name);\n return { width: 0, height: 0 };\n }\n}\n\nfunction recordPixels(ctx: Ctx, rec: PsdLayerRecord, name: string): PixelLayer['surface'] {\n const { width, height } = safeSize(ctx, rec.rect, name);\n if (!width || !height || ctx.skipPixels) return emptySurface(4);\n const ids = [...Array.from({ length: ctx.colorCount }, (_, i) => i), CHANNEL_ALPHA];\n const planes = ids.map((id) => (rec.channels.some((c) => c.id === id) ? decodeOr(ctx, rec, id, name) ?? null : undefined));\n // A channel that is in the file but unreadable leaves the layer empty (as the warning says).\n if (planes.some((p) => p === null)) return emptySurface(4);\n if (planes.every((p) => p === undefined)) return emptySurface(4);\n const data = toRgba8({\n width,\n height,\n depth: ctx.file.header.depth,\n mode: ctx.file.header.colorMode,\n color: planes.slice(0, ctx.colorCount) as Array<Uint8Array | undefined>,\n alpha: planes[ctx.colorCount] as Uint8Array | undefined,\n palette: ctx.palette,\n });\n return surfaceFromPixels({ width, height, data }, 4, rec.rect.left, rec.rect.top);\n}\n\nfunction recordMask(ctx: Ctx, rec: PsdLayerRecord, name: string): RasterMask | undefined {\n const m = rec.mask;\n if (!m) return undefined;\n const useReal = rec.channels.some((c) => c.id === CHANNEL_REAL_USER_MASK) && m.real;\n const id = useReal ? CHANNEL_REAL_USER_MASK : CHANNEL_USER_MASK;\n if (!rec.channels.some((c) => c.id === id)) return undefined;\n const rect = useReal ? m.real!.rect : m.rect;\n const def: 0 | 255 = (useReal ? m.real!.background : m.defaultColor) >= 128 ? 255 : 0;\n const flags = useReal ? m.real!.flags : m.flags;\n let surface = emptySurface(1, def);\n const { width, height } = safeSize(ctx, rect, name);\n if (width && height && !ctx.skipPixels) {\n const plane = decodeOr(ctx, rec, id, name);\n if (plane) surface = surfaceFromPixels({ width, height, data: toMask8(plane, width, height, ctx.file.header.depth) }, 1, rect.left, rect.top, def);\n }\n const mask: RasterMask = { surface, defaultValue: def };\n if (m.userDensity !== undefined && m.userDensity < 255) mask.density = percent(m.userDensity);\n if (m.userFeather !== undefined && m.userFeather > 0) mask.feather = Math.min(1000, m.userFeather);\n if (flags & MASK_FLAG_DISABLED) mask.enabled = false;\n return mask;\n}\n\nfunction baseProps(ctx: Ctx, rec: PsdLayerRecord, name: string, notes: string[]) {\n let blend = PSD_BLEND_KEYS[rec.blendKey];\n if (rec.blendKey === 'diss') {\n ctx.warn.add('Dissolve is not supported and became Normal', name);\n notes.push('Dissolve became Normal');\n } else if (!blend) {\n ctx.warn.add(`An unknown blend mode became Normal`, name);\n notes.push(`unknown blend mode \"${rec.blendKey}\"`);\n }\n if (!blend || blend === 'pass-through') blend = 'normal';\n const props = {\n name,\n visible: (rec.flags & LAYER_FLAG_HIDDEN) === 0,\n opacity: percent(rec.opacity),\n blend: blend as BlendMode,\n } as { name: string; visible: boolean; opacity: number; blend: BlendMode; clipped?: boolean; locks?: LayerLocks };\n if (rec.clipping) props.clipped = true;\n const locks = locksOf(rec);\n if (locks) props.locks = locks;\n if (rec.blocks.some((b) => EFFECT_KEYS.includes(b.key))) {\n ctx.warn.add('Layer effects (lfx2) are not rendered', name);\n notes.push('layer effects are not rendered');\n }\n if (rec.blocks.some((b) => b.key === 'artb' || b.key === 'artd' || b.key === 'abdd')) {\n ctx.warn.add('Artboards are not supported; they came in as plain groups, without their background colour and clipping', name);\n notes.push('artboard imported as a group');\n }\n if (rec.blocks.some((b) => VECTOR_KEYS.includes(b.key)) && psdKind(rec) === 'pixel') {\n ctx.warn.add('Vector masks are not supported and were ignored', name);\n notes.push('vector mask ignored');\n }\n return props;\n}\n\nfunction importNode(ctx: Ctx, node: PsdTreeNode, depth: number): ImageLayer | null {\n const rec = ctx.file.layers[node.index];\n const name = layerName(rec);\n const notes: string[] = [];\n const kind = psdKind(rec);\n const entry: PsdLayerSummary = { name, type: null, status: 'mapped', psdKind: node.children ? 'group' : kind, notes, depth };\n ctx.summary.push(entry);\n\n if (node.children) {\n const props = baseProps(ctx, rec, name, notes);\n const divider = sectionDivider(rec);\n const key = divider?.blendKey ?? rec.blendKey;\n const groupBlend: LayerBlendMode = key === 'pass' ? 'pass-through' : (PSD_BLEND_KEYS[key] ?? 'normal');\n const children: ImageLayer[] = [];\n for (const child of node.children) {\n const layer = importNode(ctx, child, depth + 1);\n if (layer) children.push(layer);\n }\n const group: GroupLayer = { type: 'group', id: ctx.newId(name), ...props, blend: groupBlend, children };\n if (divider?.kind === SECTION_CLOSED) group.collapsed = true;\n const mask = recordMask(ctx, rec, name);\n if (mask) group.mask = mask;\n entry.type = 'group';\n entry.id = group.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return group;\n }\n\n const adjBlock = rec.blocks.find((b) => ADJUSTMENT_KEYS.includes(b.key));\n if (adjBlock) {\n const parsed = parseAdjustmentBlock(adjBlock.key, adjBlock.data);\n if (!parsed) {\n const label = adjustmentLabel(adjBlock.key);\n ctx.warn.add(`${label} adjustment layers are not supported and were left out`, name);\n notes.push(`${label} is not supported`);\n entry.status = 'dropped';\n return null;\n }\n const props = baseProps(ctx, rec, name, notes);\n if (parsed.approximated) {\n ctx.warn.add(`A ${adjustmentLabel(adjBlock.key)} layer was approximated: ${parsed.approximated}`, name);\n notes.push(parsed.approximated);\n }\n const layer = { type: 'adjustment', id: ctx.newId(name), ...props, kind: parsed.kind, params: parsed.params } as ImageLayer;\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'adjustment';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n }\n\n const props = baseProps(ctx, rec, name, notes);\n if (kind !== 'pixel') {\n const what = kind === 'smart-object' ? 'Smart objects' : kind === 'type' ? 'Type layers' : kind === 'shape' ? 'Shape layers' : 'Fill layers';\n ctx.warn.add(`${what} came in as pixel layers (their stored pixels)`, name);\n notes.push(`${kind} layer imported as pixels`);\n }\n const layer: PixelLayer = { type: 'pixel', id: ctx.newId(name), ...props, surface: recordPixels(ctx, rec, name), offset: { x: 0, y: 0 } };\n const fill = u8Block(rec, 'iOpa');\n if (fill !== undefined && fill < 255) layer.fillOpacity = percent(fill);\n const mask = recordMask(ctx, rec, name);\n if (mask) layer.mask = mask;\n entry.type = 'pixel';\n entry.id = layer.id;\n entry.status = notes.length ? 'approximated' : 'mapped';\n return layer;\n}\n\nfunction mergedLayer(ctx: Ctx): PixelLayer | null {\n const { header } = ctx.file;\n const { width, height } = header;\n let planes: Uint8Array;\n try {\n planes = decodeMergedImage(ctx.file);\n } catch (e) {\n ctx.warn.extra.push(`The merged image could not be decoded (${e instanceof Error ? e.message : String(e)}).`);\n return null;\n }\n const size = rowBytes(width, header.depth) * height;\n const plane = (i: number) => (i < header.channels ? planes.subarray(i * size, (i + 1) * size) : undefined);\n const cc = ctx.colorCount;\n const hasAlpha = header.channels > cc && (ctx.file.layers.length === 0 || ctx.file.mergedAlpha);\n const data = toRgba8({\n width,\n height,\n depth: header.depth,\n mode: header.colorMode,\n color: Array.from({ length: cc }, (_, i) => plane(i)),\n alpha: hasAlpha ? plane(cc) : undefined,\n palette: ctx.palette,\n });\n if (hasAlpha) unmatteWhite(data);\n return {\n type: 'pixel',\n id: ctx.newId('Background'),\n name: 'Background',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n}\n\n/** Maps a parsed PSD to our layers. */\nexport function psdToLayers(file: PsdFile, options: PsdImportOptions = {}): PsdImportResult {\n const { header } = file;\n const warn = new Warnings();\n let n = 0;\n const ctx: Ctx = {\n file,\n warn,\n newId: options.newId ?? (() => `psd-${++n}`),\n skipPixels: options.skipPixels ?? false,\n summary: [],\n colorCount: colorChannelCount(header.colorMode, header.channels),\n palette: header.colorMode === MODE_INDEXED ? file.colorModeData : undefined,\n };\n const mode = COLOR_MODES[header.colorMode] ?? `mode ${header.colorMode}`;\n if (mode !== 'rgb' || header.depth !== 8) {\n warn.extra.push(`The file is ${header.depth}-bit ${mode}; it was converted to 8-bit RGB.`);\n }\n if (resource(file, 1039)) warn.extra.push('The embedded ICC profile is ignored; colours are read as sRGB.');\n\n let layers: ImageLayer[] = [];\n if (file.layers.length) {\n const tree = layerTree(file);\n const built: ImageLayer[] = [];\n for (const node of tree) {\n const layer = importNode(ctx, node, 0);\n if (layer) built.push(layer);\n }\n layers = built;\n if (!layers.length && !ctx.skipPixels) {\n // Every layer was left out: the merged image is still the best picture of the file.\n const bg = mergedLayer(ctx);\n if (bg) {\n layers = [bg];\n warn.extra.push('No layer could be imported, so the merged image was opened as the Background.');\n }\n }\n } else if (!ctx.skipPixels) {\n const bg = mergedLayer(ctx);\n if (bg) layers = [bg];\n }\n return {\n width: header.width,\n height: header.height,\n layers,\n warnings: warn.list(),\n summary: ctx.summary,\n resolution: resolution(file),\n info: { psb: header.psb, depth: header.depth, colorMode: mode, channels: header.channels },\n };\n}\n\n/** Parses and maps PSD / PSB bytes. Throws `PsdError` for files that cannot be read at all. */\nexport function importPsd(bytes: Uint8Array, options: PsdImportOptions = {}): PsdImportResult {\n if (bytes.length < 26) throw new PsdError('Not a PSD file: it is shorter than a PSD header.');\n return psdToLayers(readPsd(bytes), options);\n}\n","// Portions ported from photocraft/crates/io/src/psd_export.rs and photocraft/crates/psd/src/builder.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Our layer tree → an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, RLE-compressed, plus the merged image the caller rendered.\n *\n * The stack must be *prepared*: annotation layers rasterized, layer filters and parametric masks\n * baked, and no layer reading the document source (see `ImageEditorController.toPsd`).\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport { contentBounds, readRegion } from '../../raster/surface';\nimport { isActiveFilter, type ImageLayer, type RasterMask } from '../layers/model';\nimport type { CropBox } from '../types';\nimport { PsdError, PsdWriter, encodeLegacyName } from './binary';\nimport { writeAdjustmentBlock } from './adjust';\nimport { encodeRle } from './compression';\nimport {\n CHANNEL_ALPHA,\n CHANNEL_USER_MASK,\n LAYER_FLAG_HIDDEN,\n LAYER_FLAG_TRANSPARENCY_PROTECTED,\n MASK_FLAG_DISABLED,\n PSD_MAX_DIMENSION,\n SECTION_CLOSED,\n SECTION_DIVIDER,\n SECTION_OPEN,\n type PsdChannel,\n type PsdLayerMask,\n type PsdLayerRecord,\n type PsdRect,\n type PsdResource,\n type PsdTaggedBlock,\n writePsd,\n} from './file';\nimport { BLEND_TO_PSD } from './import';\nimport { matteWhite, rgbaToPlanes } from './pixels';\n\nexport interface PsdExportInput {\n width: number;\n height: number;\n /** Prepared stack, bottom to top. */\n layers: readonly ImageLayer[];\n /** The composite, `width × height` RGBA, written as the merged image. */\n merged: ArrayLike<number>;\n /** Pixels per inch (default 72). */\n resolution?: number;\n}\n\nexport interface PsdExportResult {\n bytes: Uint8Array;\n psb: boolean;\n /** Layer records written (group folders and dividers included). */\n records: number;\n}\n\n/** Layer flag bits 3 and 4: \"bit 4 is useful\" and \"pixel data irrelevant\" (group records). */\nconst FLAG_PIXELS_IRRELEVANT = 8 | 16;\nconst EMPTY_RECT: PsdRect = { top: 0, left: 0, bottom: 0, right: 0 };\n\nfunction unicodeBlock(s: string): Uint8Array {\n const w = new PsdWriter();\n w.unicode(s);\n return w.result();\n}\n\nfunction u32Bytes(v: number): Uint8Array {\n const w = new PsdWriter();\n w.u32(v);\n return w.result();\n}\n\nfunction sectionBlock(kind: number, blendKey?: string): Uint8Array {\n const w = new PsdWriter();\n w.u32(kind);\n if (blendKey) {\n w.ascii('8BIM');\n w.ascii(blendKey);\n }\n return w.result();\n}\n\nconst toRect = (r: CropBox): PsdRect => ({ top: r.y, left: r.x, bottom: r.y + r.height, right: r.x + r.width });\n\nfunction rleChannel(id: number, plane: Uint8Array, width: number, height: number, psb: boolean): PsdChannel {\n return { id, compression: 1, data: encodeRle(plane, { planes: 1, width, height, depth: 8, psb }) };\n}\n\nconst emptyChannel = (id: number): PsdChannel => ({ id, compression: 0, data: new Uint8Array(0) });\n\nfunction maskChannel(mask: RasterMask, ox: number, oy: number, psb: boolean): { channel: PsdChannel; mask: PsdLayerMask } {\n if (mask.source) throw new PsdError('A parametric mask must be baked before export.');\n const bounds = contentBounds(mask.surface);\n const def = mask.defaultValue;\n const m: PsdLayerMask = { rect: EMPTY_RECT, defaultColor: def, flags: mask.enabled === false ? MASK_FLAG_DISABLED : 0 };\n if ((mask.density ?? 1) < 1) m.userDensity = Math.round(Math.max(0, mask.density ?? 1) * 255);\n if ((mask.feather ?? 0) > 0) m.userFeather = mask.feather;\n if (!bounds) return { channel: emptyChannel(CHANNEL_USER_MASK), mask: m };\n const plane = readRegion(mask.surface, bounds);\n m.rect = toRect({ ...bounds, x: bounds.x + ox, y: bounds.y + oy });\n return { channel: rleChannel(CHANNEL_USER_MASK, new Uint8Array(plane.buffer, plane.byteOffset, plane.length), bounds.width, bounds.height, psb), mask: m };\n}\n\nfunction lockBits(layer: ImageLayer): number {\n const l = layer.locks;\n if (!l) return 0;\n return (l.all ? 0x80000000 : 0) | (l.transparency ? 1 : 0) | (l.pixels ? 2 : 0) | (l.position ? 4 : 0);\n}\n\n/** Writes the prepared stack as PSD (or PSB) bytes. */\nexport function exportPsd(input: PsdExportInput): PsdExportResult {\n const { width, height } = input;\n if (width < 1 || height < 1) throw new PsdError('The document is empty.');\n const psb = width > PSD_MAX_DIMENSION || height > PSD_MAX_DIMENSION;\n const records: PsdLayerRecord[] = [];\n let nextId = 0;\n\n const common = (layer: ImageLayer, extra: PsdTaggedBlock[] = []): Pick<PsdLayerRecord, 'opacity' | 'clipping' | 'flags' | 'legacyName' | 'blocks'> => {\n const name = layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : '');\n const blocks: PsdTaggedBlock[] = [{ key: 'luni', data: unicodeBlock(name) }, { key: 'lyid', data: u32Bytes(++nextId) }, ...extra];\n const locks = lockBits(layer);\n if (locks) blocks.push({ key: 'lspf', data: u32Bytes(locks >>> 0) });\n return {\n opacity: Math.round(Math.min(1, Math.max(0, layer.opacity)) * 255),\n clipping: layer.clipped ? 1 : 0,\n flags: (layer.visible ? 0 : LAYER_FLAG_HIDDEN) | (layer.locks?.transparency ? LAYER_FLAG_TRANSPARENCY_PROTECTED : 0),\n legacyName: name,\n blocks,\n };\n };\n\n const write = (layer: ImageLayer): void => {\n switch (layer.type) {\n case 'group': {\n records.push({\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: 'norm',\n opacity: 255,\n clipping: 0,\n flags: FLAG_PIXELS_IRRELEVANT | (layer.visible ? 0 : LAYER_FLAG_HIDDEN),\n legacyName: '</Layer group>',\n blocks: [\n { key: 'luni', data: unicodeBlock('</Layer group>') },\n { key: 'lsct', data: sectionBlock(SECTION_DIVIDER) },\n ],\n });\n for (const child of layer.children) write(child);\n const blendKey = BLEND_TO_PSD[layer.blend] ?? 'norm';\n const base = common(layer, [{ key: 'lsct', data: sectionBlock(layer.collapsed ? SECTION_CLOSED : SECTION_OPEN, blendKey) }]);\n const rec: PsdLayerRecord = {\n ...base,\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey,\n flags: base.flags | FLAG_PIXELS_IRRELEVANT,\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'adjustment': {\n const block = writeAdjustmentBlock(layer.kind, layer.params);\n const rec: PsdLayerRecord = {\n ...common(layer, [{ key: block.key, data: block.data }]),\n rect: EMPTY_RECT,\n channels: [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel),\n blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm',\n };\n if (layer.mask) {\n const m = maskChannel(layer.mask, 0, 0, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'pixel': {\n if (layer.source === 'document') throw new PsdError(`Layer \"${layer.name}\" reads the document image; resolve its pixels before export.`);\n if (layer.filters?.some(isActiveFilter)) throw new PsdError(`Layer \"${layer.name}\" has live filters; bake them before export.`);\n const extra: PsdTaggedBlock[] = [];\n if (layer.fillOpacity !== undefined && layer.fillOpacity < 1) {\n extra.push({ key: 'iOpa', data: Uint8Array.from([Math.round(Math.max(0, layer.fillOpacity) * 255), 0, 0, 0]) });\n }\n const rec: PsdLayerRecord = { ...common(layer, extra), rect: EMPTY_RECT, channels: [], blendKey: BLEND_TO_PSD[layer.blend] ?? 'norm' };\n const bounds = contentBounds(layer.surface);\n if (bounds) {\n const n = bounds.width * bounds.height;\n const [r, g, b, a] = rgbaToPlanes(readRegion(layer.surface, bounds), n);\n rec.rect = toRect({ ...bounds, x: bounds.x + layer.offset.x, y: bounds.y + layer.offset.y });\n rec.channels = [\n rleChannel(CHANNEL_ALPHA, a, bounds.width, bounds.height, psb),\n rleChannel(0, r, bounds.width, bounds.height, psb),\n rleChannel(1, g, bounds.width, bounds.height, psb),\n rleChannel(2, b, bounds.width, bounds.height, psb),\n ];\n } else {\n rec.channels = [CHANNEL_ALPHA, 0, 1, 2].map(emptyChannel);\n }\n if (layer.mask) {\n const m = maskChannel(layer.mask, layer.offset.x, layer.offset.y, psb);\n rec.channels.push(m.channel);\n rec.mask = m.mask;\n }\n records.push(rec);\n return;\n }\n case 'annotation':\n throw new PsdError(`Annotation layer \"${layer.name}\" must be rasterized before export.`);\n }\n };\n for (const layer of input.layers) write(layer);\n if (records.length > 32767) throw new PsdError('A PSD holds at most 32767 layer records.');\n\n // Merged image: RGB, plus alpha when anything is transparent.\n const n = width * height;\n if (input.merged.length < n * 4) throw new PsdError(`The merged image has ${input.merged.length} bytes, expected ${n * 4}.`);\n // Photoshop's merged image is matted over white where it is transparent.\n const [r, g, b, a] = rgbaToPlanes(matteWhite(input.merged), n);\n const transparent = a.some((v) => v !== 255);\n const planes = transparent ? [r, g, b, a] : [r, g, b];\n const merged = new Uint8Array(n * planes.length);\n planes.forEach((p, i) => merged.set(p, i * n));\n\n const res = Math.max(1, input.resolution ?? 72);\n const rw = new PsdWriter();\n for (let k = 0; k < 2; k++) {\n rw.u32(Math.round(res * 65536));\n rw.u16(1); // pixels per inch\n rw.u16(1); // inches\n }\n const version = new PsdWriter();\n version.u32(1);\n version.u8(1); // the merged image is real\n version.unicode('Pixerate Editor');\n version.unicode('Pixerate Editor');\n version.u32(1);\n const resources: PsdResource[] = [\n { id: 1005, name: '', data: rw.result() },\n { id: 1057, name: '', data: version.result() },\n ];\n\n const bytes = writePsd(\n {\n header: { psb, channels: planes.length, width, height, depth: 8, colorMode: 3 },\n colorModeData: new Uint8Array(0),\n resources,\n layers: records,\n mergedAlpha: transparent && records.length > 0,\n globalBlocks: [],\n imageCompression: 1,\n imageData: encodeRle(merged, { planes: planes.length, width, height, depth: 8, psb }),\n },\n encodeLegacyName\n );\n return { bytes, psb, records: records.length };\n}\n","import {\n AdjustmentKind,\n AdjustmentLayer,\n AdjustmentParamsMap,\n Annotation,\n CreateAnnotationPayload,\n AspectRatio,\n CropBox,\n DepthMaskOptions,\n ExportOptions,\n ImageAdjustments,\n ImageDimensions,\n ImageEditorFit,\n ImageEditorOptions,\n ImageEditorState,\n ImageEditorTool,\n ImageTransform,\n ImagePoint,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { ImageEditorRenderer, RenderOptions } from './renderer';\nimport { clampCropToImage, isQuarterTurnSwapped } from './geometry';\nimport { LATEST_PROCESS_VERSION, LEGACY_PROCESS_VERSION, normalizeProcessVersion } from './color';\nimport { createAdjustmentLayer, normalizeAdjustmentLayer, normalizeAdjustmentLayers } from './adjustment-layers';\nimport type { BlendMode } from './blend';\nimport type { ImageHistogram } from './histogram';\nimport { applyAdjustments } from './filters';\nimport {\n combineSelections,\n createSelectionMask,\n deserializeSelection,\n fullSelection,\n invertSelection as invertSelectionMask,\n resizeSelection,\n selectionFromPixels,\n serializeSelection,\n type PixelBuffer,\n type SelectionChannel,\n type SelectionMask,\n type SelectionMode,\n} from './selection/mask';\nimport { ellipseSelection, polygonSelection, rectSelection, type ShapeSelectionOptions } from './selection/shapes';\nimport { colorRange, magicWand, type ColorRangeOptions, type ColorRangeSample, type MagicWandOptions } from './selection/wand';\nimport { modifySelection as modifySelectionMask, type SelectionModifyOp } from './selection/modify';\nimport { refineSelection as refineSelectionMask, type RefineEdgeParams } from './selection/refine';\nimport type { LayerMaskProps } from './selection/layer-mask';\nimport type { AdjustmentLayerMask } from './types';\nimport { DEFAULT_ADJUSTMENTS_VALUES, DEFAULT_DEPTH_MASK_VALUES, DEFAULT_TRANSFORM_VALUES } from './layers/defaults';\nimport {\n findLayer,\n flattenLayers,\n insertLayer,\n isActiveFilter,\n mapLayer,\n newLayerId,\n type AdjustmentLayerNode,\n type AnnotationLayer,\n type DevelopFilter,\n type GroupLayer,\n type LayerBlendMode,\n type ImageLayer,\n type LayerFilter,\n type LayerTarget,\n type PixelLayer,\n type RasterMask,\n} from './layers/model';\nimport {\n BACKGROUND_LAYER_ID,\n BACKGROUND_LAYER_NAME,\n BLUR_FILTER_ID,\n DEVELOP_FILTER_ID,\n adjustmentNodeFromLegacy,\n annotationLayerFor,\n annotationLayerName,\n depthRasterMask,\n developTarget,\n isDefaultDepthMask,\n layersFromLegacy,\n legacyMaskOf,\n legacyView,\n rasterMaskFromLegacy,\n type LayerStack,\n} from './layers/migrate';\nimport {\n LAYERED_SCHEMA_VERSION,\n deserializeLayers,\n resolveExternalAssets,\n serializeLayers,\n serializeLayersExternal,\n type ImageEditorAssetStore,\n type SerializedLayeredImageEditorState,\n} from './layers/serialize';\nimport { LayerCompositor, type CompositorSources } from './layers/compositor';\nimport {\n SurfaceDraft,\n TILE,\n collectTiles,\n contentBounds,\n emptySurface,\n packRgba,\n readPixel,\n readRegion,\n solidSurface,\n surfaceFromPixels,\n tilesInRect,\n translateSurface,\n writeRegion,\n type PixelSurface,\n type Tile,\n type TiledSurface,\n type TileKey,\n TiledSurface as TiledSurfaceClass,\n finishTile,\n tileKey,\n} from '../raster/surface';\nimport {\n LayerOperationError,\n addLayerFilter as addLayerFilterOp,\n duplicateLayer as duplicateLayerOp,\n groupLayers as groupLayersOp,\n moveLayer as moveLayerOp,\n normalizeFilter,\n offsetLayer as offsetLayerOp,\n parseBlend,\n placeLayer,\n removeLayer as removeLayerOp,\n removeLayerFilter as removeLayerFilterOp,\n requireLayer,\n requireUnlocked,\n setLayerMask as setLayerMaskOp,\n setLayerProperties as setLayerPropertiesOp,\n ungroupLayer as ungroupLayerOp,\n unionRect,\n updateLayerFilter as updateLayerFilterOp,\n type LayerPlacement,\n type LayerProperties,\n} from './layers/ops';\nimport { coveringRect } from './layers/render';\nimport {\n CoverageMap,\n DEFAULT_BRUSH,\n StrokeCoverage,\n applyStrokeToTile,\n normalizeBrush,\n type BrushSettings,\n type StrokeApply,\n type StrokePoint,\n} from '../paint';\nimport { base64ToBytes, bytesToBase64, decodePng, isPng, parseDataUrl } from '../raster/png';\nimport { getAnnotationBounds } from './geometry';\nimport { importPsd, type PsdImportResult } from './psd/import';\nimport { exportPsd } from './psd/export';\n\n/** Latest serialized schema version (`3`: layers). Documents with the old shape still write 1 or 2. */\nexport const IMAGE_EDITOR_SCHEMA_VERSION = LAYERED_SCHEMA_VERSION;\n\n/** Changes to an adjustment layer; `params` is merged into the current parameters. */\nexport interface AdjustmentLayerUpdate<K extends AdjustmentKind = AdjustmentKind> {\n params?: Partial<AdjustmentParamsMap[K]>;\n enabled?: boolean;\n opacity?: number;\n blend?: BlendMode;\n name?: string;\n /** A layer mask, or `null` to remove it. */\n mask?: AdjustmentLayerMask | null;\n}\n\n/** Options shared by the selection commands. */\nexport interface SelectionCommandOptions {\n /** How the result combines with the current selection (default `replace`). */\n mode?: SelectionMode;\n /** Feather the new shape by this many pixels before combining (default 0). */\n feather?: number;\n}\n\n/** Where an AI result goes (see `applyInpaintedImage`, `applyBackgroundRemovedImage`). */\nexport interface AIResultOptions {\n /** `new-layer`: a new layer (inpainting) or a mask (background removal); `replace`: the Background's pixels. Default: by `isMultiLayer`. */\n target?: 'new-layer' | 'replace';\n}\n\n/** Where the selection tools read colours from. */\nexport interface SampleOptions {\n /**\n * `composite` (default): the image, as before (with several layers: the composite of the pixel\n * layers, or with `sampleMerged` of every layer); `active-layer`: only the active pixel layer.\n */\n sample?: 'composite' | 'active-layer';\n /** Sample the adjusted image (base adjustments and layers) instead of the source pixels. */\n sampleMerged?: boolean;\n /** Pixels to use instead of reading the source image (natural size, RGBA). */\n pixels?: PixelBuffer;\n}\n\n/** A new adjustment layer: its kind plus optional initial values and position. */\nexport interface AdjustmentLayerInit<K extends AdjustmentKind = AdjustmentKind> extends AdjustmentLayerUpdate<K> {\n kind: K;\n id?: string;\n /** Put the new layer directly above this layer (in its group). */\n aboveId?: string;\n /** Put the new layer at the top of this group. */\n parentId?: string;\n}\n\n/** A new pixel layer: empty, a solid colour over the canvas, or given pixels, plus its properties and position. */\nexport interface PixelLayerInit extends LayerPlacement {\n id?: string;\n name?: string;\n /** Fill the canvas with this colour (`#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` 0..255). */\n color?: string | number[];\n /** RGBA pixels placed at `x`, `y` (document pixels). */\n pixels?: PixelBuffer;\n x?: number;\n y?: number;\n visible?: boolean;\n /** 0..1 (default 1). */\n opacity?: number;\n fillOpacity?: number;\n blend?: BlendMode;\n}\n\n/** The paint tools' options (view state). */\nexport interface PaintOptions {\n brush: BrushSettings;\n /** `#rrggbb`: what the brush and Fill paint. */\n foreground: string;\n /** `#rrggbb`: what the eraser paints on transparency-locked layers and masks. */\n background: string;\n}\n\n/** A stroke: tool, target (default the active layer and target) and brush / colour overrides. */\nexport interface StrokeInit {\n tool?: 'brush' | 'eraser';\n layerId?: string;\n channel?: LayerTarget;\n /** Overrides of the paint options' brush. */\n brush?: Partial<BrushSettings>;\n /** Paint colour (default the foreground). Masks take its grey value. */\n color?: string | number[];\n /** Eraser colour on locked transparency / masks (default the background). */\n background?: string | number[];\n /** Kept with the stroke for replays (v1 brushes have no jitter). */\n seed?: number;\n}\n\nexport interface FillOptions {\n layerId?: string;\n channel?: LayerTarget;\n /** Default the foreground colour. */\n color?: string | number[];\n /** 0..1 (default 1). */\n opacity?: number;\n blend?: BlendMode;\n /** Limit to the selection when there is one (default true). */\n useSelection?: boolean;\n}\n\n/** What a stroke, fill or clear changed. */\nexport interface PaintStrokeResult {\n layerId: string;\n channel: LayerTarget;\n /** Document rectangle that changed (null when nothing did). */\n bounds: CropBox | null;\n dabs: number;\n}\n\n/** An opaque pre-stroke history snapshot. */\nexport type HistoryEntryHandle = { readonly __historyEntry: unique symbol };\n\n/** Composites `coverage` into the tiles of `base` under `rects` (document space), returning the new surface. */\nfunction paintCoverageInto(base: TiledSurface, coverage: CoverageMap, apply: StrokeApply, rects: CropBox[]): TiledSurface {\n const tiles = new Map(base.tiles);\n const done = new Set<TileKey>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - apply.offset.x, y: r.y - apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n if (done.has(key)) continue;\n done.add(key);\n const src = base.tiles.get(key)?.data ?? defaultTile(base);\n const data = applyStrokeToTile(coverage, src, base.channels, t.tx, t.ty, apply);\n const tile = finishTile(data, base.channels, base.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n }\n return new TiledSurfaceClass(base.channels, tiles, base.defaultValue);\n}\n\nfunction defaultTile(s: TiledSurface): Uint8ClampedArray {\n const d = new Uint8ClampedArray(TILE * TILE * s.channels);\n if (s.defaultValue) d.fill(s.defaultValue);\n return d;\n}\n\n/**\n * A stroke in progress (see `beginStroke`): `push` input points as they arrive, then `end` (one\n * undo step) or `cancel`.\n */\nexport class PaintStrokeSession {\n private readonly before: HistoryEntryHandle;\n private readonly base: TiledSurface;\n private readonly live: Map<TileKey, Tile>;\n private readonly touched = new Set<TileKey>();\n private last: StrokePoint | null = null;\n private closed = false;\n\n constructor(\n private readonly controller: ImageEditorController,\n readonly target: { id: string; channel: LayerTarget; surface: TiledSurface },\n private readonly stroke: StrokeCoverage,\n private readonly apply: StrokeApply\n ) {\n this.before = controller._captureForStroke();\n this.base = target.surface;\n this.live = new Map(this.base.tiles);\n }\n\n get layerId(): string {\n return this.target.id;\n }\n\n /** Adds input points (document pixels); the stack shows the stroke so far. */\n push(points: readonly StrokePoint[]): void {\n if (this.closed || !points.length) return;\n this.last = points[points.length - 1];\n this.update(this.stroke.push(points));\n }\n\n private update(rects: CropBox[]): void {\n if (!rects.length) return;\n const ch = this.base.channels;\n // Per target tile, the union of the new dabs inside it: only that part is recomputed.\n const regions = new Map<TileKey, { tx: number; ty: number; r: CropBox }>();\n for (const r of rects) {\n for (const t of tilesInRect({ x: r.x - this.apply.offset.x, y: r.y - this.apply.offset.y, width: r.width, height: r.height })) {\n const key = tileKey(t.tx, t.ty);\n const cur = regions.get(key);\n regions.set(key, { tx: t.tx, ty: t.ty, r: cur ? unionRect(cur.r, r)! : r });\n }\n }\n for (const [key, { tx, ty, r }] of regions) {\n const src = this.base.tiles.get(key)?.data ?? defaultTile(this.base);\n const prev = this.touched.has(key) ? this.live.get(key)?.data : undefined;\n this.touched.add(key);\n const data = applyStrokeToTile(this.stroke.coverage, src, ch, tx, ty, this.apply, prev ? { into: prev, region: r } : {});\n this.live.set(key, { data, uniform: null });\n }\n this.controller._strokeUpdate(this.target.id, this.target.channel, new TiledSurfaceClass(ch, new Map(this.live), this.base.defaultValue), rects);\n }\n\n /** Finishes the stroke (one undo step) and returns what changed. */\n end(): PaintStrokeResult {\n if (this.closed) throw new LayerOperationError('This stroke has already ended.');\n this.update(this.stroke.finish(this.last ?? undefined));\n this.closed = true;\n const ch = this.base.channels;\n const tiles = new Map(this.base.tiles);\n let changed = false;\n for (const key of this.touched) {\n const live = this.live.get(key)!;\n const tile = finishTile(live.data, ch, this.base.defaultValue);\n const prev = this.base.tiles.get(key);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n if (!sameTile(prev, tile)) changed = true;\n }\n const surface = changed ? new TiledSurfaceClass(ch, tiles, this.base.defaultValue) : null;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, surface);\n const b = this.stroke.coverage.bounds;\n return { layerId: this.target.id, channel: this.target.channel, bounds: changed ? b : null, dabs: this.stroke.dabCount };\n }\n\n /** Abandons the stroke and restores the layer. */\n cancel(): void {\n if (this.closed) return;\n this.closed = true;\n this.controller._strokeCommit(this.before, this.target.id, this.target.channel, null);\n }\n}\n\nfunction sameTile(a: Tile | undefined, b: Tile | null): boolean {\n if (!a || !b) return !a && !b;\n if (a === b) return true;\n if (a.uniform !== null || b.uniform !== null) return a.uniform === b.uniform;\n for (let i = 0; i < a.data.length; i++) if (a.data[i] !== b.data[i]) return false;\n return true;\n}\n\n/** Where a new annotation goes. */\nexport interface AnnotationPlacement {\n /** Put the annotation layer directly above this layer (default: the top of the stack). */\n aboveId?: string;\n}\n\n/** The image a history entry was recorded against. */\ninterface SourceSnapshot {\n sourceUrl: string | null;\n element: CanvasImageSource | null;\n dimensions: ImageDimensions;\n}\n\n/**\n * Undo/redo entry: the document as immutable objects. Layers, surfaces, tiles and selections are\n * shared with the other entries and the current state, so an entry costs about the tiles it changed.\n */\ninterface HistoryEntry {\n layers: ImageLayer[];\n activeLayerId: string | null;\n activeTarget: LayerTarget;\n crop: CropBox | null;\n transform: ImageTransform;\n outputOpacity: number;\n legacyOutputBlur?: number;\n selection: SelectionMask | null;\n source: SourceSnapshot;\n}\n\nexport const DEFAULT_ADJUSTMENTS: ImageAdjustments = DEFAULT_ADJUSTMENTS_VALUES;\n\n/**\n * Fills in defaults for stored adjustments. A stored object without `process` was saved before\n * process versions existed and keeps the legacy rendering.\n */\nexport function normalizeStoredAdjustments(stored: Partial<ImageAdjustments> | undefined | null): ImageAdjustments {\n if (!stored) return { ...DEFAULT_ADJUSTMENTS };\n const out: ImageAdjustments = {\n ...DEFAULT_ADJUSTMENTS,\n ...stored,\n process: normalizeProcessVersion(stored.process),\n };\n // Layers are deep-copied and cleaned; an empty or missing list is left out entirely.\n const layers = normalizeAdjustmentLayers(stored.layers);\n if (layers.length) out.layers = layers;\n else delete out.layers;\n return out;\n}\n\nconst isLayered = (data: { version?: unknown } | null | undefined): data is SerializedLayeredImageEditorState =>\n Boolean(data && data.version === LAYERED_SCHEMA_VERSION && Array.isArray((data as { layers?: unknown }).layers));\n\n/**\n * Brings a stored editor state of any schema version to a clean state of the same family:\n * version 1 (and a missing version) has no adjustment layers, version 2 has them, and both stay\n * in the document-level schema so old states load and render exactly as before. Version 3\n * (layers) is returned with its document fields cleaned. To lift a v1 / v2 state into layers,\n * use {@link migrateToLayeredState}.\n */\nexport function migrateSerializedImageEditorState(\n data: Partial<SerializedImageEditorState> & { version?: number }\n): SerializedImageEditorState {\n if (isLayered(data)) {\n const out: SerializedLayeredImageEditorState = {\n ...data,\n crop: data.crop ? { ...data.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...data.transform },\n opacity: typeof data.opacity === 'number' ? Math.min(1, Math.max(0, data.opacity)) : 1,\n assets: { ...(data.assets ?? {}) },\n activeLayerId: typeof data.activeLayerId === 'string' ? data.activeLayerId : null,\n };\n return out;\n }\n const legacy = data as Partial<LegacySerializedImageEditorState>;\n const adjustments = normalizeStoredAdjustments(legacy.adjustments);\n return {\n version: adjustments.layers?.length ? 2 : 1,\n crop: legacy.crop ? { ...legacy.crop } : null,\n transform: { ...DEFAULT_TRANSFORM, ...legacy.transform },\n adjustments: serializeAdjustments(adjustments),\n depthMask: {\n ...DEFAULT_DEPTH_MASK,\n ...legacy.depthMask,\n depthRange: [...(legacy.depthMask?.depthRange ?? DEFAULT_DEPTH_MASK.depthRange)] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(legacy.annotations ?? [])),\n ...(legacy.selection ? { selection: { ...legacy.selection } } : {}),\n };\n}\n\n/**\n * Lifts a stored state of any version into the layered schema (v3) for a canvas of\n * `width × height`: the source becomes the Background layer with the base sliders as its\n * `develop` filter and the depth mask as its parametric mask, adjustment layers go above it and\n * annotations on top (see `layersFromLegacy`).\n */\nexport function migrateToLayeredState(\n data: Partial<SerializedImageEditorState> & { version?: number },\n size: ImageDimensions\n): SerializedLayeredImageEditorState {\n const clean = migrateSerializedImageEditorState(data);\n if (isLayered(clean)) return clean;\n const stack = layersFromLegacy(clean, Math.round(size.width), Math.round(size.height));\n const { layers, assets } = serializeLayers(stack.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width: Math.round(size.width),\n height: Math.round(size.height),\n layers,\n assets,\n activeLayerId: null,\n crop: clean.crop,\n transform: clean.transform,\n opacity: stack.outputOpacity,\n };\n if (stack.legacyOutputBlur !== undefined) out.legacyOutputBlur = stack.legacyOutputBlur;\n if (clean.selection) out.selection = clean.selection;\n return out;\n}\n\nexport const DEFAULT_TRANSFORM: ImageTransform = DEFAULT_TRANSFORM_VALUES;\n\nexport const DEFAULT_DEPTH_MASK: DepthMaskOptions = DEFAULT_DEPTH_MASK_VALUES;\n\nconst DEVELOP_KEYS = ['brightness', 'contrast', 'saturation', 'exposure', 'temperature', 'tint'] as const;\n\n/**\n * ImageEditorController manages the complete lifecycle, canvas rendering pipeline,\n * tool interaction states, undo/redo history, serialization, and image export.\n */\nexport class ImageEditorController {\n private state: ImageEditorState;\n private renderer: ImageEditorRenderer;\n private imageElement: CanvasImageSource | null = null;\n private listeners = new Set<(state: ImageEditorState) => void>();\n private histogramListeners = new Set<(histogram: ImageHistogram | null) => void>();\n private onStateChange?: (state: ImageEditorState) => void;\n\n // History stacks\n private undoStack: HistoryEntry[] = [];\n private redoStack: HistoryEntry[] = [];\n private maxHistoryDepth: number;\n private maxHistoryBytes: number;\n private isApplyingHistory = false;\n private initialSnapshot: HistoryEntry;\n /** Pixels the selection tools sample (host-provided or read back from the source). */\n private samplePixels: PixelBuffer | null = null;\n private samplePixelsOverride = false;\n /** Decoded source pixels (the Background's surface), by the buffer or element they came from. */\n private sourceSurfaces = new WeakMap<object, PixelSurface>();\n private viewKey: unknown[] = [];\n /** For `compositePixels` and mask evaluation (the renderer keeps its own for the viewport). */\n private compositor = new LayerCompositor();\n\n constructor(options: ImageEditorOptions = {}) {\n this.maxHistoryDepth = options.maxHistoryDepth ?? 50;\n this.maxHistoryBytes = options.maxHistoryBytes ?? 512 * 1024 * 1024;\n this.onStateChange = options.onStateChange;\n this.renderer = new ImageEditorRenderer();\n this.renderer.onPreviewHistogram = (histogram) => {\n this.histogramListeners.forEach((l) => l(histogram));\n };\n this.renderer.layerSources = {\n surfaceOf: (layer) => this.surfaceOf(layer),\n sourceIsOpaque: () => this.sourceIsOpaque(),\n version: () => this.sourceVersion(),\n };\n\n this.state = {\n sourceUrl: typeof options.image === 'string' ? options.image : null,\n imageDimensions: { width: 800, height: 600 },\n crop: null,\n transform: { ...DEFAULT_TRANSFORM },\n adjustments: { ...DEFAULT_ADJUSTMENTS },\n depthMask: { ...DEFAULT_DEPTH_MASK },\n annotations: [],\n selectedAnnotationId: null,\n activeTool: 'select',\n zoom: 1,\n pan: { x: 0, y: 0 },\n isOriginalCompared: false,\n selection: null,\n quickMask: false,\n layers: [],\n activeLayerId: null,\n activeTarget: 'pixels',\n outputOpacity: 1,\n paint: { brush: { ...DEFAULT_BRUSH }, foreground: '#000000', background: '#ffffff' },\n fit: options.fit ?? 'margin',\n };\n\n if (options.image instanceof Object && 'width' in options.image) {\n this.imageElement = options.image as CanvasImageSource;\n this.state.sourceUrl = (options.image as any).src || null;\n this.state.imageDimensions = {\n width: (options.image as any).naturalWidth || (options.image as any).width || 800,\n height: (options.image as any).naturalHeight || (options.image as any).height || 600,\n };\n }\n\n this.applyStored(options.initialState ?? {});\n if (options.initialState?.selection) {\n try {\n this.state.selection = normalizeSelection(deserializeSelection(options.initialState.selection));\n } catch {\n // A damaged selection is dropped; the rest of the state still loads.\n }\n }\n\n this.initialSnapshot = this.captureHistoryEntry();\n\n if (typeof options.image === 'string') {\n this.loadImage(options.image);\n }\n }\n\n // --- Image Loading ---\n\n /**\n * Loads a new source image and makes it the baseline that `reset()` returns to.\n * Does not record history; use `applyInpaintedImage` / `applyBackgroundRemovedImage`\n * for undoable source replacement.\n */\n public async loadImage(src: string | HTMLImageElement): Promise<void> {\n // Keep the URL visible synchronously (UI bindings compare against it while loading).\n if (typeof src === 'string') this.state.sourceUrl = src;\n const source = await this.resolveSource(src);\n const prev = this.state.imageDimensions;\n this.imageElement = source.element;\n this.state.sourceUrl = source.sourceUrl;\n this.state.imageDimensions = source.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (prev.width !== source.dimensions.width || prev.height !== source.dimensions.height) {\n // Masks lifted from v1 / v2 documents follow the image, as they did when they were resampled at render.\n this.state.layers = resizeLegacyMasks(this.state.layers, prev, source.dimensions);\n }\n this.dropMismatchedSelection();\n\n this.initialSnapshot = this.captureHistoryEntry();\n this.notify();\n }\n\n private async resolveSource(src: string | HTMLImageElement): Promise<SourceSnapshot> {\n if (typeof src === 'string') {\n try {\n const img = await this.renderer.imageCache.get(src);\n return {\n sourceUrl: src,\n element: img,\n dimensions: {\n width: img.naturalWidth || img.width || 800,\n height: img.naturalHeight || img.height || 600,\n },\n };\n } catch {\n // In SSR / non-DOM environments, retain current dimensions\n return { sourceUrl: src, element: null, dimensions: { ...this.state.imageDimensions } };\n }\n }\n return {\n sourceUrl: (src as any).src || null,\n element: src,\n dimensions: {\n width: src.naturalWidth || src.width || 800,\n height: src.naturalHeight || src.height || 600,\n },\n };\n }\n\n public getImageElement(): CanvasImageSource | null {\n return this.imageElement;\n }\n\n // --- State & Subscription ---\n\n public getState(): ImageEditorState {\n this.syncViews();\n return this.state;\n }\n\n public subscribe(listener: (state: ImageEditorState) => void): () => void {\n this.syncViews();\n this.listeners.add(listener);\n listener(this.state);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /**\n * Where layer pixels changed since the last viewport render, when only strokes changed them\n * (the renderer then recomposites just those rectangles); `'all'` after any other change.\n */\n private pendingDirty: CropBox[] | 'all' = 'all';\n\n private notify(strokeRects?: CropBox[]): void {\n if (strokeRects && this.pendingDirty !== 'all') this.pendingDirty.push(...strokeRects);\n else if (!strokeRects) this.pendingDirty = 'all';\n this.syncViews();\n const currentState = { ...this.state };\n this.onStateChange?.(currentState);\n this.listeners.forEach((l) => l(currentState));\n }\n\n /** Recomputes the compatibility views (`adjustments`, `depthMask`, `annotations`) when the stack changed. */\n private syncViews(): void {\n const s = this.state;\n const key = [s.layers, s.outputOpacity, s.legacyOutputBlur, s.activeLayerId, s.imageDimensions.width, s.imageDimensions.height];\n if (key.length === this.viewKey.length && key.every((v, i) => v === this.viewKey[i])) return;\n this.viewKey = key;\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, s.activeLayerId);\n s.adjustments = view.adjustments;\n s.depthMask = view.depthMask;\n s.annotations = view.annotations;\n }\n\n private stack(): LayerStack {\n const out: LayerStack = { layers: this.state.layers, outputOpacity: this.state.outputOpacity };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n\n // --- History (Undo / Redo / Reset) ---\n\n private captureHistoryEntry(): HistoryEntry {\n const s = this.state;\n const entry: HistoryEntry = {\n layers: s.layers,\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n crop: s.crop ? { ...s.crop } : null,\n transform: { ...s.transform },\n outputOpacity: s.outputOpacity,\n selection: s.selection ?? null,\n source: {\n sourceUrl: s.sourceUrl,\n element: this.imageElement,\n dimensions: { ...s.imageDimensions },\n },\n };\n if (s.legacyOutputBlur !== undefined) entry.legacyOutputBlur = s.legacyOutputBlur;\n return entry;\n }\n\n private restoreHistoryEntry(entry: HistoryEntry): void {\n const s = this.state;\n s.layers = entry.layers;\n s.activeLayerId = entry.activeLayerId;\n s.activeTarget = entry.activeTarget;\n s.crop = entry.crop ? { ...entry.crop } : null;\n s.transform = { ...entry.transform };\n s.outputOpacity = entry.outputOpacity;\n if (entry.legacyOutputBlur !== undefined) s.legacyOutputBlur = entry.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.selection = entry.selection;\n if (this.imageElement !== entry.source.element && !this.samplePixelsOverride) this.samplePixels = null;\n this.imageElement = entry.source.element;\n s.sourceUrl = entry.source.sourceUrl;\n s.imageDimensions = { ...entry.source.dimensions };\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) {\n s.selectedAnnotationId = null;\n }\n }\n\n private recordHistory(): void {\n if (this.isApplyingHistory) return;\n this.undoStack.push(this.captureHistoryEntry());\n if (this.undoStack.length > this.maxHistoryDepth) {\n this.undoStack.shift();\n }\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n\n /**\n * Bytes of pixel tiles held only by undo / redo steps (not by the current state). The oldest\n * undo steps are dropped while it exceeds `maxHistoryBytes`.\n */\n public get historyBytes(): number {\n const live = new Set<Tile>();\n collectStateTiles(this.state.layers, live);\n const held = new Set<Tile>();\n for (const e of [...this.undoStack, ...this.redoStack]) collectStateTiles(e.layers, held);\n let n = 0;\n for (const t of held) if (!live.has(t)) n += t.data.byteLength;\n return n;\n }\n\n private trimHistoryBytes(): void {\n if (!(this.maxHistoryBytes > 0) || this.undoStack.length <= 1) return;\n while (this.undoStack.length > 1 && this.historyBytes > this.maxHistoryBytes) this.undoStack.shift();\n }\n\n public get canUndo(): boolean {\n return this.undoStack.length > 0;\n }\n\n public get canRedo(): boolean {\n return this.redoStack.length > 0;\n }\n\n public undo(): boolean {\n if (!this.canUndo) return false;\n this.isApplyingHistory = true;\n this.redoStack.push(this.captureHistoryEntry());\n\n const prev = this.undoStack.pop()!;\n this.restoreHistoryEntry(prev);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public redo(): boolean {\n if (!this.canRedo) return false;\n this.isApplyingHistory = true;\n this.undoStack.push(this.captureHistoryEntry());\n\n const next = this.redoStack.pop()!;\n this.restoreHistoryEntry(next);\n this.isApplyingHistory = false;\n this.notify();\n return true;\n }\n\n public reset(): void {\n this.recordHistory();\n this.restoreHistoryEntry(this.initialSnapshot);\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.state.activeTool = 'select';\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n // --- Active Tool ---\n\n public setTool(tool: ImageEditorTool): void {\n if (this.state.activeTool === tool) return;\n this.state.activeTool = tool;\n if (tool !== 'select') {\n this.state.selectedAnnotationId = null;\n }\n // If switching to crop tool and no crop is defined, initialize crop to full image\n if (tool === 'crop' && !this.state.crop) {\n this.state.crop = {\n x: 0,\n y: 0,\n width: this.state.imageDimensions.width,\n height: this.state.imageDimensions.height,\n };\n }\n this.notify();\n }\n\n // --- Crop ---\n\n public setCrop(crop: CropBox | null, addToHistory = true): void {\n if (addToHistory) {\n this.recordHistory();\n }\n this.state.crop = crop;\n this.notify();\n }\n\n public applyCropPreset(ratio: AspectRatio): void {\n this.recordHistory();\n const { width, height } = this.state.imageDimensions;\n // `ratio` is the on-screen aspect; the crop box lives in natural image space.\n if (ratio && isQuarterTurnSwapped(this.state.transform)) {\n ratio = 1 / ratio;\n }\n if (!ratio) {\n // Freeform: full image\n this.state.crop = { x: 0, y: 0, width, height };\n } else {\n let targetW = width;\n let targetH = targetW / ratio;\n\n if (targetH > height) {\n targetH = height;\n targetW = targetH * ratio;\n }\n\n const x = Math.max(0, Math.round((width - targetW) / 2));\n const y = Math.max(0, Math.round((height - targetH) / 2));\n this.state.crop = {\n x,\n y,\n width: Math.round(targetW),\n height: Math.round(targetH),\n };\n }\n this.notify();\n }\n\n public resetCrop(): void {\n this.recordHistory();\n this.state.crop = null;\n this.notify();\n }\n\n // --- Transformations (Rotate / Flip) ---\n\n public rotate(degrees: number): void {\n this.recordHistory();\n const current = this.state.transform.rotate;\n this.state.transform = { ...this.state.transform, rotate: (current + degrees) % 360 };\n this.notify();\n }\n\n public setRotation(degrees: number): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, rotate: degrees % 360 };\n this.notify();\n }\n\n public flipHorizontal(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipH: !this.state.transform.flipH };\n this.notify();\n }\n\n public flipVertical(): void {\n this.recordHistory();\n this.state.transform = { ...this.state.transform, flipV: !this.state.transform.flipV };\n this.notify();\n }\n\n // --- Layers (read) ---\n\n /** The layer stack, bottom to top (immutable objects). */\n public getLayers(): ImageLayer[] {\n return this.state.layers;\n }\n\n public getLayer(id: string): ImageLayer | undefined {\n return findLayer(this.state.layers, id)?.layer;\n }\n\n /** The active layer, or `undefined`. */\n public getActiveLayer(): ImageLayer | undefined {\n return this.state.activeLayerId ? this.getLayer(this.state.activeLayerId) : undefined;\n }\n\n /**\n * Makes a layer the active one (or none) and chooses whether painting targets its pixels or\n * its mask. View state: no undo step. Returns false for an unknown id.\n */\n public setActiveLayer(id: string | null, target: LayerTarget = 'pixels'): boolean {\n if (id !== null && !this.getLayer(id)) return false;\n const layer = id ? this.getLayer(id) : undefined;\n const t: LayerTarget = target === 'mask' && layer?.mask ? 'mask' : 'pixels';\n if (this.state.activeLayerId === id && this.state.activeTarget === t) return true;\n this.state.activeLayerId = id;\n this.state.activeTarget = t;\n this.notify();\n return true;\n }\n\n /**\n * The pixels of a pixel layer, in document space (`x`, `y` is the top-left of the returned\n * buffer), or `null` for an unknown or empty layer. Resolves the Background's source pixels.\n */\n public getLayerPixels(id: string): (PixelBuffer & { x: number; y: number }) | null {\n const layer = this.getLayer(id);\n if (!layer || layer.type !== 'pixel') return null;\n const surface = this.surfaceOf(layer);\n const b = contentBounds(surface);\n if (!b) return null;\n return { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height, data: readRegion(surface, b) };\n }\n\n private boundsCache = new WeakMap<object, CropBox | null>();\n\n /**\n * Document bounds of a layer's non-transparent pixels (pixel layers; the Background's source\n * included), or `null` for an empty or non-pixel layer.\n */\n public getLayerBounds(id: string): CropBox | null {\n const layer = this.getLayer(id);\n if (!layer || layer.type !== 'pixel') return null;\n const surface = this.surfaceOf(layer);\n let b = this.boundsCache.get(surface);\n if (b === undefined) {\n b = contentBounds(surface);\n this.boundsCache.set(surface, b);\n }\n return b ? { x: b.x + layer.offset.x, y: b.y + layer.offset.y, width: b.width, height: b.height } : null;\n }\n\n /**\n * Composites the layer stack in document space without a canvas (annotation layers need one\n * and are left out): `rect` (default: the canvas) as RGBA, straight alpha. Crop, rotation and\n * output opacity are not applied. With `layerIds`, only those layers are composited.\n */\n public compositePixels(rect?: CropBox, options: { layerIds?: string[] } = {}): PixelBuffer {\n const { width, height } = this.naturalSize;\n const r = rect ?? { x: 0, y: 0, width, height };\n let layers = this.state.layers;\n if (options.layerIds) {\n const keep = new Set(options.layerIds);\n layers = flattenLayers(layers).filter((l) => keep.has(l.id) && l.type !== 'group');\n }\n const data = this.compositor.composite(layers, r, this.compositorSources());\n return { width: r.width, height: r.height, data };\n }\n\n private compositorSources(): CompositorSources {\n const { width, height } = this.naturalSize;\n return { width, height, surfaceOf: (l) => this.surfaceOf(l), version: this.sourceVersion() };\n }\n\n private sourceIds = new WeakMap<object, number>();\n private nextSourceId = 1;\n\n /** Changes whenever the Background's source pixels can have changed. */\n private sourceVersion(): string {\n const key = (this.samplePixelsOverride ? this.samplePixels : this.imageElement ?? this.samplePixels) as object | null;\n if (!key) return '0';\n let id = this.sourceIds.get(key);\n if (!id) {\n id = this.nextSourceId++;\n this.sourceIds.set(key, id);\n }\n return String(id);\n }\n\n /** The pixels a pixel layer composites (the decoded source for the Background). */\n private surfaceOf(layer: PixelLayer): PixelSurface {\n if (layer.source !== 'document') return layer.surface;\n return this.sourceSurface() ?? layer.surface;\n }\n\n /**\n * The source image as a surface (cached): host-supplied sample pixels first, then the decoded\n * element, then pixels read back for the selection tools.\n */\n private sourceSurface(): PixelSurface | null {\n const { width, height } = this.naturalSize;\n const fromPixels = (px: PixelBuffer) => {\n let s = this.sourceSurfaces.get(px as object);\n if (!s) {\n s = surfaceFromPixels(px, 4);\n this.sourceSurfaces.set(px as object, s);\n }\n return s;\n };\n const px = this.samplePixels;\n const fits = Boolean(px && px.width === width && px.height === height);\n if (this.samplePixelsOverride && fits) return fromPixels(px!);\n const el = this.imageElement as unknown as object | null;\n if (el) {\n const cached = this.sourceSurfaces.get(el);\n if (cached) return cached;\n const decoded = this.decodeSource();\n if (decoded) {\n if (!this.samplePixelsOverride) this.samplePixels = decoded;\n const s = surfaceFromPixels(decoded, 4);\n this.sourceSurfaces.set(el, s);\n this.sourceSurfaces.set(decoded as object, s);\n return s;\n }\n }\n return fits ? fromPixels(px!) : null;\n }\n\n private opaqueCache = new WeakMap<object, boolean>();\n\n /** Whether every source pixel is opaque (some legacy renders depend on it). */\n private sourceIsOpaque(): boolean {\n const s = this.sourceSurface();\n if (!s) return true;\n let v = this.opaqueCache.get(s);\n if (v === undefined) {\n const { width, height } = this.naturalSize;\n v = isOpaqueInside(s, width, height);\n this.opaqueCache.set(s, v);\n }\n return v;\n }\n\n /** Synchronously reads the source element's pixels through a canvas, or `null`. */\n private decodeSource(): PixelBuffer | null {\n const el = this.imageElement;\n if (!el) return null;\n const { width, height } = this.naturalSize;\n try {\n const canvas = this.renderer.createCanvas(width, height);\n const ctx = canvas.getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') return null;\n ctx.drawImage(el, 0, 0, width, height);\n const data = ctx.getImageData(0, 0, width, height);\n if (!data?.data || data.data.length < width * height * 4) return null;\n return { width, height, data: data.data };\n } catch {\n return null;\n }\n }\n\n // --- Layer operations ---\n\n /** Records one undo step and applies a new stack (and any other state change in `extra`). */\n private commitLayers(next: ImageLayer[], extra?: () => void, commitToHistory = true): void {\n if (commitToHistory) this.recordHistory();\n this.state.layers = next;\n extra?.();\n if (this.state.activeLayerId && !findLayer(next, this.state.activeLayerId)) {\n this.state.activeLayerId = null;\n this.state.activeTarget = 'pixels';\n }\n this.notify();\n }\n\n /** The default position of a new layer: above the active layer, else the top of the stack. */\n private newLayerPlacement(placement: LayerPlacement): LayerPlacement {\n if (placement.aboveId || placement.belowId || placement.parentId || placement.index !== undefined) return placement;\n const active = this.state.activeLayerId ? findLayer(this.state.layers, this.state.activeLayerId) : null;\n return active ? { aboveId: active.layer.id } : {};\n }\n\n /**\n * Adds a pixel layer (one undo step) and makes it active: empty, filled with `color` over the\n * canvas, or holding `pixels` placed at `x`, `y`. Goes above the active layer unless placed.\n * Returns its id.\n */\n public addPixelLayer(init: PixelLayerInit = {}): string {\n const { width, height } = this.naturalSize;\n let surface = emptySurface(4);\n if (init.pixels) {\n const px = init.pixels;\n if (!(px.width > 0 && px.height > 0) || px.data.length < px.width * px.height * 4) {\n throw new LayerOperationError('pixels must be RGBA with width × height × 4 bytes.');\n }\n surface = surfaceFromPixels(px, 4);\n } else if (init.color !== undefined) {\n const c = parseColor(init.color);\n surface = solidSurface(4, { x: 0, y: 0, width, height }, packRgba(c[0], c[1], c[2], c[3]));\n }\n const blend = parseBlend(init.blend) as BlendMode | undefined;\n if (init.opacity !== undefined && !(init.opacity >= 0 && init.opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer: PixelLayer = {\n type: 'pixel',\n id: init.id && !ids.has(init.id) ? init.id : newLayerId(),\n name: init.name?.trim() || this.nextLayerName(),\n visible: init.visible ?? true,\n opacity: init.opacity ?? 1,\n blend: blend ?? 'normal',\n surface,\n offset: { x: Math.round(init.x ?? 0), y: Math.round(init.y ?? 0) },\n };\n if (init.fillOpacity !== undefined && init.fillOpacity !== 1) layer.fillOpacity = init.fillOpacity;\n const next = placeLayer(this.state.layers, layer, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = layer.id;\n this.state.activeTarget = 'pixels';\n });\n return layer.id;\n }\n\n /**\n * `addPixelLayer` from an image: a PNG data URL or base64 PNG is decoded without a canvas;\n * other formats and URLs need a browser. Returns the new layer id.\n */\n public async addPixelLayerFromImage(image: string, init: Omit<PixelLayerInit, 'pixels' | 'color'> = {}): Promise<string> {\n const pixels = await this.decodeImage(image);\n return this.addPixelLayer({ ...init, pixels });\n }\n\n /** RGBA pixels of an image given as a data URL, base64 PNG or URL. */\n public async decodeImage(image: string): Promise<PixelBuffer> {\n const trimmed = image.trim();\n let png: Uint8Array | null = null;\n if (/^data:image\\/png/i.test(trimmed)) png = parseDataUrl(trimmed)?.bytes ?? null;\n else if (/^[A-Za-z0-9+/=\\s]+$/.test(trimmed) && trimmed.length > 16) {\n try {\n const bytes = base64ToBytes(trimmed);\n if (isPng(bytes)) png = bytes;\n } catch {\n png = null;\n }\n }\n if (png) {\n try {\n const d = decodePng(png);\n return { width: d.width, height: d.height, data: d.data };\n } catch {\n // Fall back to the browser decoder below (interlaced PNGs).\n }\n }\n const src = png ? `data:image/png;base64,${bytesToBase64(png)}` : trimmed;\n let img: HTMLImageElement;\n try {\n img = await this.renderer.imageCache.get(src);\n } catch {\n throw new LayerOperationError('The image could not be decoded here. Pass a PNG (data URL or base64), or run in a browser for other formats and URLs.');\n }\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const ctx = this.renderer.createCanvas(w, h).getContext('2d');\n if (!ctx || typeof ctx.getImageData !== 'function') throw new LayerOperationError('Decoding this image needs a canvas; pass a PNG instead.');\n ctx.drawImage(img, 0, 0, w, h);\n const data = ctx.getImageData(0, 0, w, h);\n return { width: w, height: h, data: data.data };\n }\n\n private nextLayerName(): string {\n const names = new Set(flattenLayers(this.state.layers).map((l) => l.name));\n let n = 1;\n while (names.has(`Layer ${n}`)) n++;\n return `Layer ${n}`;\n }\n\n /** Duplicates a layer (with its children) directly above it, as one undo step. Returns the copy's id. */\n public duplicateLayer(id: string, name?: string): string {\n const res = duplicateLayerOp(this.state.layers, id, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Deletes a layer (refused when locked), as one undo step. */\n public removeLayer(id: string): void {\n const next = removeLayerOp(this.state.layers, id);\n this.commitLayers(next, () => {\n if (this.state.selectedAnnotationId && !flattenLayers(next).some((l) => l.type === 'annotation' && l.annotation.id === this.state.selectedAnnotationId)) {\n this.state.selectedAnnotationId = null;\n }\n });\n }\n\n /** Moves a layer above / below another, or into a group, as one undo step. Returns its new parent and index. */\n public moveLayer(id: string, placement: LayerPlacement): { parentId: string | null; index: number } {\n const next = moveLayerOp(this.state.layers, id, placement);\n const loc = findLayer(next, id)!;\n if (next === this.state.layers) return { parentId: loc.parent?.id ?? null, index: loc.index };\n this.commitLayers(next);\n return { parentId: loc.parent?.id ?? null, index: loc.index };\n }\n\n /**\n * Changes name, visibility, opacity, fill, blend, clipping, locks or (groups) collapsed state, as\n * one undo step (`commitToHistory = false` for the moves of a slider drag). Returns the layer.\n */\n public setLayerProperties(id: string, props: LayerProperties, commitToHistory = true): ImageLayer {\n const next = setLayerPropertiesOp(this.state.layers, id, props);\n this.commitLayers(next, undefined, commitToHistory);\n return findLayer(next, id)!.layer;\n }\n\n /** Adds an empty pass-through group above the active layer (or at `placement`) and makes it active. Returns its id. */\n public addGroup(init: LayerPlacement & { name?: string; blend?: LayerBlendMode } = {}): string {\n const blend = init.blend === undefined ? 'pass-through' : (parseBlend(init.blend, true) as LayerBlendMode);\n const group: GroupLayer = {\n type: 'group',\n id: newLayerId('group'),\n name: init.name?.trim() || 'Group',\n visible: true,\n opacity: 1,\n blend,\n children: [],\n };\n const next = placeLayer(this.state.layers, group, this.newLayerPlacement(init));\n this.commitLayers(next, () => {\n this.state.activeLayerId = group.id;\n this.state.activeTarget = 'pixels';\n });\n return group.id;\n }\n\n /** Puts sibling layers into a new group (one undo step). Returns the group id. */\n public groupLayers(ids: string[], name?: string): string {\n const res = groupLayersOp(this.state.layers, ids, name);\n this.commitLayers(res.layers, () => {\n this.state.activeLayerId = res.id;\n });\n return res.id;\n }\n\n /** Replaces a group by its children (one undo step). Returns the child ids, bottom to top. */\n public ungroupLayer(id: string): string[] {\n const res = ungroupLayerOp(this.state.layers, id);\n this.commitLayers(res.layers);\n return res.ids;\n }\n\n /**\n * Moves a pixel layer to an absolute offset (`x`, `y`) or by `dx`, `dy`, as one undo step\n * (`commitToHistory = false` for the moves of a drag). Returns the offset and document bounds.\n */\n public offsetLayer(id: string, move: { x?: number; y?: number; dx?: number; dy?: number }, commitToHistory = true): { offset: { x: number; y: number }; bounds: CropBox | null } {\n const layer = requireLayer(this.state.layers, id).layer;\n const current = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const x = move.x ?? current.x + (move.dx ?? 0);\n const y = move.y ?? current.y + (move.dy ?? 0);\n const next = offsetLayerOp(this.state.layers, id, { x, y });\n this.commitLayers(next, undefined, commitToHistory);\n return { offset: { x: Math.round(x), y: Math.round(y) }, bounds: this.getLayerBounds(id) };\n }\n\n /**\n * Adds a mask to a layer (one undo step): `reveal-all` (white), `hide-all` (black) or\n * `selection` (the selection's coverage). Replaces an existing mask. Makes the mask the paint\n * target when the layer is active.\n */\n public addLayerMask(id: string, from: 'reveal-all' | 'hide-all' | 'selection' = 'reveal-all', props: LayerMaskProps = {}): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n let mask: RasterMask;\n if (from === 'selection') {\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select first, or use reveal-all / hide-all.');\n mask = this.maskFromSelection(sel, props);\n if (layer.type === 'pixel' && (layer.offset.x || layer.offset.y)) {\n // Masks live in the layer's space (they move with it).\n mask = { ...mask, surface: translateSurface(mask.surface, -layer.offset.x, -layer.offset.y) };\n }\n } else {\n const v = from === 'reveal-all' ? 255 : 0;\n mask = withMaskProps({ surface: emptySurface(1, v), defaultValue: v }, props);\n }\n const next = setLayerMaskOp(this.state.layers, id, mask);\n this.commitLayers(next, () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'mask';\n });\n return mask;\n }\n\n /** Removes a layer's mask (one undo step). */\n public removeLayerMask(id: string): void {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, null), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /** Changes any layer mask's density, feather or enabled flag (pixels kept). Returns the mask. */\n public setLayerMaskProps(id: string, props: LayerMaskProps, commitToHistory = true): RasterMask {\n const layer = requireLayer(this.state.layers, id).layer;\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n const mask = withMaskProps(layer.mask, props);\n this.commitLayers(setLayerMaskOp(this.state.layers, id, mask), undefined, commitToHistory);\n return mask;\n }\n\n /**\n * Bakes a pixel layer's mask into its pixels (alpha × coverage, including density, feather or\n * the depth map) and removes the mask, as one undo step.\n */\n public async applyLayerMask(id: string): Promise<void> {\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Only a pixel layer's mask can be applied to its pixels; \"${id}\" is a ${layer.type} layer. Remove the mask or keep it.`);\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask.`);\n requireUnlocked(layer, 'pixels', 'apply its mask');\n const { width, height } = this.naturalSize;\n await this.renderer.prepareLayerAssets([layer], width, height);\n const sources = this.renderer.compositorSources(width, height);\n const surface = this.surfaceOf(layer);\n let out = surface;\n const b = surface.bounds;\n if (b) {\n const draft = new SurfaceDraft(surface);\n for (const t of tilesInRect(b)) {\n const data = draft.peek(t.tx, t.ty);\n if (!data) continue;\n const r = { x: t.tx * TILE + layer.offset.x, y: t.ty * TILE + layer.offset.y, width: TILE, height: TILE };\n const cov = this.compositor.maskCoverage(layer, r, sources);\n if (!cov) continue;\n const d = draft.tile(t.tx, t.ty);\n for (let p = 0, i = 3; p < cov.length; p++, i += 4) d[i] = Math.round(d[i] * cov[p]);\n }\n out = draft.commit();\n }\n const next: PixelLayer = { ...layer, surface: out };\n delete next.mask;\n delete next.source;\n this.commitLayers(mapLayer(this.state.layers, id, () => next), () => {\n if (this.state.activeLayerId === id) this.state.activeTarget = 'pixels';\n });\n }\n\n /**\n * Layer via Copy (or via Cut): a new pixel layer with the selected pixels of a pixel layer (the\n * active one by default), directly above it, as one undo step. Returns the new id.\n */\n public layerViaCopy(options: { layerId?: string; cut?: boolean; name?: string } = {}): string {\n const id = options.layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError('No layer given and none is active; pass layerId (see list_layers).');\n const loc = requireLayer(this.state.layers, id);\n const layer = loc.layer;\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer via copy needs a pixel layer; \"${id}\" is a ${layer.type} layer.`);\n const sel = this.state.selection;\n if (!sel?.bounds) throw new LayerOperationError('Nothing is selected; select the pixels to copy first.');\n if (options.cut) requireUnlocked(layer, 'pixels', 'cut from it');\n const surface = this.surfaceOf(layer);\n const sb = sel.bounds;\n const src = { x: sb.x - layer.offset.x, y: sb.y - layer.offset.y, width: sb.width, height: sb.height };\n const px = readRegion(surface, src);\n const kept = options.cut ? new Uint8ClampedArray(px) : null;\n for (let y = 0; y < sb.height; y++) {\n for (let x = 0; x < sb.width; x++) {\n const c = sel.data[(sb.y + y) * sel.width + sb.x + x];\n const i = (y * sb.width + x) * 4 + 3;\n if (kept) kept[i] = Math.round(kept[i] * (1 - c / 255));\n px[i] = Math.round(px[i] * (c / 255));\n }\n }\n const copy: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: options.name?.trim() || `${layer.name || 'Layer'} ${options.cut ? 'cut' : 'copy'}`,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface: surfaceFromPixels({ width: src.width, height: src.height, data: px }, 4, src.x, src.y),\n offset: { ...layer.offset },\n };\n let next = insertLayer(this.state.layers, copy, loc.parent?.id ?? null, loc.index + 1);\n if (kept) {\n const cutLayer: PixelLayer = { ...layer, surface: writeRegion(surface, src, kept) };\n delete cutLayer.source;\n next = mapLayer(next, id, () => cutLayer);\n }\n this.commitLayers(next, () => {\n this.state.activeLayerId = copy.id;\n this.state.activeTarget = 'pixels';\n });\n return copy.id;\n }\n\n /** Adds a `develop` or `blur` filter to a pixel layer (one undo step). Returns the layer's filters. */\n public addLayerFilter(id: string, filter: Partial<LayerFilter> & { kind: LayerFilter['kind'] }): LayerFilter[] {\n const f = normalizeFilter(filter);\n const next = addLayerFilterOp(this.state.layers, id, f);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Changes one filter (one undo step, or none with `commitToHistory = false` during a drag). Returns the filters. */\n public updateLayerFilter(id: string, filterId: string, changes: Record<string, unknown>, commitToHistory = true): LayerFilter[] {\n const next = updateLayerFilterOp(this.state.layers, id, filterId, changes);\n this.commitLayers(next, undefined, commitToHistory);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Removes one filter (one undo step). Returns the remaining filters. */\n public removeLayerFilter(id: string, filterId: string): LayerFilter[] {\n const next = removeLayerFilterOp(this.state.layers, id, filterId);\n this.commitLayers(next);\n return (findLayer(next, id)!.layer as PixelLayer).filters ?? [];\n }\n\n /** Composites `layers` (in isolation) over `rect` into a surface, loading depth maps first. */\n private async compositeToSurface(layers: ImageLayer[], rect: CropBox): Promise<PixelSurface> {\n const { width, height } = this.naturalSize;\n if (flattenLayers(layers).some((l) => l.type === 'annotation' && l.visible) && !this.renderer.canRasterize()) {\n throw new LayerOperationError('Merging annotation layers needs a canvas (a browser); hide or delete them first, or merge in the browser.');\n }\n await this.renderer.prepareLayerAssets(layers, width, height);\n const data = new LayerCompositor().composite(layers, rect, this.renderer.compositorSources(width, height));\n return surfaceFromPixels({ width: rect.width, height: rect.height, data }, 4, rect.x, rect.y);\n }\n\n /** Document bounds a layer can paint into (pixel content, annotations, or the canvas for the rest). */\n private paintBounds(layer: ImageLayer): CropBox {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n if (layer.type === 'pixel') return this.getLayerBounds(layer.id) ?? { x: 0, y: 0, width: 0, height: 0 };\n if (layer.type === 'annotation') {\n const b = getAnnotationBounds(layer.annotation);\n const pad = 4 + ('strokeWidth' in layer.annotation ? (layer.annotation as { strokeWidth: number }).strokeWidth : 0);\n return coveringRect({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });\n }\n if (layer.type === 'group') {\n let r: CropBox | null = null;\n for (const c of layer.children) r = unionRect(r, this.paintBounds(c));\n return r ?? canvas;\n }\n return canvas;\n }\n\n /**\n * Merge Down: composites a layer onto the pixel layer directly below it (which keeps its name,\n * opacity and blend; masks, fill and filters are baked), as one undo step. Returns the merged id.\n */\n public async mergeDown(id: string): Promise<string> {\n const loc = requireLayer(this.state.layers, id);\n const upper = loc.layer;\n const siblings = loc.parent ? loc.parent.children : this.state.layers;\n const lower = siblings[loc.index - 1];\n if (!lower) throw new LayerOperationError(`Layer \"${upper.name || id}\" is the bottom of its group; there is nothing below to merge into.`);\n if (lower.type !== 'pixel') throw new LayerOperationError(`Merge Down needs a pixel layer below; \"${lower.name || lower.id}\" is a ${lower.type} layer. Move the layer or merge into another one.`);\n if (!upper.visible) throw new LayerOperationError(`Layer \"${upper.name || id}\" is hidden; show it or delete it instead of merging.`);\n requireUnlocked(upper, 'all', 'merge it');\n requireUnlocked(lower, 'pixels', 'merge into it');\n const rect = unionRect(this.paintBounds(lower), upper.type === 'adjustment' ? null : this.paintBounds(upper)) ?? { x: 0, y: 0, width: 1, height: 1 };\n const base: PixelLayer = { ...lower, blend: 'normal', opacity: 1 };\n delete base.clipped;\n const surface = await this.compositeToSurface([base, upper], rect);\n const merged: PixelLayer = { ...lower, surface, offset: { x: 0, y: 0 } };\n delete merged.mask;\n delete merged.fillOpacity;\n delete merged.filters;\n delete merged.source;\n let next = mapLayer(this.state.layers, id, () => null);\n next = mapLayer(next, lower.id, () => merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n });\n return merged.id;\n }\n\n /**\n * Merge Visible: the visible top-level layers become one normal pixel layer (at the position of\n * the lowest of them); hidden layers stay. One undo step. Returns the merged id.\n */\n public async mergeVisible(): Promise<string> {\n const visible = this.state.layers.filter((l) => l.visible);\n if (!visible.length) throw new LayerOperationError('No layer is visible.');\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(visible, { x: 0, y: 0, width, height });\n const merged: PixelLayer = {\n type: 'pixel',\n id: newLayerId(),\n name: 'Merged',\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n const at = this.state.layers.indexOf(visible[0]);\n const next = this.state.layers.filter((l) => !l.visible);\n next.splice(this.state.layers.slice(0, at).filter((l) => !l.visible).length, 0, merged);\n this.commitLayers(next, () => {\n this.state.activeLayerId = merged.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return merged.id;\n }\n\n /** Flatten Image: every visible layer composited into one Background layer; hidden layers are discarded. One undo step. */\n public async flattenImage(): Promise<string> {\n const { width, height } = this.naturalSize;\n const surface = await this.compositeToSurface(this.state.layers, { x: 0, y: 0, width, height });\n const bg: PixelLayer = {\n type: 'pixel',\n id: BACKGROUND_LAYER_ID,\n name: BACKGROUND_LAYER_NAME,\n visible: true,\n opacity: 1,\n blend: 'normal',\n surface,\n offset: { x: 0, y: 0 },\n };\n this.commitLayers([bg], () => {\n this.state.activeLayerId = bg.id;\n this.state.activeTarget = 'pixels';\n this.state.selectedAnnotationId = null;\n });\n return bg.id;\n }\n\n private thumbCache = new WeakMap<object, Map<string, PixelBuffer>>();\n\n /**\n * A small RGBA preview of a pixel layer over the canvas (or of its mask with `mask: true`),\n * longest side `maxSide` px, point-sampled; `null` for layers without pixels.\n */\n public getLayerThumbnail(id: string, options: { maxSide?: number; mask?: boolean } = {}): PixelBuffer | null {\n const layer = this.getLayer(id);\n if (!layer) return null;\n const { width, height } = this.naturalSize;\n const side = Math.max(4, Math.min(256, Math.round(options.maxSide ?? 48)));\n const k = Math.min(1, side / Math.max(width, height));\n const tw = Math.max(1, Math.round(width * k));\n const th = Math.max(1, Math.round(height * k));\n const surface = options.mask ? (layer.mask && !layer.mask.source ? layer.mask.surface : null) : layer.type === 'pixel' ? this.surfaceOf(layer) : null;\n if (!surface) return null;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const key = `${tw}x${th}:${ox},${oy}`;\n let byKey = this.thumbCache.get(surface);\n const hit = byKey?.get(key);\n if (hit) return hit;\n const out = new Uint8ClampedArray(tw * th * 4);\n for (let y = 0; y < th; y++) {\n const dy = Math.min(height - 1, Math.floor(((y + 0.5) * height) / th)) - oy;\n for (let x = 0; x < tw; x++) {\n const dx = Math.min(width - 1, Math.floor(((x + 0.5) * width) / tw)) - ox;\n const v = readPixel(surface, dx, dy);\n const i = (y * tw + x) * 4;\n if (surface.channels === 1) {\n out[i] = out[i + 1] = out[i + 2] = v[0];\n out[i + 3] = 255;\n } else out.set(v, i);\n }\n }\n const thumb = { width: tw, height: th, data: out };\n if (!byKey) {\n byKey = new Map();\n this.thumbCache.set(surface, byKey);\n }\n byKey.set(key, thumb);\n return thumb;\n }\n\n // --- Painting ---\n\n /** Brush settings, colours and tool options of the paint tools (view state, no undo). */\n public setPaintOptions(options: Partial<Omit<PaintOptions, 'brush'>> & { brush?: Partial<BrushSettings> }): PaintOptions {\n const cur = this.state.paint;\n const next: PaintOptions = {\n brush: options.brush ? normalizeBrush({ ...cur.brush, ...options.brush }) : cur.brush,\n foreground: options.foreground !== undefined ? toHex(parseColor(options.foreground)) : cur.foreground,\n background: options.background !== undefined ? toHex(parseColor(options.background)) : cur.background,\n };\n this.state.paint = next;\n this.notify();\n return next;\n }\n\n /** D: black foreground, white background. */\n public resetPaintColors(): void {\n this.setPaintOptions({ foreground: '#000000', background: '#ffffff' });\n }\n\n /** X: swaps the foreground and background colours. */\n public swapPaintColors(): void {\n const { foreground, background } = this.state.paint;\n this.setPaintOptions({ foreground: background, background: foreground });\n }\n\n /** Resolves and validates what a stroke, fill or clear paints into. */\n private paintTarget(layerId: string | undefined, channel: LayerTarget | undefined, action: string) {\n const id = layerId ?? this.state.activeLayerId;\n if (!id) throw new LayerOperationError(`No layer given and none is active; pass layerId (see list_layers) to ${action}.`);\n const layer = requireLayer(this.state.layers, id).layer;\n const ch: LayerTarget = channel ?? (id === this.state.activeLayerId ? this.state.activeTarget : 'pixels');\n if (ch === 'mask') {\n if (!layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask to ${action}; add one with add_layer_mask, or target pixels.`);\n if (layer.mask.source) throw new LayerOperationError(`Layer \"${layer.name || id}\" has a depth mask, which is generated, not painted.`);\n requireUnlocked(layer, 'all', action);\n } else {\n if (layer.type !== 'pixel') throw new LayerOperationError(`Layer \"${layer.name || id}\" is a ${layer.type} layer; ${action} on a pixel layer, or on this layer's mask (channel mask).`);\n requireUnlocked(layer, 'pixels', action);\n }\n if (!layer.visible) throw new LayerOperationError(`Layer \"${layer.name || id}\" is hidden; show it before you ${action}.`);\n const offset = layer.type === 'pixel' ? layer.offset : { x: 0, y: 0 };\n const surface = ch === 'mask' ? layer.mask!.surface : this.surfaceOf(layer as PixelLayer);\n const lockTransparency = ch === 'pixels' && Boolean(layer.locks?.transparency);\n return { id, layer, channel: ch, offset, surface, lockTransparency };\n }\n\n /** The layer with its pixels (or mask) replaced by `surface`. */\n private withSurface(layer: ImageLayer, channel: LayerTarget, surface: TiledSurface): ImageLayer {\n if (channel === 'mask') return { ...layer, mask: { ...layer.mask!, surface } } as ImageLayer;\n const next = { ...(layer as PixelLayer), surface };\n delete next.source;\n return next;\n }\n\n /**\n * Starts a brush or eraser stroke on a layer's pixels or mask (default: the active layer and\n * target, the paint options' brush and colours). Feed points with `push`, finish with `end()`\n * (one undo step for the whole stroke) or `cancel()`. While it runs, the stack shows the stroke\n * without recording history.\n */\n public beginStroke(init: StrokeInit = {}): PaintStrokeSession {\n const tool = init.tool ?? 'brush';\n const target = this.paintTarget(init.layerId, init.channel, tool === 'eraser' ? 'erase' : 'paint');\n const brush = normalizeBrush({ ...this.state.paint.brush, ...(init.brush ?? {}) });\n const color = parseColor(init.color ?? this.state.paint.foreground);\n const background = parseColor(init.background ?? this.state.paint.background);\n const toMaskGrey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode: tool === 'eraser' ? 'erase' : 'paint',\n color: target.channel === 'mask' ? toMaskGrey(color) : color,\n background: target.channel === 'mask' ? toMaskGrey(background) : background,\n opacity: brush.opacity,\n blend: target.channel === 'mask' ? 'normal' : brush.blend,\n lockTransparency: target.lockTransparency,\n selection: this.state.selection?.bounds ? this.state.selection : null,\n offset: target.offset,\n };\n return new PaintStrokeSession(this, target, new StrokeCoverage(brush), apply);\n }\n\n /** @internal Shows a stroke's live surface (no history). */\n public _strokeUpdate(layerId: string, channel: LayerTarget, surface: TiledSurface, rects: CropBox[]): void {\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n this.notify(rects);\n }\n\n /** @internal Commits a finished stroke as one undo step (`before` is the pre-stroke state). */\n public _strokeCommit(before: HistoryEntryHandle, layerId: string, channel: LayerTarget, surface: TiledSurface | null): void {\n if (surface) this.state.layers = mapLayer(this.state.layers, layerId, (l) => this.withSurface(l, channel, surface));\n else this.state.layers = (before as unknown as HistoryEntry).layers;\n if (surface && !this.isApplyingHistory) {\n this.undoStack.push(before as unknown as HistoryEntry);\n if (this.undoStack.length > this.maxHistoryDepth) this.undoStack.shift();\n this.redoStack = [];\n this.trimHistoryBytes();\n }\n this.notify();\n }\n\n /** @internal The pre-stroke snapshot. */\n public _captureForStroke(): HistoryEntryHandle {\n return this.captureHistoryEntry() as unknown as HistoryEntryHandle;\n }\n\n /**\n * Paints a whole stroke in one call (agents, replays): `points` in document pixels with optional\n * pressure, through the brush or eraser. One undo step. Returns the layer, channel, the changed\n * document bounds and the number of dabs.\n */\n public paintStroke(init: StrokeInit & { points: StrokePoint[] }): PaintStrokeResult {\n if (!Array.isArray(init.points) || !init.points.length) throw new LayerOperationError('points must hold at least one { x, y } point.');\n for (const p of init.points) {\n if (!Number.isFinite(p?.x) || !Number.isFinite(p?.y)) throw new LayerOperationError('Every point needs finite x and y (document pixels).');\n }\n const session = this.beginStroke(init);\n session.push(init.points);\n return session.end();\n }\n\n /**\n * Edit › Fill: fills the selection (or, without one or with `useSelection: false`, the whole\n * canvas) on a layer's pixels or mask with a colour (default the foreground), at `opacity` with\n * `blend`. One undo step. Returns the changed bounds.\n */\n public fillPixels(options: FillOptions = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'paint');\n }\n\n /**\n * Edit › Clear: removes the selected pixels (alpha to 0; on a transparency-locked layer or a mask\n * it fills with the background colour). Without a selection (or with `useSelection: false`) the\n * whole canvas. One undo step. Returns the changed bounds.\n */\n public clearPixels(options: Omit<FillOptions, 'color' | 'blend'> = {}): PaintStrokeResult {\n return this.fillOrClear(options, 'erase');\n }\n\n private fillOrClear(options: FillOptions, mode: 'paint' | 'erase'): PaintStrokeResult {\n const target = this.paintTarget(options.layerId, options.channel, mode === 'paint' ? 'fill' : 'clear');\n const { width, height } = this.naturalSize;\n const opacity = options.opacity ?? 1;\n if (!(opacity >= 0 && opacity <= 1)) throw new LayerOperationError('opacity must be from 0 to 1.');\n const blend = (parseBlend(options.blend) as BlendMode | undefined) ?? 'normal';\n const sel = options.useSelection !== false && this.state.selection?.bounds ? this.state.selection : null;\n const region = sel?.bounds ?? { x: 0, y: 0, width, height };\n const coverage = new CoverageMap();\n const ones = new Float32Array(region.width * region.height);\n if (sel) {\n for (let y = 0; y < region.height; y++) {\n for (let x = 0; x < region.width; x++) ones[y * region.width + x] = sel.data[(region.y + y) * sel.width + region.x + x] / 255;\n }\n } else ones.fill(1);\n coverage.accumulate(region, ones, 1);\n const color = parseColor(options.color ?? this.state.paint.foreground);\n const background = parseColor(this.state.paint.background);\n const grey = (c: [number, number, number, number]) => {\n const g = Math.round(0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]);\n return [g, g, g, 255] as [number, number, number, number];\n };\n const apply: StrokeApply = {\n mode,\n color: target.channel === 'mask' ? grey(color) : color,\n background: target.channel === 'mask' ? grey(background) : background,\n opacity,\n blend: target.channel === 'mask' ? 'normal' : blend,\n lockTransparency: target.lockTransparency,\n selection: null,\n offset: target.offset,\n };\n const surface = paintCoverageInto(target.surface, coverage, apply, [region]);\n const next = mapLayer(this.state.layers, target.id, (l) => this.withSurface(l, target.channel, surface));\n this.commitLayers(next);\n return { layerId: target.id, channel: target.channel, bounds: region, dabs: 0 };\n }\n\n // --- Adjustments ---\n\n /** The pixel layer the base sliders edit: the active pixel layer, else the Background. */\n private developLayer(): PixelLayer | null {\n return developTarget(this.state.layers, this.state.activeLayerId);\n }\n\n /**\n * Sets base adjustment values as one undo step (unless `commitToHistory` is false). The base\n * sliders edit the `develop` filter of the active pixel layer (or the Background), `blur` its\n * blur filter (or `legacyOutputBlur` in migrated documents), `opacity` the output opacity, and\n * `layers` replaces the adjustment layers.\n */\n public setAdjustments(partial: Partial<ImageAdjustments>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const target = this.developLayer();\n let layers = this.state.layers;\n if (target && (DEVELOP_KEYS.some((k) => k in partial) || 'process' in partial)) {\n layers = mapLayer(layers, target.id, (l) => withDevelop(l as PixelLayer, partial));\n }\n if ('blur' in partial && typeof partial.blur === 'number') {\n const blur = Math.max(0, partial.blur);\n if (this.state.legacyOutputBlur !== undefined) this.state.legacyOutputBlur = blur;\n else if (target) layers = mapLayer(layers, target.id, (l) => withBlur(l as PixelLayer, blur));\n }\n if ('opacity' in partial && typeof partial.opacity === 'number') {\n this.state.outputOpacity = Math.min(1, Math.max(0, partial.opacity));\n }\n if ('layers' in partial) {\n layers = this.replaceAdjustmentNodes(layers, normalizeAdjustmentLayers(partial.layers));\n }\n this.state.layers = layers;\n this.notify();\n }\n\n /** True when the adjustments use an older rendering process than {@link LATEST_PROCESS_VERSION}. */\n public get isProcessOutdated(): boolean {\n this.syncViews();\n return normalizeProcessVersion(this.state.adjustments.process) < LATEST_PROCESS_VERSION;\n }\n\n /**\n * Update to Current Process: re-interprets the current adjustment values with the latest\n * rendering process (one undo step). Returns false when already current.\n */\n public upgradeProcess(): boolean {\n if (!this.isProcessOutdated) return false;\n this.setAdjustments({ process: LATEST_PROCESS_VERSION });\n return true;\n }\n\n /** Resets the base sliders (latest process). Adjustment layers are kept; see `clearAdjustmentLayers`. */\n public resetAdjustments(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) =>\n withBlur(withDevelop(l as PixelLayer, { ...DEFAULT_ADJUSTMENTS, process: LATEST_PROCESS_VERSION }), 0)\n );\n }\n delete this.state.legacyOutputBlur;\n this.state.outputOpacity = 1;\n this.notify();\n }\n\n // --- Adjustment layers ---\n\n /** The adjustment layers in stack order, bottom (first) to top (last). */\n public getAdjustmentLayers(): AdjustmentLayer[] {\n this.syncViews();\n return this.state.adjustments.layers ?? [];\n }\n\n public getAdjustmentLayer(id: string): AdjustmentLayer | undefined {\n return this.getAdjustmentLayers().find((l) => l.id === id);\n }\n\n private adjustmentNodes(layers: readonly ImageLayer[] = this.state.layers): AdjustmentLayerNode[] {\n return flattenLayers(layers).filter((l): l is AdjustmentLayerNode => l.type === 'adjustment');\n }\n\n /**\n * Where a new layer goes when no position is given: directly above the topmost layer that is\n * not an annotation, so it adjusts what adjustment layers always adjusted (annotations stay on top).\n */\n private defaultInsertIndex(layers: readonly ImageLayer[] = this.state.layers): number {\n let i = layers.length;\n while (i > 0 && layers[i - 1].type === 'annotation') i--;\n return i;\n }\n\n /** Inserts `node` by `aboveId` / `parentId` / adjustment index / default position. */\n private placeLayer(\n layers: ImageLayer[],\n node: ImageLayer,\n where: { aboveId?: string; parentId?: string; adjustmentIndex?: number }\n ): ImageLayer[] {\n if (where.aboveId) {\n const loc = findLayer(layers, where.aboveId);\n if (loc) return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n if (where.parentId) {\n const loc = findLayer(layers, where.parentId);\n if (loc && loc.layer.type === 'group') return insertLayer(layers, node, loc.layer.id);\n }\n if (where.adjustmentIndex !== undefined) {\n const nodes = this.adjustmentNodes(layers);\n const at = Math.max(0, Math.min(nodes.length, Math.round(where.adjustmentIndex)));\n if (at < nodes.length) {\n const loc = findLayer(layers, nodes[at].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index);\n }\n if (nodes.length) {\n const loc = findLayer(layers, nodes[nodes.length - 1].id)!;\n return insertLayer(layers, node, loc.parent?.id ?? null, loc.index + 1);\n }\n }\n return insertLayer(layers, node, null, this.defaultInsertIndex(layers));\n }\n\n /** `setAdjustments({ layers })`: updates nodes by id in place, removes missing ones, adds new ones. */\n private replaceAdjustmentNodes(layers: ImageLayer[], next: AdjustmentLayer[]): ImageLayer[] {\n const { width, height } = this.naturalSize;\n const byId = new Map(next.map((l) => [l.id, l]));\n let out = layers;\n for (const node of this.adjustmentNodes(layers)) {\n const update = byId.get(node.id);\n out = mapLayer(out, node.id, () => (update ? mergeAdjustmentNode(node, update, width, height) : null));\n }\n const existing = new Set(this.adjustmentNodes(out).map((n) => n.id));\n for (const l of next) {\n if (existing.has(l.id)) continue;\n out = this.placeLayer(out, adjustmentNodeFromLegacy(l, width, height), { adjustmentIndex: Infinity });\n }\n // Keep the given order among the adjustment layers.\n const order = next.map((l) => l.id);\n for (let to = 0; to < order.length; to++) out = this.moveAdjustmentNode(out, order[to], to);\n return out;\n }\n\n /**\n * Adds an adjustment layer and returns its id, as one undo step. With no position it goes\n * above the other adjustment layers (or, without any, above the topmost non-annotation layer,\n * so annotations stay on top); `index` counts adjustment layers (0 = below the others);\n * `aboveId` / `parentId` place it anywhere in the stack. `kind` alone gives neutral parameters.\n */\n public addAdjustmentLayer<K extends AdjustmentKind>(init: K | AdjustmentLayerInit<K>, index?: number): string {\n const spec: AdjustmentLayerInit<K> = typeof init === 'string' ? { kind: init } : init;\n const ids = new Set(flattenLayers(this.state.layers).map((l) => l.id));\n const layer = createAdjustmentLayer(spec.kind, {\n id: spec.id && !ids.has(spec.id) ? spec.id : undefined,\n name: spec.name,\n params: spec.params,\n enabled: spec.enabled,\n opacity: spec.opacity,\n blend: spec.blend,\n mask: spec.mask ?? undefined,\n });\n while (ids.has(layer.id)) layer.id = newLayerId('adj');\n const { width, height } = this.naturalSize;\n this.recordHistory();\n const node = adjustmentNodeFromLegacy(layer, width, height);\n const positioned = Boolean(spec.aboveId || spec.parentId);\n this.state.layers = this.placeLayer(this.state.layers, node, {\n aboveId: spec.aboveId,\n parentId: spec.parentId,\n adjustmentIndex: index !== undefined ? index : positioned ? undefined : Infinity,\n });\n this.notify();\n return layer.id;\n }\n\n /**\n * Updates a layer (`params` merged into the current ones, then normalised). Pass\n * `commitToHistory = false` for the moves of a drag after recording its start once.\n * Returns false for an unknown id.\n */\n public updateAdjustmentLayer(id: string, update: AdjustmentLayerUpdate, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n const { width, height } = this.naturalSize;\n const node = loc.layer;\n const merged = normalizeAdjustmentLayer({\n id: node.id,\n kind: node.kind,\n enabled: update.enabled ?? node.visible,\n opacity: update.opacity ?? node.opacity,\n blend: update.blend ?? node.blend,\n name: update.name ?? node.name,\n params: { ...node.params, ...(update.params ?? {}) },\n });\n if (!merged) return false;\n if (commitToHistory) this.recordHistory();\n const next = {\n ...node,\n visible: merged.enabled,\n opacity: merged.opacity,\n blend: merged.blend,\n name: merged.name ?? '',\n params: merged.params,\n } as AdjustmentLayerNode;\n if (update.mask !== undefined) {\n const mask = update.mask ? rasterMaskFromLegacy(update.mask, width, height) : null;\n if (mask) next.mask = mask;\n else delete next.mask;\n }\n this.state.layers = mapLayer(this.state.layers, id, () => next);\n this.notify();\n return true;\n }\n\n /** Removes a layer as one undo step. Returns false for an unknown id. */\n public removeAdjustmentLayer(id: string): boolean {\n const loc = findLayer(this.state.layers, id);\n if (!loc || loc.layer.type !== 'adjustment') return false;\n this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, id, () => null);\n if (this.state.activeLayerId === id) this.state.activeLayerId = null;\n this.notify();\n return true;\n }\n\n private moveAdjustmentNode(layers: ImageLayer[], id: string, toIndex: number): ImageLayer[] {\n const nodes = this.adjustmentNodes(layers);\n const from = nodes.findIndex((n) => n.id === id);\n if (from === -1) return layers;\n const to = Math.max(0, Math.min(nodes.length - 1, Math.round(toIndex)));\n if (to === from) return layers;\n const node = nodes[from];\n const anchor = nodes[to];\n const without = mapLayer(layers, id, () => null);\n const loc = findLayer(without, anchor.id)!;\n return insertLayer(without, node, loc.parent?.id ?? null, to > from ? loc.index + 1 : loc.index);\n }\n\n /** Moves a layer to `toIndex` among the adjustment layers (0 = bottom) as one undo step. Returns false if nothing moved. */\n public moveAdjustmentLayer(id: string, toIndex: number): boolean {\n const next = this.moveAdjustmentNode(this.state.layers, id, toIndex);\n if (next === this.state.layers) return false;\n this.recordHistory();\n this.state.layers = next;\n this.notify();\n return true;\n }\n\n /** Shows or hides a layer (flips it without `enabled`) as one undo step. */\n public toggleAdjustmentLayer(id: string, enabled?: boolean): boolean {\n const layer = this.getAdjustmentLayer(id);\n if (!layer) return false;\n const next = enabled ?? !layer.enabled;\n if (next === layer.enabled) return false;\n return this.updateAdjustmentLayer(id, { enabled: next });\n }\n\n /** Removes every adjustment layer as one undo step. */\n public clearAdjustmentLayers(): void {\n const nodes = this.adjustmentNodes();\n if (!nodes.length) return;\n this.recordHistory();\n let layers = this.state.layers;\n for (const n of nodes) layers = mapLayer(layers, n.id, () => null);\n this.state.layers = layers;\n if (nodes.some((n) => n.id === this.state.activeLayerId)) this.state.activeLayerId = null;\n this.notify();\n }\n\n // --- Selection ---\n\n /** The active selection, or `null`. */\n public getSelection(): SelectionMask | null {\n return this.state.selection ?? null;\n }\n\n public get hasSelection(): boolean {\n return Boolean(this.state.selection?.bounds);\n }\n\n private get naturalSize(): { width: number; height: number } {\n return {\n width: Math.max(1, Math.round(this.state.imageDimensions.width)),\n height: Math.max(1, Math.round(this.state.imageDimensions.height)),\n };\n }\n\n private dropMismatchedSelection(): void {\n const sel = this.state.selection;\n if (!sel) return;\n const { width, height } = this.naturalSize;\n if (sel.width !== width || sel.height !== height) this.state.selection = null;\n }\n\n /**\n * Replaces the selection, or combines `mask` with it by `mode`, as one undo step. A mask of a\n * different size (e.g. an AI segmentation at another resolution) is resampled to the image.\n * Returns false when nothing changed.\n */\n public setSelection(mask: SelectionMask | null, mode: SelectionMode = 'replace'): boolean {\n const { width, height } = this.naturalSize;\n let next: SelectionMask | null = null;\n if (mask) {\n const sized = mask.width === width && mask.height === height ? mask : resizeSelection(mask, width, height);\n next = combineSelections(this.state.selection, sized, mode);\n } else if (mode !== 'replace') {\n return false;\n }\n const prev = normalizeSelection(this.state.selection ?? null);\n if (prev === next || (prev && next && sameSelection(prev, next))) return false;\n this.recordHistory();\n this.state.selection = next;\n this.notify();\n return true;\n }\n\n /** Select › All. */\n public selectAll(): boolean {\n const { width, height } = this.naturalSize;\n return this.setSelection(fullSelection(width, height));\n }\n\n /** Select › Deselect. */\n public deselect(): boolean {\n return this.setSelection(null);\n }\n\n /** Select › Inverse (with nothing selected, selects everything). */\n public invertSelection(): boolean {\n const { width, height } = this.naturalSize;\n const current = this.state.selection ?? createSelectionMask(width, height);\n return this.setSelection(invertSelectionMask(current));\n }\n\n private shapeCommand(mask: SelectionMask, options: SelectionCommandOptions): boolean {\n const shaped = options.feather && options.feather > 0 ? modifySelectionMask(mask, 'feather', options.feather) : mask;\n return this.setSelection(shaped, options.mode ?? 'replace');\n }\n\n /** Rectangular marquee in natural image pixels. */\n public selectRect(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(rectSelection(width, height, rect, options), options);\n }\n\n /** Elliptical marquee inscribed in `rect`. */\n public selectEllipse(rect: CropBox, options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(ellipseSelection(width, height, rect, options), options);\n }\n\n /** Lasso / polygonal lasso. */\n public selectPolygon(points: ImagePoint[], options: SelectionCommandOptions & ShapeSelectionOptions = {}): boolean {\n const { width, height } = this.naturalSize;\n return this.shapeCommand(polygonSelection(width, height, points, options), options);\n }\n\n /**\n * Supplies the pixels the selection tools sample (natural size RGBA), e.g. decoded in a worker\n * or in a test; `null` goes back to reading the source image. Without a canvas (Node, workers)\n * these are also the Background layer's pixels.\n */\n public setSamplePixels(pixels: PixelBuffer | null): void {\n this.samplePixels = pixels;\n this.samplePixelsOverride = pixels !== null;\n }\n\n /**\n * The pixels the selection tools sample: the source image at natural size, or with\n * `sampleMerged` the composite of the layers (no crop, rotation or annotations). `null`\n * without pixel access (no DOM, tainted canvas).\n */\n public async getSamplePixels(options: SampleOptions = {}): Promise<PixelBuffer | null> {\n const { width, height } = this.naturalSize;\n let base = options.pixels ?? this.samplePixels;\n if (!base) {\n try {\n const canvas = await this.renderSource();\n const ctx = canvas.getContext('2d');\n const data = ctx?.getImageData?.(0, 0, width, height);\n if (data && data.data && data.data.length >= width * height * 4) {\n base = { width, height, data: data.data };\n this.samplePixels = base;\n this.samplePixelsOverride = false;\n }\n } catch {\n base = null;\n }\n }\n if (!base) return null;\n if (base.width !== width || base.height !== height) {\n if (!this.samplePixelsOverride && !options.pixels) this.samplePixels = null;\n return null;\n }\n const legacy = legacyView(this.stack(), width, height, null).exact;\n if (options.sample === 'active-layer') {\n const layer = this.getActiveLayer();\n if (!layer || layer.type !== 'pixel') {\n throw new LayerOperationError('Sampling the active layer needs an active pixel layer; select one, or sample the composite.');\n }\n const surface = layer.source === 'document' && options.pixels ? surfaceFromPixels(options.pixels, 4) : this.surfaceOf(layer);\n return { width, height, data: readRegion(surface, { x: -layer.offset.x, y: -layer.offset.y, width, height }) };\n }\n if (legacy && !options.sampleMerged) return base;\n this.syncViews();\n if (legacy && !options.pixels) {\n // The old shape: the source through the base adjustments and adjustment layers.\n const copy = { width: base.width, height: base.height, data: new Uint8ClampedArray(base.data as ArrayLike<number>) };\n applyAdjustments(copy as unknown as ImageData, legacyView(this.stack(), width, height, null).adjustments);\n return copy;\n }\n const pixels = options.pixels;\n const sources: CompositorSources = {\n width,\n height,\n surfaceOf: (l) => (l.source === 'document' && pixels ? surfaceFromPixels(pixels, 4) : this.surfaceOf(l)),\n version: pixels ? 'pixels' : this.sourceVersion(),\n };\n // Without `sampleMerged`, the pixels as painted: adjustment layers and filters left out.\n const stack = withoutAnnotations(this.state.layers);\n const layers = options.sampleMerged ? stack : pixelsOnly(stack);\n const data = new LayerCompositor().composite(layers, { x: 0, y: 0, width, height }, sources);\n return { width, height, data };\n }\n\n private async requirePixels(options: SampleOptions): Promise<PixelBuffer> {\n const pixels = await this.getSamplePixels(options);\n if (!pixels) throw new Error('The image pixels are not available (no canvas access); pass `pixels`.');\n return pixels;\n }\n\n /**\n * Magic wand at `point` (natural image pixels). Resolves to the resulting selection (null when\n * nothing is selected).\n */\n public async magicWand(\n point: ImagePoint,\n options: MagicWandOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(magicWand(pixels, point, options), options);\n return this.getSelection();\n }\n\n /** Select › Color Range with colour samples (`#rrggbb` or `[r, g, b]`, optionally where they were picked). */\n public async selectColorRange(\n samples: ColorRangeSample[],\n options: ColorRangeOptions & SelectionCommandOptions & SampleOptions = {}\n ): Promise<SelectionMask | null> {\n const pixels = await this.requirePixels(options);\n this.shapeCommand(colorRange(pixels, samples, options), options);\n return this.getSelection();\n }\n\n /** Select › Modify: grow, shrink, feather, border or smooth by `radius` pixels (one undo step). */\n public modifySelection(op: SelectionModifyOp, radius: number): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n return this.setSelection(modifySelectionMask(sel, op, radius));\n }\n\n /**\n * Refine Edge: snaps the selection (e.g. a coarse AI mask loaded with `loadSelectionFromImage`)\n * to the image's edges with a guided filter, then smooth / feather / contrast / shift edge.\n */\n public async refineSelection(params: RefineEdgeParams, options: SampleOptions = {}): Promise<boolean> {\n const sel = this.state.selection;\n if (!sel?.bounds) return false;\n const pixels = (params.radius ?? 0) > 0 ? await this.requirePixels(options) : null;\n return this.setSelection(refineSelectionMask(pixels, sel, params));\n }\n\n /**\n * Loads a selection from a mask image (an AI segmentation, a saved mask): a data URL / URL or\n * RGBA pixels, by luminance (white = selected) or a channel. Any size; it is resampled.\n */\n public async loadSelectionFromImage(\n source: string | PixelBuffer,\n options: { channel?: SelectionChannel; invert?: boolean; threshold?: number; mode?: SelectionMode } = {}\n ): Promise<SelectionMask | null> {\n let pixels: PixelBuffer;\n if (typeof source === 'string') {\n const img = await this.renderer.imageCache.get(source);\n const w = img.naturalWidth || img.width;\n const h = img.naturalHeight || img.height;\n const canvas = this.renderer.createCanvas(w, h);\n const ctx = canvas.getContext('2d');\n if (ctx) ctx.drawImage(img, 0, 0, w, h);\n const data = ctx?.getImageData?.(0, 0, w, h);\n if (!data) throw new Error('The mask image pixels are not available (no canvas access); pass pixels.');\n pixels = { width: w, height: h, data: data.data };\n } else {\n pixels = source;\n }\n this.setSelection(selectionFromPixels(pixels, options), options.mode ?? 'replace');\n return this.getSelection();\n }\n\n /** Quick Mask view (tints the unselected area). View state: no undo step. */\n public setQuickMask(on: boolean): void {\n if (Boolean(this.state.quickMask) === on) return;\n this.state.quickMask = on;\n this.notify();\n }\n\n // --- Layer masks ---\n\n private maskFromSelection(sel: SelectionMask, props: LayerMaskProps = {}): RasterMask {\n const { width, height } = this.naturalSize;\n const sized = sel.width === width && sel.height === height ? sel : resizeSelection(sel, width, height);\n const mask: RasterMask = { surface: surfaceFromPixels({ width, height, data: sized.data }, 1, 0, 0, 0), defaultValue: 0 };\n return withMaskProps(mask, props);\n }\n\n /**\n * Adds the selection as the layer's mask (Reveal Selection), replacing any mask, as one undo\n * step. Without a selection, returns false.\n */\n public applySelectionAsLayerMask(layerId: string, props: LayerMaskProps = {}): boolean {\n const sel = this.state.selection;\n if (!sel?.bounds || !this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, this.maskFromSelection(sel, props));\n }\n\n private setLayerMask(layerId: string, mask: RasterMask | null, commitToHistory = true): boolean {\n const loc = findLayer(this.state.layers, layerId);\n if (!loc) return false;\n if (commitToHistory) this.recordHistory();\n this.state.layers = mapLayer(this.state.layers, layerId, (l) => {\n const next = { ...l } as ImageLayer;\n if (mask) next.mask = mask;\n else delete next.mask;\n return next;\n });\n if (!mask && this.state.activeLayerId === layerId) this.state.activeTarget = 'pixels';\n this.notify();\n return true;\n }\n\n /** Sets (from a selection-shaped mask) or removes (`null`) a layer's mask as one undo step. */\n public setAdjustmentLayerMask(layerId: string, mask: SelectionMask | null, props: LayerMaskProps = {}): boolean {\n if (!this.getAdjustmentLayer(layerId)) return false;\n return this.setLayerMask(layerId, mask ? this.maskFromSelection(mask, props) : null);\n }\n\n /** Changes a layer mask's density (0..1), feather (px) or enabled flag; the mask pixels are kept. */\n public setAdjustmentLayerMaskProps(layerId: string, props: LayerMaskProps, commitToHistory = true): boolean {\n const layer = this.getLayer(layerId);\n if (!layer || layer.type !== 'adjustment' || !layer.mask) return false;\n return this.setLayerMask(layerId, withMaskProps(layer.mask, props), commitToHistory);\n }\n\n /** Load Selection from a layer mask (its pixels; density and feather are not applied). */\n public selectionFromLayerMask(layerId: string, mode: SelectionMode = 'replace'): boolean {\n const layer = this.getLayer(layerId);\n if (!layer?.mask || layer.mask.source) return false;\n const { width, height } = this.naturalSize;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const data = readRegion(layer.mask.surface, { x: -ox, y: -oy, width, height });\n const sel = createSelectionMask(width, height, new Uint8Array(data.buffer, data.byteOffset, data.length));\n return this.setSelection(sel, mode);\n }\n\n // --- Histogram ---\n\n /**\n * Histogram of the last rendered preview (after adjustments and layers, before annotations),\n * or `null` before the first render or without canvas pixel access.\n */\n public getHistogram(): ImageHistogram | null {\n return this.renderer.previewHistogram;\n }\n\n /** Calls `listener` now and whenever a render produces a new preview histogram. */\n public subscribeHistogram(listener: (histogram: ImageHistogram | null) => void): () => void {\n this.histogramListeners.add(listener);\n listener(this.renderer.previewHistogram);\n return () => {\n this.histogramListeners.delete(listener);\n };\n }\n\n // --- Depth Masking (Gleamforge) ---\n\n /** Sets the depth mask (a parametric mask on the active pixel layer, or the Background) as one undo step. */\n public setDepthMask(partial: Partial<DepthMaskOptions>): void {\n this.recordHistory();\n this.syncViews();\n const target = this.developLayer();\n if (target) {\n const merged: DepthMaskOptions = { ...this.state.depthMask, ...partial };\n merged.depthRange = [...merged.depthRange] as [number, number];\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n if (isDefaultDepthMask(merged)) {\n if (next.mask?.source) delete next.mask;\n } else next.mask = depthRasterMask(merged);\n return next;\n });\n }\n this.notify();\n }\n\n public resetDepthMask(): void {\n this.recordHistory();\n const target = this.developLayer();\n if (target?.mask?.source) {\n this.state.layers = mapLayer(this.state.layers, target.id, (l) => {\n const next = { ...l } as PixelLayer;\n delete next.mask;\n return next;\n });\n }\n this.notify();\n }\n\n // --- Annotations & Layers ---\n\n private annotationLayer(id: string): AnnotationLayer | undefined {\n return flattenLayers(this.state.layers).find(\n (l): l is AnnotationLayer => l.type === 'annotation' && l.annotation.id === id\n );\n }\n\n /** Adds an annotation as its own layer (top of the stack unless `aboveId` is given). */\n public addAnnotation(annotation: CreateAnnotationPayload, placement: AnnotationPlacement = {}): string {\n this.recordHistory();\n const id = `ann_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n const fullAnnotation = { ...annotation, id } as Annotation;\n const layer = annotationLayerFor(fullAnnotation);\n const loc = placement.aboveId ? findLayer(this.state.layers, placement.aboveId) : null;\n this.state.layers = loc\n ? insertLayer(this.state.layers, layer, loc.parent?.id ?? null, loc.index + 1)\n : insertLayer(this.state.layers, layer, null);\n this.state.selectedAnnotationId = id;\n this.notify();\n return id;\n }\n\n public updateAnnotation(id: string, partial: Partial<Annotation>, commitToHistory = true): void {\n if (commitToHistory) {\n this.recordHistory();\n }\n const layer = this.annotationLayer(id);\n if (layer) {\n const annotation = { ...layer.annotation, ...partial, id } as Annotation;\n const name = layer.name === annotationLayerName(layer.annotation) ? annotationLayerName(annotation) : layer.name;\n this.state.layers = mapLayer(this.state.layers, layer.id, () => ({ ...layer, annotation, name }));\n }\n this.notify();\n }\n\n public removeAnnotation(id: string): void {\n this.recordHistory();\n const layer = this.annotationLayer(id);\n if (layer) this.state.layers = mapLayer(this.state.layers, layer.id, () => null);\n if (this.state.selectedAnnotationId === id) {\n this.state.selectedAnnotationId = null;\n }\n this.notify();\n }\n\n public clearAnnotations(): void {\n this.recordHistory();\n let layers = this.state.layers;\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.layers = layers;\n this.state.selectedAnnotationId = null;\n this.notify();\n }\n\n public selectAnnotation(id: string | null): void {\n this.state.selectedAnnotationId = id;\n this.notify();\n }\n\n // --- Zoom & Pan ---\n\n public setZoom(zoom: number): void {\n this.state.zoom = Math.max(0.1, Math.min(10, zoom));\n this.notify();\n }\n\n public zoomIn(factor = 1.2): void {\n this.setZoom(this.state.zoom * factor);\n }\n\n public zoomOut(factor = 1.2): void {\n this.setZoom(this.state.zoom / factor);\n }\n\n public resetZoom(): void {\n this.state.zoom = 1;\n this.state.pan = { x: 0, y: 0 };\n this.notify();\n }\n\n public setPan(pan: ImagePoint): void {\n this.state.pan = pan;\n this.notify();\n }\n\n public setComparing(comparing: boolean): void {\n if (this.state.isOriginalCompared === comparing) return;\n this.state.isOriginalCompared = comparing;\n this.notify();\n }\n\n public setFit(fit: ImageEditorFit): void {\n if (this.state.fit === fit) return;\n this.state.fit = fit;\n this.notify();\n }\n\n // --- Rendering & Export ---\n\n private blankSource: HTMLCanvasElement | null = null;\n\n /**\n * What the renderer draws from: the source image, or, for a document without one (an opened\n * PSD, a v3 document loaded on its own), a transparent canvas of the document size, since every\n * pixel then comes from the layers. `null` when there is nothing to draw.\n */\n private drawSource(): CanvasImageSource | string | null {\n const source = this.imageElement || this.state.sourceUrl;\n if (source) return source;\n if (!this.isSourceless()) return null;\n const { width, height } = this.naturalSize;\n if (!this.blankSource || this.blankSource.width !== width || this.blankSource.height !== height) {\n this.blankSource = this.renderer.createCanvas(width, height);\n }\n return this.blankSource;\n }\n\n public async render(targetCanvas: HTMLCanvasElement, options?: RenderOptions): Promise<void> {\n const source = this.drawSource();\n if (!source) return;\n this.syncViews();\n const dirty = this.pendingDirty;\n this.pendingDirty = [];\n try {\n await this.renderer.renderToCanvas(targetCanvas, source, this.state, {\n ...options,\n dirty: dirty === 'all' ? undefined : dirty,\n checkpointLayerId: this.state.activeLayerId,\n });\n } catch {\n // In non-DOM or test mock environments without image loading, safely ignore\n }\n }\n\n public async renderExport(options?: ExportOptions): Promise<HTMLCanvasElement> {\n const source = this.drawSource();\n if (!source) {\n return this.renderer.createCanvas(1, 1);\n }\n this.syncViews();\n if (options?.layerIds) {\n const keep = new Set(options.layerIds);\n const layers = flattenLayers(this.state.layers).filter((l) => keep.has(l.id));\n return this.renderer.renderExport(source, { ...this.state, layers }, options);\n }\n return this.renderer.renderExport(source, this.state, options);\n }\n\n /**\n * Renders the edited image (adjustments, layers and, by default, annotations) in the\n * source image's own orientation and frame, ignoring crop, rotation and flips, so it lines\n * up with natural-pixel coordinates (selections, `select_region`). `scale` ≤ 1 shrinks it.\n * Returns `null` when no image is loaded.\n */\n public async renderSourceView(options: { scale?: number; includeAnnotations?: boolean } = {}): Promise<HTMLCanvasElement | null> {\n const source = this.drawSource();\n if (!source) return null;\n this.syncViews();\n const { width, height } = this.state.imageDimensions;\n const k = Math.min(1, Math.max(0, options.scale ?? 1));\n return this.renderer.renderExport(\n source,\n { ...this.state, crop: null, transform: { rotate: 0, flipH: false, flipV: false } },\n {\n width: Math.max(1, Math.round(width * k)),\n height: Math.max(1, Math.round(height * k)),\n includeAnnotations: options.includeAnnotations,\n }\n );\n }\n\n public async toDataURL(options: ExportOptions = {}): Promise<string> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n return canvas.toDataURL(format, quality);\n }\n\n public async toBlob(options: ExportOptions = {}): Promise<Blob> {\n const canvas = await this.renderExport(options);\n const format = options.format || 'image/png';\n const quality = options.quality ?? 0.92;\n\n return new Promise((resolve, reject) => {\n if (canvas.toBlob) {\n canvas.toBlob((blob) => {\n if (blob) resolve(blob);\n else reject(new Error('Failed to create Blob from canvas'));\n }, format, quality);\n } else {\n // Fallback for environments lacking canvas.toBlob\n try {\n const dataUrl = canvas.toDataURL(format, quality);\n const parts = dataUrl.split(',');\n const byteStr = atob(parts[1]);\n const ab = new ArrayBuffer(byteStr.length);\n const ia = new Uint8Array(ab);\n for (let i = 0; i < byteStr.length; i++) {\n ia[i] = byteStr.charCodeAt(i);\n }\n resolve(new Blob([ab], { type: format }));\n } catch (err) {\n reject(err);\n }\n }\n });\n }\n\n // --- AI Masking & Transformation Helpers ---\n\n public async renderMask(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<HTMLCanvasElement> {\n this.syncViews();\n return this.renderer.renderMask(this.state, options);\n }\n\n public async toMaskDataURL(options?: { useAnnotations?: boolean; useCrop?: boolean; useSelection?: boolean }): Promise<string> {\n const canvas = await this.renderMask(options);\n return canvas.toDataURL('image/png');\n }\n\n /**\n * Renders the current source image at natural size with no crop, rotation, flip,\n * adjustments or annotations. This is the image that `toMaskDataURL()` is aligned with\n * and the space that `applyInpaintedImage` / `applyBackgroundRemovedImage` expect back.\n */\n public async renderSource(): Promise<HTMLCanvasElement> {\n const { width, height } = this.state.imageDimensions;\n const canvas = this.renderer.createCanvas(width, height);\n const source = this.imageElement || this.state.sourceUrl;\n const ctx = canvas.getContext('2d');\n if (!ctx || !source) return canvas;\n const img = typeof source === 'string' ? await this.renderer.imageCache.get(source) : source;\n ctx.drawImage(img, 0, 0, width, height);\n return canvas;\n }\n\n /**\n * The image an AI hook should edit: the source (`toSourceDataURL`) when results replace the\n * Background, or the composite of the layers without annotations, in the source frame, when\n * they go to a new layer or mask (see `applyInpaintedImage`).\n */\n public async toAIInputDataURL(options: AIResultOptions = {}): Promise<string> {\n const target = options.target ?? this.defaultAITarget();\n if (target === 'replace') return this.toSourceDataURL();\n const canvas = await this.renderSourceView({ includeAnnotations: false });\n return canvas ? canvas.toDataURL('image/png') : this.toSourceDataURL();\n }\n\n public async toSourceDataURL(options: Pick<ExportOptions, 'format' | 'quality'> = {}): Promise<string> {\n const canvas = await this.renderSource();\n return canvas.toDataURL(options.format || 'image/png', options.quality ?? 0.92);\n }\n\n /**\n * Whether AI results go to layers: the document has more than one pixel layer or group (adjustment\n * and annotation layers are edits of the image, not images of their own).\n */\n /** `new-layer` in multi-layer documents and documents without a source image, else `replace`. */\n private defaultAITarget(): 'new-layer' | 'replace' {\n return this.isMultiLayer || this.isSourceless() ? 'new-layer' : 'replace';\n }\n\n /** No source image, and no layer reading one (an opened PSD): the layers are the whole document. */\n private isSourceless(): boolean {\n if (this.imageElement || this.state.sourceUrl || !this.state.layers.length) return false;\n return !flattenLayers(this.state.layers).some((l) => l.type === 'pixel' && l.source === 'document');\n }\n\n public get isMultiLayer(): boolean {\n return flattenLayers(this.state.layers).filter((l) => l.type === 'pixel' || l.type === 'group').length > 1;\n }\n\n /**\n * Applies an inpainted result (natural image space, see `toSourceDataURL`) as ONE undoable step.\n * In a document with several layers (see `isMultiLayer`), or with `target: 'new-layer'`, the\n * result goes to a new pixel layer above the active layer, limited to the selection. Otherwise\n * (`target: 'replace'`, the single-layer default) it replaces the Background's pixels and clears\n * the mask annotations, as before. Resolves to the new layer's id in new-layer mode.\n */\n public async applyInpaintedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, true);\n return;\n }\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const data = new Uint8ClampedArray(px.data as ArrayLike<number>);\n const sel = this.state.selection?.bounds ? this.state.selection : null;\n if (sel) for (let p = 0, i = 3; p < width * height; p++, i += 4) data[i] = Math.round((data[i] * sel.data[p]) / 255);\n return this.addPixelLayer({ name: 'Inpaint', pixels: { width, height, data } });\n }\n\n /**\n * Applies a background-removed result as ONE undoable step. In a document with several layers\n * (or with `target: 'new-layer'`) the result's alpha becomes the mask of the active pixel layer\n * (or the Background), replacing its mask. Otherwise (`target: 'replace'`, the single-layer\n * default) it replaces the Background's pixels; crop and annotations are kept (rescaled if the\n * new image has different dimensions). Resolves to the masked layer's id in mask mode.\n */\n public async applyBackgroundRemovedImage(newDataUrl: string, options: AIResultOptions = {}): Promise<string | void> {\n if ((options.target ?? this.defaultAITarget()) === 'replace') {\n await this.replaceSource(newDataUrl, false);\n return;\n }\n const target = this.developLayer();\n if (!target) throw new LayerOperationError('There is no pixel layer to mask.');\n requireUnlocked(target, 'all', 'mask it');\n const { width, height } = this.naturalSize;\n let px = await this.decodeImage(newDataUrl);\n if (px.width !== width || px.height !== height) px = resizePixels(px, width, height);\n const alpha = new Uint8ClampedArray(width * height);\n for (let p = 0; p < alpha.length; p++) alpha[p] = (px.data as ArrayLike<number>)[p * 4 + 3];\n let surface = surfaceFromPixels({ width, height, data: alpha }, 1, 0, 0, 0);\n if (target.offset.x || target.offset.y) surface = translateSurface(surface, -target.offset.x, -target.offset.y);\n this.commitLayers(setLayerMaskOp(this.state.layers, target.id, { surface, defaultValue: 0 }));\n return target.id;\n }\n\n /**\n * Rasterize Layer: an annotation layer becomes a pixel layer with the same id, name and\n * properties (needs a canvas), and a pixel layer's filters are baked into its pixels. One undo\n * step. Returns the layer id.\n */\n public async rasterizeLayer(id: string): Promise<string> {\n const layer = requireLayer(this.state.layers, id).layer;\n requireUnlocked(layer, 'all', 'rasterize it');\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n let next: PixelLayer;\n if (layer.type === 'annotation') {\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const surface = await this.compositeToSurface([content], this.paintBounds(layer));\n next = { type: 'pixel', id: layer.id, name: layer.name, visible: layer.visible, opacity: layer.opacity, blend, surface, offset: { x: 0, y: 0 } };\n if (layer.clipped) next.clipped = true;\n if (layer.mask) next.mask = layer.mask;\n if (layer.locks) next.locks = layer.locks;\n if (this.state.selectedAnnotationId === layer.annotation.id) this.state.selectedAnnotationId = null;\n } else if (layer.type === 'pixel') {\n if (!layer.filters?.length) throw new LayerOperationError(`Layer \"${layer.name || id}\" is already pixels with no filters to bake.`);\n requireUnlocked(layer, 'pixels', 'rasterize it');\n const spatial = layer.filters.some((f) => f.enabled && f.kind === 'blur');\n const content: PixelLayer = { ...layer, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const rect = spatial ? canvas : this.getLayerBounds(id) ?? canvas;\n const surface = await this.compositeToSurface([content], rect);\n next = { ...layer, surface, offset: { x: 0, y: 0 } };\n delete next.filters;\n delete next.source;\n if (layer.mask && (layer.offset.x || layer.offset.y)) {\n // The mask lives in layer space: keep it where it was in the document.\n next.mask = { ...layer.mask, surface: translateSurface(layer.mask.surface, layer.offset.x, layer.offset.y) };\n }\n } else {\n throw new LayerOperationError(`Only annotation layers and pixel layers with filters rasterize; \"${layer.name || id}\" is a ${layer.type} layer. Merge it down or flatten instead.`);\n }\n this.commitLayers(mapLayer(this.state.layers, id, () => next));\n return id;\n }\n\n // --- PSD ---\n\n /** Pixels per inch of the last opened PSD (written back by `toPsd`). */\n private psdResolution: number | undefined;\n\n /**\n * Opens PSD / PSB bytes as the document (one undo step): the layers replace the stack and the\n * document has no source image afterwards (every pixel comes from the layers). What the model\n * cannot hold is approximated or left out; `warnings` says what, naming the layers.\n * Throws `PsdError` for files that cannot be read at all.\n */\n public loadPsd(bytes: Uint8Array): { layerIds: string[]; warnings: string[]; width: number; height: number; summary: PsdImportResult['summary'] } {\n const result = importPsd(bytes, { newId: () => newLayerId() });\n this.recordHistory();\n const s = this.state;\n this.imageElement = null;\n s.sourceUrl = null;\n s.imageDimensions = { width: result.width, height: result.height };\n if (!this.samplePixelsOverride) this.samplePixels = null;\n s.layers = result.layers;\n s.crop = null;\n s.transform = { ...DEFAULT_TRANSFORM_VALUES };\n s.outputOpacity = 1;\n delete s.legacyOutputBlur;\n s.selection = null;\n s.selectedAnnotationId = null;\n const pixels = flattenLayers(result.layers).filter((l) => l.type === 'pixel');\n s.activeLayerId = pixels.length ? pixels[pixels.length - 1].id : null;\n s.activeTarget = 'pixels';\n this.psdResolution = result.resolution;\n this.lastLoadProblems = result.warnings;\n this.notify();\n return {\n layerIds: flattenLayers(result.layers).map((l) => l.id),\n warnings: result.warnings,\n width: result.width,\n height: result.height,\n summary: result.summary,\n };\n }\n\n /**\n * The document as an 8-bit RGB PSD (PSB above 30 000 px) with real layers, groups, adjustment\n * layers and masks, in the source frame. Layer filters and parametric (depth) masks are baked\n * into pixels and masks, annotation layers are rasterized (in a browser; elsewhere they are left\n * out), and the merged image is our composite. `warnings` lists every such change.\n */\n public async toPsd(): Promise<{ bytes: Uint8Array; warnings: string[]; psb: boolean; width: number; height: number }> {\n const { width, height } = this.naturalSize;\n const canvas = { x: 0, y: 0, width, height };\n const warnings: string[] = [];\n const note = new Map<string, string[]>();\n const add = (msg: string, name: string) => note.set(msg, [...(note.get(msg) ?? []), name]);\n const canRasterize = this.renderer.canRasterize();\n await this.renderer.prepareLayerAssets(this.state.layers, width, height);\n const sources = this.renderer.compositorSources(width, height);\n const compositor = new LayerCompositor();\n\n const bakeMask = (layer: ImageLayer): RasterMask | undefined => {\n const mask = layer.mask;\n if (!mask?.source) return mask;\n add('Parametric (depth) masks were baked into pixel masks', layer.name);\n const plain = { ...layer, mask: { ...mask, density: 1, feather: 0, enabled: true } } as ImageLayer;\n const ox = layer.type === 'pixel' ? layer.offset.x : 0;\n const oy = layer.type === 'pixel' ? layer.offset.y : 0;\n const coverage = compositor.maskCoverage(plain, canvas, sources);\n const bytes = new Uint8ClampedArray(width * height);\n if (coverage) for (let i = 0; i < bytes.length; i++) bytes[i] = Math.round(coverage[i] * 255);\n else bytes.fill(255);\n const baked: RasterMask = { surface: surfaceFromPixels({ width, height, data: bytes }, 1, -ox, -oy), defaultValue: 0 };\n if (mask.density !== undefined && mask.density < 1) baked.density = mask.density;\n if (mask.feather) baked.feather = mask.feather;\n if (mask.enabled === false) baked.enabled = false;\n return baked;\n };\n\n const prepare = async (layer: ImageLayer): Promise<ImageLayer | null> => {\n if (layer.type === 'group') {\n const children: ImageLayer[] = [];\n for (const c of layer.children) {\n const p = await prepare(c);\n if (p) children.push(p);\n }\n return { ...layer, children, mask: bakeMask(layer) } as ImageLayer;\n }\n if (layer.type === 'adjustment') return { ...layer, mask: bakeMask(layer) } as ImageLayer;\n if (layer.type === 'annotation') {\n if (!canRasterize) {\n add('Annotation layers need a canvas (a browser) to rasterize and were left out', layer.name);\n return null;\n }\n add('Annotation layers were written as pixel layers', layer.name);\n const blend = layer.blend === 'normal' && layer.annotation.blend ? layer.annotation.blend : layer.blend;\n const content = { ...layer, visible: true, opacity: 1, blend: 'normal', annotation: { ...layer.annotation, blend: undefined } } as AnnotationLayer;\n delete content.mask;\n delete content.clipped;\n const data = compositor.composite([content], canvas, sources);\n const out: PixelLayer = {\n type: 'pixel',\n id: layer.id,\n name: layer.name,\n visible: layer.visible,\n opacity: layer.opacity,\n blend,\n surface: surfaceFromPixels({ width, height, data }, 4),\n offset: { x: 0, y: 0 },\n };\n if (layer.clipped) out.clipped = true;\n if (layer.locks) out.locks = layer.locks;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n return out;\n }\n let out: PixelLayer = { ...layer };\n if (layer.source === 'document') {\n out.surface = this.surfaceOf(layer);\n delete out.source;\n }\n const active = layer.filters?.filter(isActiveFilter) ?? [];\n if (active.length) {\n add('Layer filters (develop, blur) were baked into the pixels', layer.name);\n const content: PixelLayer = { ...out, visible: true, opacity: 1, blend: 'normal' };\n delete content.mask;\n delete content.clipped;\n delete content.fillOpacity;\n const data = compositor.composite([content], canvas, sources);\n out = { ...out, surface: surfaceFromPixels({ width, height, data }, 4, -layer.offset.x, -layer.offset.y) };\n }\n if (layer.filters?.some((f) => !f.enabled)) add('Disabled layer filters were left out', layer.name);\n delete out.filters;\n const mask = bakeMask(layer);\n if (mask) out.mask = mask;\n else delete out.mask;\n return out;\n };\n\n const prepared: ImageLayer[] = [];\n for (const l of this.state.layers) {\n const p = await prepare(l);\n if (p) prepared.push(p);\n }\n const s = this.state;\n if (s.crop) warnings.push('The crop is not applied; the PSD is the uncropped document.');\n if (s.transform.rotate || s.transform.flipH || s.transform.flipV) warnings.push('Rotation and flips are not applied; the PSD is in the source orientation.');\n if (s.outputOpacity < 1) warnings.push('The document opacity is not written; layers keep their own opacity.');\n if (s.legacyOutputBlur) warnings.push('The document-level blur of an old document is not written.');\n for (const [msg, names] of note) warnings.push(`${msg} (${names.length === 1 ? 'layer' : 'layers'} ${names.map((n) => `\"${n}\"`).join(', ')}).`);\n\n const merged = compositor.composite(prepared, canvas, { ...sources, version: `psd:${Date.now()}` });\n const result = exportPsd({ width, height, layers: prepared, merged, resolution: this.psdResolution });\n return { bytes: result.bytes, warnings, psb: result.psb, width, height };\n }\n\n private async replaceSource(src: string, clearAnnotations: boolean): Promise<void> {\n const next = await this.resolveSource(src);\n this.recordHistory();\n\n const prev = this.state.imageDimensions;\n const sx = next.dimensions.width / (prev.width || next.dimensions.width);\n const sy = next.dimensions.height / (prev.height || next.dimensions.height);\n\n this.imageElement = next.element;\n this.state.sourceUrl = next.sourceUrl;\n this.state.imageDimensions = next.dimensions;\n if (!this.samplePixelsOverride) this.samplePixels = null;\n if (this.state.selection && (sx !== 1 || sy !== 1)) {\n this.state.selection = normalizeSelection(\n resizeSelection(this.state.selection, Math.round(next.dimensions.width), Math.round(next.dimensions.height))\n );\n }\n\n let layers = this.state.layers;\n if (sx !== 1 || sy !== 1) {\n if (this.state.crop) {\n this.state.crop = clampCropToImage(\n {\n x: Math.round(this.state.crop.x * sx),\n y: Math.round(this.state.crop.y * sy),\n width: Math.round(this.state.crop.width * sx),\n height: Math.round(this.state.crop.height * sy),\n },\n next.dimensions.width,\n next.dimensions.height\n );\n }\n layers = resizeLegacyMasks(layers, prev, next.dimensions);\n for (const l of flattenLayers(layers)) {\n if (l.type === 'annotation') {\n layers = mapLayer(layers, l.id, () => ({ ...l, annotation: scaleAnnotation(l.annotation, sx, sy) }));\n }\n }\n }\n // The Background now shows the new source.\n const bg = developTarget(layers, null);\n if (bg && bg.source !== 'document') layers = mapLayer(layers, bg.id, () => ({ ...bg, source: 'document' }));\n\n if (clearAnnotations) {\n for (const l of flattenLayers(layers)) if (l.type === 'annotation') layers = mapLayer(layers, l.id, () => null);\n this.state.selectedAnnotationId = null;\n }\n this.state.layers = layers;\n this.notify();\n }\n\n // --- Serialization ---\n\n /**\n * The serializable state. A document whose layers still have the v1 / v2 shape (the\n * Background with its sliders and depth mask, adjustment layers, annotations) is written as\n * v1 / v2 exactly as before; anything else is written as v3 with the layer pixels as PNG\n * assets. `selection` is included (run-length encoded) only when something is selected.\n */\n public toJSON(): SerializedImageEditorState;\n /**\n * With `assets: 'external'`, layer pixels go to `store` (each PNG uploaded once per store) and\n * the v3 document holds the store's references. Old-shape documents have no assets either way.\n */\n public toJSON(options: { assets: 'external'; store: ImageEditorAssetStore }): Promise<SerializedImageEditorState>;\n public toJSON(options?: { assets: 'external'; store: ImageEditorAssetStore }): SerializedImageEditorState | Promise<SerializedImageEditorState> {\n const out = this.serializeState();\n if (this.state.selection?.bounds) out.selection = serializeSelection(this.state.selection);\n if (!options || out.version !== LAYERED_SCHEMA_VERSION) return options ? Promise.resolve(out) : out;\n return serializeLayersExternal(this.state.layers, options.store).then(({ layers, assets }) => ({ ...out, layers, assets }));\n }\n\n /**\n * Loads a stored state whose v3 assets may live in an asset store (references other than data\n * URLs are fetched with `store.get`), as one undo step. Assets that cannot be fetched are left\n * out and listed in `lastLoadProblems`.\n */\n public async loadJSONAsync(data: SerializedImageEditorState, options: { store?: ImageEditorAssetStore } = {}): Promise<void> {\n if (data.version !== LAYERED_SCHEMA_VERSION || !data.assets) {\n this.loadJSON(data);\n return;\n }\n const problems: string[] = [];\n const assets = await resolveExternalAssets(data.assets, options.store, problems);\n this.loadJSON({ ...data, assets });\n this.lastLoadProblems = [...problems, ...this.lastLoadProblems];\n }\n\n /** Loads a stored state of any version (one undo step). */\n public loadJSON(data: SerializedImageEditorState): void {\n this.recordHistory();\n this.applyStored(data);\n let selection: SelectionMask | null = null;\n if (data.selection) {\n try {\n selection = normalizeSelection(deserializeSelection(data.selection));\n } catch {\n selection = null;\n }\n }\n this.state.selection = selection;\n this.dropMismatchedSelection();\n this.notify();\n }\n\n /** Problems found by the last `loadJSON` / `initialState` of a v3 document (unreadable layers or assets). */\n public lastLoadProblems: string[] = [];\n\n private applyStored(raw: Partial<SerializedImageEditorState>): void {\n const s = this.state;\n const data = migrateSerializedImageEditorState(raw as SerializedImageEditorState);\n this.lastLoadProblems = [];\n if (isLayered(data)) {\n const problems: string[] = [];\n s.layers = deserializeLayers(data.layers, { assets: data.assets ?? {}, problems });\n this.lastLoadProblems = problems;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.outputOpacity = data.opacity;\n if (typeof data.legacyOutputBlur === 'number' && data.legacyOutputBlur >= 0) s.legacyOutputBlur = data.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.activeLayerId = data.activeLayerId && findLayer(s.layers, data.activeLayerId) ? data.activeLayerId : null;\n if (!this.imageElement && typeof data.width === 'number' && typeof data.height === 'number' && data.width > 0 && data.height > 0) {\n s.imageDimensions = { width: data.width, height: data.height };\n }\n } else {\n const { width, height } = this.naturalSize;\n const stack = layersFromLegacy(data, width, height);\n s.layers = stack.layers;\n s.outputOpacity = stack.outputOpacity;\n if (stack.legacyOutputBlur !== undefined) s.legacyOutputBlur = stack.legacyOutputBlur;\n else delete s.legacyOutputBlur;\n s.crop = data.crop ? { ...data.crop } : null;\n s.transform = { ...data.transform };\n s.activeLayerId = null;\n }\n s.activeTarget = 'pixels';\n this.viewKey = [];\n this.syncViews();\n if (s.selectedAnnotationId && !s.annotations.some((a) => a.id === s.selectedAnnotationId)) s.selectedAnnotationId = null;\n }\n\n private serializeState(): SerializedImageEditorState {\n const { width, height } = this.naturalSize;\n const view = legacyView(this.stack(), width, height, null);\n if (view.exact) {\n return {\n // States without adjustment layers keep schema 1, so they round-trip unchanged.\n version: view.adjustments.layers?.length ? 2 : 1,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n adjustments: serializeAdjustments(view.adjustments),\n depthMask: {\n ...view.depthMask,\n depthRange: [...view.depthMask.depthRange] as [number, number],\n },\n annotations: JSON.parse(JSON.stringify(view.annotations)),\n };\n }\n const { layers, assets } = serializeLayers(this.state.layers);\n const out: SerializedLayeredImageEditorState = {\n version: LAYERED_SCHEMA_VERSION,\n width,\n height,\n layers,\n assets,\n activeLayerId: this.state.activeLayerId,\n crop: this.state.crop ? { ...this.state.crop } : null,\n transform: { ...this.state.transform },\n opacity: this.state.outputOpacity,\n };\n if (this.state.legacyOutputBlur !== undefined) out.legacyOutputBlur = this.state.legacyOutputBlur;\n return out;\n }\n}\n\n/**\n * Legacy-process adjustments are saved without `process` (and without an unused zero `tint`), so\n * they serialize exactly as they did before process versions existed.\n */\nfunction serializeAdjustments(adjustments: ImageAdjustments): ImageAdjustments {\n const out: ImageAdjustments = { ...adjustments };\n if (out.layers?.length) out.layers = JSON.parse(JSON.stringify(out.layers));\n else delete out.layers;\n if (normalizeProcessVersion(out.process) === LEGACY_PROCESS_VERSION) {\n delete out.process;\n if (!out.tint) delete out.tint;\n }\n return out;\n}\n\n/** A pixel layer with its develop filter updated from base-slider values (created if missing). */\nfunction withDevelop(layer: PixelLayer, partial: Partial<ImageAdjustments>): PixelLayer {\n const filters = [...(layer.filters ?? [])];\n let i = filters.findIndex((f) => f.kind === 'develop');\n if (i === -1) {\n filters.unshift({\n id: layer.source === 'document' ? DEVELOP_FILTER_ID : newLayerId('flt'),\n kind: 'develop',\n enabled: true,\n brightness: 0,\n contrast: 0,\n saturation: 0,\n exposure: 0,\n temperature: 0,\n tint: 0,\n process: LATEST_PROCESS_VERSION,\n });\n i = 0;\n }\n const next = { ...(filters[i] as DevelopFilter) };\n for (const k of DEVELOP_KEYS) {\n const v = partial[k];\n if (typeof v === 'number' && Number.isFinite(v)) next[k] = v;\n }\n if ('process' in partial) next.process = normalizeProcessVersion(partial.process);\n filters[i] = next;\n return { ...layer, filters };\n}\n\n/** A pixel layer with its blur filter set to `radius` (removed at 0). */\nfunction withBlur(layer: PixelLayer, radius: number): PixelLayer {\n const existing = layer.filters?.find((f) => f.kind === 'blur');\n const filters: LayerFilter[] = (layer.filters ?? []).filter((f) => f.kind !== 'blur');\n if (radius > 0) {\n filters.push({\n id: existing?.id ?? (layer.source === 'document' ? BLUR_FILTER_ID : newLayerId('flt')),\n kind: 'blur',\n enabled: true,\n radius,\n });\n }\n const next = { ...layer };\n if (filters.length) next.filters = filters;\n else delete next.filters;\n return next;\n}\n\n/** Updates a node from an old-shape layer (`setAdjustments({ layers })`), keeping a raster mask whose pixels did not change. */\nfunction mergeAdjustmentNode(node: AdjustmentLayerNode, update: AdjustmentLayer, width: number, height: number): AdjustmentLayerNode {\n const next = {\n ...node,\n kind: update.kind,\n params: JSON.parse(JSON.stringify(update.params)),\n visible: update.enabled !== false,\n opacity: update.opacity,\n blend: update.blend,\n name: update.name ?? '',\n } as AdjustmentLayerNode;\n if (!update.mask) delete next.mask;\n else {\n const current = node.mask && !node.mask.source ? node.mask : null;\n const memo = current ? legacyMaskOf(current, width, height) : null;\n if (current && memo && memo.width === update.mask.width && memo.height === update.mask.height && memo.rle === update.mask.rle) {\n next.mask = withMaskProps(current, {\n density: update.mask.density ?? 1,\n feather: update.mask.feather ?? 0,\n enabled: update.mask.enabled !== false,\n });\n } else {\n const m = rasterMaskFromLegacy(update.mask, width, height);\n if (m) next.mask = m;\n else delete next.mask;\n }\n }\n return next;\n}\n\n/** Applies mask property changes; `undefined` fields keep their value, defaults are left out. */\nfunction withMaskProps(mask: RasterMask, props: LayerMaskProps): RasterMask {\n const next: RasterMask = { ...mask };\n if (props.density !== undefined) {\n const d = Math.min(1, Math.max(0, props.density));\n if (d !== 1) next.density = d;\n else delete next.density;\n }\n if (props.feather !== undefined) {\n const f = Math.min(1000, Math.max(0, props.feather));\n if (f) next.feather = f;\n else delete next.feather;\n }\n if (props.enabled !== undefined) {\n if (props.enabled) delete next.enabled;\n else next.enabled = false;\n }\n return next;\n}\n\n/** Rescales adjustment-layer masks (canvas-sized) when the canvas size changes. */\nfunction resizeLegacyMasks(layers: ImageLayer[], from: ImageDimensions, to: ImageDimensions): ImageLayer[] {\n const fw = Math.max(1, Math.round(from.width));\n const fh = Math.max(1, Math.round(from.height));\n const tw = Math.max(1, Math.round(to.width));\n const th = Math.max(1, Math.round(to.height));\n if (fw === tw && fh === th) return layers;\n let out = layers;\n for (const l of flattenLayers(layers)) {\n if (l.type !== 'adjustment' || !l.mask || l.mask.source) continue;\n const old = l.mask;\n const mask = rasterMaskFromLegacy(legacyMaskOf(old, fw, fh), tw, th);\n if (!mask) continue;\n out = mapLayer(out, l.id, (n) => ({ ...n, mask: { ...mask, density: old.density, feather: old.feather, enabled: old.enabled } }));\n }\n return out;\n}\n\n/** Whether every pixel of `surface` inside the canvas is opaque. */\nfunction isOpaqueInside(surface: PixelSurface, width: number, height: number): boolean {\n const row = new Uint8ClampedArray(width * 4);\n for (let y = 0; y < height; y++) {\n readRegion(surface, { x: 0, y, width, height: 1 }, row);\n for (let i = 3; i < row.length; i += 4) if (row[i] !== 255) return false;\n }\n return true;\n}\n\nfunction collectStateTiles(layers: readonly ImageLayer[], into: Set<Tile>): void {\n for (const l of flattenLayers(layers)) {\n if (l.type === 'pixel' && l.source !== 'document') collectTiles(l.surface, into);\n if (l.mask && !l.mask.source) collectTiles(l.mask.surface, into);\n }\n}\n\n/** The stack without adjustment layers and layer filters (the pixels as painted). */\nfunction pixelsOnly(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'adjustment') continue;\n if (l.type === 'group') out.push({ ...l, children: pixelsOnly(l.children) });\n else if (l.type === 'pixel' && l.filters?.length) {\n const p = { ...l };\n delete p.filters;\n out.push(p);\n } else out.push(l);\n }\n return out;\n}\n\n/** Bilinear resample of RGBA pixels to `width × height`. */\nexport function resizePixels(px: PixelBuffer, width: number, height: number): PixelBuffer {\n const src = px.data as ArrayLike<number>;\n const out = new Uint8ClampedArray(width * height * 4);\n const kx = px.width / width;\n const ky = px.height / height;\n for (let y = 0; y < height; y++) {\n const fy = Math.min(px.height - 1, Math.max(0, (y + 0.5) * ky - 0.5));\n const y0 = Math.floor(fy);\n const y1 = Math.min(px.height - 1, y0 + 1);\n const ty = fy - y0;\n for (let x = 0; x < width; x++) {\n const fx = Math.min(px.width - 1, Math.max(0, (x + 0.5) * kx - 0.5));\n const x0 = Math.floor(fx);\n const x1 = Math.min(px.width - 1, x0 + 1);\n const tx = fx - x0;\n for (let c = 0; c < 4; c++) {\n const a = src[(y0 * px.width + x0) * 4 + c] * (1 - tx) + src[(y0 * px.width + x1) * 4 + c] * tx;\n const b = src[(y1 * px.width + x0) * 4 + c] * (1 - tx) + src[(y1 * px.width + x1) * 4 + c] * tx;\n out[(y * width + x) * 4 + c] = a * (1 - ty) + b * ty;\n }\n }\n }\n return { width, height, data: out };\n}\n\nfunction withoutAnnotations(layers: readonly ImageLayer[]): ImageLayer[] {\n const out: ImageLayer[] = [];\n for (const l of layers) {\n if (l.type === 'annotation') continue;\n out.push(l.type === 'group' ? { ...l, children: withoutAnnotations(l.children) } : l);\n }\n return out;\n}\n\n/** A colour as RGBA bytes: `#rgb`, `#rrggbb`, `#rrggbbaa` or `[r, g, b, a?]` (0..255). */\nexport function parseColor(value: string | number[]): [number, number, number, number] {\n if (Array.isArray(value)) {\n if (value.length < 3 || value.length > 4 || !value.every((v) => typeof v === 'number' && v >= 0 && v <= 255)) {\n throw new LayerOperationError('A colour array is [r, g, b] or [r, g, b, a] with values 0..255.');\n }\n return [Math.round(value[0]), Math.round(value[1]), Math.round(value[2]), Math.round(value[3] ?? 255)];\n }\n const m = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(String(value).trim());\n if (!m) throw new LayerOperationError(`Invalid colour \"${value}\"; use #rrggbb or #rrggbbaa.`);\n let hex = m[1];\n if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');\n const n = (i: number) => parseInt(hex.slice(i, i + 2), 16);\n return [n(0), n(2), n(4), hex.length === 8 ? n(6) : 255];\n}\n\n/** `#rrggbb` of RGBA bytes (alpha dropped). */\nexport function toHex(c: readonly number[]): string {\n return '#' + [c[0], c[1], c[2]].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');\n}\n\n/** `null` for an empty selection, so \"nothing selected\" has one representation. */\nfunction normalizeSelection(mask: SelectionMask | null): SelectionMask | null {\n return mask?.bounds ? mask : null;\n}\n\nfunction sameSelection(a: SelectionMask, b: SelectionMask): boolean {\n if (a.width !== b.width || a.height !== b.height) return false;\n const x = a.data;\n const y = b.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n return true;\n}\n\n/** Scales an annotation's geometry from one image size to another. */\nfunction scaleAnnotation(ann: Annotation, sx: number, sy: number): Annotation {\n const k = Math.sqrt(Math.abs(sx * sy));\n const base = { ...ann, x: ann.x * sx, y: ann.y * sy } as Annotation;\n switch (base.type) {\n case 'pen':\n return { ...base, points: base.points.map((p) => ({ x: p.x * sx, y: p.y * sy })), strokeWidth: base.strokeWidth * k };\n case 'rect':\n return { ...base, width: base.width * sx, height: base.height * sy, strokeWidth: base.strokeWidth * k };\n case 'image':\n return { ...base, width: base.width * sx, height: base.height * sy };\n case 'circle':\n return { ...base, radiusX: base.radiusX * sx, radiusY: base.radiusY * sy, strokeWidth: base.strokeWidth * k };\n case 'arrow':\n case 'line':\n return { ...base, endX: base.endX * sx, endY: base.endY * sy, strokeWidth: base.strokeWidth * k };\n case 'text':\n return { ...base, fontSize: base.fontSize * k };\n }\n}\n","import {\n loadDocument,\n type JsonObject,\n type LoadReport,\n type MigrationStep,\n} from '../persistence/migrate';\nimport { DEFAULT_DEPTH_MASK, DEFAULT_TRANSFORM, normalizeStoredAdjustments } from './controller';\nimport type {\n Annotation,\n CropBox,\n DepthMaskOptions,\n ImageAdjustments,\n ImageTransform,\n LegacySerializedImageEditorState,\n SerializedImageEditorState,\n} from './types';\nimport { deserializeSelection } from './selection/mask';\nimport type { SerializedLayer, SerializedLayeredImageEditorState } from './layers/serialize';\n\n/**\n * The newest `version` that `ImageEditorController.toJSON()` writes (3: layers). Documents whose\n * layers still have the old shape are written as version 1 or 2.\n */\nexport const IMAGE_EDITOR_STATE_VERSION = 3;\n\n/**\n * Migration steps for the document-level schema (versions 1 and 2), keyed by the version they\n * start from. Empty: version 2 only adds `adjustments.layers`, which version 1 readers keep.\n */\nexport const imageEditorStateMigrations: Record<number, MigrationStep> = {};\n\nexport interface ImageEditorDocumentLoad {\n /** State to pass to `loadJSON` / `initialState`. */\n state: SerializedImageEditorState;\n report: LoadReport;\n /** Top-level keys this build does not know; spread them into the saved JSON to keep them. */\n unknown: JsonObject;\n}\n\nconst isObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\nconst isNumber = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value);\n\nfunction expectObject(value: unknown, what: string): Record<string, unknown> {\n if (!isObject(value)) throw new Error(`${what} must be an object`);\n return value;\n}\n\nfunction checkFields(\n value: Record<string, unknown>,\n types: Record<string, (v: unknown) => boolean>,\n what: string,\n) {\n for (const [field, ok] of Object.entries(types)) {\n if (field in value && value[field] !== undefined && !ok(value[field])) {\n throw new Error(`${what}.${field} has an invalid value`);\n }\n }\n}\n\nconst isBoolean = (v: unknown) => typeof v === 'boolean';\n\nfunction parseCrop(value: unknown): CropBox | null {\n if (value === null) return null;\n const crop = expectObject(value, 'crop');\n for (const field of ['x', 'y', 'width', 'height'] as const) {\n if (!isNumber(crop[field])) throw new Error(`crop.${field} must be a number`);\n }\n return { ...(crop as unknown as CropBox) };\n}\n\nfunction parseTransform(value: unknown): ImageTransform {\n const transform = expectObject(value, 'transform');\n checkFields(transform, { rotate: isNumber, flipH: isBoolean, flipV: isBoolean }, 'transform');\n return { ...DEFAULT_TRANSFORM, ...(transform as Partial<ImageTransform>) };\n}\n\nconst ADJUSTMENT_NUMBERS = [\n 'brightness',\n 'contrast',\n 'saturation',\n 'exposure',\n 'temperature',\n 'tint',\n 'blur',\n 'opacity',\n] as const;\n\nfunction parseAdjustments(value: unknown): ImageAdjustments {\n const adjustments = expectObject(value, 'adjustments');\n checkFields(\n adjustments,\n {\n ...Object.fromEntries(ADJUSTMENT_NUMBERS.map((k) => [k, isNumber])),\n process: (v) => v === 1 || v === 2,\n },\n 'adjustments',\n );\n // Kept as stored: `loadJSON` fills defaults and keeps a missing `process` legacy.\n return { ...(adjustments as unknown as ImageAdjustments) };\n}\n\nfunction parseDepthMask(value: unknown): DepthMaskOptions {\n const mask = expectObject(value, 'depthMask');\n checkFields(\n mask,\n {\n enabled: isBoolean,\n maskSource: (v) => v === null || typeof v === 'string',\n mode: (v) => v === 'threshold' || v === 'brightness',\n depthRange: (v) => Array.isArray(v) && v.length === 2 && v.every(isNumber),\n softness: isNumber,\n invert: isBoolean,\n },\n 'depthMask',\n );\n const merged = { ...DEFAULT_DEPTH_MASK, ...(mask as Partial<DepthMaskOptions>) };\n return { ...merged, depthRange: [...merged.depthRange] as [number, number] };\n}\n\nconst ANNOTATION_TYPES = new Set(['pen', 'rect', 'circle', 'arrow', 'line', 'text', 'image']);\n\nconst isAnnotation = (value: unknown): value is Annotation =>\n isObject(value) &&\n typeof value.id === 'string' &&\n typeof value.type === 'string' &&\n ANNOTATION_TYPES.has(value.type) &&\n isNumber(value.x) &&\n isNumber(value.y);\n\n/**\n * Loads stored image editor state (a JSON string or object) with a `LoadReport`:\n *\n * - Runs `imageEditorStateMigrations` up to `IMAGE_EDITOR_STATE_VERSION`; a missing\n * `version` is taken as current.\n * - A newer `version` is still loaded but reported `readOnly`: do not save over it.\n * - Each section (`crop`, `transform`, `adjustments`, `depthMask`, `annotations`, and the\n * optional run-length encoded `selection`)\n * falls back to its default on its own; invalid annotations are dropped one by one.\n * - Unknown top-level keys are returned in `unknown`.\n *\n * Valid state from `toJSON()` comes back unchanged, so `loadJSON(state)` behaves\n * exactly as with the raw object.\n */\nexport function loadImageEditorDocument(input: unknown): ImageEditorDocumentLoad {\n let parsed: unknown = input;\n if (typeof input === 'string') {\n try {\n parsed = JSON.parse(input);\n } catch {\n parsed = input;\n }\n }\n if (isObject(parsed) && parsed.version === 3) return loadLayeredDocument(parsed);\n // Version 2 only adds `adjustments.layers`, which the version 1 sections keep as stored.\n const isV2 = isObject(parsed) && parsed.version === 2;\n if (isV2) parsed = { ...(parsed as Record<string, unknown>), version: 1 };\n const newer = isObject(parsed) && typeof parsed.version === 'number' && parsed.version > IMAGE_EDITOR_STATE_VERSION;\n let dropped = 0;\n const result = loadDocument<Omit<LegacySerializedImageEditorState, 'version'>>(\n parsed,\n {\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n adjustments: {\n parse: parseAdjustments,\n // Same as what `loadJSON` derives from missing adjustments.\n defaults: () => normalizeStoredAdjustments(undefined),\n },\n depthMask: {\n parse: parseDepthMask,\n defaults: () => ({ ...DEFAULT_DEPTH_MASK, depthRange: [...DEFAULT_DEPTH_MASK.depthRange] as [number, number] }),\n },\n annotations: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('annotations must be an array');\n const kept = value.filter(isAnnotation);\n dropped = value.length - kept.length;\n return JSON.parse(JSON.stringify(kept)) as Annotation[];\n },\n defaults: () => [],\n },\n // Optional: only present when something was selected when saving.\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n {\n // Versions 1 and 2 load as one schema; anything newer than 3 is reported read-only.\n currentVersion: newer ? IMAGE_EDITOR_STATE_VERSION : 1,\n steps: imageEditorStateMigrations,\n },\n );\n if (dropped > 0) {\n result.report.problems.push({\n section: 'annotations',\n message: `${dropped} invalid annotation${dropped === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 1, ...result.value } as LegacySerializedImageEditorState;\n if (isV2) {\n if (state.adjustments.layers?.length) state.version = 2;\n result.report.version = 2;\n }\n if (state.selection === undefined) delete state.selection;\n return { state, report: result.report, unknown: result.unknown };\n}\n\nconst LAYER_TYPES = new Set(['pixel', 'adjustment', 'group', 'annotation']);\n\n/** Keeps layers that have an id-like shape and a known type (children checked too); counts the rest. */\nfunction cleanLayers(value: unknown[], dropped: { count: number }): SerializedLayer[] {\n const out: SerializedLayer[] = [];\n for (const raw of value) {\n if (!isObject(raw) || typeof raw.type !== 'string' || !LAYER_TYPES.has(raw.type)) {\n dropped.count++;\n continue;\n }\n if (raw.type === 'group') {\n const children = Array.isArray(raw.children) ? cleanLayers(raw.children, dropped) : [];\n out.push({ ...(raw as unknown as SerializedLayer), children } as SerializedLayer);\n } else out.push(JSON.parse(JSON.stringify(raw)) as SerializedLayer);\n }\n return out;\n}\n\n/** Version 3 (layers): each section falls back on its own; unreadable layers are dropped one by one. */\nfunction loadLayeredDocument(doc: Record<string, unknown>): ImageEditorDocumentLoad {\n const dropped = { count: 0 };\n const result = loadDocument<Omit<SerializedLayeredImageEditorState, 'version'>>(\n doc,\n {\n width: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('width must be a positive number'); })()), defaults: 0 },\n height: { parse: (v) => (isNumber(v) && v > 0 ? v : (() => { throw new Error('height must be a positive number'); })()), defaults: 0 },\n layers: {\n parse: (value) => {\n if (!Array.isArray(value)) throw new Error('layers must be an array');\n return cleanLayers(value, dropped);\n },\n defaults: () => [],\n },\n assets: {\n parse: (value) => {\n const assets = expectObject(value, 'assets');\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(assets)) if (typeof v === 'string') out[k] = v;\n return out;\n },\n defaults: () => ({}),\n },\n activeLayerId: { parse: (v) => (v === null || typeof v === 'string' ? v : null), defaults: null },\n crop: { parse: parseCrop, defaults: null },\n transform: { parse: parseTransform, defaults: () => ({ ...DEFAULT_TRANSFORM }) },\n opacity: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('opacity must be a number');\n return Math.min(1, Math.max(0, v));\n },\n defaults: 1,\n },\n legacyOutputBlur: {\n parse: (v) => {\n if (!isNumber(v)) throw new Error('legacyOutputBlur must be a number');\n return Math.max(0, v);\n },\n defaults: undefined,\n },\n selection: {\n parse: (value) => {\n const mask = deserializeSelection(value);\n const v = value as { rle: string };\n return { width: mask.width, height: mask.height, rle: v.rle };\n },\n defaults: undefined,\n },\n },\n { currentVersion: 3, steps: {} },\n );\n if (dropped.count > 0) {\n result.report.problems.push({\n section: 'layers',\n message: `${dropped.count} invalid layer${dropped.count === 1 ? '' : 's'} dropped`,\n });\n }\n const state = { version: 3, ...result.value } as SerializedLayeredImageEditorState;\n if (state.selection === undefined) delete state.selection;\n if (state.legacyOutputBlur === undefined) delete state.legacyOutputBlur;\n return { state, report: result.report, unknown: result.unknown };\n}\n","// Portions ported from lightcraft-main/crates/develop/src/controls.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\nimport type { AdjustmentKind, ImageAdjustments } from './types';\n\n/**\n * Control specs: one table describes every numeric adjustment slider (id, label, section, range,\n * default, step, display precision and track style). The React and Svelte panels render from it,\n * the agent tools expose it as a schema, and values are addressed by id.\n */\n\nexport type ControlSection = 'light' | 'color' | 'effects' | 'layer';\n\n/** How a slider track is drawn. */\nexport type ControlTrack =\n /** Plain track, fill from the minimum. */\n | { kind: 'plain' }\n /** Zero in the middle. */\n | { kind: 'centered' }\n /** Blue → yellow (white-balance temperature). */\n | { kind: 'temp' }\n /** Green → magenta (tint). */\n | { kind: 'tint' }\n /** A two-colour gradient. */\n | { kind: 'gradient'; from: string; to: string }\n /** Full hue rainbow. */\n | { kind: 'rainbow' };\n\nexport interface ControlSpec {\n /** Dotted path of the value (`exposure`; inside a layer's params: `midtones.0`, `ranges.4.hue`). */\n id: string;\n label: string;\n section: ControlSection;\n min: number;\n max: number;\n step: number;\n default: number;\n /** Decimal places shown (after `displayScale`). */\n decimals: number;\n track: ControlTrack;\n /** Multiplier for display (opacity 0..1 shows as 0..100 %). */\n displayScale?: number;\n /** Unit suffix for display (`px`, `%`, `°`). */\n unit?: string;\n /** The rendering process the control needs (tint: process 2). */\n requiresProcess?: 1 | 2;\n}\n\nconst plain: ControlTrack = { kind: 'plain' };\nconst centered: ControlTrack = { kind: 'centered' };\n\n/** Every numeric base adjustment, in panel order. */\nexport const IMAGE_ADJUSTMENT_CONTROLS: readonly ControlSpec[] = [\n { id: 'brightness', label: 'Brightness', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'contrast', label: 'Contrast', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n {\n id: 'saturation', label: 'Saturation', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' },\n },\n { id: 'exposure', label: 'Exposure', section: 'light', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: 'temperature', label: 'Warmth / Temperature', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'temp' } },\n { id: 'tint', label: 'Tint', section: 'color', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'tint' }, requiresProcess: 2 },\n { id: 'blur', label: 'Blur', section: 'effects', min: 0, max: 30, step: 1, default: 0, decimals: 0, track: plain, unit: 'px' },\n { id: 'opacity', label: 'Opacity', section: 'effects', min: 0, max: 1, step: 0.05, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\nconst tone = (prefix: string, label: string): ControlSpec[] =>\n [\n ['0', 'Cyan – Red', '#00b7c7', '#e0343a'],\n ['1', 'Magenta – Green', '#d23ad2', '#2fb34a'],\n ['2', 'Yellow – Blue', '#e0c63a', '#3a5be0'],\n ].map(([i, l, from, to]) => ({\n id: `${prefix}.${i}`, label: `${label} ${l}`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0,\n track: { kind: 'gradient', from, to },\n }));\n\nconst levelsRecord = (ch: string, label: string): ControlSpec[] => [\n { id: `${ch}.inBlack`, label: `${label} input black`, section: 'layer', min: 0, max: 253, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.gamma`, label: `${label} gamma`, section: 'layer', min: 0.1, max: 9.99, step: 0.01, default: 1, decimals: 2, track: plain },\n { id: `${ch}.inWhite`, label: `${label} input white`, section: 'layer', min: 2, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n { id: `${ch}.outBlack`, label: `${label} output black`, section: 'layer', min: 0, max: 255, step: 1, default: 0, decimals: 0, track: plain },\n { id: `${ch}.outWhite`, label: `${label} output white`, section: 'layer', min: 0, max: 255, step: 1, default: 255, decimals: 0, track: plain },\n];\n\nconst RANGE_NAMES = ['Reds', 'Yellows', 'Greens', 'Cyans', 'Blues', 'Magentas'];\n\n/**\n * Numeric parameters of each adjustment-layer kind, addressed inside `layer.params`. Curves points\n * and booleans / colours (colorize, preserve luminosity, tint, filter colour) are edited by the\n * dedicated controls, not sliders.\n */\nexport const ADJUSTMENT_LAYER_CONTROLS: Readonly<Record<AdjustmentKind, readonly ControlSpec[]>> = {\n curves: [],\n levels: [\n ...levelsRecord('master', 'RGB'),\n ...levelsRecord('red', 'Red'),\n ...levelsRecord('green', 'Green'),\n ...levelsRecord('blue', 'Blue'),\n ],\n hueSaturation: [\n { id: 'hue', label: 'Hue', section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n { id: 'lightness', label: 'Lightness', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } },\n ...RANGE_NAMES.flatMap((name, i): ControlSpec[] => [\n { id: `ranges.${i}.hue`, label: `${name} hue`, section: 'layer', min: -180, max: 180, step: 1, default: 0, decimals: 0, track: { kind: 'rainbow' }, unit: '°' },\n { id: `ranges.${i}.saturation`, label: `${name} saturation`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n { id: `ranges.${i}.lightness`, label: `${name} lightness`, section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: centered },\n ]),\n ],\n vibrance: [\n { id: 'vibrance', label: 'Vibrance', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#d8406a' } },\n { id: 'saturation', label: 'Saturation', section: 'layer', min: -100, max: 100, step: 1, default: 0, decimals: 0, track: { kind: 'gradient', from: '#7a7a7a', to: '#e04a3a' } },\n ],\n colorBalance: [...tone('shadows', 'Shadows'), ...tone('midtones', 'Midtones'), ...tone('highlights', 'Highlights')],\n blackWhite: [\n ['reds', 'Reds', 40, '#e0343a'],\n ['yellows', 'Yellows', 60, '#e0c63a'],\n ['greens', 'Greens', 40, '#2fb34a'],\n ['cyans', 'Cyans', 60, '#00b7c7'],\n ['blues', 'Blues', 20, '#3a5be0'],\n ['magentas', 'Magentas', 80, '#d23ad2'],\n ].map(([id, label, def, to]) => ({\n id: id as string, label: label as string, section: 'layer' as const, min: -200, max: 300, step: 1, default: def as number, decimals: 0,\n track: { kind: 'gradient' as const, from: '#000000', to: to as string }, unit: '%',\n })),\n invert: [],\n posterize: [{ id: 'levels', label: 'Levels', section: 'layer', min: 2, max: 255, step: 1, default: 4, decimals: 0, track: plain }],\n threshold: [{ id: 'level', label: 'Threshold level', section: 'layer', min: 1, max: 255, step: 1, default: 128, decimals: 0, track: { kind: 'gradient', from: '#000000', to: '#ffffff' } }],\n photoFilter: [{ id: 'density', label: 'Density', section: 'layer', min: 0, max: 100, step: 1, default: 25, decimals: 0, track: plain, unit: '%' }],\n};\n\n/** Opacity of any adjustment layer (0..1, shown as %). */\nexport const ADJUSTMENT_LAYER_OPACITY_CONTROL: ControlSpec = {\n id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%',\n};\n\nconst BASE_BY_ID = new Map(IMAGE_ADJUSTMENT_CONTROLS.map((c) => [c.id, c]));\n\n/** The base-adjustment control with this id. */\nexport function findAdjustmentControl(id: string): ControlSpec | undefined {\n return BASE_BY_ID.get(id);\n}\n\n/** The control of a layer parameter (`opacity` included). */\nexport function findLayerControl(kind: AdjustmentKind, id: string): ControlSpec | undefined {\n if (id === 'opacity') return ADJUSTMENT_LAYER_OPACITY_CONTROL;\n return ADJUSTMENT_LAYER_CONTROLS[kind]?.find((c) => c.id === id);\n}\n\n/** `v` clamped to the control's range and snapped to its step; non-finite values give the default. */\nexport function clampControlValue(spec: ControlSpec, v: unknown): number {\n if (typeof v !== 'number' || !Number.isFinite(v)) return spec.default;\n const clamped = Math.min(spec.max, Math.max(spec.min, v));\n const steps = Math.round((clamped - spec.min) / spec.step);\n const snapped = spec.min + steps * spec.step;\n // Trim float noise from the step arithmetic.\n return Math.min(spec.max, Number(snapped.toFixed(6)));\n}\n\n/** A value as the slider shows it (`+12`, `35%`, `4px`). */\nexport function formatControlValue(spec: ControlSpec, v: number): string {\n const shown = (Number.isFinite(v) ? v : spec.default) * (spec.displayScale ?? 1);\n const s = shown.toFixed(spec.decimals);\n return `${s}${spec.unit ?? ''}`;\n}\n\n/** Where `v` sits on the track, 0 (left) to 1 (right). */\nexport function controlPosition(spec: ControlSpec, v: number): number {\n const p = (clampControlValue(spec, v) - spec.min) / (spec.max - spec.min || 1);\n return Math.min(1, Math.max(0, p));\n}\n\n/** CSS background for a control's track. */\nexport function controlTrackBackground(spec: ControlSpec): string | undefined {\n const t = spec.track;\n switch (t.kind) {\n case 'temp':\n return 'linear-gradient(to right, #3a7bd5, #d8d8d8, #f2c94c)';\n case 'tint':\n return 'linear-gradient(to right, #3fbf4f, #d8d8d8, #d23ad2)';\n case 'gradient':\n return `linear-gradient(to right, ${t.from}, ${t.to})`;\n case 'rainbow':\n return 'linear-gradient(to right, #00ffff, #0000ff, #ff00ff, #ff0000, #ffff00, #00ff00, #00ffff)';\n default:\n return undefined;\n }\n}\n\n/** Reads a dotted path (`ranges.2.hue`) from an object. */\nexport function getControlPath(obj: unknown, path: string): unknown {\n let cur: any = obj;\n for (const part of path.split('.')) {\n if (cur === null || cur === undefined) return undefined;\n cur = cur[part];\n }\n return cur;\n}\n\n/** A copy of `obj` with the dotted path set (arrays and objects along the path are cloned). */\nexport function setControlPath<T>(obj: T, path: string, value: unknown): T {\n const parts = path.split('.');\n const root: any = Array.isArray(obj) ? [...(obj as unknown[])] : { ...(obj as object) };\n let cur = root;\n for (let i = 0; i < parts.length - 1; i++) {\n const next = cur[parts[i]];\n cur[parts[i]] = Array.isArray(next) ? [...next] : next && typeof next === 'object' ? { ...next } : {};\n cur = cur[parts[i]];\n }\n cur[parts[parts.length - 1]] = value;\n return root as T;\n}\n\n/** The base adjustment values the control table describes. */\nexport function readAdjustmentControls(adjustments: ImageAdjustments): Record<string, number> {\n const out: Record<string, number> = {};\n for (const c of IMAGE_ADJUSTMENT_CONTROLS) {\n const v = (adjustments as unknown as Record<string, unknown>)[c.id];\n out[c.id] = typeof v === 'number' && Number.isFinite(v) ? v : c.default;\n }\n return out;\n}\n","import type { SelectionMask, SelectionMode } from './mask';\nimport type { ColorDistance } from './wand';\nimport type { RefineEdgeParams } from './refine';\n\n/** Selection tools of the image editor UI. */\nexport type SelectionToolKind = 'wand' | 'rect' | 'ellipse' | 'lasso';\n\nexport const SELECTION_TOOLS: readonly { id: SelectionToolKind; label: string }[] = [\n { id: 'wand', label: 'Magic Wand' },\n { id: 'rect', label: 'Rectangle' },\n { id: 'ellipse', label: 'Ellipse' },\n { id: 'lasso', label: 'Lasso' },\n];\n\nexport const SELECTION_MODE_LABELS: Record<SelectionMode, string> = {\n replace: 'New',\n add: 'Add',\n subtract: 'Subtract',\n intersect: 'Intersect',\n};\n\n/** The options bar of the selection tools (shared by the React and Svelte UIs). */\nexport interface SelectionToolOptions {\n tool: SelectionToolKind;\n mode: SelectionMode;\n /** Magic wand tolerance, 0..255. */\n tolerance: number;\n contiguous: boolean;\n antiAlias: boolean;\n /** Sample the adjusted image instead of the source. */\n sampleMerged: boolean;\n /** Sample the whole image (`composite`, default) or only the active pixel layer. */\n sample?: 'composite' | 'active-layer';\n distance: ColorDistance;\n}\n\nexport const DEFAULT_SELECTION_TOOL_OPTIONS: SelectionToolOptions = {\n tool: 'wand',\n mode: 'replace',\n tolerance: 32,\n contiguous: true,\n antiAlias: true,\n sampleMerged: false,\n distance: 'rgb',\n};\n\n/**\n * Photoshop's modifier keys: Shift adds, Alt/Option subtracts, both intersect; otherwise the\n * options bar mode.\n */\nexport function selectionModeFromModifiers(\n modifiers: { shiftKey?: boolean; altKey?: boolean },\n fallback: SelectionMode\n): SelectionMode {\n if (modifiers.shiftKey && modifiers.altKey) return 'intersect';\n if (modifiers.shiftKey) return 'add';\n if (modifiers.altKey) return 'subtract';\n return fallback;\n}\n\nexport interface RefineEdgeControl {\n id: keyof Omit<RefineEdgeParams, 'smartRadius'>;\n label: string;\n min: number;\n max: number;\n step: number;\n unit: string;\n}\n\n/** Sliders of the Refine Edge panel. */\nexport const REFINE_EDGE_CONTROLS: readonly RefineEdgeControl[] = [\n { id: 'radius', label: 'Radius', min: 0, max: 64, step: 1, unit: 'px' },\n { id: 'smooth', label: 'Smooth', min: 0, max: 100, step: 1, unit: '' },\n { id: 'feather', label: 'Feather', min: 0, max: 50, step: 0.5, unit: 'px' },\n { id: 'contrast', label: 'Contrast', min: 0, max: 100, step: 1, unit: '%' },\n { id: 'shiftEdge', label: 'Shift Edge', min: -100, max: 100, step: 1, unit: '%' },\n];\n\n/** A one-line status: \"No selection\" or \"W × H px selected (n % of the image)\". */\nexport function describeSelection(mask: SelectionMask | null | undefined): string {\n if (!mask || !mask.bounds) return 'No selection';\n let sum = 0;\n for (let i = 0; i < mask.data.length; i++) sum += mask.data[i];\n const pct = (sum / 255 / mask.data.length) * 100;\n const shown = pct >= 10 ? Math.round(pct) : Math.round(pct * 10) / 10;\n return `${mask.bounds.width} × ${mask.bounds.height} px selected (${shown}% of the image)`;\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { JsonSchema } from '../tools/schema.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind } from './types';\nimport { ADJUSTMENT_KINDS, isAdjustmentKind } from './adjustment-layers';\nimport { describeSelection } from './selection/tool-options';\nimport { SELECTION_MODES, parseSelectionMode, selectionArea } from './selection/mask';\nimport { SELECTION_MODIFY_OPS, type SelectionModifyOp } from './selection/modify';\nimport { layerMaskFromSelection } from './selection/layer-mask';\n\nconst modeSchema: JsonSchema = {\n type: 'string',\n enum: [...SELECTION_MODES],\n description: 'How the result combines with the current selection (default replace).',\n};\n\nconst num = (v: unknown, d: number) => (typeof v === 'number' && Number.isFinite(v) ? v : d);\n\nfunction summary(controller: ImageEditorController) {\n const sel = controller.getSelection();\n const { width, height } = controller.getState().imageDimensions;\n const area = selectionArea(sel);\n return {\n empty: !sel,\n bounds: sel?.bounds ?? null,\n /** Σ coverage in pixels. */\n area: Math.round(area),\n percent: Math.round((area / Math.max(1, width * height)) * 10000) / 100,\n description: describeSelection(sel),\n };\n}\n\nconst writeAnnotations = (title: string, idempotent = false) => ({\n title,\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: idempotent,\n openWorldHint: false,\n});\n\n/**\n * Agent tools for selections and masks (part of `createImageEditorToolProvider`):\n * `get_selection`, `select_region`, `select_by_color`, `modify_selection`, `refine_selection`\n * and `apply_selection_as_mask`. Coordinates are natural pixels of the source image.\n */\nexport function createImageSelectionTools(controller: ImageEditorController): AgentTool[] {\n const getSelection: AgentTool = {\n name: 'get_selection',\n title: 'Read the selection',\n description:\n 'Returns the active selection: whether it is empty, its bounds (natural image pixels), its area in pixels and percent of the image.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read the selection', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },\n handler: () => summary(controller),\n };\n\n const selectRegion: AgentTool = {\n name: 'select_region',\n title: 'Select a region',\n description:\n 'Selects a rectangle or ellipse (x, y, width, height in natural image pixels), everything (\"all\"), nothing (\"none\") or the inverse of the current selection (\"invert\"). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n shape: { type: 'string', enum: ['rect', 'ellipse', 'all', 'none', 'invert'], description: 'What to select.' },\n x: { type: 'number', description: 'rect / ellipse: left edge in natural image pixels.' },\n y: { type: 'number', description: 'rect / ellipse: top edge in natural image pixels.' },\n width: { type: 'number', description: 'rect / ellipse: width in natural image pixels.' },\n height: { type: 'number', description: 'rect / ellipse: height in natural image pixels.' },\n mode: modeSchema,\n feather: { type: 'number', minimum: 0, description: 'Feather radius in pixels (default 0).' },\n },\n required: ['shape'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select a region', true),\n handler: ({ shape, x, y, width, height, mode, feather }) => {\n const opts = { mode: parseSelectionMode(mode), feather: num(feather, 0) };\n switch (shape) {\n case 'all':\n controller.selectAll();\n break;\n case 'none':\n controller.deselect();\n break;\n case 'invert':\n controller.invertSelection();\n break;\n case 'rect':\n case 'ellipse': {\n const rect = { x: num(x, NaN), y: num(y, NaN), width: num(width, NaN), height: num(height, NaN) };\n if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite)) {\n throw new Error('rect and ellipse need numeric x, y, width and height.');\n }\n if (shape === 'rect') controller.selectRect(rect, opts);\n else controller.selectEllipse(rect, opts);\n break;\n }\n default:\n throw new Error(`Unknown shape \"${String(shape)}\". Use rect, ellipse, all, none or invert.`);\n }\n return summary(controller);\n },\n };\n\n const selectByColor: AgentTool = {\n name: 'select_by_color',\n title: 'Select by colour',\n description:\n 'Magic wand at (x, y) with tolerance (0-255) and contiguous, or with \"color\" (\"#rrggbb\") every pixel of a similar colour (Color Range, fuzziness 0-200). One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Seed point x in natural image pixels.' },\n y: { type: 'number', description: 'Seed point y in natural image pixels.' },\n color: { type: 'string', description: '#rrggbb: select this colour everywhere instead of a seed point.' },\n tolerance: { type: 'number', minimum: 0, maximum: 255, description: 'Magic wand tolerance (default 32).' },\n contiguous: { type: 'boolean', description: 'Only the connected region around (x, y) (default true).' },\n fuzziness: { type: 'number', minimum: 0, maximum: 200, description: 'Color Range fuzziness (default 40).' },\n sampleMerged: { type: 'boolean', description: 'Sample the adjusted image rather than the source.' },\n sample: {\n type: 'string',\n enum: ['composite', 'active-layer'],\n description: 'composite (default): the whole image; active-layer: only the active pixel layer (see set_active_layer).',\n },\n mode: modeSchema,\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Select by colour', true),\n handler: async ({ x, y, color, tolerance, contiguous, fuzziness, sampleMerged, sample, mode }) => {\n const common = {\n mode: parseSelectionMode(mode),\n sampleMerged: sampleMerged === true,\n sample: (sample as 'composite' | 'active-layer' | undefined) ?? 'composite',\n };\n if (typeof color === 'string') {\n await controller.selectColorRange([{ color }], { ...common, fuzziness: num(fuzziness, 40) });\n } else if (typeof x === 'number' && typeof y === 'number') {\n await controller.magicWand({ x, y }, { ...common, tolerance: num(tolerance, 32), contiguous: contiguous !== false });\n } else {\n throw new Error('Pass x and y (magic wand) or color (#rrggbb).');\n }\n return summary(controller);\n },\n };\n\n const modify: AgentTool = {\n name: 'modify_selection',\n title: 'Modify the selection',\n description: 'Select › Modify: grow, shrink, feather, border or smooth the selection by radius pixels. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n op: { type: 'string', enum: [...SELECTION_MODIFY_OPS], description: 'How to modify the selection.' },\n radius: { type: 'number', minimum: 0, maximum: 500, description: 'Radius in pixels.' },\n },\n required: ['op', 'radius'],\n additionalProperties: false,\n },\n annotations: writeAnnotations('Modify the selection'),\n handler: ({ op, radius }) => {\n if (!(SELECTION_MODIFY_OPS as readonly string[]).includes(op as string)) throw new Error(`Unknown op \"${String(op)}\".`);\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n controller.modifySelection(op as SelectionModifyOp, Math.min(500, Math.max(0, num(radius, 0))));\n return summary(controller);\n },\n };\n\n const refine: AgentTool = {\n name: 'refine_selection',\n title: 'Refine the selection edge',\n description:\n 'Refine Edge: snaps the selection edge to the image within radius pixels (edge-aware guided filter), then smooth (0-100), feather (px), contrast (0-100) and shiftEdge (-100..100 %). Use it after loading a coarse AI mask. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n radius: { type: 'number', minimum: 0, maximum: 64, description: 'Edge detection radius in pixels.' },\n smartRadius: { type: 'boolean', description: 'Adapt the radius to hard and soft edges.' },\n smooth: { type: 'number', minimum: 0, maximum: 100, description: 'Smooth the edge outline (0-100).' },\n feather: { type: 'number', minimum: 0, maximum: 250, description: 'Feather the edge (pixels).' },\n contrast: { type: 'number', minimum: 0, maximum: 100, description: 'Sharpen the edge transition (0-100).' },\n shiftEdge: { type: 'number', minimum: -100, maximum: 100, description: 'Move the edge out (+) or in (-), in percent.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Refine the selection edge'),\n handler: async (params) => {\n if (!controller.getSelection()) throw new Error('Nothing is selected.');\n await controller.refineSelection({\n radius: num(params.radius, 8),\n smartRadius: params.smartRadius === true,\n smooth: num(params.smooth, 0),\n feather: num(params.feather, 0),\n contrast: num(params.contrast, 0),\n shiftEdge: num(params.shiftEdge, 0),\n });\n return summary(controller);\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_selection_as_mask',\n title: 'Mask a layer with the selection',\n description:\n 'Uses the selection as an adjustment layer mask: on layerId, or on a new layer of kind (with optional params), so the adjustment only affects the selected area. density (0-1) and feather (px) soften the mask non-destructively. One undoable step.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { type: 'string', description: 'Existing adjustment layer to mask.' },\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Kind of a new layer to add and mask (instead of layerId).' },\n params: { type: 'object', description: 'Parameters for the new layer (see list_controls).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0-1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Mask feather in pixels.' },\n },\n additionalProperties: false,\n },\n annotations: writeAnnotations('Mask a layer with the selection'),\n handler: ({ layerId, kind, params, density, feather }) => {\n const sel = controller.getSelection();\n if (!sel) throw new Error('Nothing is selected.');\n const props = {\n ...(typeof density === 'number' ? { density } : {}),\n ...(typeof feather === 'number' ? { feather } : {}),\n };\n let id: string;\n if (typeof layerId === 'string') {\n if (!controller.getAdjustmentLayer(layerId)) throw new Error(`No adjustment layer \"${layerId}\".`);\n controller.applySelectionAsLayerMask(layerId, props);\n id = layerId;\n } else if (isAdjustmentKind(kind)) {\n id = controller.addAdjustmentLayer({\n kind: kind as AdjustmentKind,\n params: params as never,\n mask: layerMaskFromSelection(sel, props),\n });\n } else {\n throw new Error('Pass layerId (an existing layer) or kind (a new adjustment layer).');\n }\n const layer = controller.getAdjustmentLayer(id)!;\n const { rle: _rle, ...mask } = layer.mask!;\n return { layerId: id, kind: layer.kind, mask, selection: summary(controller) };\n },\n };\n\n return [getSelection, selectRegion, selectByColor, modify, refine, applyMask];\n}\n","import type { CaptureRenderRequest, CaptureSurface } from '../tools/capture.js';\nimport type { ImageEditorController } from './controller';\nimport type { SelectionMask } from './selection/mask';\n\nconst NO_CANVAS = 'Rendering is not available in this environment.';\n\n/** Darkens what is outside the selection (natural pixels), so its coverage reads at a glance. */\nfunction selectionShade(selection: SelectionMask, doc: Document): HTMLCanvasElement | null {\n const canvas = doc.createElement('canvas');\n canvas.width = selection.width;\n canvas.height = selection.height;\n const context = canvas.getContext('2d');\n if (!context) return null;\n const pixels = context.createImageData(selection.width, selection.height);\n for (let i = 0; i < selection.data.length; i++) {\n pixels.data[i * 4 + 3] = Math.round(150 * (1 - selection.data[i] / 255));\n }\n context.putImageData(pixels, 0, 0);\n return canvas;\n}\n\n/** Draws a region of the image editor's source view for `capture_view`. */\nasync function renderImageRegion(controller: ImageEditorController, request: CaptureRenderRequest): Promise<string> {\n const state = controller.getState();\n // Never render above natural size; small regions are enlarged when drawn.\n const k = Math.min(1, request.scale);\n const view = await controller.renderSourceView({ scale: k });\n if (!view) throw new Error('No image is loaded, so there is nothing to capture.');\n if (typeof document === 'undefined') throw new Error(NO_CANVAS);\n const out = document.createElement('canvas');\n out.width = request.width;\n out.height = request.height;\n const context = out.getContext('2d');\n if (!context) throw new Error(NO_CANVAS);\n const { rect } = request;\n // Upscaled regions keep hard pixel edges, so the agent sees real pixels.\n context.imageSmoothingEnabled = request.scale < 1;\n if (request.format === 'jpeg') {\n context.fillStyle = '#ffffff';\n context.fillRect(0, 0, out.width, out.height);\n }\n const kx = view.width / state.imageDimensions.width;\n const ky = view.height / state.imageDimensions.height;\n context.drawImage(view, rect.x * kx, rect.y * ky, rect.width * kx, rect.height * ky, 0, 0, out.width, out.height);\n if (request.includeOverlays) {\n const s = request.scale;\n const selection = state.selection;\n if (selection?.bounds && selection.width === state.imageDimensions.width && selection.height === state.imageDimensions.height) {\n const shade = selectionShade(selection, document);\n if (shade) context.drawImage(shade, rect.x, rect.y, rect.width, rect.height, 0, 0, out.width, out.height);\n }\n if (state.crop) {\n context.save();\n context.strokeStyle = '#ffd400';\n context.lineWidth = 2;\n context.setLineDash([6, 4]);\n context.strokeRect((state.crop.x - rect.x) * s, (state.crop.y - rect.y) * s, state.crop.width * s, state.crop.height * s);\n context.restore();\n }\n }\n const url = out.toDataURL(request.format === 'jpeg' ? 'image/jpeg' : 'image/png', request.quality);\n if (!url.startsWith('data:image/') || url.indexOf(',') === url.length - 1) throw new Error(NO_CANVAS);\n return url;\n}\n\n/**\n * The image editor as a capture surface. Units are natural pixels of the source image,\n * the same space as `get_selection` and `select_region`: crop, rotation and flips are not\n * applied (use `render_preview` for the final framing), adjustments, layers and annotations\n * are. Regions: the whole image, the selection's bounds, or a rect. Small regions are\n * enlarged up to 8× with hard pixel edges for close inspection.\n */\nexport function createImageCaptureSurface(controller: ImageEditorController): CaptureSurface {\n const whole = () => {\n const { width, height } = controller.getState().imageDimensions;\n return width > 0 && height > 0 ? { x: 0, y: 0, width, height } : null;\n };\n return {\n units: 'image pixels',\n content: whole,\n selection: () => ({ rect: controller.getState().selection?.bounds ?? null }),\n overlays: 'a shade over unselected pixels and the crop box outline',\n maxScale: 8,\n render: (request) => renderImageRegion(controller, request),\n };\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { BlendMode } from './blend';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport { getRotatedSize, normalizeAngle } from './geometry';\nimport type { ImageHistogram } from './histogram';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\n\nconst LAYER_ID = { type: 'string', description: 'Adjustment layer id from get_adjustments (e.g. \"adj_1\").' } as const;\n\nconst round = (n: number, digits = 2) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** Mean and the 1st / 99th percentiles of a 256-bin histogram. */\nfunction binStats(bins: ArrayLike<number>, samples: number) {\n if (!samples) return { mean: null, p1: null, p99: null };\n let sum = 0;\n let seen = 0;\n let p1: number | null = null;\n let p99: number | null = null;\n for (let i = 0; i < bins.length; i++) {\n sum += i * bins[i];\n seen += bins[i];\n if (p1 === null && seen >= samples * 0.01) p1 = i;\n if (p99 === null && seen >= samples * 0.99) p99 = i;\n }\n return { mean: round(sum / samples, 1), p1, p99 };\n}\n\n/** Sums 256 bins into `count` equal buckets. */\nfunction rebin(bins: ArrayLike<number>, count: number): number[] {\n const out = new Array<number>(count).fill(0);\n const width = bins.length / count;\n for (let i = 0; i < bins.length; i++) out[Math.floor(i / width)] += bins[i];\n return out;\n}\n\nfunction histogramSummary(h: ImageHistogram, bins: number | undefined) {\n const pct = (n: number) => (h.samples ? round((n / h.samples) * 100) : 0);\n const channels = { red: h.red, green: h.green, blue: h.blue, luminance: h.luminance };\n return {\n samples: h.samples,\n transparent: h.transparent,\n clipping: {\n shadowsPercent: { red: pct(h.shadows[0]), green: pct(h.shadows[1]), blue: pct(h.shadows[2]) },\n highlightsPercent: { red: pct(h.highlights[0]), green: pct(h.highlights[1]), blue: pct(h.highlights[2]) }\n },\n stats: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, binStats(v, h.samples)])),\n ...(bins ? { bins: Object.fromEntries(Object.entries(channels).map(([k, v]) => [k, rebin(v, bins)])) } : {})\n };\n}\n\n/**\n * Agent tools for the image editor's layers, geometry and history: `get_image_info`,\n * `get_histogram`, `update_adjustment_layer`, `remove_adjustment_layer`,\n * `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`, `flip_image`,\n * `undo_image` and `redo_image`. Each write is one undo step and returns what changed.\n */\nexport function createImageEditTools(controller: ImageEditorController): AgentTool[] {\n const layerOrThrow = (id: string) => {\n const layers = controller.getAdjustmentLayers();\n const index = layers.findIndex((l) => l.id === id);\n if (index === -1) {\n const ids = layers.map((l) => l.id);\n throw new Error(`Unknown adjustment layer \"${id}\". ${ids.length ? `Layers: ${ids.join(', ')}.` : 'There are no adjustment layers.'}`);\n }\n return { layer: layers[index], index };\n };\n const layerJson = (id: string) => JSON.parse(JSON.stringify(controller.getAdjustmentLayer(id)));\n const geometry = () => {\n const s = controller.getState();\n const region = s.crop ?? { x: 0, y: 0, ...s.imageDimensions };\n const out = getRotatedSize(region.width, region.height, s.transform);\n return {\n image: { ...s.imageDimensions },\n crop: s.crop ? { ...s.crop } : null,\n rotation: normalizeAngle(s.transform.rotate),\n flipH: s.transform.flipH,\n flipV: s.transform.flipV,\n output: { width: Math.round(out.width), height: Math.round(out.height) }\n };\n };\n const history = () => ({ canUndo: controller.canUndo, canRedo: controller.canRedo });\n const requireImage = () => {\n const { width, height } = controller.getState().imageDimensions;\n if (!width || !height) throw new Error('No image is loaded. Ask the user to open one first.');\n return { width, height };\n };\n\n const getImageInfo: AgentTool = {\n name: 'get_image_info',\n title: 'Read image geometry',\n description:\n 'Returns the source image size in pixels, the crop box (natural image pixels, or null for the whole image), rotation in degrees, flips, the output size after crop and rotation, the number of adjustment layers and whether undo / redo are available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Read image geometry', ...READ },\n handler: () => ({ ...geometry(), layers: controller.getAdjustmentLayers().length, ...history() })\n };\n\n const getHistogram: AgentTool = {\n name: 'get_histogram',\n title: 'Read the histogram',\n description:\n 'Returns the histogram of the last rendered preview (after adjustments and layers): per channel and luminance mean and 1st / 99th percentiles (0-255), and the percentage of pixels clipped to pure black or white per channel. ' +\n 'Use it to judge exposure and clipping numerically. Pass bins for the counts themselves. Fails before the first render: call render_preview first.',\n inputSchema: {\n type: 'object',\n properties: {\n bins: { type: 'integer', enum: [16, 32, 64, 256], description: 'Also return the counts in this many buckets per channel (default: no counts).' }\n },\n additionalProperties: false\n },\n annotations: { title: 'Read the histogram', ...READ },\n handler: ({ bins }) => {\n const h = controller.getHistogram();\n if (!h) throw new Error('No histogram yet: it comes from the rendered preview. Call render_preview, then try again.');\n return histogramSummary(h, bins);\n }\n };\n\n const updateLayer: AgentTool = {\n name: 'update_adjustment_layer',\n title: 'Change an adjustment layer',\n description:\n 'Shows or hides a layer, renames it, or changes its blend mode or opacity, as one undo step. Change its parameters with set_adjustments (\"layers.<id>.<control>\"). Returns the updated layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n enabled: { type: 'boolean', description: 'false hides the layer without deleting it.' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (see list_controls).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1.' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' }\n },\n required: ['layer'],\n additionalProperties: false\n },\n annotations: { title: 'Change an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, enabled, blend, opacity, name }) => {\n layerOrThrow(layer);\n if (enabled === undefined && blend === undefined && opacity === undefined && name === undefined) {\n throw new Error('Nothing to change. Pass enabled, blend, opacity or name.');\n }\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n controller.updateAdjustmentLayer(layer, { enabled, blend: blend as BlendMode | undefined, opacity, name });\n return { layer: layerJson(layer) };\n }\n };\n\n const removeLayer: AgentTool = {\n name: 'remove_adjustment_layer',\n title: 'Delete an adjustment layer',\n description: 'Deletes an adjustment layer and its mask as one undo step. To hide it instead, use update_adjustment_layer with enabled false. Returns the remaining layer ids, bottom to top.',\n inputSchema: { type: 'object', properties: { layer: LAYER_ID }, required: ['layer'], additionalProperties: false },\n annotations: { title: 'Delete an adjustment layer', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },\n handler: ({ layer }) => {\n layerOrThrow(layer);\n controller.removeAdjustmentLayer(layer);\n return { removed: layer, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const moveLayer: AgentTool = {\n name: 'move_adjustment_layer',\n title: 'Reorder an adjustment layer',\n description:\n 'Moves an adjustment layer to a position in the stack (0 = bottom, applied first) as one undo step. Layers apply bottom to top, so order changes the result. Returns the layer ids, bottom to top.',\n inputSchema: {\n type: 'object',\n properties: {\n layer: LAYER_ID,\n to: { type: 'integer', minimum: 0, description: 'New index, 0 = bottom; past the top means the top.' }\n },\n required: ['layer', 'to'],\n additionalProperties: false\n },\n annotations: { title: 'Reorder an adjustment layer', ...WRITE_IDEMPOTENT },\n handler: ({ layer, to }) => {\n const { index } = layerOrThrow(layer);\n const top = controller.getAdjustmentLayers().length - 1;\n const target = Math.min(to, top);\n if (target === index) throw new Error(`Layer \"${layer}\" is already at index ${index} (0 = bottom, ${top} = top).`);\n controller.moveAdjustmentLayer(layer, target);\n return { layer, index: target, layers: controller.getAdjustmentLayers().map((l) => l.id) };\n }\n };\n\n const setCrop: AgentTool = {\n name: 'set_crop',\n title: 'Crop the image',\n description:\n 'Sets the crop box in natural image pixels of the un-rotated source (origin top-left; see get_image_info for the size) as one undo step. Non-destructive: reset_crop restores the whole image. Returns the new geometry and output size.',\n inputSchema: {\n type: 'object',\n properties: {\n x: { type: 'number', minimum: 0, description: 'Left edge in source pixels.' },\n y: { type: 'number', minimum: 0, description: 'Top edge in source pixels.' },\n width: { type: 'number', exclusiveMinimum: 0, description: 'Width in source pixels.' },\n height: { type: 'number', exclusiveMinimum: 0, description: 'Height in source pixels.' }\n },\n required: ['x', 'y', 'width', 'height'],\n additionalProperties: false\n },\n annotations: { title: 'Crop the image', ...WRITE_IDEMPOTENT },\n handler: ({ x, y, width, height }) => {\n const image = requireImage();\n if (x + width > image.width + 0.5 || y + height > image.height + 0.5) {\n throw new Error(`The crop box (${x}, ${y}, ${width} × ${height}) goes outside the ${image.width} × ${image.height} image.`);\n }\n controller.setCrop({ x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) });\n return geometry();\n }\n };\n\n const resetCrop: AgentTool = {\n name: 'reset_crop',\n title: 'Remove the crop',\n description: 'Removes the crop so the whole image shows again, as one undo step. Returns the new geometry.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Remove the crop', ...WRITE_IDEMPOTENT },\n handler: () => {\n if (!controller.getState().crop) throw new Error('The image is not cropped.');\n controller.resetCrop();\n return geometry();\n }\n };\n\n const setRotation: AgentTool = {\n name: 'set_rotation',\n title: 'Rotate the image',\n description:\n 'Sets the image rotation in degrees clockwise, as one undo step. Quarter turns (90, 180, 270) keep every pixel; other angles straighten the image inside a larger frame with transparent corners, so crop afterwards. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { degrees: { type: 'number', minimum: -360, maximum: 360, description: 'Absolute rotation in degrees clockwise (0 = upright).' } },\n required: ['degrees'],\n additionalProperties: false\n },\n annotations: { title: 'Rotate the image', ...WRITE_IDEMPOTENT },\n handler: ({ degrees }) => {\n requireImage();\n controller.setRotation(degrees);\n return geometry();\n }\n };\n\n const flipImage: AgentTool = {\n name: 'flip_image',\n title: 'Flip the image',\n description: 'Mirrors the image horizontally (left-right) or vertically (top-bottom), as one undo step. Flipping twice restores it. Returns the new geometry.',\n inputSchema: {\n type: 'object',\n properties: { axis: { type: 'string', enum: ['horizontal', 'vertical'], description: 'horizontal mirrors left-right; vertical mirrors top-bottom.' } },\n required: ['axis'],\n additionalProperties: false\n },\n annotations: { title: 'Flip the image', ...WRITE },\n handler: ({ axis }) => {\n requireImage();\n if (axis === 'horizontal') controller.flipHorizontal();\n else controller.flipVertical();\n return geometry();\n }\n };\n\n const undo: AgentTool = {\n name: 'undo_image',\n title: 'Undo an image edit',\n description: 'Undoes the last image edit (one tool call is one step). Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Undo an image edit', ...WRITE },\n handler: () => {\n if (!controller.undo()) throw new Error('Nothing to undo.');\n return history();\n }\n };\n\n const redo: AgentTool = {\n name: 'redo_image',\n title: 'Redo an image edit',\n description: 'Redoes the last undone image edit. Returns whether undo and redo are still available.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Redo an image edit', ...WRITE },\n handler: () => {\n if (!controller.redo()) throw new Error('Nothing to redo.');\n return history();\n }\n };\n\n return [getImageInfo, getHistogram, updateLayer, removeLayer, moveLayer, setCrop, resetCrop, setRotation, flipImage, undo, redo];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { contentBounds } from '../raster/surface';\nimport { findLayer, type ImageLayer, type LayerFilter, type RasterMask } from './layers/model';\nimport { LayerOperationError, requireLayer, type LayerPlacement } from './layers/ops';\nimport { adjustmentKindLabel } from './adjustment-layers';\nimport { BLEND_MODES } from './blend';\nimport type { BlendMode } from './blend';\nimport type { CropBox } from './types';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst round = (n: number, digits = 3) => Math.round(n * 10 ** digits) / 10 ** digits;\n\n/** A short description of a mask for agents (no pixels). */\nexport function maskSummary(mask: RasterMask | undefined): Record<string, unknown> | null {\n if (!mask) return null;\n const out: Record<string, unknown> = {\n kind: mask.source ? 'depth' : 'raster',\n enabled: mask.enabled !== false,\n density: mask.density ?? 1,\n feather: mask.feather ?? 0,\n };\n if (!mask.source) {\n out.defaultValue = mask.defaultValue;\n out.paintedBounds = contentBounds(mask.surface);\n }\n return out;\n}\n\n/**\n * A plain description of a layer (and its children) for agents: ids, types, names, document\n * bounds, visibility, opacity, fill, blend, clipping, locks, mask summary and whether it is\n * active. `boundsOf` gives a pixel layer's document bounds.\n */\nexport function describeLayer(\n layer: ImageLayer,\n activeId: string | null,\n boundsOf: (layer: ImageLayer) => CropBox | null\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n id: layer.id,\n type: layer.type,\n name: layer.name || (layer.type === 'adjustment' ? adjustmentKindLabel(layer.kind) : ''),\n visible: layer.visible,\n opacity: round(layer.opacity),\n blend: layer.blend,\n clipped: Boolean(layer.clipped),\n locks: layer.locks ? { ...layer.locks } : {},\n mask: maskSummary(layer.mask),\n active: layer.id === activeId,\n };\n switch (layer.type) {\n case 'pixel':\n out.bounds = boundsOf(layer);\n out.offset = { ...layer.offset };\n out.fillOpacity = round(layer.fillOpacity ?? 1);\n out.filters = (layer.filters ?? []).map((f) => ({ ...f }));\n if (layer.source === 'document') out.source = 'document';\n break;\n case 'adjustment':\n out.kind = layer.kind;\n out.params = JSON.parse(JSON.stringify(layer.params));\n break;\n case 'annotation':\n out.annotation = { id: layer.annotation.id, type: layer.annotation.type };\n break;\n case 'group':\n out.collapsed = Boolean(layer.collapsed);\n out.children = layer.children.map((c) => describeLayer(c, activeId, boundsOf));\n break;\n }\n return out;\n}\n\nconst LAYER_ID = { type: 'string', description: 'Layer id from list_layers.' } as const;\nconst PLACEMENT = {\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (same group).' },\n belowId: { type: 'string', description: 'Put the layer directly below this layer (same group).' },\n parentId: { type: 'string', description: 'Put the layer inside this group (at index, default its top).' },\n index: { type: 'integer', minimum: 0, description: 'Position among the siblings, 0 = bottom (with parentId, or at the top level).' },\n} as const;\nconst BLEND = { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' } as const;\n\nfunction placementFrom(args: Record<string, unknown>): LayerPlacement {\n const p: LayerPlacement = {};\n if (typeof args.aboveId === 'string') p.aboveId = args.aboveId;\n if (typeof args.belowId === 'string') p.belowId = args.belowId;\n if (typeof args.parentId === 'string') p.parentId = args.parentId;\n if (typeof args.index === 'number') p.index = args.index;\n return p;\n}\n\n/**\n * Agent tools for the image editor's layer stack: `list_layers`, `set_active_layer`,\n * `add_pixel_layer`, `duplicate_layer`, `remove_layer`, `move_layer`, `set_layer_properties`,\n * `group_layers`, `ungroup_layer`, `merge_down`, `merge_visible`, `flatten_image`, `offset_layer`,\n * `add_layer_mask`, `apply_layer_mask`, `remove_layer_mask`, `set_layer_mask_properties`,\n * `layer_via_copy`, `add_layer_filter`, `update_layer_filter` and `remove_layer_filter`. Each write\n * is one undo step, validates before changing anything and returns what it changed.\n */\nexport function createImageLayerTools(controller: ImageEditorController): AgentTool[] {\n const describe = (id: string) => {\n const s = controller.getState();\n const loc = findLayer(s.layers, id);\n if (!loc) return null;\n return {\n ...describeLayer(loc.layer, s.activeLayerId, (x) => controller.getLayerBounds(x.id)),\n parentId: loc.parent?.id ?? null,\n index: loc.index,\n };\n };\n const exists = (id: unknown, what = 'layerId') => {\n if (typeof id !== 'string' || !id) throw new LayerOperationError(`${what} is required (a layer id from list_layers).`);\n requireLayer(controller.getLayers(), id);\n return id;\n };\n\n const listLayers: AgentTool = {\n name: 'list_layers',\n title: 'List image layers',\n description:\n 'Returns the layer stack bottom to top as a tree: id, type (pixel, adjustment, group, annotation), name, document bounds (pixel layers), visible, opacity, fillOpacity, blend, clipped, locks, a mask summary, filters (pixel layers), kind and params (adjustment layers), children (groups) and which layer is active. Also returns the active layer id and target (pixels or mask) and the canvas size.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'List image layers', ...READ },\n handler: () => {\n const s = controller.getState();\n return {\n canvas: { ...s.imageDimensions },\n activeLayerId: s.activeLayerId,\n activeTarget: s.activeTarget,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n };\n },\n };\n\n const setActive: AgentTool = {\n name: 'set_active_layer',\n title: 'Set the active layer',\n description:\n 'Makes a layer active (the base sliders of set_adjustments then edit its develop filter when it is a pixel layer) and chooses whether painting targets its pixels or its mask. View state, not an undo step. Returns the active layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Layer to activate (from list_layers).' },\n target: { type: 'string', enum: ['pixels', 'mask'], description: 'Paint target (default pixels; mask needs a mask).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set the active layer', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, target }) => {\n const id = exists(layerId);\n const layer = controller.getLayer(id)!;\n if (target === 'mask' && !layer.mask) throw new LayerOperationError(`Layer \"${layer.name || id}\" has no mask; add one with add_layer_mask.`);\n controller.setActiveLayer(id, (target as 'pixels' | 'mask' | undefined) ?? 'pixels');\n return { layer: describe(id), activeTarget: controller.getState().activeTarget };\n },\n };\n\n const addPixelLayer: AgentTool = {\n name: 'add_pixel_layer',\n title: 'Add a pixel layer',\n description:\n 'Adds a pixel layer and makes it active: empty (default), filled with color over the canvas, or holding an image (PNG data URL or base64 PNG; other formats and URLs only in a browser) placed at x, y. Goes above the active layer unless aboveId / belowId / parentId / index is given. One undo step. Returns the new layer with its id, bounds and position.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Layer name (default \"Layer N\").' },\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa; omit for an empty layer.' },\n image: { type: 'string', description: 'Image to place: PNG data URL or base64 PNG (or any URL in a browser).' },\n x: { type: 'integer', description: 'Left of the image in document pixels (default 0).' },\n y: { type: 'integer', description: 'Top of the image in document pixels (default 0).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1 (default 1).' },\n blend: BLEND,\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add a pixel layer', ...WRITE },\n handler: async (args) => {\n if (args.color !== undefined && args.image !== undefined) throw new LayerOperationError('Give color or image, not both.');\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n const init = {\n name: args.name as string | undefined,\n x: args.x as number | undefined,\n y: args.y as number | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n ...placementFrom(args),\n };\n const id =\n typeof args.image === 'string'\n ? await controller.addPixelLayerFromImage(args.image, init)\n : controller.addPixelLayer({ ...init, color: args.color as string | undefined });\n return { layer: describe(id) };\n },\n };\n\n const duplicate: AgentTool = {\n name: 'duplicate_layer',\n title: 'Duplicate a layer',\n description: 'Duplicates a layer (groups with their children) directly above it and makes the copy active. One undo step. Returns the copy with its new id.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, name: { type: 'string', description: 'Name of the copy (default \"<name> copy\").' } },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Duplicate a layer', ...WRITE },\n handler: ({ layerId, name }) => ({ layer: describe(controller.duplicateLayer(exists(layerId), name as string | undefined)) }),\n };\n\n const remove: AgentTool = {\n name: 'remove_layer',\n title: 'Delete a layer',\n description:\n 'Deletes a layer (a group with its children). Refused for a layer locked with all. Prefer hiding it (set_layer_properties visible false) when it may be wanted back. One undo step. Returns the removed id and the remaining top-level ids.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayer(id);\n return { removed: id, layers: controller.getLayers().map((l) => l.id) };\n },\n };\n\n const move: AgentTool = {\n name: 'move_layer',\n title: 'Move a layer in the stack',\n description:\n 'Moves a layer above or below another layer (aboveId / belowId, in that layer\\'s group), into a group (parentId, optional index), or to an index at the top level. Give exactly one of aboveId, belowId, parentId, or index alone. One undo step. Returns the new parentId and index.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, ...PLACEMENT },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer in the stack', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n return { layerId: id, ...controller.moveLayer(id, placementFrom(args)) };\n },\n };\n\n const setProps: AgentTool = {\n name: 'set_layer_properties',\n title: 'Set layer properties',\n description:\n 'Changes a layer\\'s name, visible, opacity (0..1), fillOpacity (0..1, pixel layers: fades the pixels but not masks), blend (pass-through is for groups), clipped (clip to the layer below), locks ({pixels, transparency, position, all}) or collapsed (groups). Only the given fields change. Refused (except visible, locks and collapsed) on a layer locked with all. One undo step. Returns the layer after the change.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n name: { type: 'string', description: 'New name.' },\n visible: { type: 'boolean', description: 'Show (true) or hide (false).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0..1.' },\n fillOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill 0..1 (pixel layers).' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode; pass-through for groups only.' },\n clipped: { type: 'boolean', description: 'Clip to the nearest unclipped layer below.' },\n locks: {\n type: 'object',\n description: 'Locks to set (missing keys are unlocked).',\n properties: {\n pixels: { type: 'boolean', description: 'Refuse painting and filling.' },\n transparency: { type: 'boolean', description: 'Painting keeps alpha.' },\n position: { type: 'boolean', description: 'Refuse moving.' },\n all: { type: 'boolean', description: 'Everything, including properties and deletion.' },\n },\n additionalProperties: false,\n },\n collapsed: { type: 'boolean', description: 'Collapse a group in the layers panel.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer properties', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const props: Record<string, unknown> = {};\n for (const k of ['name', 'visible', 'opacity', 'fillOpacity', 'blend', 'clipped', 'locks', 'collapsed']) if (args[k] !== undefined) props[k] = args[k];\n if (!Object.keys(props).length) throw new LayerOperationError('Give at least one property to change.');\n controller.setLayerProperties(id, props);\n return { layer: describe(id) };\n },\n };\n\n const group: AgentTool = {\n name: 'group_layers',\n title: 'Group layers',\n description:\n 'Puts sibling layers (same group or top level) into a new pass-through group at the position of the topmost one, keeping their order. One undo step. Returns the group with its id and children.',\n inputSchema: {\n type: 'object',\n properties: {\n layerIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'Ids of the layers to group (from list_layers).' },\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n },\n required: ['layerIds'],\n additionalProperties: false,\n },\n annotations: { title: 'Group layers', ...WRITE },\n handler: ({ layerIds, name }) => {\n const ids = (layerIds as string[]).map((id) => exists(id, 'layerIds[]'));\n return { group: describe(controller.groupLayers(ids, name as string | undefined)) };\n },\n };\n\n const addGroup: AgentTool = {\n name: 'add_group',\n title: 'Add an empty group',\n description:\n 'Adds an empty group (pass-through unless blend is given) above the active layer, or at aboveId / belowId / parentId / index, and makes it active. Move layers into it with move_layer parentId, or use group_layers to group existing layers. One undo step. Returns the group.',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Group name (default \"Group\").' },\n blend: { type: 'string', enum: [...BLEND_MODES.map((m) => m.id), 'pass-through'], description: 'Blend mode (default pass-through).' },\n ...PLACEMENT,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Add an empty group', ...WRITE },\n handler: (args) => {\n for (const k of ['aboveId', 'belowId', 'parentId'] as const) if (args[k] !== undefined) exists(args[k], k);\n return { group: describe(controller.addGroup({ name: args.name as string | undefined, blend: args.blend as never, ...placementFrom(args) })) };\n },\n };\n\n const ungroup: AgentTool = {\n name: 'ungroup_layer',\n title: 'Ungroup a group',\n description: 'Replaces a group by its children at its position (the group\\'s own opacity, blend and mask are dropped). One undo step. Returns the child ids bottom to top.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'Group id.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Ungroup a group', ...WRITE },\n handler: ({ layerId }) => ({ childIds: controller.ungroupLayer(exists(layerId)) }),\n };\n\n const mergeDown: AgentTool = {\n name: 'merge_down',\n title: 'Merge a layer down',\n description:\n 'Composites a layer (pixel, adjustment, group or annotation) onto the pixel layer directly below it. The lower layer keeps its name, opacity and blend; masks, fill and filters are baked into its pixels. Refused when the layer below is not a pixel layer or is locked, or when the layer is hidden. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: { layerId: { ...LAYER_ID, description: 'The upper layer to merge down.' } }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Merge a layer down', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.mergeDown(exists(layerId))) }),\n };\n\n const mergeVisible: AgentTool = {\n name: 'merge_visible',\n title: 'Merge visible layers',\n description: 'Composites every visible top-level layer into one normal pixel layer at the position of the lowest of them; hidden layers stay. One undo step. Returns the merged layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Merge visible layers', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.mergeVisible()) }),\n };\n\n const flatten: AgentTool = {\n name: 'flatten_image',\n title: 'Flatten the image',\n description: 'Composites every visible layer into one Background pixel layer and discards hidden layers. Crop, rotation and output opacity stay. One undo step. Returns the Background layer.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Flatten the image', ...DESTRUCTIVE },\n handler: async () => ({ layer: describe(await controller.flattenImage()) }),\n };\n\n const offset: AgentTool = {\n name: 'offset_layer',\n title: 'Move a layer\\'s pixels',\n description:\n 'Moves a pixel layer (and its mask) to an absolute offset x, y or by dx, dy, in document pixels (integers). Refused when its position is locked. One undo step. Returns the new offset and document bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n x: { type: 'integer', description: 'New horizontal offset in document pixels.' },\n y: { type: 'integer', description: 'New vertical offset in document pixels.' },\n dx: { type: 'integer', description: 'Move right by this many pixels (negative: left).' },\n dy: { type: 'integer', description: 'Move down by this many pixels (negative: up).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Move a layer\\'s pixels', ...WRITE },\n handler: ({ layerId, x, y, dx, dy }) => {\n const id = exists(layerId);\n if ([x, y, dx, dy].every((v) => v === undefined)) throw new LayerOperationError('Give x and y, or dx and dy.');\n if ((x !== undefined || y !== undefined) && (dx !== undefined || dy !== undefined)) throw new LayerOperationError('Give x / y or dx / dy, not both.');\n return { layerId: id, ...controller.offsetLayer(id, { x: x as number | undefined, y: y as number | undefined, dx: dx as number | undefined, dy: dy as number | undefined }) };\n },\n };\n\n const addMask: AgentTool = {\n name: 'add_layer_mask',\n title: 'Add a layer mask',\n description:\n 'Adds a mask to any layer, replacing an existing one: from reveal-all (white, paint black to hide), hide-all (black, paint white to reveal) or selection (the selection\\'s coverage). density 0..1 and feather px soften it non-destructively. One undo step. Returns the mask summary.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n from: { type: 'string', enum: ['reveal-all', 'hide-all', 'selection'], description: 'What the mask starts as (default reveal-all).' },\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Mask density 0..1 (default 1).' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px (default 0).' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer mask', ...WRITE },\n handler: ({ layerId, from, density, feather }) => {\n const id = exists(layerId);\n const mask = controller.addLayerMask(id, (from as 'reveal-all' | 'hide-all' | 'selection' | undefined) ?? 'reveal-all', {\n density: density as number | undefined,\n feather: feather as number | undefined,\n });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const applyMask: AgentTool = {\n name: 'apply_layer_mask',\n title: 'Apply a layer mask',\n description: \"Bakes a pixel layer's mask into its pixels (alpha × coverage, including density and feather) and removes the mask. Pixel layers only; refused when its pixels are locked. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Apply a layer mask', ...DESTRUCTIVE },\n handler: async ({ layerId }) => {\n const id = exists(layerId);\n await controller.applyLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const removeMask: AgentTool = {\n name: 'remove_layer_mask',\n title: 'Delete a layer mask',\n description: \"Deletes a layer's mask without applying it (the layer shows everywhere again). Disable it instead with set_layer_mask_properties enabled false to keep it. One undo step. Returns the layer.\",\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Delete a layer mask', ...DESTRUCTIVE },\n handler: ({ layerId }) => {\n const id = exists(layerId);\n controller.removeLayerMask(id);\n return { layer: describe(id) };\n },\n };\n\n const maskProps: AgentTool = {\n name: 'set_layer_mask_properties',\n title: 'Set layer mask properties',\n description: \"Changes a layer mask's density (0..1), feather (px) or enabled flag; its pixels are kept. One undo step. Returns the mask summary.\",\n inputSchema: {\n type: 'object',\n properties: {\n layerId: LAYER_ID,\n density: { type: 'number', minimum: 0, maximum: 1, description: 'Density 0..1.' },\n feather: { type: 'number', minimum: 0, maximum: 1000, description: 'Feather σ in px.' },\n enabled: { type: 'boolean', description: 'false ignores the mask but keeps it.' },\n },\n required: ['layerId'],\n additionalProperties: false,\n },\n annotations: { title: 'Set layer mask properties', ...WRITE_IDEMPOTENT },\n handler: ({ layerId, density, feather, enabled }) => {\n const id = exists(layerId);\n if ([density, feather, enabled].every((v) => v === undefined)) throw new LayerOperationError('Give density, feather or enabled.');\n const mask = controller.setLayerMaskProps(id, { density: density as number | undefined, feather: feather as number | undefined, enabled: enabled as boolean | undefined });\n return { layerId: id, mask: maskSummary(mask) };\n },\n };\n\n const viaCopy: AgentTool = {\n name: 'layer_via_copy',\n title: 'New layer from the selection',\n description:\n 'Layer via Copy: copies the selected pixels of a pixel layer (layerId, default the active layer) to a new layer directly above it and makes it active. cut: true also removes them from the source (Layer via Cut). Needs a selection. One undo step. Returns the new layer.',\n inputSchema: {\n type: 'object',\n properties: {\n layerId: { ...LAYER_ID, description: 'Pixel layer to copy from (default the active layer).' },\n cut: { type: 'boolean', description: 'Remove the pixels from the source too (default false).' },\n name: { type: 'string', description: 'Name of the new layer.' },\n },\n additionalProperties: false,\n },\n annotations: { title: 'New layer from the selection', ...WRITE },\n handler: ({ layerId, cut, name }) => {\n if (layerId !== undefined) exists(layerId);\n return { layer: describe(controller.layerViaCopy({ layerId: layerId as string | undefined, cut: cut === true, name: name as string | undefined })) };\n },\n };\n\n const rasterize: AgentTool = {\n name: 'rasterize_layer',\n title: 'Rasterize a layer',\n description:\n 'Rasterize Layer: turns an annotation layer into a pixel layer (same id, name, opacity, blend and mask; needs a browser) or bakes a pixel layer\\'s filters (develop, blur) into its pixels. One undo step. Returns the layer.',\n inputSchema: { type: 'object', properties: { layerId: LAYER_ID }, required: ['layerId'], additionalProperties: false },\n annotations: { title: 'Rasterize a layer', ...DESTRUCTIVE },\n handler: async ({ layerId }) => ({ layer: describe(await controller.rasterizeLayer(exists(layerId))) }),\n };\n\n const filterArgs = {\n enabled: { type: 'boolean', description: 'false keeps the filter but skips it.' },\n radius: { type: 'number', minimum: 0, maximum: 50, description: 'blur: box radius in px (default 2).' },\n brightness: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n contrast: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n saturation: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n exposure: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n temperature: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 (default 0).' },\n tint: { type: 'number', minimum: -100, maximum: 100, description: 'develop: -100..100 green to magenta (default 0).' },\n } as const;\n const filterValues = (args: Record<string, unknown>) => {\n const out: Record<string, unknown> = {};\n for (const k of Object.keys(filterArgs)) if (args[k] !== undefined) out[k] = args[k];\n return out;\n };\n\n const addFilter: AgentTool = {\n name: 'add_layer_filter',\n title: 'Add a layer filter',\n description:\n 'Adds a live filter to a pixel layer: develop (the base sliders: brightness, contrast, saturation, exposure, temperature, tint) or blur (radius px). One of each kind per layer, applied bottom to top before the mask. One undo step. Returns the layer\\'s filters with their ids.',\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, kind: { type: 'string', enum: ['develop', 'blur'], description: 'Filter kind.' }, ...filterArgs },\n required: ['layerId', 'kind'],\n additionalProperties: false,\n },\n annotations: { title: 'Add a layer filter', ...WRITE },\n handler: (args) => {\n const id = exists(args.layerId);\n const filters = controller.addLayerFilter(id, { kind: args.kind as LayerFilter['kind'], ...filterValues(args) } as never);\n return { layerId: id, filters };\n },\n };\n\n const updateFilter: AgentTool = {\n name: 'update_layer_filter',\n title: 'Change a layer filter',\n description: \"Changes one filter of a pixel layer (only the given values; the kind cannot change). One undo step. Returns the layer's filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers or add_layer_filter.' }, ...filterArgs },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Change a layer filter', ...WRITE_IDEMPOTENT },\n handler: (args) => {\n const id = exists(args.layerId);\n const changes = filterValues(args);\n if (!Object.keys(changes).length) throw new LayerOperationError('Give at least one value to change.');\n return { layerId: id, filters: controller.updateLayerFilter(id, args.filterId as string, changes) };\n },\n };\n\n const removeFilter: AgentTool = {\n name: 'remove_layer_filter',\n title: 'Remove a layer filter',\n description: \"Removes one filter from a pixel layer. One undo step. Returns the layer's remaining filters.\",\n inputSchema: {\n type: 'object',\n properties: { layerId: LAYER_ID, filterId: { type: 'string', description: 'Filter id from list_layers.' } },\n required: ['layerId', 'filterId'],\n additionalProperties: false,\n },\n annotations: { title: 'Remove a layer filter', ...DESTRUCTIVE },\n handler: ({ layerId, filterId }) => {\n const id = exists(layerId);\n return { layerId: id, filters: controller.removeLayerFilter(id, filterId as string) };\n },\n };\n\n return [\n listLayers,\n setActive,\n addPixelLayer,\n duplicate,\n remove,\n move,\n setProps,\n group,\n addGroup,\n ungroup,\n mergeDown,\n mergeVisible,\n flatten,\n offset,\n addMask,\n applyMask,\n removeMask,\n maskProps,\n viaCopy,\n rasterize,\n addFilter,\n updateFilter,\n removeFilter,\n ];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { BLEND_MODES, type BlendMode } from './blend';\nimport { MAX_BRUSH_SIZE, type BrushSettings, type StrokePoint } from '../paint';\nimport type { LayerTarget } from './layers/model';\n\nconst WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst TARGET = {\n layerId: { type: 'string', description: 'Layer to paint (default the active layer; see list_layers).' },\n channel: { type: 'string', enum: ['pixels', 'mask'], description: \"pixels (default for a pixel layer) or the layer's mask: black hides, white reveals.\" },\n} as const;\n\nconst BRUSH = {\n type: 'object',\n description: 'Brush settings; missing fields use the current paint options (default round, hard, 20 px).',\n properties: {\n size: { type: 'number', minimum: 1, maximum: MAX_BRUSH_SIZE, description: 'Diameter in px (default 20).' },\n hardness: { type: 'number', minimum: 0, maximum: 1, description: 'Edge hardness 0 (soft) .. 1 (hard, default).' },\n spacing: { type: 'number', minimum: 0.01, maximum: 10, description: 'Dab spacing as a fraction of the diameter (default 0.1).' },\n angle: { type: 'number', minimum: -360, maximum: 360, description: 'Tip angle in degrees (default 0).' },\n roundness: { type: 'number', minimum: 0.01, maximum: 1, description: 'Tip roundness (default 1).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Stroke opacity: overlapping dabs never exceed it (default 1).' },\n flow: { type: 'number', minimum: 0, maximum: 1, description: 'Build-up per dab towards the opacity (default 1).' },\n smoothing: { type: 'number', minimum: 0, maximum: 0.95, description: 'Point smoothing 0..0.95 (default 0).' },\n pressureSize: { type: 'boolean', description: 'Point pressure scales the size (default true).' },\n pressureOpacity: { type: 'boolean', description: 'Point pressure scales the opacity (default false).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'How paint combines with the layer (default normal).' },\n },\n additionalProperties: false,\n} as const;\n\nfunction readTarget(args: Record<string, unknown>): { layerId?: string; channel?: LayerTarget } {\n return { layerId: args.layerId as string | undefined, channel: args.channel as LayerTarget | undefined };\n}\n\n/**\n * Agent tools for painting: `paint_stroke` (brush or eraser along points, on pixels or a mask),\n * `fill_pixels` and `clear_pixels` (Edit › Fill / Clear inside the selection). Each is one undo\n * step and returns the layer, channel and changed bounds.\n */\nexport function createImagePaintTools(controller: ImageEditorController): AgentTool[] {\n const paintStroke: AgentTool = {\n name: 'paint_stroke',\n title: 'Paint a brush stroke',\n description:\n 'Paints one stroke with the brush (or removes pixels with the eraser) through points in document pixels (optional pressure 0..1 each) on a pixel layer or a layer mask. The selection limits the paint; a transparency-locked layer keeps its alpha (the eraser then paints the background colour). One undo step. Returns the layerId, channel, changed bounds and dab count.',\n inputSchema: {\n type: 'object',\n properties: {\n tool: { type: 'string', enum: ['brush', 'eraser'], description: 'brush (default) or eraser.' },\n points: {\n type: 'array',\n minItems: 1,\n maxItems: 10000,\n description: 'The path in document pixels; one point paints one dab.',\n items: {\n type: 'object',\n properties: {\n x: { type: 'number', description: 'Document x in px.' },\n y: { type: 'number', description: 'Document y in px.' },\n pressure: { type: 'number', minimum: 0, maximum: 1, description: 'Pen pressure (default 1).' },\n },\n required: ['x', 'y'],\n additionalProperties: false,\n },\n },\n color: { type: 'string', description: 'Paint colour #rrggbb (default the foreground). On a mask its grey value: #000000 hides, #ffffff reveals.' },\n brush: BRUSH,\n ...TARGET,\n },\n required: ['points'],\n additionalProperties: false,\n },\n annotations: { title: 'Paint a brush stroke', ...WRITE },\n handler: (args) =>\n controller.paintStroke({\n tool: args.tool as 'brush' | 'eraser' | undefined,\n points: args.points as StrokePoint[],\n color: args.color as string | undefined,\n brush: args.brush as Partial<BrushSettings> | undefined,\n ...readTarget(args),\n }),\n };\n\n const fill: AgentTool = {\n name: 'fill_pixels',\n title: 'Fill pixels',\n description:\n 'Edit › Fill: fills the selection (or the whole canvas without one, or with useSelection false) on a pixel layer or a mask with a colour (default the foreground) at opacity, with a blend mode. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n color: { type: 'string', description: 'Fill colour #rrggbb or #rrggbbaa (default the foreground).' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Fill opacity 0..1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Fill pixels', ...WRITE },\n handler: (args) =>\n controller.fillPixels({\n color: args.color as string | undefined,\n opacity: args.opacity as number | undefined,\n blend: args.blend as BlendMode | undefined,\n useSelection: args.useSelection as boolean | undefined,\n ...readTarget(args),\n }),\n };\n\n const clear: AgentTool = {\n name: 'clear_pixels',\n title: 'Clear pixels',\n description:\n 'Edit › Clear: removes the selected pixels of a pixel layer (or, without a selection or with useSelection false, all of them); on a transparency-locked layer or a mask it fills with the background colour instead. One undo step. Returns the layerId, channel and changed bounds.',\n inputSchema: {\n type: 'object',\n properties: {\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'How much to clear 0..1 (default 1).' },\n useSelection: { type: 'boolean', description: 'Limit to the selection when there is one (default true).' },\n ...TARGET,\n },\n additionalProperties: false,\n },\n annotations: { title: 'Clear pixels', ...DESTRUCTIVE },\n handler: (args) =>\n controller.clearPixels({ opacity: args.opacity as number | undefined, useSelection: args.useSelection as boolean | undefined, ...readTarget(args) }),\n };\n\n return [paintStroke, fill, clear];\n}\n","import type { AgentTool } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport { describeLayer } from './layer-tools';\nimport { base64ToBytes, bytesToBase64 } from '../raster/png';\nimport { PsdError } from './psd/binary';\nimport { importPsd } from './psd/import';\n\nconst READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false } as const;\nconst DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false } as const;\n\nconst DATA = {\n type: 'string',\n minLength: 1,\n description: 'The PSD or PSB file as base64 (a `data:` URL prefix is accepted).',\n} as const;\n\n/** Base64 (or a data URL) → bytes, refusing anything that is not base64 text. */\nexport function psdBytesFromBase64(data: unknown): Uint8Array {\n if (typeof data !== 'string' || !data.trim()) throw new PsdError('data is required: the PSD file as base64.');\n const body = data.trim().replace(/^data:[^,]*;base64,/i, '');\n if (!/^[A-Za-z0-9+/=_\\s-]+$/.test(body)) throw new PsdError('data is not base64; send the PSD file bytes base64-encoded.');\n const bytes = base64ToBytes(body);\n if (bytes.length < 26) throw new PsdError(`data decodes to ${bytes.length} bytes, too short for a PSD (at least 26).`);\n return bytes;\n}\n\n/**\n * Agent tools for Photoshop files: `inspect_psd` (what a file holds and how it would map),\n * `import_psd` (open it as the document) and `export_psd` (the document as a PSD).\n */\nexport function createImagePsdTools(controller: ImageEditorController): AgentTool[] {\n const inspect: AgentTool = {\n name: 'inspect_psd',\n title: 'Inspect a PSD',\n description:\n 'Reads a PSD / PSB without opening it: size, bit depth, colour mode, resolution, and every layer (bottom first, a group before its children) with its PSD kind, the layer type it would become and its status (mapped, approximated or dropped, with notes), plus the import warnings. Changes nothing. Call before import_psd to see what would be lost.',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Inspect a PSD', ...READ },\n handler: ({ data }) => {\n const res = importPsd(psdBytesFromBase64(data), { skipPixels: true });\n return {\n width: res.width,\n height: res.height,\n ...res.info,\n resolution: res.resolution ?? null,\n layers: res.summary.map(({ id: _id, ...s }) => s),\n warnings: res.warnings,\n };\n },\n };\n\n const open: AgentTool = {\n name: 'import_psd',\n title: 'Open a PSD as the document',\n description:\n 'Opens a PSD / PSB as the document, replacing the current layers, crop and rotation (one undo step; undo brings the previous document back). Pixel, group and adjustment layers, masks, blend modes, opacity, fill, clipping, visibility and locks map directly; type, smart-object, fill and shape layers come in as their pixels; unsupported adjustments are left out. Returns the new layer tree with ids, the size and the warnings (each names the affected layers).',\n inputSchema: { type: 'object', properties: { data: DATA }, required: ['data'], additionalProperties: false },\n annotations: { title: 'Open a PSD as the document', ...DESTRUCTIVE },\n handler: ({ data }) => {\n const res = controller.loadPsd(psdBytesFromBase64(data));\n const s = controller.getState();\n return {\n width: res.width,\n height: res.height,\n layerIds: res.layerIds,\n layers: s.layers.map((l) => describeLayer(l, s.activeLayerId, (x) => controller.getLayerBounds(x.id))),\n activeLayerId: s.activeLayerId,\n warnings: res.warnings,\n };\n },\n };\n\n const save: AgentTool = {\n name: 'export_psd',\n title: 'Export the document as a PSD',\n description:\n 'Writes the document as an 8-bit RGB PSD (a PSB above 30000 px) with real layers, groups, adjustment layers and masks, in the source frame (crop and rotation are not applied). Layer filters and depth masks are baked, annotation layers are rasterized (in a browser; elsewhere left out), and the merged image is the editor\\'s composite. Changes nothing. Returns the file as base64, its size in bytes, whether it is a PSB, and warnings for everything that was changed or left out.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: { title: 'Export the document as a PSD', ...READ },\n handler: async () => {\n const res = await controller.toPsd();\n return {\n data: bytesToBase64(res.bytes),\n mimeType: 'image/vnd.adobe.photoshop',\n size: res.bytes.length,\n psb: res.psb,\n width: res.width,\n height: res.height,\n warnings: res.warnings,\n };\n },\n };\n\n return [inspect, open, save];\n}\n","import type { AgentTool, AgentToolProvider } from '../tools/registry.js';\nimport type { ImageEditorController } from './controller';\nimport type { AdjustmentKind, ImageAdjustments } from './types';\nimport { normalizeProcessVersion } from './color';\nimport {\n ADJUSTMENT_KINDS,\n isAdjustmentKind,\n} from './adjustment-layers';\nimport { BLEND_MODES, isBlendMode } from './blend';\nimport {\n ADJUSTMENT_LAYER_CONTROLS,\n ADJUSTMENT_LAYER_OPACITY_CONTROL,\n IMAGE_ADJUSTMENT_CONTROLS,\n clampControlValue,\n findAdjustmentControl,\n findLayerControl,\n readAdjustmentControls,\n setControlPath,\n} from './controls';\nimport { createImageSelectionTools } from './selection-tools';\nimport { createImageCaptureSurface } from './capture';\nimport { createCaptureTool } from '../tools/capture.js';\nimport { createImageEditTools } from './edit-tools';\nimport { createImageLayerTools } from './layer-tools';\nimport { createImagePaintTools } from './paint-tools';\nimport { createPigmentTools } from '../paint/pigment-tools';\nimport { createImagePsdTools } from './psd-tools';\n\nexport interface ImageEditorToolProviderOptions {\n /** Provider id (unique per registry). Default `image-editor`. */\n id?: string;\n /** Always prefix tool names with `<namespace>__`. */\n namespace?: string;\n}\n\nconst LAYER_PATH = /^layers\\.([^.]+)\\.(.+)$/;\n\nfunction adjustmentsSummary(adjustments: ImageAdjustments) {\n return {\n process: normalizeProcessVersion(adjustments.process),\n values: readAdjustmentControls(adjustments),\n layers: (adjustments.layers ?? []).map((l) => JSON.parse(JSON.stringify(l))),\n };\n}\n\n/**\n * Agent tools for an image editor, for a `ToolRegistry`:\n *\n * - `get_adjustments`: base slider values (by control id), process version and adjustment layers.\n * - `list_controls`: the control table (ids, ranges, defaults, steps), layer kinds and blend modes.\n * - `set_adjustments {values, label}`: sets base controls (`exposure`) and layer parameters\n * (`layers.<id>.vibrance`, `layers.<id>.opacity`) in one undo step; values are clamped to the\n * control ranges and the applied values are echoed.\n * - `add_adjustment_layer {kind, params?, opacity?, blend?, name?}`.\n * - `render_preview {maxSide}`: the rendered image as a PNG, longest side at most `maxSide`.\n * - `capture_view {region, rect?, ...}`: a screenshot of the whole image, the selection or a rect in\n * natural pixels (up to 8× for close inspection), optionally with the selection shaded.\n * - Selections and masks: `get_selection`, `select_region`, `select_by_color`, `modify_selection`,\n * `refine_selection`, `apply_selection_as_mask` (see `createImageSelectionTools`).\n * - Layers, geometry and history: `get_image_info`, `get_histogram`, `update_adjustment_layer`,\n * `remove_adjustment_layer`, `move_adjustment_layer`, `set_crop`, `reset_crop`, `set_rotation`,\n * `flip_image`, `undo_image`, `redo_image` (see `createImageEditTools`).\n */\nexport function createImageEditorToolProvider(\n controller: ImageEditorController,\n options: ImageEditorToolProviderOptions = {}\n): AgentToolProvider {\n const getAdjustments: AgentTool = {\n name: 'get_adjustments',\n title: 'Read image adjustments',\n description:\n 'Returns the image editor adjustment values by control id, the rendering process version and the adjustment layers (bottom to top).',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'Read image adjustments',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => adjustmentsSummary(controller.getState().adjustments),\n };\n\n const listControls: AgentTool = {\n name: 'list_controls',\n title: 'List adjustment controls',\n description:\n 'Lists every adjustment control (id, label, section, min, max, step, default), the parameters of each adjustment-layer kind, and the blend modes.',\n inputSchema: { type: 'object', properties: {}, additionalProperties: false },\n annotations: {\n title: 'List adjustment controls',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: () => ({\n controls: IMAGE_ADJUSTMENT_CONTROLS.map(({ id, label, section, min, max, step, default: def, requiresProcess }) => ({\n id, label, section, min, max, step, default: def, ...(requiresProcess ? { requiresProcess } : {}),\n })),\n layerKinds: ADJUSTMENT_KINDS.map(({ kind, label }) => ({\n kind,\n label,\n controls: [...ADJUSTMENT_LAYER_CONTROLS[kind], ADJUSTMENT_LAYER_OPACITY_CONTROL].map(({ id, label: l, min, max, step, default: def }) => ({\n id, label: l, min, max, step, default: def,\n })),\n })),\n blendModes: BLEND_MODES.map((m) => m.id),\n layerPathFormat: 'layers.<layerId>.<controlId>',\n }),\n };\n\n const setAdjustments: AgentTool = {\n name: 'set_adjustments',\n title: 'Set image adjustments',\n description:\n 'Sets adjustment values by control id in one undoable step. Base controls use their id (e.g. \"exposure\"); layer parameters use \"layers.<layerId>.<controlId>\" (e.g. \"layers.adj_1.vibrance\"). Values are clamped to each control range; the applied values are returned.',\n inputSchema: {\n type: 'object',\n properties: {\n values: {\n type: 'object',\n description: 'Map of control id to number. See list_controls for ids and ranges.',\n additionalProperties: { type: 'number' },\n },\n label: { type: 'string', description: 'Short description of the change (echoed back).' },\n },\n required: ['values'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Set image adjustments',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: ({ values, label }) => {\n const entries = Object.entries((values ?? {}) as Record<string, unknown>);\n if (!entries.length) throw new Error('No values given. Pass {\"values\": {\"<control id>\": number}}.');\n const state = controller.getState();\n const base: Partial<ImageAdjustments> = {};\n const layers = (state.adjustments.layers ?? []).map((l) => ({ ...l }));\n const applied: Record<string, number> = {};\n const clamped: string[] = [];\n const unknown: string[] = [];\n for (const [id, raw] of entries) {\n const m = LAYER_PATH.exec(id);\n if (m) {\n const layer = layers.find((l) => l.id === m[1]);\n const spec = layer ? findLayerControl(layer.kind, m[2]) : undefined;\n if (!layer || !spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n if (spec.id === 'opacity') layer.opacity = v;\n else layer.params = setControlPath(layer.params, spec.id, v) as typeof layer.params;\n applied[id] = v;\n continue;\n }\n const spec = findAdjustmentControl(id);\n if (!spec) {\n unknown.push(id);\n continue;\n }\n const v = clampControlValue(spec, raw);\n if (v !== raw) clamped.push(id);\n (base as Record<string, number>)[id] = v;\n applied[id] = v;\n }\n if (unknown.length) {\n const accepted = IMAGE_ADJUSTMENT_CONTROLS.map((c) => c.id).join(', ');\n throw new Error(\n `Unknown control id(s): ${unknown.join(', ')}. Accepted: ${accepted}, or layers.<layerId>.<controlId> for an existing layer.`\n );\n }\n const changedLayers = Object.keys(applied).some((id) => id.startsWith('layers.'));\n controller.setAdjustments(changedLayers ? { ...base, layers } : base);\n const result: Record<string, unknown> = { applied, clamped };\n if (typeof label === 'string' && label) result.label = label;\n const notes: string[] = [];\n if ('tint' in applied && normalizeProcessVersion(controller.getState().adjustments.process) < 2) {\n notes.push('Tint has no effect on process 1 edits.');\n }\n if (notes.length) result.notes = notes;\n return result;\n },\n };\n\n const addLayer: AgentTool = {\n name: 'add_adjustment_layer',\n title: 'Add an adjustment layer',\n description:\n 'Adds a non-destructive adjustment layer on top of the stack (one undoable step) and returns it. Kinds: ' +\n ADJUSTMENT_KINDS.map((k) => k.kind).join(', ') +\n '. Omitted params are neutral.',\n inputSchema: {\n type: 'object',\n properties: {\n kind: { type: 'string', enum: ADJUSTMENT_KINDS.map((k) => k.kind), description: 'Adjustment kind.' },\n params: { type: 'object', description: 'Initial parameters (see list_controls); merged with neutral defaults.' },\n opacity: { type: 'number', minimum: 0, maximum: 1, description: 'Layer opacity 0-1 (default 1).' },\n blend: { type: 'string', enum: BLEND_MODES.map((m) => m.id), description: 'Blend mode (default normal).' },\n name: { type: 'string', description: 'Layer name shown in the layers panel.' },\n aboveId: { type: 'string', description: 'Put the layer directly above this layer (from list_layers). Default: above the other adjustment layers, below annotations.' },\n parentId: { type: 'string', description: 'Put the layer at the top of this group (from list_layers).' },\n },\n required: ['kind'],\n additionalProperties: false,\n },\n annotations: {\n title: 'Add an adjustment layer',\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: false,\n },\n handler: ({ kind, params, opacity, blend, name, aboveId, parentId }) => {\n if (!isAdjustmentKind(kind)) throw new Error(`Unknown adjustment kind \"${kind}\".`);\n if (blend !== undefined && !isBlendMode(blend)) throw new Error(`Unknown blend mode \"${blend}\".`);\n if (aboveId !== undefined && parentId !== undefined) throw new Error('Give aboveId or parentId, not both.');\n for (const ref of [aboveId, parentId]) {\n if (ref !== undefined && !controller.getLayer(ref as string)) throw new Error(`Unknown layer \"${ref}\". Call list_layers for ids.`);\n }\n if (parentId !== undefined && controller.getLayer(parentId as string)?.type !== 'group') throw new Error(`Layer \"${parentId}\" is not a group.`);\n const id = controller.addAdjustmentLayer({ kind: kind as AdjustmentKind, params, opacity, blend, name, aboveId, parentId });\n const layer = controller.getAdjustmentLayer(id)!;\n return { layer: JSON.parse(JSON.stringify(layer)), index: controller.getAdjustmentLayers().findIndex((l) => l.id === id) };\n },\n };\n\n const renderPreview: AgentTool = {\n name: 'render_preview',\n title: 'Render a preview',\n description:\n 'Renders the edited image (crop, adjustments, layers, annotations) as a PNG whose longest side is at most maxSide pixels. With layerId, renders only that layer (a group with its children) through the same crop and rotation.',\n inputSchema: {\n type: 'object',\n properties: {\n maxSide: { type: 'integer', minimum: 16, maximum: 2048, description: 'Longest side in pixels (default 512).' },\n layerId: { type: 'string', description: 'Render only this layer (from list_layers).' },\n },\n additionalProperties: false,\n },\n annotations: {\n title: 'Render a preview',\n readOnlyHint: true,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: false,\n },\n handler: async ({ maxSide, layerId }) => {\n if (layerId !== undefined && !controller.getLayer(layerId as string)) throw new Error(`Unknown layer \"${layerId}\". Call list_layers for ids.`);\n const side = Math.max(16, Math.min(2048, Math.round(typeof maxSide === 'number' ? maxSide : 512)));\n const state = controller.getState();\n const crop = state.crop ?? { x: 0, y: 0, ...state.imageDimensions };\n const quarter = Math.abs(Math.round((state.transform.rotate % 180) / 90)) % 2 === 1;\n const w = quarter ? crop.height : crop.width;\n const h = quarter ? crop.width : crop.height;\n const k = Math.min(1, side / Math.max(w, h, 1));\n const dataUrl = await controller.toDataURL({\n format: 'image/png',\n width: Math.max(1, Math.round(w * k)),\n height: Math.max(1, Math.round(h * k)),\n ...(layerId !== undefined ? { layerIds: [layerId as string] } : {}),\n });\n const comma = dataUrl.indexOf(',');\n if (!dataUrl.startsWith('data:image/png') || comma < 0 || comma === dataUrl.length - 1) {\n throw new Error('Rendering is not available in this environment.');\n }\n return {\n content: [\n { type: 'image', data: dataUrl.slice(comma + 1), mimeType: 'image/png' },\n { type: 'text', text: `Preview ${Math.round(w * k)}×${Math.round(h * k)}` },\n ],\n };\n },\n };\n\n return {\n id: options.id ?? 'image-editor',\n namespace: options.namespace,\n tools: [\n getAdjustments,\n listControls,\n setAdjustments,\n addLayer,\n renderPreview,\n createCaptureTool(createImageCaptureSurface(controller), { subject: 'the image (source orientation, before crop and rotation)', defaultPadding: 0 }),\n ...createImageSelectionTools(controller),\n ...createImageEditTools(controller),\n ...createImageLayerTools(controller),\n ...createImagePaintTools(controller),\n ...createPigmentTools(),\n ...createImagePsdTools(controller),\n ],\n };\n}\n\n","import type { CurvePoint } from './types';\nimport { curveLutFromLevels } from './adjustment-layers';\n\n/**\n * Framework-neutral helpers for a Curves editor drawn in a `width` × `height` box (levels 0..255\n * on both axes, output up). The React and Svelte curve editors share them.\n */\n\nconst clampLevel = (v: number) => Math.min(255, Math.max(0, Math.round(v)));\n\n/** Box coordinates of a curve point. */\nexport function curvePointToBox(p: CurvePoint, width: number, height: number): { x: number; y: number } {\n return { x: (p.input / 255) * width, y: height - (p.output / 255) * height };\n}\n\n/** The curve point (whole levels, clamped) under box coordinates. */\nexport function boxToCurvePoint(x: number, y: number, width: number, height: number): CurvePoint {\n return { input: clampLevel((x / (width || 1)) * 255), output: clampLevel(((height - y) / (height || 1)) * 255) };\n}\n\n/** An SVG polyline path of the evaluated curve (spline, clamped, constant beyond the end points). */\nexport function curveSvgPath(points: readonly CurvePoint[], width: number, height: number, samples = 64): string {\n const lut = curveLutFromLevels(points);\n const n = lut.length - 1;\n let d = '';\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n const y = lut[Math.round(t * n)];\n d += `${i === 0 ? 'M' : ' L'}${round2(t * width)} ${round2(height - y * height)}`;\n }\n return d;\n}\n\n/** Index of the point within `radius` box units of (x, y), nearest first; -1 for none. */\nexport function hitTestCurvePoint(\n points: readonly CurvePoint[],\n x: number,\n y: number,\n width: number,\n height: number,\n radius = 8\n): number {\n let best = -1;\n let bestD = radius * radius;\n points.forEach((p, i) => {\n const b = curvePointToBox(p, width, height);\n const d = (b.x - x) ** 2 + (b.y - y) ** 2;\n if (d <= bestD) {\n best = i;\n bestD = d;\n }\n });\n return best;\n}\n\n/** Inserts a point in input order (replacing one at the same input). Returns the new list and its index. */\nexport function insertCurvePoint(points: readonly CurvePoint[], p: CurvePoint): { points: CurvePoint[]; index: number } {\n const point = { input: clampLevel(p.input), output: clampLevel(p.output) };\n const out = points.filter((q) => q.input !== point.input).map((q) => ({ ...q }));\n let index = out.findIndex((q) => q.input > point.input);\n if (index === -1) index = out.length;\n out.splice(index, 0, point);\n return { points: out, index };\n}\n\n/** Moves point `index`, keeping its input strictly between its neighbours'. */\nexport function moveCurvePoint(points: readonly CurvePoint[], index: number, p: CurvePoint): CurvePoint[] {\n const out = points.map((q) => ({ ...q }));\n if (index < 0 || index >= out.length) return out;\n const lo = index > 0 ? out[index - 1].input + 1 : 0;\n const hi = index < out.length - 1 ? out[index + 1].input - 1 : 255;\n out[index] = { input: Math.min(hi, Math.max(lo, clampLevel(p.input))), output: clampLevel(p.output) };\n return out;\n}\n\n/** Removes point `index`; a curve keeps at least two points. */\nexport function removeCurvePoint(points: readonly CurvePoint[], index: number): CurvePoint[] {\n if (points.length <= 2 || index < 0 || index >= points.length) return points.map((q) => ({ ...q }));\n return points.filter((_, i) => i !== index).map((q) => ({ ...q }));\n}\n\nconst round2 = (v: number) => Math.round(v * 100) / 100;\n","import type { ImagePoint } from '../types';\nimport type { ImageEditorController } from '../controller';\nimport { ellipsePoints, normalizeRect } from './shapes';\nimport { selectionModeFromModifiers, type SelectionToolOptions } from './tool-options';\n\nexport interface GestureModifiers {\n shiftKey?: boolean;\n altKey?: boolean;\n}\n\n/**\n * Pointer gestures of the selection tools, shared by the React and Svelte image editors: feed\n * it pointer positions in natural image pixels (`canvasToImagePoint(..., false)`).\n *\n * - Magic wand: selects on `begin` (async, as it may read pixels).\n * - Rectangle / ellipse: drag a box; a click without a drag deselects (replace mode).\n * - Lasso: drag a free-hand path, closed on `end`.\n *\n * Shift adds, Alt subtracts, both intersect (overriding the options bar mode). While dragging,\n * {@link draftPath} is the outline to show (`RenderOptions.draftSelectionPath`).\n */\nexport class SelectionGesture {\n private start: ImagePoint | null = null;\n private points: ImagePoint[] = [];\n private current: ImagePoint | null = null;\n private modifiers: GestureModifiers = {};\n /** The last error (e.g. pixels unavailable for the wand), for the UI to show. */\n public error: string | null = null;\n\n constructor(\n readonly controller: ImageEditorController,\n private readonly getOptions: () => SelectionToolOptions\n ) {}\n\n get active(): boolean {\n return this.start !== null;\n }\n\n /** The outline of the shape being drawn, or `null`. */\n get draftPath(): ImagePoint[] | null {\n if (!this.start || !this.current) return null;\n const { tool } = this.getOptions();\n if (tool === 'lasso') return this.points.length > 1 ? this.points.slice() : null;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current.x - this.start.x,\n height: this.current.y - this.start.y,\n });\n if (rect.width < 1 && rect.height < 1) return null;\n if (tool === 'ellipse') return ellipsePoints(rect);\n return [\n { x: rect.x, y: rect.y },\n { x: rect.x + rect.width, y: rect.y },\n { x: rect.x + rect.width, y: rect.y + rect.height },\n { x: rect.x, y: rect.y + rect.height },\n ];\n }\n\n /** Pointer down. Resolves once a wand click has been applied. */\n async begin(point: ImagePoint, modifiers: GestureModifiers = {}): Promise<void> {\n const options = this.getOptions();\n this.error = null;\n this.modifiers = modifiers;\n if (options.tool === 'wand') {\n this.start = null;\n try {\n await this.controller.magicWand(point, {\n tolerance: options.tolerance,\n contiguous: options.contiguous,\n antiAlias: options.antiAlias,\n distance: options.distance,\n sampleMerged: options.sampleMerged,\n sample: options.sample,\n mode: selectionModeFromModifiers(modifiers, options.mode),\n });\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n return;\n }\n this.start = point;\n this.current = point;\n this.points = [point];\n }\n\n /** Pointer move; returns true when the draft changed (re-render). */\n move(point: ImagePoint): boolean {\n if (!this.start) return false;\n this.current = point;\n if (this.getOptions().tool === 'lasso') {\n const last = this.points[this.points.length - 1];\n if (Math.hypot(point.x - last.x, point.y - last.y) < 0.5) return false;\n this.points.push(point);\n }\n return true;\n }\n\n /** Pointer up: commits the shape. Returns true when the selection changed. */\n end(point?: ImagePoint, modifiers?: GestureModifiers): boolean {\n if (!this.start) return false;\n if (point) this.move(point);\n const options = this.getOptions();\n const mode = selectionModeFromModifiers(modifiers ?? this.modifiers, options.mode);\n const path = this.draftPath;\n const rect = normalizeRect({\n x: this.start.x,\n y: this.start.y,\n width: this.current!.x - this.start.x,\n height: this.current!.y - this.start.y,\n });\n this.start = null;\n this.current = null;\n this.points = [];\n if (!path) return mode === 'replace' ? this.controller.deselect() : false;\n const opts = { mode, antiAlias: options.antiAlias };\n if (options.tool === 'lasso') return path.length >= 3 ? this.controller.selectPolygon(path, opts) : false;\n return options.tool === 'ellipse' ? this.controller.selectEllipse(rect, opts) : this.controller.selectRect(rect, opts);\n }\n\n /** Abandons a drag without changing the selection. */\n cancel(): void {\n this.start = null;\n this.current = null;\n this.points = [];\n }\n}\n","/**\n * Region-based mosaic (pixelation) and box-blur filters for redacting or anonymizing\n * rectangular bounding boxes on an ImageData-like buffer.\n */\n\nexport interface MosaicRegion {\n\tx: number;\n\ty: number;\n\twidth: number;\n\theight: number;\n\t/**\n\t * When true, coordinates are interpreted as normalized values:\n\t * - [0..1] when all values are <= 1\n\t * - [0..1000] when any value is > 1 (e.g. Gemini 0..1000 bounding boxes)\n\t */\n\tnormalized?: boolean;\n}\n\nexport interface MosaicImageDataLike {\n\tdata: Uint8ClampedArray;\n\twidth: number;\n\theight: number;\n}\n\n/**\n * Resolves a MosaicRegion to clamped integer pixel bounds [x0, y0, x1, y1] within the image dimensions.\n */\nexport function resolveMosaicRegionBounds(\n\tregion: MosaicRegion,\n\timgWidth: number,\n\timgHeight: number\n): { x0: number; y0: number; x1: number; y1: number } | null {\n\tif (imgWidth <= 0 || imgHeight <= 0 || region.width <= 0 || region.height <= 0) {\n\t\treturn null;\n\t}\n\n\tlet rx = region.x;\n\tlet ry = region.y;\n\tlet rw = region.width;\n\tlet rh = region.height;\n\n\tconst isZeroToOne =\n\t\trx >= 0 &&\n\t\try >= 0 &&\n\t\trw > 0 &&\n\t\trh > 0 &&\n\t\trx <= 1 &&\n\t\try <= 1 &&\n\t\trw <= 1 &&\n\t\trh <= 1 &&\n\t\t(region.normalized === true ||\n\t\t\t(region.normalized === undefined &&\n\t\t\t\t(imgWidth > 1 || imgHeight > 1) &&\n\t\t\t\t(!Number.isInteger(rx) || !Number.isInteger(ry) || !Number.isInteger(rw) || !Number.isInteger(rh))));\n\n\tif (isZeroToOne) {\n\t\trx = rx * imgWidth;\n\t\try = ry * imgHeight;\n\t\trw = rw * imgWidth;\n\t\trh = rh * imgHeight;\n\t} else if (region.normalized === true) {\n\t\t// 0..1000 normalized coordinates\n\t\trx = (rx / 1000) * imgWidth;\n\t\try = (ry / 1000) * imgHeight;\n\t\trw = (rw / 1000) * imgWidth;\n\t\trh = (rh / 1000) * imgHeight;\n\t}\n\n\tconst x0 = Math.max(0, Math.min(imgWidth, Math.floor(rx)));\n\tconst y0 = Math.max(0, Math.min(imgHeight, Math.floor(ry)));\n\tconst x1 = Math.max(0, Math.min(imgWidth, Math.ceil(rx + rw)));\n\tconst y1 = Math.max(0, Math.min(imgHeight, Math.ceil(ry + rh)));\n\n\tif (x1 <= x0 || y1 <= y0) return null;\n\treturn { x0, y0, x1, y1 };\n}\n\n/**\n * Pixelates each rectangular region by averaging `blockSize x blockSize` blocks.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionMosaic<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tblockSize: number = 12\n): T {\n\tconst { data, width, height } = imageData;\n\tconst step = Math.max(2, Math.round(blockSize));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\n\t\tfor (let by = y0; by < y1; by += step) {\n\t\t\tconst bh = Math.min(step, y1 - by);\n\t\t\tfor (let bx = x0; bx < x1; bx += step) {\n\t\t\t\tconst bw = Math.min(step, x1 - bx);\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tconst count = bw * bh;\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\trSum += data[idx];\n\t\t\t\t\t\tgSum += data[idx + 1];\n\t\t\t\t\t\tbSum += data[idx + 2];\n\t\t\t\t\t\taSum += data[idx + 3];\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst rAvg = Math.round(rSum / count);\n\t\t\t\tconst gAvg = Math.round(gSum / count);\n\t\t\t\tconst bAvg = Math.round(bSum / count);\n\t\t\t\tconst aAvg = Math.round(aSum / count);\n\n\t\t\t\tfor (let py = by; py < by + bh; py++) {\n\t\t\t\t\tconst rowOffset = py * width;\n\t\t\t\t\tfor (let px = bx; px < bx + bw; px++) {\n\t\t\t\t\t\tconst idx = (rowOffset + px) * 4;\n\t\t\t\t\t\tdata[idx] = rAvg;\n\t\t\t\t\t\tdata[idx + 1] = gAvg;\n\t\t\t\t\t\tdata[idx + 2] = bAvg;\n\t\t\t\t\t\tdata[idx + 3] = aAvg;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n\n/**\n * Applies a separable 2-pass box blur to each rectangular region.\n * Mutates `imageData.data` in-place and returns `imageData`.\n */\nexport function applyRegionBlur<T extends MosaicImageDataLike>(\n\timageData: T,\n\tregions: MosaicRegion[],\n\tradius: number = 10\n): T {\n\tconst { data, width, height } = imageData;\n\tconst rad = Math.max(1, Math.min(64, Math.round(radius)));\n\n\tfor (const region of regions) {\n\t\tconst bounds = resolveMosaicRegionBounds(region, width, height);\n\t\tif (!bounds) continue;\n\n\t\tconst { x0, y0, x1, y1 } = bounds;\n\t\tconst rw = x1 - x0;\n\t\tconst rh = y1 - y0;\n\t\tconst temp = new Uint8ClampedArray(rw * rh * 4);\n\n\t\t// Horizontal pass into temp\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst srcY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, x - rad);\n\t\t\t\tconst kEnd = Math.min(rw - 1, x + rad);\n\t\t\t\tfor (let kx = kStart; kx <= kEnd; kx++) {\n\t\t\t\t\tconst srcIdx = (srcY * width + (x0 + kx)) * 4;\n\t\t\t\t\trSum += data[srcIdx];\n\t\t\t\t\tgSum += data[srcIdx + 1];\n\t\t\t\t\tbSum += data[srcIdx + 2];\n\t\t\t\t\taSum += data[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (y * rw + x) * 4;\n\t\t\t\ttemp[dstIdx] = Math.round(rSum / count);\n\t\t\t\ttemp[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\ttemp[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\ttemp[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\n\t\t// Vertical pass from temp back into imageData.data\n\t\tfor (let y = 0; y < rh; y++) {\n\t\t\tconst dstY = y0 + y;\n\t\t\tfor (let x = 0; x < rw; x++) {\n\t\t\t\tlet rSum = 0;\n\t\t\t\tlet gSum = 0;\n\t\t\t\tlet bSum = 0;\n\t\t\t\tlet aSum = 0;\n\t\t\t\tlet count = 0;\n\n\t\t\t\tconst kStart = Math.max(0, y - rad);\n\t\t\t\tconst kEnd = Math.min(rh - 1, y + rad);\n\t\t\t\tfor (let ky = kStart; ky <= kEnd; ky++) {\n\t\t\t\t\tconst srcIdx = (ky * rw + x) * 4;\n\t\t\t\t\trSum += temp[srcIdx];\n\t\t\t\t\tgSum += temp[srcIdx + 1];\n\t\t\t\t\tbSum += temp[srcIdx + 2];\n\t\t\t\t\taSum += temp[srcIdx + 3];\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\n\t\t\t\tconst dstIdx = (dstY * width + (x0 + x)) * 4;\n\t\t\t\tdata[dstIdx] = Math.round(rSum / count);\n\t\t\t\tdata[dstIdx + 1] = Math.round(gSum / count);\n\t\t\t\tdata[dstIdx + 2] = Math.round(bSum / count);\n\t\t\t\tdata[dstIdx + 3] = Math.round(aSum / count);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn imageData;\n}\n","/**\n * The layers panel's view model, shared by the React and Svelte panels: the rows to show (top of\n * the stack first, groups indented, collapsed groups hiding their children), where a drop lands,\n * and the menu of layer operations with whether each one applies to the active layer.\n */\n\nimport { adjustmentKindLabel } from '../adjustment-layers';\nimport type { ImageEditorController } from '../controller';\nimport { findLayer, type ImageLayer } from './model';\nimport type { LayerPlacement } from './ops';\n\nexport interface LayerPanelRow {\n layer: ImageLayer;\n /** Nesting depth (0 = top level). */\n depth: number;\n parentId: string | null;\n /** Index among its siblings, 0 = bottom. */\n index: number;\n active: boolean;\n /** Display name (adjustment layers without a name show their kind). */\n label: string;\n /** Clipped to the layer below (drawn with an indent arrow). */\n clipped: boolean;\n}\n\n/** Rows top to bottom; children of collapsed groups are left out. */\nexport function layerPanelRows(layers: readonly ImageLayer[], activeId: string | null): LayerPanelRow[] {\n const rows: LayerPanelRow[] = [];\n const walk = (list: readonly ImageLayer[], depth: number, parentId: string | null) => {\n for (let i = list.length - 1; i >= 0; i--) {\n const layer = list[i];\n rows.push({\n layer,\n depth,\n parentId,\n index: i,\n active: layer.id === activeId,\n label: layerLabel(layer),\n clipped: Boolean(layer.clipped),\n });\n if (layer.type === 'group' && !layer.collapsed) walk(layer.children, depth + 1, layer.id);\n }\n };\n walk(layers, 0, null);\n return rows;\n}\n\nexport function layerLabel(layer: ImageLayer): string {\n if (layer.name) return layer.name;\n if (layer.type === 'adjustment') return adjustmentKindLabel(layer.kind);\n return layer.type === 'group' ? 'Group' : 'Layer';\n}\n\n/** A short type badge for rows without a thumbnail. */\nexport function layerBadge(layer: ImageLayer): string {\n switch (layer.type) {\n case 'pixel':\n return 'px';\n case 'adjustment':\n return 'adj';\n case 'group':\n return 'grp';\n case 'annotation':\n return layer.annotation.type === 'text' ? 'T' : 'ann';\n }\n}\n\n/** Where a row was dropped on: the top or bottom part of the row, or the middle of a group row. */\nexport type DropZone = 'above' | 'below' | 'inside';\n\n/** The zone for a pointer at `fraction` (0 top .. 1 bottom) of a row's height. */\nexport function dropZone(fraction: number, isGroup: boolean): DropZone {\n if (isGroup && fraction > 0.3 && fraction < 0.7) return 'inside';\n return fraction < 0.5 ? 'above' : 'below';\n}\n\n/** The placement for `move_layer` when `dragId` is dropped on row `targetId` in `zone`, or `null` for a no-op. */\nexport function dropPlacement(layers: readonly ImageLayer[], dragId: string, targetId: string, zone: DropZone): LayerPlacement | null {\n if (dragId === targetId) return null;\n const target = findLayer(layers, targetId);\n if (!target) return null;\n if (zone === 'inside' && target.layer.type === 'group') return { parentId: targetId };\n // The panel lists the top first: dropping above a row means higher in the stack.\n return zone === 'above' ? { aboveId: targetId } : { belowId: targetId };\n}\n\nexport type LayerMenuAction =\n | 'new-layer'\n | 'new-group'\n | 'duplicate'\n | 'delete'\n | 'group-selected'\n | 'ungroup'\n | 'merge-down'\n | 'merge-visible'\n | 'flatten'\n | 'via-copy'\n | 'via-cut'\n | 'add-mask'\n | 'add-mask-selection'\n | 'apply-mask'\n | 'delete-mask'\n | 'toggle-mask'\n | 'toggle-clip'\n | 'rasterize';\n\nexport interface LayerMenuItem {\n action: LayerMenuAction;\n label: string;\n enabled: boolean;\n}\n\n/** The layer menu for the controller's current state (active layer, selection). */\nexport function layerMenu(controller: ImageEditorController): LayerMenuItem[] {\n const s = controller.getState();\n const active = controller.getActiveLayer();\n const loc = active ? findLayer(s.layers, active.id) : null;\n const siblings = loc ? (loc.parent ? loc.parent.children : s.layers) : [];\n const below = loc ? siblings[loc.index - 1] : undefined;\n const hasSel = controller.hasSelection;\n const isPixel = active?.type === 'pixel';\n return [\n { action: 'new-layer', label: 'New Layer', enabled: true },\n { action: 'new-group', label: 'New Group', enabled: true },\n { action: 'duplicate', label: 'Duplicate Layer', enabled: Boolean(active) },\n { action: 'delete', label: 'Delete Layer', enabled: Boolean(active) },\n { action: 'group-selected', label: 'Group Layer', enabled: Boolean(active) },\n { action: 'ungroup', label: 'Ungroup', enabled: active?.type === 'group' },\n { action: 'merge-down', label: 'Merge Down', enabled: Boolean(active?.visible && below?.type === 'pixel') },\n { action: 'merge-visible', label: 'Merge Visible', enabled: s.layers.some((l) => l.visible) },\n { action: 'flatten', label: 'Flatten Image', enabled: s.layers.length > 0 },\n { action: 'via-copy', label: 'Layer via Copy', enabled: isPixel && hasSel },\n { action: 'via-cut', label: 'Layer via Cut', enabled: isPixel && hasSel },\n { action: 'add-mask', label: 'Add Layer Mask', enabled: Boolean(active) },\n { action: 'add-mask-selection', label: 'Add Mask from Selection', enabled: Boolean(active) && hasSel },\n { action: 'apply-mask', label: 'Apply Layer Mask', enabled: isPixel && Boolean(active?.mask) },\n { action: 'delete-mask', label: 'Delete Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'toggle-mask', label: active?.mask?.enabled === false ? 'Enable Layer Mask' : 'Disable Layer Mask', enabled: Boolean(active?.mask) },\n { action: 'rasterize', label: 'Rasterize Layer', enabled: active?.type === 'annotation' || (isPixel && Boolean(active?.type === 'pixel' && active.filters?.length)) },\n { action: 'toggle-clip', label: active?.clipped ? 'Release Clipping Mask' : 'Create Clipping Mask', enabled: Boolean(loc && loc.index > 0) },\n ];\n}\n\n/** Runs a menu action on the active layer. Resolves to the error message when it was refused. */\nexport async function runLayerMenuAction(controller: ImageEditorController, action: LayerMenuAction): Promise<string | null> {\n const active = controller.getActiveLayer();\n const id = active?.id;\n try {\n switch (action) {\n case 'new-layer':\n controller.addPixelLayer();\n break;\n case 'new-group':\n controller.addGroup();\n break;\n case 'duplicate':\n if (id) controller.duplicateLayer(id);\n break;\n case 'delete':\n if (id) controller.removeLayer(id);\n break;\n case 'group-selected':\n if (id) controller.groupLayers([id]);\n break;\n case 'ungroup':\n if (id) controller.ungroupLayer(id);\n break;\n case 'merge-down':\n if (id) await controller.mergeDown(id);\n break;\n case 'merge-visible':\n await controller.mergeVisible();\n break;\n case 'flatten':\n await controller.flattenImage();\n break;\n case 'via-copy':\n case 'via-cut':\n controller.layerViaCopy({ cut: action === 'via-cut' });\n break;\n case 'add-mask':\n if (id) controller.addLayerMask(id, 'reveal-all');\n break;\n case 'add-mask-selection':\n if (id) controller.addLayerMask(id, 'selection');\n break;\n case 'apply-mask':\n if (id) await controller.applyLayerMask(id);\n break;\n case 'delete-mask':\n if (id) controller.removeLayerMask(id);\n break;\n case 'toggle-mask':\n if (id && active?.mask) controller.setLayerMaskProps(id, { enabled: active.mask.enabled === false });\n break;\n case 'rasterize':\n if (id) await controller.rasterizeLayer(id);\n break;\n case 'toggle-clip':\n if (id) controller.setLayerProperties(id, { clipped: !active?.clipped });\n break;\n }\n return null;\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n}\n","/**\n * The paint tools' view model, shared by the React and Svelte editors: the brush options bar's\n * controls, Photoshop's shortcuts (B, E, V, [ ], Shift+[ ], number keys, D, X) and a pointer\n * gesture that turns pointer events into strokes (Brush, Eraser) or layer moves (Move).\n */\n\nimport type { ControlSpec } from '../controls';\nimport type { ImageEditorController, PaintStrokeSession } from '../controller';\nimport type { ImagePoint } from '../types';\nimport { MAX_BRUSH_SIZE, type BrushSettings } from '../../paint';\n\nconst plain = { kind: 'plain' } as const;\n\n/** Controls of the brush options bar (`hardness`, `opacity`, `flow`, `smoothing` shown in %). */\nexport const BRUSH_OPTION_CONTROLS: readonly (ControlSpec & { id: 'size' | 'hardness' | 'opacity' | 'flow' | 'smoothing' })[] = [\n { id: 'size', label: 'Size', section: 'layer', min: 1, max: 500, step: 1, default: 20, decimals: 0, track: plain, unit: 'px' },\n { id: 'hardness', label: 'Hardness', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'opacity', label: 'Opacity', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'flow', label: 'Flow', section: 'layer', min: 0, max: 1, step: 0.01, default: 1, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n { id: 'smoothing', label: 'Smoothing', section: 'layer', min: 0, max: 0.95, step: 0.01, default: 0, decimals: 0, track: plain, displayScale: 100, unit: '%' },\n];\n\n/** The paint tools in the toolbar. */\nexport const PAINT_TOOLS = [\n { id: 'move', label: 'Move', shortcut: 'V' },\n { id: 'brush', label: 'Brush', shortcut: 'B' },\n { id: 'eraser', label: 'Eraser', shortcut: 'E' },\n] as const;\n\nexport type PaintTool = (typeof PAINT_TOOLS)[number]['id'];\n\nexport const isPaintTool = (tool: string): tool is PaintTool => tool === 'move' || tool === 'brush' || tool === 'eraser';\n\n/** Photoshop's bracket step for a brush size. */\nfunction sizeStep(size: number): number {\n return size < 10 ? 1 : size < 50 ? 5 : size < 100 ? 10 : size < 200 ? 25 : size < 500 ? 50 : 100;\n}\n\nexport interface ShortcutEvent {\n key: string;\n shiftKey?: boolean;\n ctrlKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n /** The event target: shortcuts are ignored while typing in inputs. */\n target?: unknown;\n}\n\nconst isTyping = (t: unknown) => {\n const el = t as { tagName?: string; isContentEditable?: boolean } | null;\n return Boolean(el && (el.isContentEditable || el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'));\n};\n\n/**\n * Applies a paint shortcut. Returns true when it was one (the caller then prevents the default).\n * B / E / V choose the tool; [ and ] change the size, with Shift the hardness by 25 %; 1..9 set the\n * opacity to 10..90 % and 0 to 100 %; D resets the colours to black and white; X swaps them.\n */\nexport function handlePaintShortcut(controller: ImageEditorController, e: ShortcutEvent): boolean {\n if (e.ctrlKey || e.metaKey || e.altKey || isTyping(e.target)) return false;\n const brush = controller.getState().paint.brush;\n const set = (b: Partial<BrushSettings>) => controller.setPaintOptions({ brush: b });\n const key = e.key;\n switch (key.toLowerCase()) {\n case 'b':\n controller.setTool('brush');\n return true;\n case 'e':\n controller.setTool('eraser');\n return true;\n case 'v':\n controller.setTool('move');\n return true;\n case 'd':\n controller.resetPaintColors();\n return true;\n case 'x':\n controller.swapPaintColors();\n return true;\n }\n if (key === '[' || key === '{') {\n if (e.shiftKey || key === '{') set({ hardness: Math.max(0, Math.round((brush.hardness - 0.25) * 4) / 4) });\n else set({ size: Math.max(1, brush.size - sizeStep(brush.size - 1)) });\n return true;\n }\n if (key === ']' || key === '}') {\n if (e.shiftKey || key === '}') set({ hardness: Math.min(1, Math.round((brush.hardness + 0.25) * 4) / 4) });\n else set({ size: Math.min(MAX_BRUSH_SIZE, brush.size + sizeStep(brush.size)) });\n return true;\n }\n if (/^[0-9]$/.test(key) && isPaintTool(controller.getState().activeTool)) {\n set({ opacity: key === '0' ? 1 : Number(key) / 10 });\n return true;\n }\n return false;\n}\n\n/** One pointer sample in document pixels. */\nexport interface PaintPointerSample extends ImagePoint {\n /** 0..1; a mouse is 1. */\n pressure: number;\n}\n\n/** Pressure of a pointer event: pens report it; mice report 0.5 while pressed, which means \"full\". */\nexport function pointerPressure(e: { pointerType?: string; pressure?: number }): number {\n if (e.pointerType === 'pen' && typeof e.pressure === 'number' && e.pressure > 0) return Math.min(1, e.pressure);\n return 1;\n}\n\n/**\n * Turns pointer events into strokes (Brush, Eraser on the active layer's target) or layer moves\n * (Move on the active pixel layer). `down` returns an error message when the tool cannot act.\n */\nexport class PaintGesture {\n private stroke: PaintStrokeSession | null = null;\n private move: { id: string; start: ImagePoint; origin: { x: number; y: number }; moved: boolean } | null = null;\n /** The last refusal (e.g. \"layer is locked\"), for the editor to show. */\n error: string | null = null;\n\n constructor(private readonly controller: ImageEditorController) {}\n\n get active(): boolean {\n return Boolean(this.stroke || this.move);\n }\n\n down(sample: PaintPointerSample): string | null {\n this.error = null;\n const tool = this.controller.getState().activeTool;\n try {\n if (tool === 'move') {\n const layer = this.controller.getActiveLayer();\n if (!layer) throw new Error('Select a layer to move in the Layers panel.');\n if (layer.type !== 'pixel') throw new Error(`Only pixel layers move; \"${layer.name}\" is a ${layer.type} layer.`);\n if (layer.locks?.all || layer.locks?.position) throw new Error(`Layer \"${layer.name}\" has its position locked.`);\n this.move = { id: layer.id, start: { x: sample.x, y: sample.y }, origin: { ...layer.offset }, moved: false };\n return null;\n }\n if (tool === 'brush' || tool === 'eraser') {\n this.stroke = this.controller.beginStroke({ tool });\n this.stroke.push([sample]);\n return null;\n }\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.stroke = null;\n this.move = null;\n }\n return this.error;\n }\n\n /** Feeds pointer samples (all coalesced events of one move, in order). */\n moveTo(samples: PaintPointerSample[]): void {\n if (!samples.length) return;\n if (this.stroke) {\n this.stroke.push(samples);\n return;\n }\n const m = this.move;\n if (m) {\n const last = samples[samples.length - 1];\n const x = m.origin.x + Math.round(last.x - m.start.x);\n const y = m.origin.y + Math.round(last.y - m.start.y);\n try {\n // The first move of a drag records the undo step; the rest replace it.\n this.controller.offsetLayer(m.id, { x, y }, !m.moved);\n m.moved = true;\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n this.move = null;\n }\n }\n }\n\n up(): void {\n if (this.stroke) {\n try {\n this.stroke.end();\n } catch (err) {\n this.error = err instanceof Error ? err.message : String(err);\n }\n }\n this.stroke = null;\n this.move = null;\n }\n\n cancel(): void {\n this.stroke?.cancel();\n this.stroke = null;\n this.move = null;\n }\n}\n","/** Framework-free helpers behind the PSD buttons of the React and Svelte layer panels. */\n\nimport type { ImageEditorController } from '../controller';\n\n/** Types the file pickers accept. */\nexport const PSD_FILE_ACCEPT = '.psd,.psb,image/vnd.adobe.photoshop,application/x-photoshop';\n\nfunction blobBytes(file: Blob): Promise<Uint8Array> {\n if (typeof file.arrayBuffer === 'function') return file.arrayBuffer().then((b) => new Uint8Array(b));\n return new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(new Uint8Array(reader.result as ArrayBuffer));\n reader.onerror = () => reject(reader.error ?? new Error('The file could not be read.'));\n reader.readAsArrayBuffer(file);\n });\n}\n\n/** Reads a picked file and opens it as the document (one undo step). Returns the import warnings. */\nexport async function openPsdFile(controller: ImageEditorController, file: Blob): Promise<string[]> {\n return controller.loadPsd(await blobBytes(file)).warnings;\n}\n\n/** A file name for the exported document: `name` with a `.psd` / `.psb` extension. */\nexport function psdFileName(name: string, psb: boolean): string {\n const base = name.replace(/\\.[a-z0-9]{2,4}$/i, '') || 'image';\n return `${base}.${psb ? 'psb' : 'psd'}`;\n}\n\n/**\n * Writes the document as a PSD and offers it as a download (in a browser). Returns the file\n * name and the export warnings.\n */\nexport async function downloadPsd(controller: ImageEditorController, name = 'image'): Promise<{ fileName: string; warnings: string[] }> {\n const res = await controller.toPsd();\n const fileName = psdFileName(name, res.psb);\n if (typeof document !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([res.bytes as BlobPart], { type: 'image/vnd.adobe.photoshop' });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = fileName;\n a.click();\n setTimeout(() => URL.revokeObjectURL(url), 0);\n }\n return { fileName, warnings: res.warnings };\n}\n"]}
|