Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Question] How can we load an SVG file in a headless environment? #668

Closed
1 of 3 tasks
laurentvd opened this issue Nov 4, 2022 · 6 comments
Closed
1 of 3 tasks
Labels

Comments

@laurentvd
Copy link

Describe your question
How can I load an SVG file in a headless environment? It seems like Two.js expects the DOM to be present as it parses the loaded SVG using innerHTML and then children. I've tried to make it work using jsdom, but that didn't help. Is it possible to make it work?

Your code (either pasted here, or a link to a hosted example)

const el = two.load(pathToMySvg, function() {
    console.log(el);
});

Screenshots
The error:

for (i = 0; i < dom.temp.children.length; i++) {
                                  ^
TypeError: Cannot read properties of undefined (reading 'length')

Environment (please select one):

  • Code executes in browser (e.g: using script tag to load library)
  • Packaged software (e.g: ES6 imports, react, angular, vue.js)
  • Running headless (usually Node.js)

Additional context
Probably not relevant, but we're using Node 16.

@jonobr1
Copy link
Owner

jonobr1 commented Nov 5, 2022

Hmmm, great question. You are correct in how Two.js creates a virtual DOM with native browser DOM elements. It uses this to iterate through all the children and parse what would be text as nodes. This is how it expects that:

// src/utils/root.js
let root;

if (typeof window !== 'undefined') {
  root = window;
} else if (typeof global !== 'undefined') {
  root = global;
} else if (typeof self !== 'undefined') {
  root = self;
}

// src/utils/dom.js
dom.temp = (root.document ? root.document.createElement('div') : {});

Is there a way that jsdom can extend itself to the global node.js object? This way root.document could exist and have the createElement method.

@laurentvd
Copy link
Author

laurentvd commented Nov 6, 2022

Thanks for pointing me in the right direction! I managed to make it work using the following code.

global.window = new JSDOM().window;
global.document = window.document;

Note that it also needs a global document since for example src/effects/texture.js doesn't check for root properly:

// src/effects/texture.js
if (root.document) {
    anchor = document.createElement('a');
}

Also, it seems JSDOM has to be loaded and configured before Two.js is loaded to make it work. Additionally, JSDOM advises against this kind of setup. This is from their docs:

We strongly advise against trying to "execute scripts" by mashing together the jsdom and Node global environments (e.g. by doing global.window = dom.window), and then executing scripts or test code inside the Node global environment. Instead, you should treat jsdom like you would a browser, and run all scripts and tests that need access to a DOM inside the jsdom environment, using window.eval or runScripts: "dangerously". This might require, for example, creating a browserify bundle to execute as a <script> element—just like you would in a browser.

I feel that there's still work to be done to make Two.js fully headless-mode compatible. Maybe we can add a warning with a link to JSDOM in the docs for now? What do you think? And do you think it would ever be feasible to make Two.js fully headless without the need for tools like JSDOM? I'm fairly new to Two.js, but I feel it should be possible. But I'm probably missing a really challenging part here ;)

@jonobr1
Copy link
Owner

jonobr1 commented Nov 7, 2022

Thanks for posting your answer. This is super helpful for anyone that is using the current version of Two.js in a headless environment.

There are three functions that really rely on the DOM in some capacity that I haven't moved away from using because it's (at the time of writing) it was difficult to do. They are as follows:

  1. Traversing and parsing SVG nodes in two.load and two.interpret. The SVG interpretation doesn't generate Two.js scenes and objects directly from a string e.g: "<svg />". It generates them from DOM rendered SVGs e.g: <svg />. It's a subtle difference in formation, but a large difference in Two.js's codebase. To parse SVG strings is something on the roadmap (see: [Enhancement] Move Two.Utils.read.XXX to Two.XXX.parse #650) and in this case it would be much easier to move the load and interpret methods to headless.
  2. Two.Texture uses a hidden <a /> tag to generate absolute URLs of textures and create canonical resources in a texture registry. This means that if you load an image from "../some-path/my-image.jpg" multiple times, Two.js only loads it once. I haven't looked, but there's probably a way to generate absolute URLs of files in JavaScript.
  3. Two.Text uses <canvas /> elements' measureText method when invoking text.getBoundingClientRect() to get the width of the rendered text. It's much more accurate than anything Two.js currently does.

So, we'd have to address these three functions (doable!) to make it fully headless.

@laurentvd
Copy link
Author

Great. I'd agree that this is challenging but doable.

  1. I'd say this is the hardest one. Is there already progress on this?
  2. I understand what you're doing. For the web, we should probably make use of the URL API like this: (new URL(relativePath, location)).href. Technical challenges aside, I'm not sure what you'd expect to happen when in a headless context where there's no location.
  3. I don't think this should be an issue. AFAIK node-canvas supports measureText as well.

@jonobr1
Copy link
Owner

jonobr1 commented Nov 9, 2022

Thanks this is really helpful.

  1. Progress has yet to be made on this. Again, you can follow this issue for the latest: [Enhancement] Move Two.Utils.read.XXX to Two.XXX.parse #650
  2. That makes sense and seems relatively straightforward. For headless environments we could simply lose the "absolutify" transformation of a URL.
  3. I believe creating / accessing a canvas is different in node-canvas vs the Browser, so some slight updates would need to be made there.

I'll create new issues for numbers 2, 3.

@laurentvd
Copy link
Author

I think we can close this issue now that we have the actionable issues created, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants