@micrio/tiler-base
Version:
The base Micrio client-side tiler package used by the Micrio CLI and GUI tools
106 lines (87 loc) • 3.94 kB
text/typescript
import type { FormatType, ImageInfo, ImageType, State, TileResult } from '../types.js';
import type { Uploader } from './uploader.js';
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:State,
uploader:Uploader,
f:string,
outDir:string,
folder:string,
format:FormatType,
type:ImageType,
opts:{
omniId?:string;
omniFrame?:number;
omniTotalFrames?:number;
} = {}
) : Promise<ImageInfo> {
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<{id:string}>(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:State, destDir: string, file:string, format:FormatType) : Promise<TileResult> => 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:any, info?:TileResult) => {
if(error||!info) err(error??'Could not tile image');
else ok(info);
})
}).catch(e => err(e?.message ?? 'Could not read file'));
});