jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.29k stars 455 forks source link

Headless support for DataURL Textures #470

Open christianecker-vil opened 4 years ago

christianecker-vil commented 4 years ago

Hi,

I am struggling when writing unit tests in a react/jest environment for my canvas drawing methods. In contrast to the built-in browser canvas, I use node-canvas:

import { createCanvas, Image } from "canvas";
...

const canvas = createCanvas(width, height);
Two.Utils.shim(canvas, Image);
const two = new Two({
    width: width,
    height: height,
    domElement: canvas,
});

When creating a new Texture by const texture = new Two.Texture(imgAsDataUrl); I run in the same error as described in [#468 ] (ReferenceError: require is not defined)

To overcome it, I tried overriding Two.Utils.isHeadless = false;, injecting an already loaded Image into a Texture Object and registering the Texture manually by calling Two.Texture.Register.img(texture). But the image is not rendered to the canvas (I probably miss internals of Two.js, which need to be called to make it work)

Also, I tried writing the DataURL image to disk using fs and providing new Two.Texture() with the file name. But this again fails with ReferenceError: require is not defined in my environment.

I guess it would be easiest if there was a way to also provide images as DataUrl in headless mode, or am I missing anything?

Thanks for your help!

jonobr1 commented 4 years ago

Are you using the latest dev branch from GitHub? That has a fix for issue 468. I'll follow up to confirm the dataURL support.

christianecker-vil commented 4 years ago

Switch to the dev branch fixed the error but in my case (testing environment) not really solves it: When I write the dataURL image to a local image.png file and create the Texture using the filename results in

ENOENT: no such file or directory, open 'http://localhost/image.png'

      at Function.loadHeadlessBuffer (node_modules/@jonobr1/two.js/build/two.js:6062:23)
      at Object.img (node_modules/@jonobr1/two.js/build/two.js:6162:19)
      at Function.load (node_modules/@jonobr1/two.js/build/two.js:6236:30)
      at Texture._update (node_modules/@jonobr1/two.js/build/two.js:6481:19)
      at new Texture (node_modules/@jonobr1/two.js/build/two.js:6008:10)
jonobr1 commented 4 years ago

When using Two.js in node, it uses the fs module and loads images that way. Specifying URLs doesn't work. This is something Two.js should handle though.

christianecker-vil commented 4 years ago

I did not specify the URL, my code looks like:

fs.writeFile("./image.png", buf);
const texture = new Two.Texture("./image.png");
jonobr1 commented 4 years ago

Hmm, in theory I feel like that should work. But, your error shows that your application is resolving "./image.png" to http://localhost/image.png. It's hard to help debug this issue without seeing more of your application... Perhaps you could try wrapping your path with the path.resolve method from the native path module in node like so:

const path = require("path");
const texture = new Two.Texture(path.resolve(__dirname, "./image.png"));

To unpack a little of what other issues lie here.

  1. Two.Texture doesn't currently support loading from Buffers, but it should.
  2. Two.Texture doesn't currently support loading from URLs, but it should.
  3. Subsequently, if you want to upload a dataURI to Two.js then you should be able to through a buffer.

e.g:

var sprite = two.makeSprite(Buffer.from(dataURI, 'base64'), x, y);
HalCanary commented 4 years ago
const texture = new Two.Texture(path.resolve(__dirname, "./image.png"));

Have you tried this?

const texture = new Two.Texture("file://" + path.resolve(__dirname, "./image.png"));
christianecker-vil commented 4 years ago

Tried both but unfortunately the resulting paths don't work here:

const texture = new Two.Texture(path.resolve(__dirname, "./image.png"));

ENOENT: no such file or directory, open 'http://localhost/Users/.../image.png'

const texture = new Two.Texture("file://" + path.resolve(__dirname, "./image.png"));

ENOENT: no such file or directory, open 'file:///Users/../image.png'

I suppose that the comment here is still valid 😄

getAbsoluteURL: function(path) {
    if (!anchor) {
      // TODO: Fix for headless environments
      return path;
    }
    anchor.href = path;
    return anchor.href;
  },
jonobr1 commented 4 years ago

I'll reiterate, if you can share more of your application I'll be able to help out. The errors you're showing reveal that Two.js thinks you're running a local server and attempting to find assets from the http protocol instead of directly from node's FileSystem.

The getAbsoluteURL needs a fix there, but I don't believe for the issue you're running into. I say this, because this example works fine for me with the latest dev branch:

var { createCanvas, Image } = require('canvas');
var Two = require('two.js');

var fs = require('fs');
var path = require('path');

var width = 800;
var height = 600;

var canvas = createCanvas(width, height);
Two.Utils.shim(canvas, Image);

var time = Date.now();

var two = new Two({
  width: width,
  height: height,
  domElement: canvas
});

var uri = path.resolve(__dirname, './images/thumbnail.png');
var sprite = two.makeSprite(uri, two.width / 2, two.height / 2);

two.render();

var settings = { compressionLevel: 3, filters: canvas.PNG_FILTER_NONE };
fs.writeFileSync(path.resolve(__dirname, './images/rectangle.png'), canvas.toBuffer('image/png', settings));
console.log('Finished rendering. Time took: ', Date.now() - time);

process.exit();
christianecker-vil commented 4 years ago

Thanks for getting back and providing the example. Seems like in my environment the file uri gets overridden so I can't get the example to work.

I continued reading on loading images with the canvas package and found out about the loadImage method (https://github.com/Automattic/node-canvas/issues/1581#issuecomment-629274227).

/**
 * Convenience function for loading an image with a Promise interface. This
 * function works in both Node.js and Web browsers; however, the `src` must be
 * a string in Web browsers (it can only be a Buffer in Node.js).
 * @param src URL, `data: ` URI or (Node.js only) a local file path or Buffer
 * instance.
 */
export function loadImage(src: string|Buffer, options?: any): Promise<Image>

Could it also be a solution to pass a loaded Image when creating a new Texture the way:

import { createCanvas, Image, loadImage } from "canvas";
...

const canvas = createCanvas(width, height);
Two.Utils.shim(canvas, Image);
const ctx = canvas.getContext("2d");

var two = new Two({
    width: width,
    height: height,
    domElement: canvas,
});

const img = await loadImage(imgAsDataUrl);
const texture = new Two.Texture(img);

two.update();

However, I tried it on a fork of two.js but always end up with a nodejs error as soon as I call two.update();:

FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal.
 1: 0x100080c68 node::Abort() [/usr/local/bin/node]
 2: 0x100080dec node::errors::TryCatchScope::~TryCatchScope() [/usr/local/bin/node]
 3: 0x1001876f0 v8::V8::ToLocalEmpty() [/usr/local/bin/node]
 4: 0x10300f07a Pattern::New(Nan::FunctionCallbackInfo<v8::Value> const&) [/Users/christianecker/Dev/DV/break-protector/node_modules/canvas/build/Release/canvas.node]
 5: 0x1030029c8 Nan::imp::FunctionCallbackWrapper(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/christianecker/Dev/DV/break-protector/node_modules/canvas/build/Release/canvas.node]
 6: 0x1001f08f0 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo) [/usr/local/bin/node]
 7: 0x1001efbe4 v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<true>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/usr/local/bin/node]
 8: 0x1001ef914 v8::internal::Builtins::InvokeApiFunction(v8::internal::Isolate*, bool, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*, v8::internal::Handle<v8::internal::HeapObject>) [/usr/local/bin/node]
 9: 0x1002a8f94 v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/usr/local/bin/node]
10: 0x1002a922a v8::internal::Execution::New(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*) [/usr/local/bin/node]
11: 0x1001a3074 v8::Function::NewInstanceWithSideEffectType(v8::Local<v8::Context>, int, v8::Local<v8::Value>*, v8::SideEffectType) const [/usr/local/bin/node]
12: 0x103016ae6 Context2d::CreatePattern(Nan::FunctionCallbackInfo<v8::Value> const&) [/Users/christianecker/Dev/DV/break-protector/node_modules/canvas/build/Release/canvas.node]
13: 0x1030029c8 Nan::imp::FunctionCallbackWrapper(v8::FunctionCallbackInfo<v8::Value> const&) [/Users/christianecker/Dev/DV/break-protector/node_modules/canvas/build/Release/canvas.node]
14: 0x1001f08f0 v8::internal::FunctionCallbackArguments::Call(v8::internal::CallHandlerInfo) [/usr/local/bin/node]
15: 0x1001efecf v8::internal::MaybeHandle<v8::internal::Object> v8::internal::(anonymous namespace)::HandleApiCallHelper<false>(v8::internal::Isolate*, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::HeapObject>, v8::internal::Handle<v8::internal::FunctionTemplateInfo>, v8::internal::Handle<v8::internal::Object>, v8::internal::BuiltinArguments) [/usr/local/bin/node]
16: 0x1001ef5d0 v8::internal::Builtin_Impl_HandleApiCall(v8::internal::BuiltinArguments, v8::internal::Isolate*) [/usr/local/bin/node]
17: 0x100950a19 Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit [/usr/local/bin/node]
18: 0x1008d62c4 Builtins_InterpreterEntryTrampoline [/usr/local/bin/node]
jonobr1 commented 4 years ago

Thanks for sharing. So, I've made updates to the Two.Texture on the latest dev branch with a more robust fix: https://github.com/jonobr1/two.js/commit/ace14453effd460c2479d5eeceba45b53912a5e5. You can now use the node-canvas Image class as a valid argument into textures. You can still use resolved file paths and I will track using Buffers. In your case @christianecker-vil, you should be able to use your latest example with the code I've just deployed. If that doesn't work then please let me know and we can debug further.

Hope this helps!

christianecker-vil commented 4 years ago

Thank you for this quick fix, just worked like a charm!

jonobr1 commented 4 years ago

Not the same, but related is this new enhancement to support TypedArrays: https://github.com/jonobr1/two.js/issues/472