diff --git a/bin/ctb2rsla.mjs b/bin/ctb2rsla.mjs new file mode 100755 index 00000000..0c305255 --- /dev/null +++ b/bin/ctb2rsla.mjs @@ -0,0 +1,39 @@ +#!/usr/bin/env node + +import fs from 'fs/promises'; +import path from 'path'; +import { CTB } from '../src/kiri/mode/sla/work/x_ctb.js'; + +let [,, input, output] = process.argv; + +if (!input) { + console.error("usage: node bin/ctb2rsla.mjs input.ctb [output.rsla]"); + process.exit(1); +} + +output = output || input.replace(/\.ctb$/i, "") + ".rsla"; + +let data = await fs.readFile(input); +let ctb = CTB.read(data); +let { header, machine } = ctb; +let lastPercent = -1; + +console.log([ + "ctb", + machine || "unknown-machine", + `${header.resolutionX}x${header.resolutionY}`, + `${header.layerCount} layers`, + `${path.basename(output)}` +].join(" ")); + +let file = await CTB.toRSLA(data, (progress, message) => { + let percent = Math.floor(progress * 100); + if (percent !== lastPercent) { + lastPercent = percent; + process.stderr.write(`\r${message} ${percent}%`); + } +}); + +if (lastPercent >= 0) process.stderr.write("\n"); +await fs.writeFile(output, Buffer.from(file)); +console.log(`wrote ${output}`); diff --git a/src/kiri/app/conf/defaults.js b/src/kiri/app/conf/defaults.js index 1428728b..9a7df16f 100644 --- a/src/kiri/app/conf/defaults.js +++ b/src/kiri/app/conf/defaults.js @@ -102,6 +102,7 @@ function device_from_code(code,mode) { slaFormat: valueOf(set.sla_format, ''), slaFileExt: valueOf(set.sla_file_ext, ''), slaFormatVersion: valueOf(set.sla_format_version, 1), + slaMachineEditable: valueOf(set.sla_machine_editable, false), deviceZMax: valueOf(set.z_move_max, 0), gcodeTime: valueOf(set.time_factor, 1), maxHeight: valueOf(set.build_height, 150), @@ -373,7 +374,8 @@ export const conf = { resolutionY: 900, slaFormat: "", slaFileExt: "", - slaFormatVersion: 1 + slaFormatVersion: 1, + slaMachineEditable: false }, // process defaults SLA:Process p:{ diff --git a/src/kiri/app/devices.js b/src/kiri/app/devices.js index a66a224b..ab7be544 100644 --- a/src/kiri/app/devices.js +++ b/src/kiri/app/devices.js @@ -3,6 +3,7 @@ import { $, h } from '../../moto/webui.js'; import { api } from './api.js'; import { conf } from './conf/defaults.js'; +import { canEditSLAMachine } from '../mode/sla/core/formats.js'; import { space } from '../../moto/space.js'; import { devices as devlist } from '../../pack/kiri-devs.js'; import { settings, conf as setconf } from './conf/manager.js'; @@ -185,7 +186,7 @@ function setDeviceCode(code, devicename) { } } - let allowSLADeviceEdit = mode !== 'SLA' || dev.slaFormat === 'vsla' || dev.slaFormat === 'rsla'; + let allowSLADeviceEdit = mode !== 'SLA' || canEditSLAMachine(dev); // disable editing for non-local devices [ diff --git a/src/kiri/dev/sla/Any.Generic.Chitubox.CTB.json b/src/kiri/dev/sla/Any.Generic.Chitubox.CTB.json new file mode 100644 index 00000000..66ea3645 --- /dev/null +++ b/src/kiri/dev/sla/Any.Generic.Chitubox.CTB.json @@ -0,0 +1,16 @@ +{ + "no_clone": false, + "cmd": {}, + "settings": { + "bed_width": 192, + "bed_depth": 120, + "bed_height": 1, + "max_height": 250, + "resolution_x": 3840, + "resolution_y": 2400, + "sla_format": "ctb", + "sla_file_ext": "ctb", + "sla_format_version": 3, + "sla_machine_editable": true + } +} diff --git a/src/kiri/dev/sla/Creality.LD-006.json b/src/kiri/dev/sla/Creality.LD-006.json new file mode 100644 index 00000000..9577927f --- /dev/null +++ b/src/kiri/dev/sla/Creality.LD-006.json @@ -0,0 +1,15 @@ +{ + "no_clone": true, + "cmd": {}, + "settings": { + "bed_width": 192, + "bed_depth": 120, + "bed_height": 1, + "max_height": 250, + "resolution_x": 3840, + "resolution_y": 2400, + "sla_format": "ctb", + "sla_file_ext": "ctb", + "sla_format_version": 3 + } +} diff --git a/src/kiri/dev/sla/Elegoo.Mars.3.CTB.json b/src/kiri/dev/sla/Elegoo.Mars.3.CTB.json new file mode 100644 index 00000000..95bb02ba --- /dev/null +++ b/src/kiri/dev/sla/Elegoo.Mars.3.CTB.json @@ -0,0 +1,15 @@ +{ + "no_clone": true, + "cmd": {}, + "settings": { + "bed_width": 143, + "bed_depth": 89.6, + "bed_height": 1, + "max_height": 175, + "resolution_x": 4098, + "resolution_y": 2560, + "sla_format": "ctb", + "sla_file_ext": "ctb", + "sla_format_version": 3 + } +} diff --git a/src/kiri/dev/sla/Elegoo.Saturn.2.CTB.json b/src/kiri/dev/sla/Elegoo.Saturn.2.CTB.json new file mode 100644 index 00000000..51ec90de --- /dev/null +++ b/src/kiri/dev/sla/Elegoo.Saturn.2.CTB.json @@ -0,0 +1,15 @@ +{ + "no_clone": true, + "cmd": {}, + "settings": { + "bed_width": 218.88, + "bed_depth": 123.12, + "bed_height": 1, + "max_height": 250, + "resolution_x": 7680, + "resolution_y": 4320, + "sla_format": "ctb", + "sla_file_ext": "ctb", + "sla_format_version": 3 + } +} diff --git a/src/kiri/mode/sla/app/init-ui.js b/src/kiri/mode/sla/app/init-ui.js index 234e2b22..5418034e 100644 --- a/src/kiri/mode/sla/app/init-ui.js +++ b/src/kiri/mode/sla/app/init-ui.js @@ -2,6 +2,7 @@ import { $ } from '../../../../moto/webui.js'; import { api } from '../../../app/api.js'; +import { getSLAFileExt } from '../core/formats.js'; export const SLA = { init, @@ -63,11 +64,7 @@ export function printDownload(output, api, names) { $('print-layers').value = layers; $('print-time').value = `${print_hrs}:${print_min}:${print_sec}`; - let ext = device.slaFileExt || ({ - 'Anycubic.Photon': 'photon', - 'Anycubic.Photon.S': 'photons' - }[device.deviceName]) || 'cxdlp'; - + let ext = getSLAFileExt(device); ext = ext.startsWith(".") ? ext : `.${ext}`; download.innerText = `${api.language.current.ex_dwnl || 'download'} ${ext}`; download.onclick = () => { saveFile(api, file, ext) }; diff --git a/src/kiri/mode/sla/core/formats.js b/src/kiri/mode/sla/core/formats.js new file mode 100644 index 00000000..d71e5f41 --- /dev/null +++ b/src/kiri/mode/sla/core/formats.js @@ -0,0 +1,68 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +const DEVICE_FORMAT = { + 'Anycubic.Photon': 'photon', + 'Anycubic.Photon.S': 'photons' +}; + +export const SLA_FORMATS = { + photon: { + id: 'photon', + ext: 'photon', + proprietary: true, + available: true + }, + photons: { + id: 'photons', + ext: 'photons', + proprietary: true, + available: true + }, + cxdlp: { + id: 'cxdlp', + ext: 'cxdlp', + proprietary: true, + available: true + }, + vsla: { + id: 'vsla', + ext: 'vsla', + proprietary: false, + available: true, + editableMachine: true + }, + rsla: { + id: 'rsla', + ext: 'rsla', + proprietary: false, + available: true, + editableMachine: true + }, + ctb: { + id: 'ctb', + ext: 'ctb', + proprietary: true, + available: true, + priority: 1, + family: 'chitubox' + } +}; + +export function getSLAFormat(device) { + return device.slaFormat || DEVICE_FORMAT[device.deviceName] || 'cxdlp'; +} + +export function getSLAFormatRecord(device) { + let id = getSLAFormat(device); + return SLA_FORMATS[id] || SLA_FORMATS.cxdlp; +} + +export function getSLAFileExt(device) { + let rec = getSLAFormatRecord(device); + return device.slaFileExt || rec.ext || rec.id; +} + +export function canEditSLAMachine(device) { + return device.slaMachineEditable === true || + getSLAFormatRecord(device).editableMachine === true; +} diff --git a/src/kiri/mode/sla/work/export.js b/src/kiri/mode/sla/work/export.js index 86ecd904..65e5f7d0 100644 --- a/src/kiri/mode/sla/work/export.js +++ b/src/kiri/mode/sla/work/export.js @@ -1,10 +1,12 @@ /** Copyright Stewart Allen -- All Rights Reserved */ import { CXDLP } from './x_cxdlp.js'; +import { CTB } from './x_ctb.js'; import { photon } from './x_photon.js'; import { SLA } from './init-work.js'; import { RSLA } from './x_rsla.js'; import { VSLA } from './x_vsla.js'; +import { getSLAFormat } from '../core/formats.js'; /** * DRIVER CONTRACT - runs in worker @@ -35,10 +37,7 @@ export function sla_export(print, online, ondone) { layermax = Math.max(widget.slices.length); }); - let format = device.slaFormat || ({ - 'Anycubic.Photon': 'photon', - 'Anycubic.Photon.S': 'photons', - }[device.deviceName]) || 'cxdlp'; + let format = getSLAFormat(device); if (format === 'vsla') { return VSLA.encode(print, (progress, message) => { @@ -58,6 +57,15 @@ export function sla_export(print, online, ondone) { }); } + if (format === 'ctb') { + return CTB.encode(print, (progress, message) => { + online({ progress, message }); + }, photon).then(output => { + let { file, layers, volume } = output; + ondone({ width, height, file, layers, volume }, [file]); + }); + } + if (format === 'photon' || format === 'photons') { let legacyMode = SLA.legacy || alias > 1, part1 = legacyMode ? 0.25 : 0.85, diff --git a/src/kiri/mode/sla/work/x_ctb.js b/src/kiri/mode/sla/work/x_ctb.js new file mode 100644 index 00000000..ea6458fc --- /dev/null +++ b/src/kiri/mode/sla/work/x_ctb.js @@ -0,0 +1,750 @@ +/** Copyright Stewart Allen -- All Rights Reserved */ + +import { JSZip } from '../../../../ext/jszip-esm.js'; +import { PNG } from '../../../../ext/pngjs.esm.js'; + +const CTB_V3_MAGIC = 0x12fd0086; +const HEADER_SIZE = 0x70; +const LAYER_TABLE_RECORD_SIZE = 36; +const LAYER_FULL_RECORD_SIZE = 84; +const PRINT_PARAMS_SIZE = 60; +const MACHINE_INFO_SIZE = 76; +const RSLA_MIME = "application/vnd.gridspace.rsla+zip"; + +function encode(print, progress, renderer) { + if (renderer) { + return Promise.resolve(encodeWithPhoton(print, progress, renderer)); + } + return import('./x_photon.js').then(({ photon }) => + encodeWithPhoton(print, progress, photon)); +} + +function encodeWithPhoton(print, progress, photon) { + let { settings, widgets } = print; + let { device, process } = settings; + let width = device.resolutionX; + let height = device.resolutionY; + let scaleX = width / device.bedWidth; + let scaleY = height / device.bedDepth; + let layermax = 0; + let layers = []; + let volume = 0; + let seed = makeSeed(print); + + widgets = widgets.filter(w => !w.track.ignore && !w.meta.disabled); + widgets.forEach(widget => { + layermax = Math.max(layermax, widget.slices.length); + }); + + for (let index=0; index { + let pixels = decodeLayer(view, header, layer); + let file = `layers/${index.toString().padStart(6, "0")}.png`; + zip.file(file, imageToPNG(pixels, header.resolutionX, header.resolutionY)); + rslaLayers.push({ + index, + z: round(layer.z), + exposure: round(layer.exposure), + lightOffDelay: round(layer.lightOffDelay), + source: { + offset: layer.imageOffset, + length: layer.imageLength + }, + file + }); + if (progress) progress((index / layers.length) * 0.85, "ctb_decode"); + }); + + zip.file("manifest.json", JSON.stringify(createRSLAManifest(ctb, rslaLayers), null, 2)); + + return zip.generateAsync({ + type: "arraybuffer", + compression: "DEFLATE", + compressionOptions: { level: 6 }, + streamFiles: true, + mimeType: RSLA_MIME + }, meta => { + if (progress) progress(0.85 + (meta.percent / 100) * 0.15, "zip_gen"); + }); +} + +function decodeLayer(view, header, layer) { + let data = new Uint8Array( + view.buffer, + view.byteOffset + layer.imageOffset, + layer.imageLength + ); + let decoded = decodeRLE(decipher(header.encryptionSeed, layer.index, data), + header.resolutionX * header.resolutionY); + return decoded; +} + +function writeCTB(params) { + let { device, process, layers, volume, seed } = params; + let machine = device.deviceName || "Generic CTB"; + let machineBytes = new TextEncoder().encode(machine); + let printTime = Math.round((process.slaBaseLayers * process.slaBaseOn) + + Math.max(0, layers.length - process.slaBaseLayers) * process.slaLayerOn); + let printParamsOffset = HEADER_SIZE; + let machineInfoOffset = printParamsOffset + PRINT_PARAMS_SIZE; + let nameOffset = machineInfoOffset + MACHINE_INFO_SIZE; + let layerTableOffset = nameOffset + machineBytes.length; + let imageOffset = layerTableOffset + layers.length * LAYER_TABLE_RECORD_SIZE; + + layers.forEach(layer => { + layer.fullOffset = imageOffset; + layer.imageOffset = imageOffset + LAYER_FULL_RECORD_SIZE; + layer.imageLength = layer.data.length; + layer.blockLength = LAYER_FULL_RECORD_SIZE + layer.imageLength; + imageOffset = layer.imageOffset + layer.imageLength; + }); + + let buffer = new ArrayBuffer(imageOffset); + let writer = new BinaryWriter(buffer); + + writeHeader(writer, { + device, + process, + layers, + seed, + printTime, + printParamsOffset, + machineInfoOffset, + layerTableOffset + }); + writer.seek(printParamsOffset); + writePrintParams(writer, { process, volume }); + writer.seek(machineInfoOffset); + writeMachineInfo(writer, { machineBytes, nameOffset }); + writer.seek(nameOffset); + writer.writeBytes(machineBytes); + writer.seek(layerTableOffset); + layers.forEach(layer => writeLayerRecord(writer, layer)); + layers.forEach(layer => { + writer.seek(layer.fullOffset); + writeFullLayerRecord(writer, layer); + writer.writeBytes(layer.data); + }); + + return buffer; +} + +function writeHeader(writer, params) { + let { + device, process, layers, seed, printTime, + printParamsOffset, machineInfoOffset, layerTableOffset + } = params; + + writer.writeU32(CTB_V3_MAGIC); + writer.writeU32(3); + writer.writeF32(device.bedWidth); + writer.writeF32(device.bedDepth); + writer.writeF32(device.maxHeight || device.bedHeight); + writer.skip(12); + writer.writeF32(process.slaSlice); + writer.writeF32(process.slaLayerOn); + writer.writeF32(process.slaBaseOn); + writer.writeF32(process.slaLayerOff); + writer.writeU32(process.slaBaseLayers); + writer.writeU32(device.resolutionX); + writer.writeU32(device.resolutionY); + writer.writeU32(0); + writer.writeU32(layerTableOffset); + writer.writeU32(layers.length); + writer.writeU32(0); + writer.writeU32(printTime); + writer.writeU32(1); + writer.writeU32(printParamsOffset); + writer.writeU32(PRINT_PARAMS_SIZE); + writer.writeU32(process.slaAntiAlias || 1); + writer.writeU16(0x00ff); + writer.writeU16(0x00ff); + writer.writeU32(seed); + writer.writeU32(machineInfoOffset); + writer.writeU32(MACHINE_INFO_SIZE); +} + +function writePrintParams(writer, params) { + let { process, volume } = params; + + writer.writeF32(process.slaBasePeelDist); + writer.writeF32(process.slaBasePeelLiftRate * 60); + writer.writeF32(process.slaPeelDist); + writer.writeF32(process.slaPeelLiftRate * 60); + writer.writeF32(process.slaPeelDropRate * 60); + writer.writeF32(volume); + writer.writeF32(0); + writer.writeF32(0); + writer.writeF32(process.slaBaseOff); + writer.writeF32(process.slaLayerOff); + writer.writeU32(process.slaBaseLayers); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); +} + +function writeMachineInfo(writer, params) { + let { machineBytes, nameOffset } = params; + + writer.skip(0x1c); + writer.writeU32(nameOffset); + writer.writeU32(machineBytes.length); + writer.writeU32(machineBytes.length); + writer.writeU32(0x2000000f); + writer.writeU32(0x01c45dd1); + writer.writeU32(8); + writer.writeU32(0x01080000); + writer.writeF32(6.8); + writer.writeU32(0); + writer.writeU32(10); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); +} + +function writeLayerRecord(writer, layer) { + writer.writeF32(layer.z); + writer.writeF32(layer.exposure); + writer.writeF32(layer.lightOffDelay); + writer.writeU32(layer.imageOffset); + writer.writeU32(layer.imageLength); + writer.writeU32(0); + writer.writeU32(LAYER_FULL_RECORD_SIZE); + writer.writeU32(0); + writer.writeU32(0); +} + +function writeFullLayerRecord(writer, layer) { + writeLayerRecord(writer, layer); + writer.writeU32(layer.blockLength); + writer.writeF32(layer.liftDistance); + writer.writeF32(layer.liftSpeed); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeU32(0); + writer.writeF32(0); + writer.writeF32(0x00ff); +} + +function readHeader(view) { + let magic = view.getUint32(0, true); + let version = view.getUint32(4, true); + if (magic !== CTB_V3_MAGIC) { + throw new Error(`invalid CTB magic 0x${magic.toString(16)}`); + } + if (version !== 3) { + throw new Error(`unsupported CTB version ${version}`); + } + + return { + magic, + version, + bedWidth: view.getFloat32(0x08, true), + bedDepth: view.getFloat32(0x0c, true), + bedHeight: view.getFloat32(0x10, true), + layerHeight: view.getFloat32(0x20, true), + exposure: view.getFloat32(0x24, true), + bottomExposure: view.getFloat32(0x28, true), + lightOffDelay: view.getFloat32(0x2c, true), + bottomLayers: view.getUint32(0x30, true), + resolutionX: view.getUint32(0x34, true), + resolutionY: view.getUint32(0x38, true), + previewOffset: view.getUint32(0x3c, true), + layerTableOffset: view.getUint32(0x40, true), + layerCount: view.getUint32(0x44, true), + previewOneLength: view.getUint32(0x48, true), + printParamsOffset: view.getUint32(0x54, true), + printParamsLength: view.getUint32(0x58, true), + antiAliasLevel: view.getUint32(0x5c, true), + lightPWM: view.getUint16(0x60, true), + bottomLightPWM: view.getUint16(0x62, true), + encryptionSeed: view.getUint32(0x64, true), + machineInfoOffset: view.getUint32(0x68, true), + machineInfoLength: view.getUint32(0x6c, true) + }; +} + +function readLayerTable(view, header) { + let layers = []; + let offset = header.layerTableOffset; + + for (let index=0; index= header.layerTableOffset) { + record.full = readFullLayerRecord(view, fullOffset); + } + return record; +} + +function readFullLayerRecord(view, offset) { + return { + offset, + z: view.getFloat32(offset + 0, true), + exposure: view.getFloat32(offset + 4, true), + lightOffDelay: view.getFloat32(offset + 8, true), + imageOffset: view.getUint32(offset + 12, true), + imageLength: view.getUint32(offset + 16, true), + unknown20: view.getUint32(offset + 20, true), + fullRecordLength: view.getUint32(offset + 24, true), + unknown28: view.getUint32(offset + 28, true), + unknown32: view.getUint32(offset + 32, true), + blockLength: view.getUint32(offset + 36, true), + liftDistance: view.getFloat32(offset + 40, true), + liftSpeed: view.getFloat32(offset + 44, true), + unknown48: view.getUint32(offset + 48, true), + unknown52: view.getUint32(offset + 52, true), + unknown56: view.getUint32(offset + 56, true), + unknown60: view.getUint32(offset + 60, true), + unknown64: view.getUint32(offset + 64, true), + unknown68: view.getUint32(offset + 68, true), + unknown72: view.getUint32(offset + 72, true), + unknown76: view.getFloat32(offset + 76, true), + lightPWM: view.getFloat32(offset + 80, true) + }; +} + +function readPrintParams(view, header) { + let offset = header.printParamsOffset; + if (!offset || header.printParamsLength < 60) return {}; + + return { + bottomLiftDistance: view.getFloat32(offset + 0, true), + bottomLiftSpeed: view.getFloat32(offset + 4, true), + liftDistance: view.getFloat32(offset + 8, true), + liftSpeed: view.getFloat32(offset + 12, true), + retractSpeed: view.getFloat32(offset + 16, true), + volume: view.getFloat32(offset + 20, true), + weight: view.getFloat32(offset + 24, true), + cost: view.getFloat32(offset + 28, true), + bottomLightOffDelay: view.getFloat32(offset + 32, true), + lightOffDelay: view.getFloat32(offset + 36, true), + bottomLayers: view.getUint32(offset + 40, true), + unknown44: view.getUint32(offset + 44, true), + unknown48: view.getUint32(offset + 48, true), + unknown52: view.getUint32(offset + 52, true), + unknown56: view.getUint32(offset + 56, true) + }; +} + +function readMachineName(view, header) { + if (!header.machineInfoOffset || !header.machineInfoLength) return ""; + + let nameOffset = view.getUint32(header.machineInfoOffset + 0x1c, true); + let nameLength = view.getUint32(header.machineInfoOffset + 0x20, true); + if (!nameOffset || !nameLength) return ""; + + let chars = []; + for (let i=0; i> 8), length & 0xff); + } else if (length < 0x200000) { + output.push(0xc0 | (length >> 16), (length >> 8) & 0xff, length & 0xff); + } else if (length < 0x10000000) { + output.push( + 0xe0 | (length >> 24), + (length >> 16) & 0xff, + (length >> 8) & 0xff, + length & 0xff + ); + } else { + throw new Error(`CTB RLE run too long (${length})`); + } +} + +function quantize(value) { + return value ? Math.max(1, Math.min(0x7f, Math.round(value / 2))) : 0; +} + +function decodeRLE(input, pixelCount) { + let output = new Uint8Array(pixelCount); + let offset = 0; + let out = 0; + + while (offset < input.length && out < pixelCount) { + let code = input[offset++]; + let repeat = (code & 0x80) !== 0; + code &= 0x7f; + + let stride = repeat ? readRunLength(input, offset) : 1; + if (repeat) offset += stride.bytes; + + let value = code === 0 ? 0 : (code << 1) | 1; + let end = out + (repeat ? stride.length : stride); + if (end > pixelCount) { + throw new Error(`CTB RLE overrun in decoded layer (${end} > ${pixelCount})`); + } + output.fill(value, out, end); + out = end; + } + + if (out !== pixelCount) { + throw new Error(`CTB RLE underrun in decoded layer (${out} < ${pixelCount})`); + } + + return output; +} + +function readRunLength(input, offset) { + let slen = input[offset]; + if (slen === undefined) throw new Error("truncated CTB RLE run length"); + + if ((slen & 0x80) === 0) { + return { length: slen, bytes: 1 }; + } + if ((slen & 0xc0) === 0x80) { + requireBytes(input, offset, 2); + return { + length: ((slen & 0x3f) << 8) | input[offset + 1], + bytes: 2 + }; + } + if ((slen & 0xe0) === 0xc0) { + requireBytes(input, offset, 3); + return { + length: ((slen & 0x1f) << 16) | (input[offset + 1] << 8) | input[offset + 2], + bytes: 3 + }; + } + if ((slen & 0xf0) === 0xe0) { + requireBytes(input, offset, 4); + return { + length: (((slen & 0x0f) << 24) | (input[offset + 1] << 16) | + (input[offset + 2] << 8) | input[offset + 3]) >>> 0, + bytes: 4 + }; + } + + throw new Error(`unsupported CTB RLE run length prefix 0x${slen.toString(16)}`); +} + +function requireBytes(input, offset, length) { + if (offset + length > input.length) { + throw new Error("truncated CTB RLE run length"); + } +} + +function decipher(seed, layerIndex, input) { + if (!seed) return input.slice(); + + let output = new Uint8Array(input.length); + let keyring = newKeyring(seed, layerIndex); + for (let i=0; i>> 0, 0x2d83cdac) + 0xd8a83423) >>> 0; + let keySeed = (Math.imul(layerIndex >>> 0, 0x1e1530cd) + 0xec3d47cd) >>> 0; + let key = Math.imul(keySeed, init) >>> 0; + let index = 0; + + return { + next() { + let byte = (key >>> (8 * index)) & 0xff; + index++; + if ((index & 3) === 0) { + key = (key + init) >>> 0; + index = 0; + } + return byte; + } + }; +} + +function imageToPNG(image, width, height) { + return PNG.sync.write({ width, height, data: image }, { + colorType: 0, + inputColorType: 0, + inputHasAlpha: false + }); +} + +function toDataView(input) { + return input instanceof DataView + ? input + : new DataView(input.buffer || input, input.byteOffset || 0, input.byteLength); +} + +function round(value) { + return Number.parseFloat(Number(value || 0).toFixed(5)); +} + +function makeSeed(print) { + let { device, process } = print.settings; + let hash = 0x811c9dc5; + let text = [ + device.deviceName, + device.resolutionX, + device.resolutionY, + device.bedWidth, + device.bedDepth, + process.slaSlice, + process.slaLayerOn, + process.slaBaseOn + ].join("|"); + + for (let i=0; i>> 0; + } + + return hash || 0x5eed1234; +} + +class BinaryWriter { + constructor(buffer) { + this.view = new DataView(buffer); + this.pos = 0; + } + + seek(pos) { + this.pos = pos; + } + + skip(length) { + this.pos += length; + } + + writeBytes(bytes) { + new Uint8Array(this.view.buffer, this.pos, bytes.length).set(bytes); + this.pos += bytes.length; + } + + writeU8(value) { + this.view.setUint8(this.pos, value); + this.pos += 1; + } + + writeU16(value) { + this.view.setUint16(this.pos, value, true); + this.pos += 2; + } + + writeU32(value) { + this.view.setUint32(this.pos, value >>> 0, true); + this.pos += 4; + } + + writeF32(value) { + this.view.setFloat32(this.pos, Number(value || 0), true); + this.pos += 4; + } +} + +export const CTB = { + encode, + read, + toRSLA, + decodeLayer, + encodeRLE +};