molstar
Version:
A comprehensive macromolecular library.
212 lines (211 loc) • 9.04 kB
JavaScript
/**
* 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,
}
}
};