update .ctb options
This commit is contained in:
parent
b65d4ae57e
commit
7e105f255e
11 changed files with 573 additions and 128 deletions
|
|
@ -210,6 +210,23 @@ Same header structure as CTB v3, with additional support for:
|
|||
- PageNumber field in layer table
|
||||
- Transition layers
|
||||
|
||||
#### 1.4 CTB Encrypted v4-5
|
||||
|
||||
**Extensions:** `.ctb`, `.encrypted.ctb`
|
||||
**Magic:** `0x12FD0107` (LE)
|
||||
**Versions:** 4, 5 (default: 5)
|
||||
**Settings block:** 288-byte slicer-settings table encrypted with AES-256-CBC, no padding, zero-padded to 16-byte blocks
|
||||
**Layer data:** CTB variable-length 7-bit grayscale RLE, then CTB layer XOR using `LayerXorKey` (`0xEFBEADDE` by default); some files additionally AES-encrypt layer-data subranges
|
||||
|
||||
Encrypted CTB is a different container from plain CTB v3/v4/v5, not just a flag on the v3 layout. The fixed 48-byte header points at the encrypted settings block and a final encrypted SHA-256 signature. The AES key/IV are derived from UVtools-compatible Chitubox constants with the `UVtools` XOR mask. Kiri keeps these primitives native in JS in `x_ctb_crypto.js`; full writer support still needs the encrypted settings table, layer pointer table, resin parameters, previews, and final signature wired into `x_ctb`.
|
||||
|
||||
Key tables:
|
||||
- File header: 48 bytes
|
||||
- Slicer settings: 288 bytes
|
||||
- Layer pointer: 16 bytes
|
||||
- Layer definition: 88 bytes
|
||||
- Per-layer settings flag: `0x40` allow, `0x00` disallow
|
||||
|
||||
### 2. Anycubic Family
|
||||
|
||||
#### 2.1 Legacy Anycubic Formats (v1)
|
||||
|
|
@ -654,6 +671,46 @@ PrinterModel = SL1
|
|||
}
|
||||
```
|
||||
|
||||
### RSLA / VSLA (Grid.Space Generic)
|
||||
|
||||
**Extensions:** `.rsla`, `.vsla`
|
||||
**Type:** ZIP with JSON manifest
|
||||
**Layer Format:** RSLA stores raster PNG layers; VSLA stores SVG-like vector layer geometry
|
||||
|
||||
These formats are intended as a vendor-neutral interchange and debugging target. They carry enough process metadata to preserve behavior before conversion to a proprietary printer format.
|
||||
|
||||
**Shared manifest process fields:**
|
||||
```json
|
||||
{
|
||||
"process": {
|
||||
"bottomLayers": 5,
|
||||
"transitionLayers": 8,
|
||||
"exposure": 2.4,
|
||||
"bottomExposure": 28,
|
||||
"lightOffDelay": 0,
|
||||
"bottomLightOffDelay": 0,
|
||||
"waitBeforeCure": 0.5,
|
||||
"bottomWaitBeforeCure": 2,
|
||||
"waitAfterCure": 0,
|
||||
"waitAfterLift": 0,
|
||||
"liftHeight": 5,
|
||||
"bottomLiftHeight": 6,
|
||||
"liftSpeed": 180,
|
||||
"bottomLiftSpeed": 60,
|
||||
"retractHeight": 5,
|
||||
"retractSpeed": 180,
|
||||
"lightPWM": 255,
|
||||
"bottomLightPWM": 255,
|
||||
"motion": "normal",
|
||||
"antiAlias": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each layer also carries a resolved per-layer `process` object with `bottom`, `transition`, `transitionIndex`, effective exposure, waits, motion, and PWM. Proprietary writers should prefer this resolved per-layer model where their format supports it, then fall back to global process fields where it does not.
|
||||
|
||||
VSLA layer SVG roots additionally include process data attributes such as `data-exposure`, `data-light-off-delay`, `data-light-pwm`, `data-bottom`, and `data-transition` so the per-layer intent is visible without parsing the manifest.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Guidance for Sandboxed Environments
|
||||
|
|
@ -898,6 +955,7 @@ def decode_ctb_variable_rle(data: bytes, width: int, height: int) -> bytes:
|
|||
**CTB:**
|
||||
- `UVtools.Core/FileFormats/ChituboxFile.cs`: `DecodeCtbImage`, `EncodeCtbImage`, `LayerRleCryptBuffer`
|
||||
- `UVtools.Core/FileFormats/CTBEncryptedFile.cs`: encrypted CTB v4-v5 container and AES handling
|
||||
- `src/kiri/mode/sla/work/x_ctb_crypto.js`: native JS AES-256-CBC/no-padding, CTB layer XOR, encrypted CTB constants
|
||||
|
||||
**Anycubic:**
|
||||
- `UVtools.Core/FileFormats/AnycubicFile.cs`: `FileMark`, `Header`, `LayerDef`, `GetAvailableVersionsForExtension`, `PrinterModel`
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
"bed_height": 1,
|
||||
"max_height": 300,
|
||||
"resolution_x": 3840,
|
||||
"resolution_y": 2160
|
||||
"resolution_y": 2160,
|
||||
"sla_format": "cxdlp",
|
||||
"sla_file_ext": "cxdlp"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
"bed_height": 1,
|
||||
"max_height": 160,
|
||||
"resolution_x": 1620,
|
||||
"resolution_y": 2560
|
||||
"resolution_y": 2560,
|
||||
"sla_format": "cxdlp",
|
||||
"sla_file_ext": "cxdlp"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
"bed_height": 1,
|
||||
"max_height": 200,
|
||||
"resolution_x": 3840,
|
||||
"resolution_y": 2400
|
||||
"resolution_y": 2400,
|
||||
"sla_format": "cxdlp",
|
||||
"sla_file_ext": "cxdlp"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ export const SLA_FORMATS = {
|
|||
priority: 1,
|
||||
family: 'chitubox'
|
||||
},
|
||||
ctbe: {
|
||||
id: 'ctbe',
|
||||
ext: 'encrypted.ctb',
|
||||
proprietary: true,
|
||||
available: false,
|
||||
priority: 1,
|
||||
family: 'chitubox-encrypted'
|
||||
},
|
||||
goo: {
|
||||
id: 'goo',
|
||||
ext: 'goo',
|
||||
|
|
|
|||
128
src/kiri/mode/sla/work/meta.js
Normal file
128
src/kiri/mode/sla/work/meta.js
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
const DEFAULT_PWM = 255;
|
||||
const TILT_MOTION = {
|
||||
liftHeight: 0.05,
|
||||
liftSpeed: 0.05,
|
||||
retractHeight: 0,
|
||||
retractSpeed: 0.05,
|
||||
motorTime: 4
|
||||
};
|
||||
|
||||
export function createProcessMetadata(device, process) {
|
||||
let transitionLayers = intValue(process.slaTransitionLayers);
|
||||
let lightPWM = pwmValue(process.slaLightPWM);
|
||||
let bottomLightPWM = pwmValue(process.slaBaseLightPWM, lightPWM);
|
||||
let normalMotion = motionFor(device, process, false);
|
||||
let bottomMotion = motionFor(device, process, true);
|
||||
|
||||
return {
|
||||
bottomLayers: intValue(process.slaBaseLayers),
|
||||
transitionLayers,
|
||||
exposure: number(process.slaLayerOn),
|
||||
bottomExposure: number(process.slaBaseOn),
|
||||
lightOffDelay: number(process.slaLayerOff),
|
||||
bottomLightOffDelay: number(process.slaBaseOff),
|
||||
waitBeforeCure: number(process.slaWaitBeforeCure ?? process.slaLayerOff),
|
||||
bottomWaitBeforeCure: number(process.slaBaseWaitBeforeCure ?? process.slaBaseOff),
|
||||
waitAfterCure: number(process.slaWaitAfterCure),
|
||||
bottomWaitAfterCure: number(process.slaBaseWaitAfterCure),
|
||||
waitAfterLift: number(process.slaWaitAfterLift),
|
||||
bottomWaitAfterLift: number(process.slaBaseWaitAfterLift),
|
||||
liftHeight: normalMotion.liftHeight,
|
||||
bottomLiftHeight: bottomMotion.liftHeight,
|
||||
liftSpeed: normalMotion.liftSpeed,
|
||||
bottomLiftSpeed: bottomMotion.liftSpeed,
|
||||
retractHeight: normalMotion.retractHeight,
|
||||
bottomRetractHeight: bottomMotion.retractHeight,
|
||||
retractSpeed: normalMotion.retractSpeed,
|
||||
bottomRetractSpeed: bottomMotion.retractSpeed,
|
||||
lightPWM,
|
||||
bottomLightPWM,
|
||||
motion: device.slaMotion || "normal",
|
||||
firstLayerOffset: number(process.slaFirstOffset),
|
||||
antiAlias: intValue(process.slaAntiAlias, 1)
|
||||
};
|
||||
}
|
||||
|
||||
export function createLayerMetadata(device, process, index, z, area) {
|
||||
let meta = createProcessMetadata(device, process);
|
||||
let bottom = index < meta.bottomLayers;
|
||||
let transitionIndex = bottom ? -1 : index - meta.bottomLayers;
|
||||
let transition = transitionIndex >= 0 && transitionIndex < meta.transitionLayers;
|
||||
let motion = motionFor(device, process, bottom);
|
||||
|
||||
return {
|
||||
index,
|
||||
z: round(z),
|
||||
area: round(area),
|
||||
bottom,
|
||||
transition,
|
||||
transitionIndex: transition ? transitionIndex : -1,
|
||||
exposure: round(layerExposure(meta, bottom, transitionIndex)),
|
||||
lightOffDelay: bottom ? meta.bottomLightOffDelay : meta.lightOffDelay,
|
||||
waitBeforeCure: bottom ? meta.bottomWaitBeforeCure : meta.waitBeforeCure,
|
||||
waitAfterCure: bottom ? meta.bottomWaitAfterCure : meta.waitAfterCure,
|
||||
waitAfterLift: bottom ? meta.bottomWaitAfterLift : meta.waitAfterLift,
|
||||
liftHeight: motion.liftHeight,
|
||||
liftSpeed: motion.liftSpeed,
|
||||
retractHeight: motion.retractHeight,
|
||||
retractSpeed: motion.retractSpeed,
|
||||
lightPWM: bottom ? meta.bottomLightPWM : meta.lightPWM
|
||||
};
|
||||
}
|
||||
|
||||
export function motionFor(device, process, bottom) {
|
||||
if (device.slaMotion === "tilt") {
|
||||
return TILT_MOTION;
|
||||
}
|
||||
|
||||
let normalDrop = process.slaPeelDropRate;
|
||||
let bottomDrop = process.slaBasePeelDropRate ?? normalDrop;
|
||||
return {
|
||||
liftHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
|
||||
liftSpeed: number(bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
|
||||
retractHeight: number(bottom ? process.slaBasePeelDist : process.slaPeelDist),
|
||||
retractSpeed: number(bottom ? bottomDrop : normalDrop) * 60,
|
||||
motorTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
export function estimatePrintTime(device, process, layers) {
|
||||
let meta = createProcessMetadata(device, process);
|
||||
let seconds = 0;
|
||||
for (let index=0; index<layers; index++) {
|
||||
let layer = createLayerMetadata(device, process, index, 0, 0);
|
||||
let motor = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
|
||||
seconds += layer.exposure + layer.waitBeforeCure + layer.waitAfterCure + layer.waitAfterLift + motor;
|
||||
}
|
||||
return Math.ceil(seconds);
|
||||
}
|
||||
|
||||
export function round(value) {
|
||||
return Number.parseFloat(Number(value || 0).toFixed(5));
|
||||
}
|
||||
|
||||
function layerExposure(meta, bottom, transitionIndex) {
|
||||
if (bottom || meta.transitionLayers <= 0 || transitionIndex < 0 || transitionIndex >= meta.transitionLayers) {
|
||||
return bottom ? meta.bottomExposure : meta.exposure;
|
||||
}
|
||||
|
||||
let step = (transitionIndex + 1) / (meta.transitionLayers + 1);
|
||||
return meta.bottomExposure + (meta.exposure - meta.bottomExposure) * step;
|
||||
}
|
||||
|
||||
function pwmValue(value, fallback = DEFAULT_PWM) {
|
||||
value = intValue(value, fallback);
|
||||
return Math.max(0, Math.min(255, value));
|
||||
}
|
||||
|
||||
function intValue(value, fallback = 0) {
|
||||
value = Number.parseInt(value);
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
value = Number(value);
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
import { JSZip } from '../../../../ext/jszip-esm.js';
|
||||
import { PNG } from '../../../../ext/pngjs.esm.js';
|
||||
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
|
||||
import { ctbLayerCrypt } from './x_ctb_crypto.js';
|
||||
|
||||
const CTB_V3_MAGIC = 0x12fd0086;
|
||||
const HEADER_SIZE = 0x70;
|
||||
|
|
@ -715,37 +716,11 @@ function requireBytes(input, offset, 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;
|
||||
return ctbLayerCrypt(seed, layerIndex, input);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
return ctbLayerCrypt(seed, layerIndex, input);
|
||||
}
|
||||
|
||||
function imageToPNG(image, width, height) {
|
||||
|
|
|
|||
321
src/kiri/mode/sla/work/x_ctb_crypto.js
Normal file
321
src/kiri/mode/sla/work/x_ctb_crypto.js
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
const UVTOOLS_KEY = "UVtools";
|
||||
const SECRET_KEY = "hQ36XB6yTk+zO02ysyiowt8yC1buK+nbLWyfY40EXoU=";
|
||||
const SECRET_IV = "Wld+ampndVJecmVjYH5cWQ==";
|
||||
const BLOCK_SIZE = 16;
|
||||
const SBOX = new Uint8Array(256);
|
||||
const INV_SBOX = new Uint8Array(256);
|
||||
const RCON = new Uint8Array(15);
|
||||
|
||||
initTables();
|
||||
|
||||
export const CTB_ENCRYPTED_MAGIC = 0x12fd0107;
|
||||
export const CTB_ENCRYPTED_VERSION = 5;
|
||||
export const CTB_LAYER_XOR_KEY = 0xefbeadde;
|
||||
export const CTB_ENCRYPTED_HEADER_SIZE = 48;
|
||||
export const CTB_ENCRYPTED_SETTINGS_SIZE = 288;
|
||||
export const CTB_ENCRYPTED_LAYER_POINTER_SIZE = 16;
|
||||
export const CTB_ENCRYPTED_LAYER_DEF_SIZE = 88;
|
||||
export const CTB_ENCRYPTED_MIN_RLE_AES_LENGTH = 512;
|
||||
export const CTB_PER_LAYER_SETTINGS_ALLOW = 0x40;
|
||||
export const CTB_PER_LAYER_SETTINGS_DISALLOW = 0x00;
|
||||
|
||||
export const CTB_ENCRYPTED_KEY = xorCipher(base64Bytes(SECRET_KEY), UVTOOLS_KEY);
|
||||
export const CTB_ENCRYPTED_IV = xorCipher(base64Bytes(SECRET_IV), UVTOOLS_KEY);
|
||||
|
||||
export function ctbLayerCrypt(seed, layerIndex, input) {
|
||||
let output = new Uint8Array(input);
|
||||
ctbLayerCryptInPlace(seed, layerIndex, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function ctbLayerCryptInPlace(seed, layerIndex, input) {
|
||||
if (!seed) return input;
|
||||
|
||||
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;
|
||||
|
||||
for (let i=0; i<input.length; i++) {
|
||||
input[i] ^= (key >>> (8 * index)) & 0xff;
|
||||
index++;
|
||||
if ((index & 3) === 0) {
|
||||
key = (key + init) >>> 0;
|
||||
index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
export function ctbEncrypt(data) {
|
||||
return aesCbcCrypt(data, true, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
|
||||
}
|
||||
|
||||
export function ctbDecrypt(data) {
|
||||
return aesCbcCrypt(data, false, CTB_ENCRYPTED_KEY, CTB_ENCRYPTED_IV);
|
||||
}
|
||||
|
||||
export async function ctbSignature(checksumValue = 0xcafebabe) {
|
||||
let bytes = new Uint8Array(8);
|
||||
let view = new DataView(bytes.buffer);
|
||||
view.setUint32(0, checksumValue >>> 0, true);
|
||||
view.setUint32(4, Math.floor(checksumValue / 0x100000000), true);
|
||||
return ctbEncrypt(await sha256(bytes));
|
||||
}
|
||||
|
||||
function aesCbcCrypt(data, encrypt, key, iv) {
|
||||
if (key.length !== 32) {
|
||||
throw new Error(`CTB AES key must be 32 bytes (${key.length})`);
|
||||
}
|
||||
if (iv.length !== BLOCK_SIZE) {
|
||||
throw new Error(`CTB AES IV must be 16 bytes (${iv.length})`);
|
||||
}
|
||||
|
||||
let keys = expandKey(key);
|
||||
let input = encrypt ? zeroPad(data) : toBytes(data);
|
||||
if (input.length % BLOCK_SIZE !== 0) {
|
||||
throw new Error(`CTB AES input must align to 16 bytes (${input.length})`);
|
||||
}
|
||||
|
||||
let output = new Uint8Array(input.length);
|
||||
let previous = new Uint8Array(iv);
|
||||
|
||||
for (let offset=0; offset<input.length; offset += BLOCK_SIZE) {
|
||||
let block = input.slice(offset, offset + BLOCK_SIZE);
|
||||
|
||||
if (encrypt) {
|
||||
xorBlock(block, previous);
|
||||
block = encryptBlock(block, keys);
|
||||
previous = block;
|
||||
output.set(block, offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
let current = block;
|
||||
block = decryptBlock(block, keys);
|
||||
xorBlock(block, previous);
|
||||
previous = current;
|
||||
output.set(block, offset);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function encryptBlock(block, keys) {
|
||||
let state = new Uint8Array(block);
|
||||
let rounds = keys.rounds;
|
||||
|
||||
addRoundKey(state, keys.bytes, 0);
|
||||
for (let round=1; round<rounds; round++) {
|
||||
subBytes(state);
|
||||
shiftRows(state);
|
||||
mixColumns(state);
|
||||
addRoundKey(state, keys.bytes, round);
|
||||
}
|
||||
subBytes(state);
|
||||
shiftRows(state);
|
||||
addRoundKey(state, keys.bytes, rounds);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function decryptBlock(block, keys) {
|
||||
let state = new Uint8Array(block);
|
||||
let rounds = keys.rounds;
|
||||
|
||||
addRoundKey(state, keys.bytes, rounds);
|
||||
for (let round=rounds - 1; round>0; round--) {
|
||||
invShiftRows(state);
|
||||
invSubBytes(state);
|
||||
addRoundKey(state, keys.bytes, round);
|
||||
invMixColumns(state);
|
||||
}
|
||||
invShiftRows(state);
|
||||
invSubBytes(state);
|
||||
addRoundKey(state, keys.bytes, 0);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function expandKey(key) {
|
||||
let nk = 8;
|
||||
let nb = 4;
|
||||
let rounds = 14;
|
||||
let bytes = new Uint8Array(nb * (rounds + 1) * 4);
|
||||
bytes.set(key);
|
||||
|
||||
let temp = new Uint8Array(4);
|
||||
let words = nb * (rounds + 1);
|
||||
for (let word=nk; word<words; word++) {
|
||||
let prev = (word - 1) * 4;
|
||||
temp[0] = bytes[prev];
|
||||
temp[1] = bytes[prev + 1];
|
||||
temp[2] = bytes[prev + 2];
|
||||
temp[3] = bytes[prev + 3];
|
||||
|
||||
if (word % nk === 0) {
|
||||
let first = temp[0];
|
||||
temp[0] = SBOX[temp[1]] ^ RCON[word / nk];
|
||||
temp[1] = SBOX[temp[2]];
|
||||
temp[2] = SBOX[temp[3]];
|
||||
temp[3] = SBOX[first];
|
||||
} else if (word % nk === 4) {
|
||||
temp[0] = SBOX[temp[0]];
|
||||
temp[1] = SBOX[temp[1]];
|
||||
temp[2] = SBOX[temp[2]];
|
||||
temp[3] = SBOX[temp[3]];
|
||||
}
|
||||
|
||||
let prior = (word - nk) * 4;
|
||||
let out = word * 4;
|
||||
bytes[out] = bytes[prior] ^ temp[0];
|
||||
bytes[out + 1] = bytes[prior + 1] ^ temp[1];
|
||||
bytes[out + 2] = bytes[prior + 2] ^ temp[2];
|
||||
bytes[out + 3] = bytes[prior + 3] ^ temp[3];
|
||||
}
|
||||
|
||||
return { bytes, rounds };
|
||||
}
|
||||
|
||||
function addRoundKey(state, keys, round) {
|
||||
let offset = round * BLOCK_SIZE;
|
||||
for (let i=0; i<BLOCK_SIZE; i++) {
|
||||
state[i] ^= keys[offset + i];
|
||||
}
|
||||
}
|
||||
|
||||
function subBytes(state) {
|
||||
for (let i=0; i<BLOCK_SIZE; i++) state[i] = SBOX[state[i]];
|
||||
}
|
||||
|
||||
function invSubBytes(state) {
|
||||
for (let i=0; i<BLOCK_SIZE; i++) state[i] = INV_SBOX[state[i]];
|
||||
}
|
||||
|
||||
function shiftRows(state) {
|
||||
let t = state.slice();
|
||||
state[1] = t[5]; state[5] = t[9]; state[9] = t[13]; state[13] = t[1];
|
||||
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
|
||||
state[3] = t[15]; state[7] = t[3]; state[11] = t[7]; state[15] = t[11];
|
||||
}
|
||||
|
||||
function invShiftRows(state) {
|
||||
let t = state.slice();
|
||||
state[1] = t[13]; state[5] = t[1]; state[9] = t[5]; state[13] = t[9];
|
||||
state[2] = t[10]; state[6] = t[14]; state[10] = t[2]; state[14] = t[6];
|
||||
state[3] = t[7]; state[7] = t[11]; state[11] = t[15]; state[15] = t[3];
|
||||
}
|
||||
|
||||
function mixColumns(state) {
|
||||
for (let c=0; c<BLOCK_SIZE; c += 4) {
|
||||
let a0 = state[c];
|
||||
let a1 = state[c + 1];
|
||||
let a2 = state[c + 2];
|
||||
let a3 = state[c + 3];
|
||||
let t = a0 ^ a1 ^ a2 ^ a3;
|
||||
let u = a0;
|
||||
state[c] ^= t ^ xtime(a0 ^ a1);
|
||||
state[c + 1] ^= t ^ xtime(a1 ^ a2);
|
||||
state[c + 2] ^= t ^ xtime(a2 ^ a3);
|
||||
state[c + 3] ^= t ^ xtime(a3 ^ u);
|
||||
}
|
||||
}
|
||||
|
||||
function invMixColumns(state) {
|
||||
for (let c=0; c<BLOCK_SIZE; c += 4) {
|
||||
let a0 = state[c];
|
||||
let a1 = state[c + 1];
|
||||
let a2 = state[c + 2];
|
||||
let a3 = state[c + 3];
|
||||
state[c] = mul(a0, 14) ^ mul(a1, 11) ^ mul(a2, 13) ^ mul(a3, 9);
|
||||
state[c + 1] = mul(a0, 9) ^ mul(a1, 14) ^ mul(a2, 11) ^ mul(a3, 13);
|
||||
state[c + 2] = mul(a0, 13) ^ mul(a1, 9) ^ mul(a2, 14) ^ mul(a3, 11);
|
||||
state[c + 3] = mul(a0, 11) ^ mul(a1, 13) ^ mul(a2, 9) ^ mul(a3, 14);
|
||||
}
|
||||
}
|
||||
|
||||
function initTables() {
|
||||
RCON[1] = 1;
|
||||
for (let i=2; i<RCON.length; i++) RCON[i] = xtime(RCON[i - 1]);
|
||||
|
||||
for (let i=0; i<256; i++) {
|
||||
let inv = i ? gfPow(i, 254) : 0;
|
||||
let s = inv ^ rot(inv, 1) ^ rot(inv, 2) ^ rot(inv, 3) ^ rot(inv, 4) ^ 0x63;
|
||||
SBOX[i] = s & 0xff;
|
||||
INV_SBOX[SBOX[i]] = i;
|
||||
}
|
||||
}
|
||||
|
||||
function xtime(value) {
|
||||
return ((value << 1) ^ ((value & 0x80) ? 0x1b : 0)) & 0xff;
|
||||
}
|
||||
|
||||
function mul(a, b) {
|
||||
let out = 0;
|
||||
while (b) {
|
||||
if (b & 1) out ^= a;
|
||||
a = xtime(a);
|
||||
b >>= 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function gfPow(value, power) {
|
||||
let out = 1;
|
||||
while (power) {
|
||||
if (power & 1) out = mul(out, value);
|
||||
value = mul(value, value);
|
||||
power >>= 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function rot(value, shift) {
|
||||
return ((value << shift) | (value >> (8 - shift))) & 0xff;
|
||||
}
|
||||
|
||||
function zeroPad(data) {
|
||||
data = toBytes(data);
|
||||
let length = Math.ceil(data.length / BLOCK_SIZE) * BLOCK_SIZE;
|
||||
if (length === data.length) return data;
|
||||
|
||||
let output = new Uint8Array(length);
|
||||
output.set(data);
|
||||
return output;
|
||||
}
|
||||
|
||||
function xorBlock(block, key) {
|
||||
for (let i=0; i<BLOCK_SIZE; i++) block[i] ^= key[i];
|
||||
}
|
||||
|
||||
function xorCipher(bytes, key) {
|
||||
let output = new Uint8Array(bytes.length);
|
||||
for (let i=0; i<bytes.length; i++) {
|
||||
output[i] = bytes[i] ^ key.charCodeAt(i % key.length);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function base64Bytes(text) {
|
||||
let binary = atob(text);
|
||||
let bytes = new Uint8Array(binary.length);
|
||||
for (let i=0; i<binary.length; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function toBytes(data) {
|
||||
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
||||
}
|
||||
|
||||
async function sha256(bytes) {
|
||||
let subtle = globalThis.crypto && globalThis.crypto.subtle;
|
||||
if (!subtle) {
|
||||
throw new Error("CTB encrypted signature requires crypto.subtle SHA-256");
|
||||
}
|
||||
return new Uint8Array(await subtle.digest("SHA-256", bytes));
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
|
||||
import { createLayerMetadata, createProcessMetadata, estimatePrintTime, motionFor } from './meta.js';
|
||||
|
||||
const FAMILY = "GOO";
|
||||
const FILE_VERSION = "V3.0";
|
||||
|
|
@ -11,13 +12,6 @@ const HEADER_SIZE = 195477;
|
|||
const LAYER_DEF_SIZE = 70;
|
||||
const FOOTER_SIZE = 11;
|
||||
const MAX_RUN = 0x0fffffff;
|
||||
const TILT_MOTION = {
|
||||
liftHeight: 0.05,
|
||||
liftSpeed: 0.05,
|
||||
retractHeight: 0,
|
||||
retractSpeed: 0.05,
|
||||
motorTime: 4
|
||||
};
|
||||
|
||||
const FORMAT_RECORDS = {
|
||||
goo: { ext: "goo", machine: "Elegoo GOO", notes: "Elegoo GOO" },
|
||||
|
|
@ -46,9 +40,9 @@ function encodeGOO(print, progress, photon, rec) {
|
|||
let previewLarge = createPreview(290, 290);
|
||||
|
||||
let { ctx, volume } = renderRasterLayers(print, photon, params => {
|
||||
let { index, rendered, bottom, z, ctx } = params;
|
||||
let { index, rendered, z, ctx } = params;
|
||||
let { device, process, width, height } = ctx;
|
||||
let motion = layerMotion(device, process, bottom);
|
||||
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
|
||||
let raster = imageToRowMajor(rendered.image, width, height, normalizePixel);
|
||||
|
||||
accumulatePreview(previewSmall, rendered.image, width, height);
|
||||
|
|
@ -57,22 +51,22 @@ function encodeGOO(print, progress, photon, rec) {
|
|||
layers.push({
|
||||
index,
|
||||
data: encodeRLE(raster),
|
||||
z: round(z),
|
||||
z: layerMeta.z,
|
||||
pausePositionZ: ctx.device.maxHeight || ctx.device.bedHeight || 0,
|
||||
exposure: bottom ? process.slaBaseOn : process.slaLayerOn,
|
||||
lightOffDelay: 0,
|
||||
waitTimeAfterCure: 0,
|
||||
waitTimeAfterLift: 0,
|
||||
waitTimeBeforeCure: bottom ? process.slaBaseOff : process.slaLayerOff,
|
||||
liftHeight: motion.liftHeight,
|
||||
liftSpeed: motion.liftSpeed,
|
||||
exposure: layerMeta.exposure,
|
||||
lightOffDelay: layerMeta.lightOffDelay,
|
||||
waitTimeAfterCure: layerMeta.waitAfterCure,
|
||||
waitTimeAfterLift: layerMeta.waitAfterLift,
|
||||
waitTimeBeforeCure: layerMeta.waitBeforeCure,
|
||||
liftHeight: layerMeta.liftHeight,
|
||||
liftSpeed: layerMeta.liftSpeed,
|
||||
liftHeight2: 0,
|
||||
liftSpeed2: 0,
|
||||
retractHeight: motion.retractHeight,
|
||||
retractSpeed: motion.retractSpeed,
|
||||
retractHeight: layerMeta.retractHeight,
|
||||
retractSpeed: layerMeta.retractSpeed,
|
||||
retractHeight2: 0,
|
||||
retractSpeed2: 0,
|
||||
lightPWM: 255
|
||||
lightPWM: layerMeta.lightPWM
|
||||
});
|
||||
}, progress ? (value) => progress(value * 0.85, `${rec.ext}_encode`) : null);
|
||||
|
||||
|
|
@ -107,10 +101,9 @@ function writeGOO(params) {
|
|||
function writeHeader(writer, params) {
|
||||
let { ctx, layers, volume, previewSmall, previewLarge, rec } = params;
|
||||
let { device, process, width, height } = ctx;
|
||||
let bottomOff = process.slaBaseOff || 0;
|
||||
let layerOff = process.slaLayerOff || 0;
|
||||
let bottomMotion = layerMotion(device, process, true);
|
||||
let layerMotionValue = layerMotion(device, process, false);
|
||||
let meta = createProcessMetadata(device, process);
|
||||
let bottomMotion = motionFor(device, process, true);
|
||||
let layerMotionValue = motionFor(device, process, false);
|
||||
|
||||
writer.writeString(FILE_VERSION, 4);
|
||||
writer.writeBytes(FILE_MAGIC);
|
||||
|
|
@ -136,17 +129,17 @@ function writeHeader(writer, params) {
|
|||
writer.writeF32(device.bedDepth);
|
||||
writer.writeF32(device.maxHeight || device.bedHeight || 0);
|
||||
writer.writeF32(process.slaSlice);
|
||||
writer.writeF32(process.slaLayerOn);
|
||||
writer.writeF32(meta.exposure);
|
||||
writer.writeU8(1);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(bottomOff);
|
||||
writer.writeF32(meta.bottomWaitBeforeCure);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(0);
|
||||
writer.writeF32(layerOff);
|
||||
writer.writeF32(process.slaBaseOn);
|
||||
writer.writeU32(process.slaBaseLayers || 0);
|
||||
writer.writeF32(meta.waitBeforeCure);
|
||||
writer.writeF32(meta.bottomExposure);
|
||||
writer.writeU32(meta.bottomLayers);
|
||||
writer.writeF32(bottomMotion.liftHeight);
|
||||
writer.writeF32(bottomMotion.liftSpeed);
|
||||
writer.writeF32(layerMotionValue.liftHeight);
|
||||
|
|
@ -156,8 +149,8 @@ function writeHeader(writer, params) {
|
|||
writer.writeF32(layerMotionValue.retractHeight);
|
||||
writer.writeF32(layerMotionValue.retractSpeed);
|
||||
for (let i=0; i<8; i++) writer.writeF32(0);
|
||||
writer.writeU16(255);
|
||||
writer.writeU16(255);
|
||||
writer.writeU16(meta.bottomLightPWM);
|
||||
writer.writeU16(meta.lightPWM);
|
||||
writer.writeU8(0);
|
||||
writer.writeU32(estimatePrintTime(device, process, layers.length));
|
||||
writer.writeF32(round(volume));
|
||||
|
|
@ -197,20 +190,6 @@ function writeLayer(writer, layer) {
|
|||
writer.writeBytes(DELIMITER);
|
||||
}
|
||||
|
||||
function layerMotion(device, process, bottom) {
|
||||
if (device.slaMotion === "tilt") {
|
||||
return TILT_MOTION;
|
||||
}
|
||||
|
||||
return {
|
||||
liftHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
|
||||
liftSpeed: (bottom ? process.slaBasePeelLiftRate : process.slaPeelLiftRate) * 60,
|
||||
retractHeight: bottom ? process.slaBasePeelDist : process.slaPeelDist,
|
||||
retractSpeed: (bottom ? process.slaBasePeelDropRate : process.slaPeelDropRate) * 60,
|
||||
motorTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
function writeFooter(writer) {
|
||||
writer.writeU8(0);
|
||||
writer.writeU8(0);
|
||||
|
|
@ -506,20 +485,6 @@ function accumulatePreview(preview, image, width, height) {
|
|||
}
|
||||
}
|
||||
|
||||
function estimatePrintTime(device, process, layers) {
|
||||
let bottom = Math.min(process.slaBaseLayers || 0, layers);
|
||||
let normal = Math.max(0, layers - bottom);
|
||||
let motorTime = device.slaMotion === "tilt" ? TILT_MOTION.motorTime : 0;
|
||||
let bottomTime = number(process.slaBaseOn) + number(process.slaBaseOff) + motorTime;
|
||||
let normalTime = number(process.slaLayerOn) + number(process.slaLayerOff) + motorTime;
|
||||
return Math.ceil(bottom * bottomTime + normal * normalTime);
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
value = Number(value);
|
||||
return Number.isFinite(value) ? value : 0;
|
||||
}
|
||||
|
||||
function checksum(bytes) {
|
||||
let sum = 0;
|
||||
for (let i=1; i<bytes.length; i++) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { JSZip } from '../../../../ext/jszip-esm.js';
|
|||
import { PNG } from '../../../../ext/pngjs.esm.js';
|
||||
import { photon } from './x_photon.js';
|
||||
import { imageToRowMajor, renderRasterLayers, round } from './raster.js';
|
||||
import { createLayerMetadata, createProcessMetadata } from './meta.js';
|
||||
|
||||
const FORMAT = "rsla";
|
||||
const MIME = "application/vnd.gridspace.rsla+zip";
|
||||
|
|
@ -14,14 +15,17 @@ function encode(print, progress) {
|
|||
|
||||
let { ctx, volume } = renderRasterLayers(print, photon, params => {
|
||||
let { index, rendered, ctx } = params;
|
||||
let { process, width, height } = ctx;
|
||||
let { device, process, width, height } = ctx;
|
||||
let z = process.slaFirstOffset + process.slaSlice * index;
|
||||
let layerMeta = createLayerMetadata(device, process, index, z, rendered.area);
|
||||
|
||||
let file = `layers/${index.toString().padStart(6, "0")}.png`;
|
||||
let layer = {
|
||||
index,
|
||||
z: round(process.slaFirstOffset + process.slaSlice * index),
|
||||
area: round(rendered.area),
|
||||
file
|
||||
z: layerMeta.z,
|
||||
area: layerMeta.area,
|
||||
file,
|
||||
process: layerMeta
|
||||
};
|
||||
|
||||
layers.push(layer);
|
||||
|
|
@ -79,18 +83,7 @@ function createManifest(params) {
|
|||
black: "off"
|
||||
},
|
||||
process: {
|
||||
bottomLayers: process.slaBaseLayers,
|
||||
exposure: process.slaLayerOn,
|
||||
bottomExposure: process.slaBaseOn,
|
||||
lightOffDelay: process.slaLayerOff,
|
||||
bottomLightOffDelay: process.slaBaseOff,
|
||||
liftHeight: process.slaPeelDist,
|
||||
bottomLiftHeight: process.slaBasePeelDist,
|
||||
liftSpeed: process.slaPeelLiftRate * 60,
|
||||
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
|
||||
retractSpeed: process.slaPeelDropRate * 60,
|
||||
firstLayerOffset: process.slaFirstOffset,
|
||||
antiAlias: process.slaAntiAlias
|
||||
...createProcessMetadata(device, process)
|
||||
},
|
||||
layers
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { JSZip } from '../../../../ext/jszip-esm.js';
|
||||
import { createLayerMetadata, createProcessMetadata, round } from './meta.js';
|
||||
|
||||
const FORMAT = "vsla";
|
||||
const MIME = "application/vnd.gridspace.vsla+zip";
|
||||
|
|
@ -18,7 +19,7 @@ function encode(print, progress) {
|
|||
let layers = [];
|
||||
let volume = 0;
|
||||
for (let index=0; index<layermax; index++) {
|
||||
let layer = collectLayer({ index, widgets, process });
|
||||
let layer = collectLayer({ index, widgets, device, process });
|
||||
volume += layer.area * process.slaSlice;
|
||||
layers.push(layer);
|
||||
if (progress) progress((index / layermax) * 0.7, "vector_gen");
|
||||
|
|
@ -49,7 +50,7 @@ function encode(print, progress) {
|
|||
}
|
||||
|
||||
function collectLayer(params) {
|
||||
let { index, widgets, process } = params;
|
||||
let { index, widgets, device, process } = params;
|
||||
let polygons = [];
|
||||
let area = 0;
|
||||
let z = process.slaFirstOffset + process.slaSlice * index;
|
||||
|
|
@ -74,11 +75,14 @@ function collectLayer(params) {
|
|||
}
|
||||
});
|
||||
|
||||
let layerMeta = createLayerMetadata(device, process, index, z, area);
|
||||
|
||||
return {
|
||||
index,
|
||||
z: round(z),
|
||||
area: round(area),
|
||||
z: layerMeta.z,
|
||||
area: layerMeta.area,
|
||||
file: `layers/${index.toString().padStart(6, "0")}.svg`,
|
||||
process: layerMeta,
|
||||
polygons
|
||||
};
|
||||
}
|
||||
|
|
@ -91,7 +95,8 @@ function createManifest(params) {
|
|||
z: layer.z,
|
||||
area: layer.area,
|
||||
file: layer.file,
|
||||
empty: layer.polygons.length === 0
|
||||
empty: layer.polygons.length === 0,
|
||||
process: layer.process
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -125,18 +130,7 @@ function createManifest(params) {
|
|||
layerCount: layers.length,
|
||||
volume: round(volume),
|
||||
process: {
|
||||
bottomLayers: process.slaBaseLayers,
|
||||
exposure: process.slaLayerOn,
|
||||
bottomExposure: process.slaBaseOn,
|
||||
lightOffDelay: process.slaLayerOff,
|
||||
bottomLightOffDelay: process.slaBaseOff,
|
||||
liftHeight: process.slaPeelDist,
|
||||
bottomLiftHeight: process.slaBasePeelDist,
|
||||
liftSpeed: process.slaPeelLiftRate * 60,
|
||||
bottomLiftSpeed: process.slaBasePeelLiftRate * 60,
|
||||
retractSpeed: process.slaPeelDropRate * 60,
|
||||
firstLayerOffset: process.slaFirstOffset,
|
||||
antiAlias: process.slaAntiAlias
|
||||
...createProcessMetadata(device, process)
|
||||
},
|
||||
layers: layerList
|
||||
};
|
||||
|
|
@ -150,9 +144,10 @@ function createLayerSVG(params) {
|
|||
return ` <path fill="#000" fill-rule="evenodd" d="${polyToPath(poly, device)}"/>`;
|
||||
}).join("\n");
|
||||
|
||||
let proc = layer.process;
|
||||
return [
|
||||
`<?xml version="1.0" encoding="UTF-8"?>`,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}mm" height="${depth}mm" viewBox="0 0 ${width} ${depth}" data-layer="${layer.index}" data-z="${layer.z}">`,
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="${width}mm" height="${depth}mm" viewBox="0 0 ${width} ${depth}" data-layer="${layer.index}" data-z="${layer.z}" data-exposure="${proc.exposure}" data-light-off-delay="${proc.lightOffDelay}" data-light-pwm="${proc.lightPWM}" data-bottom="${proc.bottom}" data-transition="${proc.transition}">`,
|
||||
body,
|
||||
`</svg>`,
|
||||
``
|
||||
|
|
@ -184,10 +179,6 @@ function appendPath(parts, poly, device) {
|
|||
parts.push("Z");
|
||||
}
|
||||
|
||||
function round(value) {
|
||||
return Number.parseFloat(Number(value || 0).toFixed(5));
|
||||
}
|
||||
|
||||
export const VSLA = {
|
||||
encode
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue