From 78341297a25f29f42e02d383a18fc9123f279b81 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 19:26:14 +0000 Subject: [PATCH] Add TIFF (CMYK/Multilayer) and PDF (RGB/CMYK) export formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/js/libs/pdf-writer.js | 240 +++++++++++++++++++++++++++ frontend/src/js/libs/tiff-writer.js | 195 ++++++++++++++++++++++ frontend/src/js/modules/file/save.js | 117 +++++++++++-- 3 files changed, 539 insertions(+), 13 deletions(-) create mode 100644 frontend/src/js/libs/pdf-writer.js create mode 100644 frontend/src/js/libs/tiff-writer.js diff --git a/frontend/src/js/libs/pdf-writer.js b/frontend/src/js/libs/pdf-writer.js new file mode 100644 index 0000000..93433e2 --- /dev/null +++ b/frontend/src/js/libs/pdf-writer.js @@ -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} + */ + 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; diff --git a/frontend/src/js/libs/tiff-writer.js b/frontend/src/js/libs/tiff-writer.js new file mode 100644 index 0000000..be28162 --- /dev/null +++ b/frontend/src/js/libs/tiff-writer.js @@ -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 }; diff --git a/frontend/src/js/modules/file/save.js b/frontend/src/js/modules/file/save.js index e1b1588..241dfe5 100644 --- a/frontend/src/js/modules/file/save.js +++ b/frontend/src/js/modules/file/save.js @@ -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) {