UNPKG

molstar

Version:

A comprehensive macromolecular library.

158 lines (157 loc) 8.17 kB
#!/usr/bin/env node "use strict"; /** * Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Adam Midlik <midlik@gmail.com> * * Command-line application for rendering images from MolViewSpec files * From Molstar NPM package: * npm install molstar canvas gl jpeg-js pngjs * npx mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj * From Molstar source code: * npm install * npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons * npm run build * node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); const argparse_1 = require("argparse"); const fs_1 = tslib_1.__importDefault(require("fs")); const gl_1 = tslib_1.__importDefault(require("gl")); const jpeg_js_1 = tslib_1.__importDefault(require("jpeg-js")); const path_1 = tslib_1.__importDefault(require("path")); const pngjs_1 = tslib_1.__importDefault(require("pngjs")); const canvas3d_1 = require("../../mol-canvas3d/canvas3d"); const font_atlas_1 = require("../../mol-geo/geometry/text/font-atlas"); const headless_plugin_context_1 = require("../../mol-plugin/headless-plugin-context"); const spec_1 = require("../../mol-plugin/spec"); const headless_screenshot_1 = require("../../mol-plugin/util/headless-screenshot"); const mol_task_1 = require("../../mol-task"); const data_source_1 = require("../../mol-util/data-source"); const json_1 = require("../../mol-util/json"); const param_definition_1 = require("../../mol-util/param-definition"); // MolViewSpec must be imported after HeadlessPluginContext const mp4_export_1 = require("../../extensions/mp4-export"); const behavior_1 = require("../../extensions/mvs/behavior"); const formats_1 = require("../../extensions/mvs/components/formats"); const load_1 = require("../../extensions/mvs/load"); const mvs_data_1 = require("../../extensions/mvs/mvs-data"); (0, data_source_1.setFSModule)(fs_1.default); (0, font_atlas_1.setCanvasModule)(require('canvas')); const DEFAULT_SIZE = '800x800'; /** Return parsed command line arguments for `main` */ function parseArguments() { const parser = new argparse_1.ArgumentParser({ description: 'Command-line application for rendering images from MolViewSpec files' }); parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj or .mvsx format. File format is inferred from the file extension.' }); parser.add_argument('-o', '--output', { required: true, nargs: '+', help: 'File path(s) for output files (one output path for each input file). Output format is inferred from the file extension (.png or .jpg)' }); parser.add_argument('-s', '--size', { help: `Output image resolution, {width}x{height}. Default: ${DEFAULT_SIZE}.`, default: DEFAULT_SIZE }); parser.add_argument('-m', '--molj', { action: 'store_true', help: `Save Mol* state (.molj) in addition to rendered images (use the same output file paths but with .molj extension)` }); parser.add_argument('-n', '--no-extensions', { action: 'store_true', help: `Do not apply builtin MVS-loading extensions (not a part of standard MVS specification)` }); const args = parser.parse_args(); try { const parts = args.size.split('x'); if (parts.length !== 2) throw new Error('Must contain two x-separated parts'); args.size = { width: parseIntStrict(parts[0]), height: parseIntStrict(parts[1]) }; } catch (_a) { parser.error(`argument: --size: invalid image size string: '${args.size}' (must be two x-separated integers (width and height), e.g. '400x300')`); } if (args.input.length !== args.output.length) { parser.error(`argument: --output: must specify the same number of input and output file paths (specified ${args.input.length} input path${args.input.length !== 1 ? 's' : ''} but ${args.output.length} output path${args.output.length !== 1 ? 's' : ''})`); } return { ...args }; } /** Main workflow for rendering images from MolViewSpec files */ async function main(args) { const plugin = await createHeadlessPlugin(args); for (let i = 0; i < args.input.length; i++) { const input = args.input[i]; const output = args.output[i]; console.log(`Processing ${input} -> ${output}`); let mvsData; let sourceUrl; if (input.toLowerCase().endsWith('.mvsj')) { const data = fs_1.default.readFileSync(input, { encoding: 'utf8' }); mvsData = mvs_data_1.MVSData.fromMVSJ(data); sourceUrl = `file://${path_1.default.resolve(input)}`; } else if (input.toLowerCase().endsWith('.mvsx')) { const data = fs_1.default.readFileSync(input); const mvsx = await plugin.runTask(mol_task_1.Task.create('Load MVSX', async (ctx) => (0, formats_1.loadMVSX)(plugin, ctx, data))); mvsData = mvsx.mvsData; sourceUrl = mvsx.sourceUrl; } else { throw new Error(`Input file name must end with .mvsj or .mvsx: ${input}`); } await (0, load_1.loadMVS)(plugin, mvsData, { sanityChecks: true, sourceUrl: sourceUrl, extensions: args.no_extensions ? [] : undefined }); fs_1.default.mkdirSync(path_1.default.dirname(output), { recursive: true }); if (args.molj) { await plugin.saveStateSnapshot(withExtension(output, '.molj')); } if (output.toLowerCase().endsWith('.mp4')) { await plugin.saveAnimation(output); } else { await plugin.saveImage(output); } checkState(plugin); } await plugin.clear(); plugin.dispose(); } /** Return a new and initiatized HeadlessPlugin */ async function createHeadlessPlugin(args) { const externalModules = { gl: gl_1.default, pngjs: pngjs_1.default, 'jpeg-js': jpeg_js_1.default }; const spec = (0, spec_1.DefaultPluginSpec)(); spec.behaviors.push(spec_1.PluginSpec.Behavior(behavior_1.MolViewSpec)); spec.behaviors.push(spec_1.PluginSpec.Behavior(mp4_export_1.Mp4Export)); const headlessCanvasOptions = (0, headless_screenshot_1.defaultCanvas3DParams)(); const canvasOptions = { ...param_definition_1.ParamDefinition.getDefaultValues(canvas3d_1.Canvas3DParams), cameraResetDurationMs: headlessCanvasOptions.cameraResetDurationMs, postprocessing: headlessCanvasOptions.postprocessing, }; const plugin = new headless_plugin_context_1.HeadlessPluginContext(externalModules, spec, args.size, { canvas: canvasOptions }); try { await plugin.init(); } catch (error) { plugin.dispose(); throw error; } return plugin; } /** Parse integer, fail early. */ function parseIntStrict(str) { if (str === '') throw new Error('Is empty string'); const result = Number(str); if (isNaN(result)) throw new Error('Is NaN'); if (Math.floor(result) !== result) throw new Error('Is not integer'); return result; } /** Replace the file extension in `filename` by `extension`. If `filename` has no extension, add it. */ function withExtension(filename, extension) { const oldExtension = path_1.default.extname(filename); return filename.slice(0, -oldExtension.length) + extension; } /** Check Mol* state, print and throw error if any cell is not OK. */ function checkState(plugin) { const cells = Array.from(plugin.state.data.cells.values()); const badCell = cells.find(cell => cell.status !== 'ok'); if (badCell) { console.error(`Building Mol* state failed`); console.error(` Transformer: ${badCell.transform.transformer.id}`); console.error(` Params: ${(0, json_1.onelinerJsonString)(badCell.transform.params)}`); console.error(` Error: ${badCell.errorText}`); console.error(``); throw new Error(`Building Mol* state failed: ${badCell.errorText}`); } } main(parseArguments());