Add TIFF (CMYK/Multilayer) and PDF (RGB/CMYK) export formats
- tiff-writer.js: pure-JS multipage TIFF encoder with RGBA and CMYK color modes; RGB→CMYK conversion; one IFD per layer for multilayer - pdf-writer.js: pure-JS PDF-1.4 writer; RGB pages use browser JPEG encoding (DCTDecode); CMYK pages embed raw DeviceCMYK pixel data; multipage support (one page per visible layer) - save.js: adds TIFF (CMYK), TIFF (Multilayer), PDF (RGB), PDF (CMYK) to the export dialog; CMYK types bypass the separated-layers loop and composite with a white background before color conversion
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Minimal pure-JS PDF writer for image export.
|
||||
*
|
||||
* Supports:
|
||||
* - Single-page and multipage (one page per canvas)
|
||||
* - RGB color space: images JPEG-encoded (browser-native, small files)
|
||||
* - CMYK color space: raw DeviceCMYK bytes (print-ready, no alpha)
|
||||
*
|
||||
* PDF-1.4 structure used. No external dependencies.
|
||||
*
|
||||
* Usage:
|
||||
* PdfWriter.fromCanvases(canvases, { colorMode: 'rgb'|'cmyk', quality: 0.9, dpi: 300 })
|
||||
* .then(blob => FileSaver.saveAs(blob, 'file.pdf'));
|
||||
*/
|
||||
|
||||
import { rgbToCmyk } from './tiff-writer.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Encode a JS string to a Uint8Array of bytes (Latin-1 safe). */
|
||||
function strBytes(s) {
|
||||
var a = new Uint8Array(s.length);
|
||||
for (var i = 0; i < s.length; i++) a[i] = s.charCodeAt(i) & 0xff;
|
||||
return a;
|
||||
}
|
||||
|
||||
/** Concatenate multiple Uint8Arrays / ArrayBuffers into one Uint8Array. */
|
||||
function concat(parts) {
|
||||
var total = 0;
|
||||
for (var i = 0; i < parts.length; i++)
|
||||
total += parts[i].byteLength || parts[i].length;
|
||||
var out = new Uint8Array(total), pos = 0;
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i] instanceof ArrayBuffer ? new Uint8Array(parts[i]) : parts[i];
|
||||
out.set(p, pos);
|
||||
pos += p.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** canvas → raw CMYK Uint8Array (W*H*4 bytes, alpha discarded). */
|
||||
function canvasToCmykBytes(canvas) {
|
||||
var idata = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
var out = new Uint8Array(canvas.width * canvas.height * 4);
|
||||
for (var px = 0, i = 0, len = idata.length; px < len; px += 4, i += 4) {
|
||||
var cmyk = rgbToCmyk(idata[px], idata[px + 1], idata[px + 2]);
|
||||
out[i] = cmyk[0];
|
||||
out[i + 1] = cmyk[1];
|
||||
out[i + 2] = cmyk[2];
|
||||
out[i + 3] = cmyk[3];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** canvas → JPEG Uint8Array via browser encoding. Returns a Promise. */
|
||||
function canvasToJpegBytes(canvas, quality) {
|
||||
return new Promise(function(resolve) {
|
||||
canvas.toBlob(function(blob) {
|
||||
blob.arrayBuffer().then(function(buf) {
|
||||
resolve(new Uint8Array(buf));
|
||||
});
|
||||
}, 'image/jpeg', quality || 0.92);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PDF object builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build a complete PDF byte stream for an array of pages.
|
||||
*
|
||||
* @param {Array<{width, height, colorSpace, imgBytes, filter}>} pages
|
||||
* @param {number} dpi — used for MediaBox sizing (px → pt: pt = px * 72 / dpi)
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
function buildPDF(pages, dpi) {
|
||||
dpi = dpi || 300;
|
||||
var px2pt = 72 / dpi;
|
||||
|
||||
// Object registry: we'll collect byte-offset of each object for xref.
|
||||
var objs = []; // each element is the raw bytes of "N 0 obj ... endobj\n"
|
||||
var objNums = {}; // logical name → 1-based index
|
||||
|
||||
function addObj(name, content) {
|
||||
var n = objs.length + 1;
|
||||
if (name) objNums[name] = n;
|
||||
var s = n + ' 0 obj\n' + content + '\nendobj\n';
|
||||
objs.push(strBytes(s));
|
||||
return n;
|
||||
}
|
||||
|
||||
function addStreamObj(name, dict, dataBytes) {
|
||||
var n = objs.length + 1;
|
||||
if (name) objNums[name] = n;
|
||||
var header = n + ' 0 obj\n' + dict + '\nstream\n';
|
||||
var footer = '\nendstream\nendobj\n';
|
||||
var combined = concat([strBytes(header), dataBytes, strBytes(footer)]);
|
||||
objs.push(combined);
|
||||
return n;
|
||||
}
|
||||
|
||||
// 1. Catalog
|
||||
addObj('catalog', '<< /Type /Catalog /Pages 2 0 R >>');
|
||||
|
||||
// 2. Pages (placeholder — children added later)
|
||||
var pagesIdx = objs.length + 1;
|
||||
addObj('pages', ''); // placeholder
|
||||
|
||||
// 3. Per-page objects
|
||||
var pageObjNums = [];
|
||||
for (var p = 0; p < pages.length; p++) {
|
||||
var pg = pages[p];
|
||||
var W_pt = (pg.width * px2pt).toFixed(3);
|
||||
var H_pt = (pg.height * px2pt).toFixed(3);
|
||||
var imgName = 'Im' + (p + 1);
|
||||
var imgIdx = objs.length + 2; // will be added after content stream
|
||||
|
||||
// Content stream: scale and paint image
|
||||
var contentStr = 'q ' + W_pt + ' 0 0 ' + H_pt + ' 0 0 cm /' + imgName + ' Do Q';
|
||||
var contentNum = addStreamObj(null,
|
||||
'<< /Length ' + contentStr.length + ' >>',
|
||||
strBytes(contentStr));
|
||||
|
||||
// Image XObject
|
||||
var samples = pg.colorSpace === 'DeviceCMYK' ? 4 : 3;
|
||||
var imgDict = '<< /Type /XObject /Subtype /Image'
|
||||
+ ' /Width ' + pg.width
|
||||
+ ' /Height ' + pg.height
|
||||
+ ' /ColorSpace /' + pg.colorSpace
|
||||
+ ' /BitsPerComponent 8'
|
||||
+ (pg.filter ? ' /Filter /' + pg.filter : '')
|
||||
+ ' /Length ' + pg.imgBytes.length
|
||||
+ ' >>';
|
||||
var imgNum = addStreamObj(null, imgDict, pg.imgBytes);
|
||||
|
||||
// Page object
|
||||
var pageNum = addObj(null,
|
||||
'<< /Type /Page /Parent ' + pagesIdx + ' 0 R'
|
||||
+ ' /MediaBox [0 0 ' + W_pt + ' ' + H_pt + ']'
|
||||
+ ' /Contents ' + contentNum + ' 0 R'
|
||||
+ ' /Resources << /XObject << /' + imgName + ' ' + imgNum + ' 0 R >> >>'
|
||||
+ ' >>');
|
||||
pageObjNums.push(pageNum);
|
||||
}
|
||||
|
||||
// Fill in Pages object properly
|
||||
var kidsStr = pageObjNums.map(function(n) { return n + ' 0 R'; }).join(' ');
|
||||
var pagesContent = '<< /Type /Pages /Kids [' + kidsStr + '] /Count ' + pages.length + ' >>';
|
||||
objs[pagesIdx - 1] = strBytes(pagesIdx + ' 0 obj\n' + pagesContent + '\nendobj\n');
|
||||
|
||||
// ---- Assemble file ----
|
||||
var header = strBytes('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n'); // binary hint comment
|
||||
var offsets = [];
|
||||
var parts = [header];
|
||||
var bytePos = header.length;
|
||||
|
||||
for (var i = 0; i < objs.length; i++) {
|
||||
offsets.push(bytePos);
|
||||
parts.push(objs[i]);
|
||||
bytePos += objs[i].length;
|
||||
}
|
||||
|
||||
// xref table
|
||||
var xrefOffset = bytePos;
|
||||
var xrefLines = 'xref\n0 ' + (objs.length + 1) + '\n';
|
||||
xrefLines += '0000000000 65535 f \n';
|
||||
for (var i = 0; i < offsets.length; i++) {
|
||||
xrefLines += String(offsets[i]).padStart(10, '0') + ' 00000 n \n';
|
||||
}
|
||||
parts.push(strBytes(xrefLines));
|
||||
|
||||
// trailer
|
||||
var trailerStr = 'trailer\n<< /Size ' + (objs.length + 1)
|
||||
+ ' /Root 1 0 R >>\nstartxref\n' + xrefOffset + '\n%%EOF\n';
|
||||
parts.push(strBytes(trailerStr));
|
||||
|
||||
return concat(parts);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var PdfWriter = {
|
||||
|
||||
/**
|
||||
* Export an array of canvases as a PDF.
|
||||
*
|
||||
* @param {HTMLCanvasElement|HTMLCanvasElement[]} canvases
|
||||
* @param {object} [opts]
|
||||
* @param {'rgb'|'cmyk'} [opts.colorMode='rgb']
|
||||
* @param {number} [opts.quality=0.92] JPEG quality for RGB mode (0–1)
|
||||
* @param {number} [opts.dpi=300] dots-per-inch for page sizing
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
fromCanvases: function(canvases, opts) {
|
||||
if (!Array.isArray(canvases)) canvases = [canvases];
|
||||
opts = opts || {};
|
||||
var colorMode = opts.colorMode === 'cmyk' ? 'cmyk' : 'rgb';
|
||||
var dpi = +(opts.dpi || 300) | 0;
|
||||
var quality = opts.quality != null ? opts.quality : 0.92;
|
||||
|
||||
if (colorMode === 'cmyk') {
|
||||
// Synchronous path: raw CMYK bytes
|
||||
var pages = canvases.map(function(cv) {
|
||||
return {
|
||||
width: cv.width,
|
||||
height: cv.height,
|
||||
colorSpace: 'DeviceCMYK',
|
||||
filter: null,
|
||||
imgBytes: canvasToCmykBytes(cv),
|
||||
};
|
||||
});
|
||||
var pdfBytes = buildPDF(pages, dpi);
|
||||
return Promise.resolve(new Blob([pdfBytes], { type: 'application/pdf' }));
|
||||
} else {
|
||||
// Async path: JPEG-encode each canvas
|
||||
var promises = canvases.map(function(cv) {
|
||||
return canvasToJpegBytes(cv, quality).then(function(bytes) {
|
||||
return {
|
||||
width: cv.width,
|
||||
height: cv.height,
|
||||
colorSpace: 'DeviceRGB',
|
||||
filter: 'DCTDecode',
|
||||
imgBytes: bytes,
|
||||
};
|
||||
});
|
||||
});
|
||||
return Promise.all(promises).then(function(pages) {
|
||||
var pdfBytes = buildPDF(pages, dpi);
|
||||
return new Blob([pdfBytes], { type: 'application/pdf' });
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default PdfWriter;
|
||||
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* 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 };
|
||||
@@ -8,6 +8,8 @@ import canvasToBlob from './../../../../node_modules/blueimp-canvas-to-blob/js/c
|
||||
import filesaver from './../../../../node_modules/file-saver/dist/FileSaver.min.js';
|
||||
import GIF from './../../../../node_modules/gif.js.optimized/';
|
||||
import CanvasToTIFF from './../../libs/canvastotiff.js';
|
||||
import TiffWriter from './../../libs/tiff-writer.js';
|
||||
import PdfWriter from './../../libs/pdf-writer.js';
|
||||
import Tools_settings_class from "../tools/settings";
|
||||
|
||||
var instance = null;
|
||||
@@ -42,7 +44,11 @@ class File_save_class {
|
||||
WEBP: "Weppy File Format",
|
||||
GIF: "Graphics Interchange Format",
|
||||
BMP: "Windows Bitmap",
|
||||
TIFF: "Tag Image File Format",
|
||||
TIFF: "TIFF (RGBA)",
|
||||
TIFF_CMYK: "TIFF (CMYK, print)",
|
||||
TIFF_LAYERS: "TIFF (Multilayer)",
|
||||
PDF: "PDF Document (RGB)",
|
||||
PDF_CMYK: "PDF Document (CMYK, print)",
|
||||
};
|
||||
|
||||
this.default_extension = 'PNG';
|
||||
@@ -154,7 +160,12 @@ class File_save_class {
|
||||
_this.save_dialog_onchange(true);
|
||||
},
|
||||
on_finish: function (params) {
|
||||
if (params.layers == 'Separated' || params.layers == 'Separated (original types)') {
|
||||
// These types handle their own layering internally — skip the separated loop.
|
||||
var _type = params.type ? params.type.split(' ')[0] : '';
|
||||
var _multilayerType = _type === 'TIFF_LAYERS' || _type === 'TIFF_CMYK'
|
||||
|| _type === 'PDF' || _type === 'PDF_CMYK';
|
||||
|
||||
if (!_multilayerType && (params.layers == 'Separated' || params.layers == 'Separated (original types)')) {
|
||||
var active_layer = config.layer.id;
|
||||
var original_layer_type = params.layers;
|
||||
|
||||
@@ -277,7 +288,7 @@ class File_save_class {
|
||||
else
|
||||
document.getElementById('popup-tr-delay').style.display = 'none';
|
||||
|
||||
if (type == 'JSON' || type == 'GIF')
|
||||
if (type == 'JSON' || type == 'GIF' || type == 'TIFF_LAYERS' || type == 'TIFF_CMYK' || type == 'PDF_CMYK')
|
||||
document.getElementById('popup-tr-layers').style.display = 'none';
|
||||
else
|
||||
document.getElementById('popup-tr-layers').style.display = '';
|
||||
@@ -414,12 +425,16 @@ class File_save_class {
|
||||
}, data_header);
|
||||
}
|
||||
else if (type == 'TIFF') {
|
||||
//tiff
|
||||
var data_header = "image/tiff";
|
||||
|
||||
CanvasToTIFF.toBlob(canvas, function(blob) {
|
||||
_this.update_file_size(blob.size);
|
||||
}, data_header);
|
||||
}, {});
|
||||
}
|
||||
else if (type == 'TIFF_CMYK' || type == 'TIFF_LAYERS') {
|
||||
// size estimate: W*H*4 bytes of pixel data + small overhead
|
||||
_this.update_file_size(config.WIDTH * config.HEIGHT * 4 + 512);
|
||||
}
|
||||
else if (type == 'PDF' || type == 'PDF_CMYK') {
|
||||
_this.update_file_size('-');
|
||||
}
|
||||
else if (type == 'JSON') {
|
||||
//json
|
||||
@@ -493,8 +508,8 @@ class File_save_class {
|
||||
}
|
||||
}
|
||||
|
||||
if (type != 'JSON' && (type == 'JPG' || config.TRANSPARENCY == false)) {
|
||||
//add white background
|
||||
// CMYK and JPG need an opaque white background (no alpha channel in output)
|
||||
if (type != 'JSON' && (type == 'JPG' || type == 'TIFF_CMYK' || config.TRANSPARENCY == false)) {
|
||||
ctx.globalCompositeOperation = 'destination-over';
|
||||
this.fillCanvasBackground(ctx, '#ffffff');
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
@@ -568,14 +583,61 @@ class File_save_class {
|
||||
}, data_header);
|
||||
}
|
||||
else if (type == 'TIFF') {
|
||||
//tiff
|
||||
//tiff - single page RGBA (existing behaviour)
|
||||
if (this.Helper.strpos(fname, '.tiff') == false)
|
||||
fname = fname + ".tiff";
|
||||
var data_header = "image/tiff";
|
||||
|
||||
CanvasToTIFF.toBlob(canvas, function(blob) {
|
||||
filesaver.saveAs(blob, fname);
|
||||
}, data_header);
|
||||
}, {});
|
||||
}
|
||||
else if (type == 'TIFF_CMYK') {
|
||||
//tiff - single page CMYK, print-ready
|
||||
if (this.Helper.strpos(fname, '.tiff') == false)
|
||||
fname = fname + ".tiff";
|
||||
var resolution = this.Tools_settings.get_setting('resolution') || 300;
|
||||
|
||||
TiffWriter.toCMYK(canvas, function(buf) {
|
||||
filesaver.saveAs(new Blob([buf], {type: 'image/tiff'}), fname);
|
||||
}, {dpi: resolution});
|
||||
}
|
||||
else if (type == 'TIFF_LAYERS') {
|
||||
//tiff - multipage: one IFD per visible layer
|
||||
if (this.Helper.strpos(fname, '.tiff') == false)
|
||||
fname = fname + ".tiff";
|
||||
var resolution = this.Tools_settings.get_setting('resolution') || 300;
|
||||
var layerCanvases = this._collect_layer_canvases();
|
||||
|
||||
TiffWriter.toMultipageRGBA(layerCanvases, function(buf) {
|
||||
filesaver.saveAs(new Blob([buf], {type: 'image/tiff'}), fname);
|
||||
}, {dpi: resolution});
|
||||
}
|
||||
else if (type == 'PDF') {
|
||||
//pdf - RGB, one page per visible layer
|
||||
if (this.Helper.strpos(fname, '.pdf') == false)
|
||||
fname = fname + ".pdf";
|
||||
var resolution = this.Tools_settings.get_setting('resolution') || 300;
|
||||
var quality_val = quality;
|
||||
|
||||
var pdfCanvases;
|
||||
if (user_response.layers == 'Selected') {
|
||||
pdfCanvases = [canvas];
|
||||
} else {
|
||||
pdfCanvases = this._collect_layer_canvases();
|
||||
}
|
||||
|
||||
PdfWriter.fromCanvases(pdfCanvases, {colorMode: 'rgb', quality: quality_val, dpi: resolution})
|
||||
.then(function(blob) { filesaver.saveAs(blob, fname); });
|
||||
}
|
||||
else if (type == 'PDF_CMYK') {
|
||||
//pdf - CMYK, one page per visible layer, print-ready
|
||||
if (this.Helper.strpos(fname, '.pdf') == false)
|
||||
fname = fname + ".pdf";
|
||||
var resolution = this.Tools_settings.get_setting('resolution') || 300;
|
||||
var layerCanvases = this._collect_layer_canvases();
|
||||
|
||||
PdfWriter.fromCanvases(layerCanvases, {colorMode: 'cmyk', dpi: resolution})
|
||||
.then(function(blob) { filesaver.saveAs(blob, fname); });
|
||||
}
|
||||
else if (type == 'JSON') {
|
||||
//json - full data with layers
|
||||
@@ -719,9 +781,38 @@ class File_save_class {
|
||||
return JSON.stringify(export_data, null, "\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns one canvas per visible layer, each composited individually.
|
||||
* Used for multilayer TIFF and multipage PDF export.
|
||||
*/
|
||||
_collect_layer_canvases() {
|
||||
var canvases = [];
|
||||
for (var i = 0; i < config.layers.length; i++) {
|
||||
if (config.layers[i].visible == false) continue;
|
||||
var c = document.createElement('canvas');
|
||||
var cx = c.getContext('2d');
|
||||
c.width = config.WIDTH;
|
||||
c.height = config.HEIGHT;
|
||||
this.disable_canvas_smooth(cx);
|
||||
this.Base_layers.convert_layers_to_canvas(cx, config.layers[i].id, false);
|
||||
canvases.push(c);
|
||||
}
|
||||
// Fall back to full composite if no layers found
|
||||
if (canvases.length === 0) {
|
||||
var c = document.createElement('canvas');
|
||||
var cx = c.getContext('2d');
|
||||
c.width = config.WIDTH;
|
||||
c.height = config.HEIGHT;
|
||||
this.disable_canvas_smooth(cx);
|
||||
this.Base_layers.convert_layers_to_canvas(cx, null, false);
|
||||
canvases.push(c);
|
||||
}
|
||||
return canvases;
|
||||
}
|
||||
|
||||
/**
|
||||
* removes smoothing, because it look ugly during zoom
|
||||
*
|
||||
*
|
||||
* @param {ctx} ctx
|
||||
*/
|
||||
disable_canvas_smooth(ctx) {
|
||||
|
||||
Reference in New Issue
Block a user