Explorar el Código

Forsake lazy imports (#772)

* Removed LazyImports (gl, pngjs, jpeg-js required as param of HeadlessPluginConstructor)

* Added a few methods to HeadlessPluginContext for rendering image without saving to file

* Updated CHANGELOG

* Lint

* Rolled back removing @types/jpeg-js from deps
midlik hace 2 años
padre
commit
2bc381fe05

+ 1 - 0
CHANGELOG.md

@@ -20,6 +20,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Fix camera project/unproject when using offset viewport
 - Add support for loading all blocks from a mmcif file as a trajectory
 - Add `Frustum3D` and `Plane3D` math primitives
+- Remove LazyImports (introduced in v3.31.1)
 
 ## [v3.32.0] - 2023-03-20
 

+ 6 - 2
src/examples/image-renderer/index.ts

@@ -12,13 +12,16 @@
 import { ArgumentParser } from 'argparse';
 import fs from 'fs';
 import path from 'path';
+import gl from 'gl';
+import pngjs from 'pngjs';
+import jpegjs from 'jpeg-js';
 
 import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
 import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
 import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
 import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
 import { DefaultPluginSpec } from '../../mol-plugin/spec';
-import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
+import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
 import { setFSModule } from '../../mol-util/data-source';
 
 
@@ -45,7 +48,8 @@ async function main() {
     console.log('Outputs:', args.outDirectory);
 
     // Create a headless plugin
-    const plugin = new HeadlessPluginContext(DefaultPluginSpec(), { width: 800, height: 800 });
+    const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
+    const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
     await plugin.init();
 
     // Download and visualize data in the plugin

+ 28 - 6
src/mol-plugin/headless-plugin-context.ts

@@ -5,33 +5,55 @@
  */
 
 import fs from 'fs';
+import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
+import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
+
 import { Canvas3D } from '../mol-canvas3d/canvas3d';
 import { PostprocessingProps } from '../mol-canvas3d/passes/postprocessing';
 import { PluginContext } from './context';
 import { PluginSpec } from './spec';
-import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions } from './util/headless-screenshot';
+import { HeadlessScreenshotHelper, HeadlessScreenshotHelperOptions, ExternalModules, RawImageData } from './util/headless-screenshot';
 
 
 /** PluginContext that can be used in Node.js (without DOM) */
 export class HeadlessPluginContext extends PluginContext {
     renderer: HeadlessScreenshotHelper;
 
-    constructor(spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
+    /** External modules (`gl` and optionally `pngjs` and `jpeg-js`) must be provided to the constructor (this is to avoid Mol* being dependent on these packages which are only used here) */
+    constructor(externalModules: ExternalModules, spec: PluginSpec, canvasSize: { width: number, height: number } = { width: 640, height: 480 }, rendererOptions?: HeadlessScreenshotHelperOptions) {
         super(spec);
-        this.renderer = new HeadlessScreenshotHelper(canvasSize, undefined, rendererOptions);
+        this.renderer = new HeadlessScreenshotHelper(externalModules, canvasSize, undefined, rendererOptions);
         (this.canvas3d as Canvas3D) = this.renderer.canvas3d;
     }
 
-    /** Render the current plugin state to a PNG or JPEG file */
+    /** Render the current plugin state and save to a PNG or JPEG file */
     async saveImage(outPath: string, imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, format?: 'png' | 'jpeg', jpegQuality = 90) {
         this.canvas3d!.commit(true);
         return await this.renderer.saveImage(outPath, imageSize, props, format, jpegQuality);
     }
 
+    /** Render the current plugin state and return as raw image data */
+    async getImageRaw(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<RawImageData> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImageRaw(imageSize, props);
+    }
+
+    /** Render the current plugin state and return as a PNG object */
+    async getImagePng(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>): Promise<PNG> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImagePng(imageSize, props);
+    }
+
+    /** Render the current plugin state and return as a JPEG object */
+    async getImageJpeg(imageSize?: { width: number, height: number }, props?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
+        this.canvas3d!.commit(true);
+        return await this.renderer.getImageJpeg(imageSize, props);
+    }
+
     /** Get the current plugin state */
-    getStateSnapshot() {
+    async getStateSnapshot() {
         this.canvas3d!.commit(true);
-        return this.managers.snapshot.getStateSnapshot({ params: {} });
+        return await this.managers.snapshot.getStateSnapshot({ params: {} });
     }
 
     /** Save the current plugin state to a MOLJ file */

+ 16 - 12
src/mol-plugin/util/headless-screenshot.ts

@@ -10,8 +10,8 @@
 
 import fs from 'fs';
 import path from 'path';
-import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import is done by LazyImports
-import { type PNG } from 'pngjs'; // Only import type here, the actual import is done by LazyImports
+import { type BufferRet as JpegBufferRet } from 'jpeg-js'; // Only import type here, the actual import must be provided by the caller
+import { type PNG } from 'pngjs'; // Only import type here, the actual import must be provided by the caller
 
 import { Canvas3D, Canvas3DContext, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
 import { ImagePass, ImageProps } from '../../mol-canvas3d/passes/image';
@@ -22,16 +22,14 @@ 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 { LazyImports } from '../../mol-util/lazy-imports';
 import { ParamDefinition } from '../../mol-util/param-definition';
 
 
-const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
+export interface ExternalModules {
     'gl': typeof import('gl'),
-    'jpeg-js': typeof import('jpeg-js'),
-    'pngjs': typeof import('pngjs'),
-};
-
+    'jpeg-js'?: typeof import('jpeg-js'),
+    'pngjs'?: typeof import('pngjs'),
+}
 
 export type HeadlessScreenshotHelperOptions = {
     webgl?: WebGLContextAttributes,
@@ -51,11 +49,11 @@ export class HeadlessScreenshotHelper {
     readonly canvas3d: Canvas3D;
     readonly imagePass: ImagePass;
 
-    constructor(readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
+    constructor(readonly externalModules: ExternalModules, readonly canvasSize: { width: number, height: number }, canvas3d?: Canvas3D, options?: HeadlessScreenshotHelperOptions) {
         if (canvas3d) {
             this.canvas3d = canvas3d;
         } else {
-            const glContext = lazyImports.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
+            const glContext = this.externalModules.gl(this.canvasSize.width, this.canvasSize.height, options?.webgl ?? defaultWebGLAttributes());
             const webgl = createContext(glContext);
             const input = InputObserver.create();
             const attribs = { ...Canvas3DContext.DefaultAttribs };
@@ -93,14 +91,20 @@ export class HeadlessScreenshotHelper {
 
     async getImagePng(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>): Promise<PNG> {
         const imageData = await this.getImageRaw(imageSize, postprocessing);
-        const generatedPng = new lazyImports.pngjs.PNG({ width: imageData.width, height: imageData.height });
+        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(imageSize?: { width: number, height: number }, postprocessing?: Partial<PostprocessingProps>, jpegQuality: number = 90): Promise<JpegBufferRet> {
         const imageData = await this.getImageRaw(imageSize, postprocessing);
-        const generatedJpeg = lazyImports['jpeg-js'].encode(imageData, jpegQuality);
+        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;
     }
 

+ 0 - 47
src/mol-util/lazy-imports.ts

@@ -1,47 +0,0 @@
-/**
- * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Adam Midlik <midlik@gmail.com>
- *
- * Manage dependencies which are not listed in `package.json` for performance reasons.
- */
-
-
-const _loadedExtraPackages: { [dep: string]: any } = {};
-
-/** Define imports that only get imported when first needed.
- * Example usage:
- * ```
- * const lazyImports = LazyImports.create('gl', 'jpeg-js', 'pngjs') as {
- *     'gl': typeof import('gl'),
- *     'jpeg-js': typeof import('jpeg-js'),
- *     'pngjs': typeof import('pngjs'),
- * };
- * ...
- * lazyImports.pngjs.blablabla("I'm being imported now");
- * lazyImports.pngjs.blablabla("I'm cached :D");
- * ```
- */
-export class LazyImports {
-    static create<U extends string>(...packages: U[]): { [dep in U]: any } {
-        return new LazyImports(packages) as any;
-    }
-    private constructor(private packages: string[]) {
-        for (const p of packages) {
-            Object.defineProperty(this, p, {
-                get: () => this.getPackage(p),
-            });
-        }
-    }
-    private getPackage(packageName: string) {
-        if (!_loadedExtraPackages[packageName]) {
-            try {
-                _loadedExtraPackages[packageName] = require(packageName);
-            } catch {
-                const message = `Package '${packageName}' is not installed. (Some packages are not listed in the 'molstar' package dependencies for performance reasons. If you're seeing this error, you'll probably need them. If your project depends on 'molstar', add these to your dependencies: ${this.packages.join(', ')}. If you're running 'molstar' directly, run this: npm install --no-save ${this.packages.join(' ')})`;
-                throw new Error(message);
-            }
-        }
-        return _loadedExtraPackages[packageName];
-    }
-}