Matches the existing vendor/easy-asterisk convention (used by services/asterisk.sh) instead of two one-off top-level directories that cluttered the repo root and didn't look like anything else next to setup.sh, lib/, services/, extras/. Only the two services' own SRC_DIR path resolution and header comments needed updating — nothing else in the repo referenced the old ./ai-stack / ./paintplus paths. Also documents vendor/ in README.md's Layout section.
196 lines
7.2 KiB
JavaScript
196 lines
7.2 KiB
JavaScript
/**
|
|
* TIFF writer with support for:
|
|
* - Single-page RGBA (32-bit, interleaved, with alpha)
|
|
* - Single-page CMYK (8-bit per channel, no alpha — print-ready)
|
|
* - Multipage variants of both (one IFD per canvas layer)
|
|
*
|
|
* TIFF spec references: TIFF 6.0, ISO 12234-2 (CMYK)
|
|
* PhotometricInterpretation 2 = RGB, 5 = CMYK
|
|
*/
|
|
|
|
// --- RGB → CMYK conversion -------------------------------------------
|
|
|
|
function rgbToCmyk(r, g, b) {
|
|
var rn = r / 255, gn = g / 255, bn = b / 255;
|
|
var k = 1 - Math.max(rn, gn, bn);
|
|
if (k >= 1) return [0, 0, 0, 255];
|
|
var d = 1 - k;
|
|
return [
|
|
Math.round((d - rn) / d * 255),
|
|
Math.round((d - gn) / d * 255),
|
|
Math.round((d - bn) / d * 255),
|
|
Math.round(k * 255),
|
|
];
|
|
}
|
|
|
|
// --- Low-level TIFF binary builder ------------------------------------
|
|
|
|
/**
|
|
* Build a multipage TIFF buffer from an array of canvases.
|
|
*
|
|
* @param {HTMLCanvasElement[]} canvases
|
|
* @param {'rgba'|'cmyk'} colorMode
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.littleEndian=false]
|
|
* @param {number} [opts.dpi=300]
|
|
* @returns {ArrayBuffer}
|
|
*/
|
|
function buildTIFF(canvases, colorMode, opts) {
|
|
opts = opts || {};
|
|
var lsb = !!opts.littleEndian;
|
|
var dpi = +(opts.dpi || 300) | 0;
|
|
|
|
var isCMYK = colorMode === 'cmyk';
|
|
|
|
// IFD field counts differ: RGBA has ExtraSamples tag, CMYK does not.
|
|
var ENTRY_COUNT = isCMYK ? 14 : 15;
|
|
var IFD_SIZE = 2 + ENTRY_COUNT * 12 + 4; // count + entries + nextIFD ptr
|
|
var FIELDS_SIZE = 64; // BPS(8)+XRes(8)+YRes(8)+sw(20)+dt(20)
|
|
var PAGE_OH = IFD_SIZE + FIELDS_SIZE;
|
|
|
|
// Compute page start offsets inside the final buffer.
|
|
var offsets = [];
|
|
var total = 8; // TIFF header
|
|
for (var i = 0; i < canvases.length; i++) {
|
|
offsets.push(total);
|
|
total += PAGE_OH + canvases[i].width * canvases[i].height * 4;
|
|
}
|
|
|
|
var buf = new ArrayBuffer(total);
|
|
var view = new DataView(buf);
|
|
var u8 = new Uint8Array(buf);
|
|
var pos = 0;
|
|
|
|
function s16(v) { view.setUint16(pos, v, lsb); pos += 2; }
|
|
function s32(v) { view.setUint32(pos, v, lsb); pos += 4; }
|
|
function entry(tag, type, count, value) {
|
|
s16(tag); s16(type); s32(count);
|
|
// SHORT with count==1 gets packed into the value field with padding.
|
|
if (type === 3 && count === 1) { s16(value); s16(0); }
|
|
else { s32(value); }
|
|
}
|
|
|
|
// Date helpers
|
|
var d = new Date();
|
|
var p2 = function(n) { return n < 10 ? '0' + n : '' + n; };
|
|
var dtStr = d.getFullYear() + ':' + p2(d.getMonth() + 1) + ':' + p2(d.getDate())
|
|
+ ' ' + p2(d.getHours()) + ':' + p2(d.getMinutes()) + ':' + p2(d.getSeconds());
|
|
var swStr = 'tiff-writer 1.0\0\0\0\0\0'; // 20 chars (null-padded)
|
|
|
|
// ---- TIFF header ----
|
|
s16(lsb ? 0x4949 : 0x4d4d);
|
|
s16(42);
|
|
s32(8); // offset to first IFD
|
|
|
|
// ---- Per-page IFDs + image data ----
|
|
for (var p = 0; p < canvases.length; p++) {
|
|
var cv = canvases[p];
|
|
var W = cv.width, H = cv.height;
|
|
var pageBase = offsets[p];
|
|
var fBase = pageBase + IFD_SIZE; // start of fields section
|
|
var imgBase = fBase + FIELDS_SIZE; // start of image data
|
|
var nextIFD = p + 1 < canvases.length ? offsets[p + 1] : 0;
|
|
|
|
// IFD entry count
|
|
s16(ENTRY_COUNT);
|
|
|
|
entry(0x00fe, 4, 1, 0); // NewSubfileType
|
|
entry(0x0100, 4, 1, W); // ImageWidth
|
|
entry(0x0101, 4, 1, H); // ImageLength
|
|
entry(0x0102, 3, 4, fBase); // BitsPerSample (offset → 4 shorts)
|
|
entry(0x0103, 3, 1, 1); // Compression: none
|
|
entry(0x0106, 3, 1, isCMYK ? 5 : 2); // PhotometricInterp: 5=CMYK, 2=RGB
|
|
entry(0x0111, 4, 1, imgBase); // StripOffsets
|
|
entry(0x0115, 3, 1, 4); // SamplesPerPixel: 4
|
|
entry(0x0117, 4, 1, W * H * 4); // StripByteCounts
|
|
entry(0x011a, 5, 1, fBase + 8); // XResolution
|
|
entry(0x011b, 5, 1, fBase + 16); // YResolution
|
|
entry(0x0128, 3, 1, 2); // ResolutionUnit: inch
|
|
entry(0x0131, 2, 20, fBase + 24); // Software (20 bytes)
|
|
entry(0x0132, 2, 20, fBase + 44); // DateTime (20 bytes)
|
|
if (!isCMYK) {
|
|
entry(0x0152, 3, 1, 2); // ExtraSamples: assoc. alpha (RGBA only)
|
|
}
|
|
|
|
s32(nextIFD);
|
|
|
|
// ---- Fields section (64 bytes) ----
|
|
// BitsPerSample: 8,8,8,8 as four SHORTs (8 bytes)
|
|
s16(8); s16(8); s16(8); s16(8);
|
|
// XResolution RATIONAL (8 bytes)
|
|
s32(dpi); s32(1);
|
|
// YResolution RATIONAL (8 bytes)
|
|
s32(dpi); s32(1);
|
|
// Software string (20 bytes, null-padded)
|
|
for (var i = 0; i < 20; i++)
|
|
view.setUint8(pos++, swStr.charCodeAt(i) & 0xff);
|
|
// DateTime string (20 bytes, null-padded)
|
|
for (var i = 0; i < 20; i++)
|
|
view.setUint8(pos++, i < dtStr.length ? dtStr.charCodeAt(i) & 0xff : 0);
|
|
|
|
// ---- Image data ----
|
|
var idata = cv.getContext('2d').getImageData(0, 0, W, H).data;
|
|
if (isCMYK) {
|
|
// Convert RGBA → CMYK and write 4 bytes per pixel (alpha discarded)
|
|
for (var px = 0, len = idata.length; px < len; px += 4) {
|
|
var cmyk = rgbToCmyk(idata[px], idata[px + 1], idata[px + 2]);
|
|
u8[pos++] = cmyk[0];
|
|
u8[pos++] = cmyk[1];
|
|
u8[pos++] = cmyk[2];
|
|
u8[pos++] = cmyk[3];
|
|
}
|
|
} else {
|
|
// Write RGBA directly
|
|
u8.set(idata, pos);
|
|
pos += idata.length;
|
|
}
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
// --- Public API -------------------------------------------------------
|
|
|
|
var TiffWriter = {
|
|
|
|
/** Single-page 32-bit RGBA TIFF */
|
|
toRGBA: function(canvas, callback, opts) {
|
|
setTimeout(function() {
|
|
callback(buildTIFF([canvas], 'rgba', opts));
|
|
}, 9);
|
|
},
|
|
|
|
/** Single-page CMYK TIFF (print-ready, no alpha) */
|
|
toCMYK: function(canvas, callback, opts) {
|
|
setTimeout(function() {
|
|
callback(buildTIFF([canvas], 'cmyk', opts));
|
|
}, 9);
|
|
},
|
|
|
|
/** Multipage RGBA TIFF — one IFD per canvas in the array */
|
|
toMultipageRGBA: function(canvases, callback, opts) {
|
|
setTimeout(function() {
|
|
callback(buildTIFF(canvases, 'rgba', opts));
|
|
}, 9);
|
|
},
|
|
|
|
/** Multipage CMYK TIFF — one IFD per canvas in the array */
|
|
toMultipageCMYK: function(canvases, callback, opts) {
|
|
setTimeout(function() {
|
|
callback(buildTIFF(canvases, 'cmyk', opts));
|
|
}, 9);
|
|
},
|
|
|
|
/** Convenience: returns a Blob instead of ArrayBuffer */
|
|
toBlob: function(canvases, colorMode, callback, opts) {
|
|
if (!Array.isArray(canvases)) canvases = [canvases];
|
|
setTimeout(function() {
|
|
var buf = buildTIFF(canvases, colorMode, opts);
|
|
callback(new Blob([buf], { type: 'image/tiff' }));
|
|
}, 9);
|
|
},
|
|
};
|
|
|
|
export default TiffWriter;
|
|
export { rgbToCmyk };
|