@micrio/tiler-base
Version:
The base Micrio client-side tiler package used by the Micrio CLI and GUI tools
82 lines (81 loc) • 3.97 kB
JavaScript
import path from 'path';
import sharp from 'sharp';
import { promises as fs } from 'node:fs';
import { fsExists, sanitize, walkSync } from './utils.js';
import { api } from './micrioApi.js';
// Detect a PDFjs generated page
const pdfPageRx = /^(.*\.pdf)\.(\d+)\.(png|tif)$/;
/** Process an image to be uploaded to Micrio */
export async function process(state, uploader, f, outDir, folder, format, type, opts = {}) {
const isOmni = type == 'omni';
const isPdfPage = pdfPageRx.test(f);
if (!await fsExists(f))
throw new Error(`File '${f}' not found`);
const fName = isPdfPage ? path.basename(f).replace(/\.(tif|png)$/, '') : path.basename(f);
const res = opts.omniId ? { id: opts.omniId } : await api(state.account, uploader.agent, `${folder}/create`, {
name: encodeURIComponent(fName), type, format
});
if (!res)
throw new Error('Could not create image in Micrio! Do you have the correct permissions?');
outDir = sanitize(outDir, outDir);
const baseDir = path.join(outDir, res.id, isOmni ? opts.omniFrame.toString() : '');
const { width, height } = await tile(state, baseDir, f, format);
if (!height || !width)
throw new Error('Could not read image dimensions');
// If this is an extracted PNG file out of an original PDF file, we no longer need it
if (isPdfPage)
await fs.rm(f);
// Sharp (libvips) always puts the tiles in `name_files` -- rename to our standard
await fs.rename(baseDir + '_files', baseDir);
// Delete libvips output meta data file, not needed
await fs.rm(path.join(baseDir, 'vips-properties.xml'));
// Remove the libvips-generated deepzoom meta file
await fs.rm(baseDir + '.dzi');
// Update status to Micrio
// `omniId` is only defined for the SECOND and later frames of an omni object
// So the first frame of an omni object will do this call.
if (!opts.omniId)
await api(state.account, uploader.agent, `${folder}/@${res.id}/status`, {
width, height, status: 6, format, length: opts.omniTotalFrames
});
// Get all tiles from all subfolders of the output directory
uploader.add(await walkSync(baseDir));
// Add a final Uploader job to set the Micrio image status to Completed (4)
// TODO: It's possible that this function is called if there are still ongoing tile uploads
// of this image. Fix this by adding a separate `oncomplete` trigger in Uploader for this individual
// tiled image, which should trigger this.
if (type != 'omni')
uploader.add([() => api(state.account, uploader.agent, `${folder}/@${res.id}/status`, { status: 4 })]);
return { id: res.id, width, height, baseDir, omniFrame: opts?.omniFrame };
}
/** Do the actual image tiling using Sharp (libvips) */
const tile = (state, destDir, file, format) => new Promise((ok, err) => {
fs.stat(file).then(s => {
if (state?.job)
state.job.bytesSource += s.size;
sharp(file, {
// Manual hard limit at 1,000,000 x 1,000,000 px
limitInputPixels: 1E6 * 1E6,
// By default, sharp has a low limit
unlimited: true
}).toFormat(format, {
// Default is WebP, and 75 is OK, otherwise it's JPG
quality: format == 'webp' ? 75 : 85
}).tile({
// Tile size
size: 1024,
// Micrio doesn't require an extra padded pixel
overlap: 0,
depth: 'onepixel',
container: 'fs',
// This command makes the image into a deepzoom tile pyramid
// The output of this operation will result in a directory with all zoom levels and tiles
layout: 'dz'
}).toFile(destDir, (error, info) => {
if (error || !info)
err(error ?? 'Could not tile image');
else
ok(info);
});
}).catch(e => err(e?.message ?? 'Could not read file'));
});