UNPKG

molstar

Version:

A comprehensive macromolecular library.

212 lines (211 loc) 9.04 kB
/** * Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Jesse Liang <jesse.liang@rcsb.org> * @author Sebastian Bittrich <sebastian.bittrich@rcsb.org> * @author Ke Ma <mark.ma@rcsb.org> * @author Adam Midlik <midlik@gmail.com> */ import fs from 'fs'; import path from 'path'; import { Canvas3D, Canvas3DContext, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d'; import { Passes } from '../../mol-canvas3d/passes/passes'; import { PostprocessingParams } from '../../mol-canvas3d/passes/postprocessing'; import { createContext } from '../../mol-gl/webgl/context'; import { AssetManager } from '../../mol-util/assets'; import { ColorNames } from '../../mol-util/color/names'; import { PixelData } from '../../mol-util/image'; import { InputObserver } from '../../mol-util/input/input-observer'; import { ParamDefinition } from '../../mol-util/param-definition'; /** To render Canvas3D when running in Node.js (without DOM) */ export class HeadlessScreenshotHelper { constructor(externalModules, canvasSize, canvas3d, options) { var _a, _b, _c; this.externalModules = externalModules; this.canvasSize = canvasSize; if (canvas3d) { this.canvas3d = canvas3d; } else { const glContext = this.externalModules.gl(this.canvasSize.width, this.canvasSize.height, (_a = options === null || options === void 0 ? void 0 : options.webgl) !== null && _a !== void 0 ? _a : defaultWebGLAttributes()); const webgl = createContext(glContext); const input = InputObserver.create(); const attribs = { ...Canvas3DContext.DefaultAttribs }; const props = { ...Canvas3DContext.DefaultProps }; const assetManager = new AssetManager(); const passes = new Passes(webgl, assetManager, props); const pixelScale = 1; const syncPixelScale = () => { }; const setProps = () => { }; const dispose = () => { input.dispose(); webgl.destroy(); }; this.canvas3d = Canvas3D.create({ webgl, input, passes, attribs, props, assetManager, pixelScale, syncPixelScale, setProps, dispose }, (_b = options === null || options === void 0 ? void 0 : options.canvas) !== null && _b !== void 0 ? _b : defaultCanvas3DParams()); } this.imagePass = this.canvas3d.getImagePass((_c = options === null || options === void 0 ? void 0 : options.imagePass) !== null && _c !== void 0 ? _c : defaultImagePassParams()); this.imagePass.setSize(this.canvasSize.width, this.canvasSize.height); } async getImageData(runtime, width, height) { this.imagePass.setSize(width, height); await this.imagePass.render(runtime); this.imagePass.colorTarget.bind(); const array = new Uint8Array(width * height * 4); this.canvas3d.webgl.readPixels(0, 0, width, height, array); const pixelData = PixelData.create(array, width, height); PixelData.flipY(pixelData); PixelData.divideByAlpha(pixelData); // ImageData is not defined in Node.js return { data: new Uint8ClampedArray(array), width, height }; } async getImageRaw(runtime, imageSize, postprocessing) { var _a, _b; const width = (_a = imageSize === null || imageSize === void 0 ? void 0 : imageSize.width) !== null && _a !== void 0 ? _a : this.canvasSize.width; const height = (_b = imageSize === null || imageSize === void 0 ? void 0 : imageSize.height) !== null && _b !== void 0 ? _b : this.canvasSize.height; this.canvas3d.commit(true); this.imagePass.setProps({ postprocessing: ParamDefinition.merge(PostprocessingParams, this.canvas3d.props.postprocessing, postprocessing), }); return this.getImageData(runtime, width, height); } async getImagePng(runtime, imageSize, postprocessing) { const imageData = await this.getImageRaw(runtime, imageSize, postprocessing); if (!this.externalModules.pngjs) { throw new Error("External module 'pngjs' was not provided. If you want to use getImagePng, you must import 'pngjs' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor."); } const generatedPng = new this.externalModules.pngjs.PNG({ width: imageData.width, height: imageData.height }); generatedPng.data = Buffer.from(imageData.data.buffer); return generatedPng; } async getImageJpeg(runtime, imageSize, postprocessing, jpegQuality = 90) { const imageData = await this.getImageRaw(runtime, imageSize, postprocessing); if (!this.externalModules['jpeg-js']) { throw new Error("External module 'jpeg-js' was not provided. If you want to use getImageJpeg, you must import 'jpeg-js' and provide it to the HeadlessPluginContext/HeadlessScreenshotHelper constructor."); } const generatedJpeg = this.externalModules['jpeg-js'].encode(imageData, jpegQuality); return generatedJpeg; } async saveImage(runtime, outPath, imageSize, postprocessing, format, jpegQuality = 90) { if (!format) { const extension = path.extname(outPath).toLowerCase(); if (extension === '.png') format = 'png'; else if (extension === '.jpg' || extension === '.jpeg') format = 'jpeg'; else throw new Error(`Cannot guess image format from file path '${outPath}'. Specify format explicitly or use path with one of these extensions: .png, .jpg, .jpeg`); } if (format === 'png') { const generatedPng = await this.getImagePng(runtime, imageSize, postprocessing); await writePngFile(generatedPng, outPath); } else if (format === 'jpeg') { const generatedJpeg = await this.getImageJpeg(runtime, imageSize, postprocessing, jpegQuality); await writeJpegFile(generatedJpeg, outPath); } else { throw new Error(`Invalid format: ${format}`); } } } async function writePngFile(png, outPath) { await new Promise(resolve => { png.pack().pipe(fs.createWriteStream(outPath)).on('finish', resolve); }); } async function writeJpegFile(jpeg, outPath) { await new Promise(resolve => { fs.writeFile(outPath, jpeg.data, () => resolve()); }); } export function defaultCanvas3DParams() { return { camera: { mode: 'orthographic', helper: { axes: { name: 'off', params: {} } }, stereo: { name: 'off', params: {} }, fov: 90, manualReset: false, }, cameraResetDurationMs: 0, cameraFog: { name: 'on', params: { intensity: 50 } }, renderer: { ...DefaultCanvas3DParams.renderer, backgroundColor: ColorNames.white, }, postprocessing: { ...DefaultCanvas3DParams.postprocessing, occlusion: { name: 'off', params: {} }, outline: { name: 'off', params: {} }, antialiasing: { name: 'fxaa', params: { edgeThresholdMin: 0.0312, edgeThresholdMax: 0.063, iterations: 12, subpixelQuality: 0.3 } }, background: { variant: { name: 'off', params: {} } }, shadow: { name: 'off', params: {} }, } }; } export function defaultWebGLAttributes() { return { antialias: true, preserveDrawingBuffer: true, alpha: true, // the renderer requires an alpha channel depth: true, // the renderer requires a depth buffer premultipliedAlpha: true, // the renderer outputs PMA }; } export function defaultImagePassParams() { return { cameraHelper: { axes: { name: 'off', params: {} }, }, multiSample: { ...DefaultCanvas3DParams.multiSample, mode: 'on', sampleLevel: 4, reuseOcclusion: false, } }; } export const STYLIZED_POSTPROCESSING = { occlusion: { name: 'on', params: { samples: 32, multiScale: { name: 'off', params: {} }, radius: 5, bias: 0.8, blurKernelSize: 15, blurDepthBias: 0.5, resolutionScale: 1, color: ColorNames.black, transparentThreshold: 0.4, } }, outline: { name: 'on', params: { scale: 1, threshold: 0.95, color: ColorNames.black, includeTransparent: true, } } };