molstar
Version:
A comprehensive macromolecular library.
220 lines (219 loc) • 9.68 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>
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.STYLIZED_POSTPROCESSING = exports.HeadlessScreenshotHelper = void 0;
exports.defaultCanvas3DParams = defaultCanvas3DParams;
exports.defaultWebGLAttributes = defaultWebGLAttributes;
exports.defaultImagePassParams = defaultImagePassParams;
const tslib_1 = require("tslib");
const fs_1 = tslib_1.__importDefault(require("fs"));
const path_1 = tslib_1.__importDefault(require("path"));
const canvas3d_1 = require("../../mol-canvas3d/canvas3d");
const passes_1 = require("../../mol-canvas3d/passes/passes");
const postprocessing_1 = require("../../mol-canvas3d/passes/postprocessing");
const context_1 = require("../../mol-gl/webgl/context");
const assets_1 = require("../../mol-util/assets");
const names_1 = require("../../mol-util/color/names");
const image_1 = require("../../mol-util/image");
const input_observer_1 = require("../../mol-util/input/input-observer");
const param_definition_1 = require("../../mol-util/param-definition");
/** To render Canvas3D when running in Node.js (without DOM) */
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 = (0, context_1.createContext)(glContext);
const input = input_observer_1.InputObserver.create();
const attribs = { ...canvas3d_1.Canvas3DContext.DefaultAttribs };
const props = { ...canvas3d_1.Canvas3DContext.DefaultProps };
const assetManager = new assets_1.AssetManager();
const passes = new passes_1.Passes(webgl, assetManager, props);
const pixelScale = 1;
const syncPixelScale = () => { };
const setProps = () => { };
const dispose = () => {
input.dispose();
webgl.destroy();
};
this.canvas3d = canvas3d_1.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 = image_1.PixelData.create(array, width, height);
image_1.PixelData.flipY(pixelData);
image_1.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: param_definition_1.ParamDefinition.merge(postprocessing_1.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_1.default.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}`);
}
}
}
exports.HeadlessScreenshotHelper = HeadlessScreenshotHelper;
async function writePngFile(png, outPath) {
await new Promise(resolve => {
png.pack().pipe(fs_1.default.createWriteStream(outPath)).on('finish', resolve);
});
}
async function writeJpegFile(jpeg, outPath) {
await new Promise(resolve => {
fs_1.default.writeFile(outPath, jpeg.data, () => resolve());
});
}
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: {
...canvas3d_1.DefaultCanvas3DParams.renderer,
backgroundColor: names_1.ColorNames.white,
},
postprocessing: {
...canvas3d_1.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: {} },
}
};
}
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
};
}
function defaultImagePassParams() {
return {
cameraHelper: {
axes: { name: 'off', params: {} },
},
multiSample: {
...canvas3d_1.DefaultCanvas3DParams.multiSample,
mode: 'on',
sampleLevel: 4,
reuseOcclusion: false,
}
};
}
exports.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: names_1.ColorNames.black,
transparentThreshold: 0.4,
}
}, outline: {
name: 'on', params: {
scale: 1,
threshold: 0.95,
color: names_1.ColorNames.black,
includeTransparent: true,
}
}
};
;