add sla .ctb format and a few related printer targets

This commit is contained in:
Stewart Allen 2026-05-15 16:53:58 -04:00
commit a1af70b496
11 changed files with 937 additions and 11 deletions

39
bin/ctb2rsla.mjs Executable file
View file

@ -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}`);

View file

@ -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:{

View file

@ -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
[

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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) };

View file

@ -0,0 +1,68 @@
/** Copyright Stewart Allen <sa@grid.space> -- 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;
}

View file

@ -1,10 +1,12 @@
/** Copyright Stewart Allen <sa@grid.space> -- 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,

View file

@ -0,0 +1,750 @@
/** Copyright Stewart Allen <sa@grid.space> -- 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<layermax; index++) {
let rendered = photon.renderLayerWasm({
index,
width,
height,
widgets,
scaleX,
scaleY,
masks: []
});
if (rendered.end) {
break;
}
let pixels = renderToCTBPixels(rendered.image, width, height);
let encoded = encipher(seed, index, encodeRLE(pixels));
let bottom = index < process.slaBaseLayers;
volume += rendered.area * process.slaSlice;
layers.push({
index,
z: process.slaFirstOffset + process.slaSlice * (index + 1),
exposure: bottom ? process.slaBaseOn : process.slaLayerOn,
lightOffDelay: bottom ? process.slaBaseOff : process.slaLayerOff,
liftDistance: bottom ? process.slaBasePeelDist : process.slaPeelDist,
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
data: encoded
});
if (progress) progress((index / layermax) * 0.85, "ctb_encode");
}
let file = writeCTB({ device, process, layers, volume, seed });
if (progress) progress(1, "ctb_write");
return { file, layers: layers.length, volume };
}
function read(input) {
let view = input instanceof DataView
? input
: new DataView(input.buffer || input, input.byteOffset || 0, input.byteLength);
let header = readHeader(view);
let layers = readLayerTable(view, header);
return {
header,
print: readPrintParams(view, header),
machine: readMachineName(view, header),
layers,
layout: inferLayout(header, layers)
};
}
function toRSLA(input, progress) {
let view = toDataView(input);
let ctb = read(view);
let { header, layers } = ctb;
let zip = new JSZip();
let rslaLayers = [];
layers.forEach((layer, 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.layerCount; index++) {
layers.push(readLayerRecord(view, header, offset, index));
offset += LAYER_TABLE_RECORD_SIZE;
}
return layers;
}
function readLayerRecord(view, header, offset, index) {
let record = {
index,
tableOffset: 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)
};
let fullOffset = record.imageOffset - LAYER_FULL_RECORD_SIZE;
if (fullOffset >= 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<nameLength; i++) {
let c = view.getUint8(nameOffset + i);
if (c === 0) break;
chars.push(String.fromCharCode(c));
}
return chars.join("");
}
function inferLayout(header, layers) {
let tableEnd = header.layerTableOffset + header.layerCount * LAYER_TABLE_RECORD_SIZE;
let firstLayer = layers[0];
let firstFullRecordOffset = firstLayer ? firstLayer.imageOffset - LAYER_FULL_RECORD_SIZE : 0;
return {
headerSize: HEADER_SIZE,
layerTableRecordSize: LAYER_TABLE_RECORD_SIZE,
layerFullRecordSize: LAYER_FULL_RECORD_SIZE,
tableEnd,
firstFullRecordOffset,
hasFullRecordsBeforeImages: firstFullRecordOffset === tableEnd
};
}
function createRSLAManifest(ctb, layers) {
let { header, print, machine } = ctb;
return {
format: "rsla",
version: 1,
units: "mm",
coordinateSystem: {
layerImage: "top-left",
model: "bed-center",
x: "right",
y: "back",
z: "up"
},
source: {
format: "ctb",
version: header.version,
machine
},
bed: {
width: round(header.bedWidth),
depth: round(header.bedDepth),
height: round(header.bedHeight)
},
resolution: {
x: header.resolutionX,
y: header.resolutionY
},
layerHeight: round(header.layerHeight),
layerCount: layers.length,
raster: {
encoding: "png",
color: "grayscale",
cure: "nonzero",
white: "cure",
black: "off"
},
process: {
bottomLayers: print.bottomLayers ?? header.bottomLayers,
exposure: round(header.exposure),
bottomExposure: round(header.bottomExposure),
lightOffDelay: round(print.lightOffDelay ?? header.lightOffDelay),
bottomLightOffDelay: round(print.bottomLightOffDelay),
liftHeight: round(print.liftDistance),
bottomLiftHeight: round(print.bottomLiftDistance),
liftSpeed: round(print.liftSpeed),
bottomLiftSpeed: round(print.bottomLiftSpeed),
retractSpeed: round(print.retractSpeed),
antiAlias: header.antiAliasLevel,
lightPWM: header.lightPWM,
bottomLightPWM: header.bottomLightPWM
},
layers
};
}
function renderToCTBPixels(image, width, height) {
let data = new Uint8Array(width * height);
for (let x=0; x<width; x++) {
for (let y=0; y<height; y++) {
data[y * width + x] = image[x * height + y] || 0;
}
}
return data;
}
function encodeRLE(input) {
let output = [];
let color = quantize(input[0]);
let run = 1;
for (let offset=1; offset<input.length; offset++) {
let next = quantize(input[offset]);
if (next === color) {
run++;
continue;
}
writeRun(output, color, run);
color = next;
run = 1;
}
writeRun(output, color, run);
return new Uint8Array(output);
}
function writeRun(output, color, length) {
if (length === 1) {
output.push(color);
return;
}
output.push(color | 0x80);
if (length < 0x80) {
output.push(length);
} else if (length < 0x4000) {
output.push(0x80 | (length >> 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<input.length; i++) {
output[i] = input[i] ^ keyring.next();
}
return output;
}
function encipher(seed, layerIndex, input) {
return decipher(seed, layerIndex, input);
}
function newKeyring(seed, layerIndex) {
let init = (Math.imul(seed >>> 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<text.length; i++) {
hash ^= text.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 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
};