// @ts-check /** * A picture of a page: the page file drawn into a PNG, for use outside * Kokkoja. */ import { pageToSvg } from './page_file.js'; /** * The resolution of an exported picture, in dots per inch: enough to print * handwriting cleanly, and an A4 page is then 1654 by 2339 pixels. */ export const EXPORT_DPI = 200; /** A very large page is exported at a lower resolution, to stay within this. */ const MOST_PIXELS_ON_A_SIDE = 10000; const CRC_TABLE = new Uint32Array(256).map((_, n) => { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; return c; }); /** * @param {Uint8Array} bytes * @returns {number} The checksum that ends a PNG chunk. */ function crc32(bytes) { let c = 0xffffffff; for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } /** * Record a PNG's resolution in it (a pHYs chunk, placed after the header * chunk), so that other programs print the page at its true size, and so * that Kokkoja places it at that size if it is loaded as an image. * @param {Uint8Array} png * @param {number} dpi * @returns {Uint8Array} */ function withResolution(png, dpi) { const HEADER_END = 8 + 25; // The signature, then the IHDR chunk. const chunk = new Uint8Array(21); const view = new DataView(chunk.buffer); const perMetre = Math.round(dpi / 0.0254); view.setUint32(0, 9); chunk.set([0x70, 0x48, 0x59, 0x73], 4); // "pHYs" view.setUint32(8, perMetre); view.setUint32(12, perMetre); chunk[16] = 1; // The unit is the metre. view.setUint32(17, crc32(chunk.subarray(4, 17))); const out = new Uint8Array(png.length + chunk.length); out.set(png.subarray(0, HEADER_END)); out.set(chunk, HEADER_END); out.set(png.subarray(HEADER_END), HEADER_END + chunk.length); return out; } /** * @param {import('./page_model.js').Page} page * @param {Parameters[1]} options As for the page file; * layers named in `hidden` are left out. * @returns {Promise} A PNG picture of the page. */ export async function pageToPng(page, options) { const scale = Math.min(EXPORT_DPI / 254, MOST_PIXELS_ON_A_SIDE / Math.max(page.width, page.height)); const address = URL.createObjectURL( new Blob([pageToSvg(page, options)], { type: 'image/svg+xml' })); try { const drawing = new Image(); drawing.src = address; await drawing.decode(); const canvas = document.createElement('canvas'); canvas.width = Math.round(page.width * scale); canvas.height = Math.round(page.height * scale); const context = /** @type {CanvasRenderingContext2D} */ (canvas.getContext('2d')); context.drawImage(drawing, 0, 0, canvas.width, canvas.height); /** @type {Blob} */ const blob = await new Promise((resolve, reject) => canvas.toBlob( (result) => (result ? resolve(result) : reject(new Error('The picture could not be made.'))), 'image/png')); const png = withResolution(new Uint8Array(await blob.arrayBuffer()), scale * 254); return new Blob([png], { type: 'image/png' }); } finally { URL.revokeObjectURL(address); } }