Kokkoja: first public assembly
Assembled from the development repository by tools/make_release.sh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
95c80d04c9
39 files changed
+9905
No files matched your search
@@ -0,0 +1,88 @@
|
||||
// @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<ArrayBuffer>} png
|
||||
* @param {number} dpi
|
||||
* @returns {Uint8Array<ArrayBuffer>}
|
||||
*/
|
||||
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<typeof pageToSvg>[1]} options As for the page file;
|
||||
* layers named in `hidden` are left out.
|
||||
* @returns {Promise<Blob>} 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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user