ToastUI Image Editor loadImageFromURL doesn't work

10.5k Views Asked by At

Please note this is a self-answered question.

This question is about the ToastUI Image Editor v3.3.0, but it may also apply to newer versions.

When you load an image using this official example:

// Create image editor
var imageEditor = new tui.component.ImageEditor('#my-image-editor canvas', {
    cssMaxWidth: 1000, // Component default value: 1000
    cssMaxHeight: 800  // Component default value: 800
});

// Load image
imageEditor.loadImageFromURL('img/sampleImage.jpg', 'My sample image')

The editor will not load the image. The function neither throws, nor returns anything indicating a failure and you don't get any error messages. It returns a promise that resolves as specified in the documentation.

It only loads an image by specifying it in the initial config and you can't change it afterwards:

// Create image editor
var imageEditor = new tui.component.ImageEditor('#my-image-editor canvas', {
     includeUI: {
         loadImage: {
             path: 'img/sampleImage.jpg',
             name: 'My sample image'
         },
     },
    cssMaxWidth: 1000, // Component default value: 1000
    cssMaxHeight: 800  // Component default value: 800
});

It appears that the loadImageFromURL function is broken and according to other users loadImageFromFile has the same problem.

Issues about this have been raised on GitHub, but have basically been ignored. It's been a month now and unfortunately it still hasn't been fixed.

So the question is how can the image editor be tricked into working while this issue exists.

Here is a fiddle showing that it doesn't work: https://fiddle.sencha.com/#view/editor&fiddle/2org

4

There are 4 best solutions below

2
On BEST ANSWER

TL;DR:
Here is a working fiddle: https://fiddle.sencha.com/#view/editor&fiddle/2p0o


Long version:

There are four problems:

  • You need to load an initial image, otherwise you can't use the editing controls.
  • You need to wait until the image editor object is ready before calling loadImageFromURL, otherwise you may get an error or a silent failure
  • When the image is loaded you need to tell the image editor the new size, otherwise the image will be hidden or sized incorrectly.
  • If you load an external image, the external server has to set the Access-Control-Allow-Origin header and explicitly allow your domain to access it, otherwise the image editor can not access it.

This first problem can be solved by loading a blank image like this:

var imageEditor = new tui.ImageEditor('#tui-image-editor-container', {
    includeUI: {
        loadImage: {
            path: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
            name: 'Blank'
        },
        theme: whiteTheme,
        menuBarPosition: 'bottom'
    },
    cssMaxWidth: 700,
    cssMaxHeight: 700
});

The second problem can be solved by waiting for the image editor to get out of its lock state using undocumented functionality. You can patch your loadImageFromURL during runtime like this:

imageEditor.loadImageFromURL = (function() {
    var cached_function = imageEditor.loadImageFromURL;
    function waitUntilImageEditorIsUnlocked(imageEditor) {
        return new Promise((resolve,reject)=>{
            const interval = setInterval(()=>{
                if (!imageEditor._invoker._isLocked) {
                    clearInterval(interval);
                    resolve();
                }
            }, 100);
        })
    }
    return function() {
        return waitUntilImageEditorIsUnlocked(imageEditor).then(()=>cached_function.apply(this, arguments));
    };
})();

The third problem can be resolved by taking the object that the promise returned by loadImageFromURL resolves with and passing the new and old width/height properties to the ui.resizeEditor function like this:

imageEditor.loadImageFromURL("https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg/526px-Wikipedia-logo-v2.svg.png", "SampleImage").then(result=>{
    imageEditor.ui.resizeEditor({
        imageSize: {oldWidth: result.oldWidth, oldHeight: result.oldHeight, newWidth: result.newWidth, newHeight: result.newHeight},
    });
}).catch(err=>{
    console.error("Something went wrong:", err);
})

The fourth problem can be a bit confusing. Let me explain. On websites you can include pretty much any external image you want in using an <img> tag, but if you want to access an external image using JavaScript, the server providing the image has to explicitly allow you to do that using the access-control-allow-origin header. On Amazon S3 for instance, the servers don't allow this by default. You have to manually set the server to allow your or any domain to access it. See here. If you are using a different server, you could for example set the access-control-allow-origin to * like wikipedia has done on this image. Then you (and the image editor) could access that image from the JavaScript of any domain.

0
On

For those who are using Rails, when it came to the fourth problem stated by @Forivin, this is what I did to get it working.

The issue is when Toast would call the image stored on S3 I would get a CORS error on Chrome, but firefox was fine. There are lots of articles on this, essentially I found the best way is to use a proxy in my code. I can still have my CORS origin pointing to my host, and since the call is coming from my host via proxy, S3 and Chrome are happy. My S3 CORS config looks like this (allows subdomains):

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>http://*.mycompany.com</AllowedOrigin>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

In your rails project do this:

Add rack-proxy gem in your Gemfile

gem 'rack-proxy'

Create a proxy file. The s3 path is URI encoded and appended to end of route. The route is just used for proxy so it can be anything, as it will be rerouted to s3.

app/proxy/s3_proxy.rb

class S3Proxy < Rack::Proxy

  def perform_request(env)
    if env['REQUEST_PATH'] =~ %r{^/my/dummy/path}
      s3_path = CGI.unescape(env['REQUEST_PATH'][15..-1])

      uri = URI.parse(s3_path)
      env['HTTP_HOST'] = uri.host
      env['SERVER_PORT'] = uri.port
      env['REQUEST_PATH'] = s3_path
      env['REQUEST_URI'] = s3_path
      env['PATH_INFO'] = s3_path
      env['rack.url_scheme'] = 'https'

      super(env)
    else
      @app.call(env)
    end
  end

end

Add to application.rb file:

require "./app/proxy/s3_proxy"

class Application < Rails::Application
  ...

  config.middleware.use S3Proxy
end

routes.rb

get "/my/dummy/path/:s3_url", to: "my_controller#dummy_path"

Controller method in my_controller.rb. Doesnt matter what gets rendered here as it will be redirected by proxy. We could probably get away with no method since proxy will change anyway.

  def dummy_path
    render plain: ""
  end

And finally in my Vue code, I call Toast editor by first populating with a blank white image. Then when the component is mounted, I load the s3 image and overwrite the existing image and resize the canvas. I found I needed a slight delay when it is mounted before reading s3 image. The s3 image is a presigned url that I pass in props.

<template lang="pug">
.v-image-editor-tool
  tui-image-editor(:include-ui='useDefaultUI' :options="editorOptions" ref="tuiImageEditor")
</template>

<script lang="coffee">
import { ImageEditor } from '@toast-ui/vue-image-editor'
import 'tui-image-editor/dist/tui-image-editor.min.css'

export default app =
  props: ['imageUrl']
  data: ->
    useDefaultUI: true
    editorOptions:
      cssMaxWidth: 700
      cssMaxHeight: 700
      usageStatistics: false
      includeUI:
        loadImage:
          path: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
          name: 'Blank'
        menuBarPosition: 'bottom'

  mounted: ->
    fn = => this.$refs.tuiImageEditor.invoke('loadImageFromURL', @imageUrl, 'Image').then (result) =>
      this.$refs.tuiImageEditor.invoke('ui.resizeEditor', { imageSize: { newWidth: result.newWidth, newHeight: result.newHeight }})
    setTimeout(fn, 600)

  components:
    'tui-image-editor': ImageEditor
</script>

0
On

Add this attribute to the image tag you want to edit before opening it:

crossorigin="anonymous"

As explained here: https://github.com/nhn/tui.image-editor/issues/68#issuecomment-930106372

0
On

I found better and cleaner way to this

There is a method called activeMenuEvent that not exist in document.

So you can load image like this

ref.loadImageFromURL(image.url, image.title)
   .then(value => {
   ref.ui.activeMenuEvent();
   ref.ui.resizeEditor({imageSize: value});
});