Tui.image-editor: Implement Image Resize

Created on 28 Nov 2018  路  9Comments  路  Source: nhn/tui.image-editor

Hello!
This is not an issue, but I did want to bring this feature request to the maintainers' attention. (Not sure where else to put this.)

I was wondering if an image resizing tool could be implemented. Not resizing the canvas but the image itself.

Thank you for your work on this project. 馃樃

Enhancement

Most helpful comment

Any update on this resize feature?

All 9 comments

Also an option to resize the image before or while it is loading into the editor.

With this option a user can select/capture a huge multi megapixel picture to work with but in the editor its scaled down to a predefined image size.

@emyl3 @beppe34
I'll think about it. Thanks. :)

@beppe34
We've think about the feature exactly same you said. As you know, TOAST UI Image-Editor is running on the browser environment. So it has quite less memory resources than native image editing tools.
I'll keep research on resize image for the editing smart phone camera's 10M+ Pixel photos with pretty good image quality.

Hi, we are a recent convert from Aviary/Froala and not having the ability to resize an image seems like a pretty basic feature for an image editor. Our users are wondering why they can't resize. If you could add this feature it would be greatly appreciated. Thanks!!

Any update on this resize feature?

Any update on this resize feature?

Any update on this resize feature?

I downloaded the code and poked around. I don't see any reason why resize couldn't be supported as I've done it myself outside of the TUI ecosystem with Pica. Fabric supports manipulating the canvases / images to resize already. It seems reasonable that undo history might need to be optionally cleared for this operation when deciding to work with exceedingly large images.

I started working on the feature, but got hanged up on a few details during development around the complex web of modularity for UI / commands. The example doesn't render all the actions properly (they are all black) for me, although I am using the latest release in a project of mine just fine. Will keep poking at it, although it seems to me that it would be a very simple matter for a dev already familiar with the framework to jump in and implement this in very little time.

For anyone needing a solution not baked in, here's the Pica-based solution which is very rough, but may give you a starting place. I literally just hacked this together, so bugs may ensue :) You could also change this to using Fabric quite easily if needed.

  resize(maxWidth = 0, maxHeight = 0) {
    console.log('resizing with maximum width and height', maxWidth, maxHeight);
    const pica = Pica();

    const srcCanvas = document.createElement('canvas');
    const image = new Image();
    image.src = this.getImageData();
    image.onload = () => {
      const origAspectRatio = image.width / image.height;
      let targetWidth, targetHeight;

      console.log('original aspect ratio', origAspectRatio);

      // determine which dimension is going to be the limiting one (hit the max), so we can us it and scale the other by it
      const maxWidthRatio = image.width ? maxWidth / image.width : 0;
      const maxHeightRatio = image.height ? maxHeight / image.height : 0;

      if (maxWidthRatio >= 1 && maxHeightRatio >= 1) {
        console.log('image is already small enough, nothing to do');
        return;
      } else if (maxWidthRatio === 0 && maxHeightRatio === 0) {
        console.log('no resize dimensions specified, nothing to do');
        return;
      } else if (maxHeightRatio > maxWidthRatio) {
        targetWidth = maxWidth;
        targetHeight = maxWidth / origAspectRatio;
        console.log('clamping width to max, resizing dimensions to', targetWidth, targetHeight);
      } else if (maxWidthRatio > maxHeightRatio) {
        targetHeight = maxHeight;
        targetWidth = maxHeight * origAspectRatio;
        console.log('clamping height to max, resizing dimensions to', targetWidth, targetHeight);
      }

      srcCanvas.width = image.width;
      srcCanvas.height = image.height;

      const ctx = srcCanvas.getContext('2d');
      ctx.drawImage(image, 0, 0);

      // create the destination canvas
      const destCanvas = document.createElement('canvas');
      destCanvas.width = targetWidth;
      destCanvas.height = targetHeight;

      pica.resize(srcCanvas, destCanvas).then(async () => {
        console.log('resized');

        this.data = destCanvas.toDataURL();

        // THIS FUNCTION JUST INITIALIZES THE TUI IMAGE EDITOR WITH THE ABOVE DATA
        // see: includeUI.loadImage.path
        await this.initImageEditor();
      });
    };
  }

I downloaded the code and poked around. I don't see any reason why resize couldn't be supported as I've done it myself outside of the TUI ecosystem with Pica. Fabric supports manipulating the canvases / images to resize already. It seems reasonable that undo history might need to be optionally cleared for this operation when deciding to work with exceedingly large images.

I started working on the feature, but got hanged up on a few details during development around the complex web of modularity for UI / commands. The example doesn't render all the actions properly (they are all black) for me, although I am using the latest release in a project of mine just fine. Will keep poking at it, although it seems to me that it would be a very simple matter for a dev already familiar with the framework to jump in and implement this in very little time.

For anyone needing a solution not baked in, here's the Pica-based solution which is very rough, but may give you a starting place. I literally just hacked this together, so bugs may ensue :) You could also change this to using Fabric quite easily if needed.

  resize(maxWidth = 0, maxHeight = 0) {
    console.log('resizing with maximum width and height', maxWidth, maxHeight);
    const pica = Pica();

    const srcCanvas = document.createElement('canvas');
    const image = new Image();
    image.src = this.getImageData();
    image.onload = () => {
      const origAspectRatio = image.width / image.height;
      let targetWidth, targetHeight;

      console.log('original aspect ratio', origAspectRatio);

      // determine which dimension is going to be the limiting one (hit the max), so we can us it and scale the other by it
      const maxWidthRatio = image.width ? maxWidth / image.width : 0;
      const maxHeightRatio = image.height ? maxHeight / image.height : 0;

      if (maxWidthRatio >= 1 && maxHeightRatio >= 1) {
        console.log('image is already small enough, nothing to do');
        return;
      } else if (maxWidthRatio === 0 && maxHeightRatio === 0) {
        console.log('no resize dimensions specified, nothing to do');
        return;
      } else if (maxHeightRatio > maxWidthRatio) {
        targetWidth = maxWidth;
        targetHeight = maxWidth / origAspectRatio;
        console.log('clamping width to max, resizing dimensions to', targetWidth, targetHeight);
      } else if (maxWidthRatio > maxHeightRatio) {
        targetHeight = maxHeight;
        targetWidth = maxHeight * origAspectRatio;
        console.log('clamping height to max, resizing dimensions to', targetWidth, targetHeight);
      }

      srcCanvas.width = image.width;
      srcCanvas.height = image.height;

      const ctx = srcCanvas.getContext('2d');
      ctx.drawImage(image, 0, 0);

      // create the destination canvas
      const destCanvas = document.createElement('canvas');
      destCanvas.width = targetWidth;
      destCanvas.height = targetHeight;

      pica.resize(srcCanvas, destCanvas).then(async () => {
        console.log('resized');

        this.data = destCanvas.toDataURL();

        // THIS FUNCTION JUST INITIALIZES THE TUI IMAGE EDITOR WITH THE ABOVE DATA
        // see: includeUI.loadImage.path
        await this.initImageEditor();
      });
    };
  }

Can you explain me how you added this feature?

  • How to add button to open menu for resize?

In my case, I actually used a modified version of https://www.npmjs.com/package/ngx-tui-image-editor and included it in my project. Here's the code I used, which you may find needs some fix-up. Note that the resize occurs completely outside of the tui.image-editor. I found it difficult to delve into the innards there and thus settled for adding a layer with some buttons to resize.

```/**

import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
ViewChild,
Output,
ViewEncapsulation
} from '@angular/core';
import ImageEditor from 'tui-image-editor';
import Pica from 'pica';
import mixinDeep from 'mixin-deep';
const clone = require('rfdc')();

import {
IAddTextEvent,
IMousedownEvent,
IGraphicObjectProps,
IOptions
} from './toast-ui-image-editor.models';

import 'tui-image-editor/dist/svg/icon-a.svg';
import 'tui-image-editor/dist/svg/icon-b.svg';
import 'tui-image-editor/dist/svg/icon-c.svg';
import 'tui-image-editor/dist/svg/icon-d.svg';

enum editorEvents {
addText = 'addText',
mousedown = 'mousedown',
objectActivated = 'objectActivated',
objectMoved = 'objectMoved',
objectScaled = 'objectScaled',
redoStackChanged = 'redoStackChanged',
textEditing = 'textEditing',
undoStackChanged = 'undoStackChanged'
}
const includeUIOptions = {
includeUI: {
initMenu: 'filter'
}
};
const editorDefaultOptions: IOptions = {
cssMaxWidth: 700,
cssMaxHeight: 500
};

interface IImageEditor extends ImageEditor {
off(eventName: string): void;
}

@Component({
selector: 'ngx-tui-image-editor',
template: `


Resize:
1x
1.5x
2x

<div style='width: 100%; height: 100%'>
  <div
    #imageEditor usageStatistics={false} [id]="id"
    style='width: 100%; height: 100%;'
  ></div>
</div>

`,
styleUrls: [
'../../../../../node_modules/tui-image-editor/dist/tui-image-editor.css'
],
encapsulation: ViewEncapsulation.None
})
export class ToastUiImageEditorComponent implements AfterViewInit, OnDestroy {
@Input() includeUI = true;
@Input() options: IOptions;
@Input() id: string;
@Input() readonly data: string;

@Output() addText = new EventEmitter();
@Output() mousedown = new EventEmitter();
@Output() objectActivated = new EventEmitter();
@Output() objectMoved = new EventEmitter();
@Output() objectScaled = new EventEmitter();
@Output() redoStackChanged = new EventEmitter();
@Output() textEditing = new EventEmitter();
@Output() undoStackChanged = new EventEmitter();

@ViewChild('imageEditor', { static: true }) editorRef: ElementRef;

editorInstance: ImageEditor;
newData: string;

async ngAfterViewInit() {
console.log('ToastUiImageEditorComponent(): ngAfterViewInit()');
this.newData = this.data;
await this.initImageEditor();
}

async initImageEditor() {
// get defaults
let options = mixinDeep({}, clone(editorDefaultOptions));
// console.log('options now', JSON.stringify(options));

if (this.includeUI) {
  // get includeUI defaults
  options = mixinDeep(options, clone(includeUIOptions));
  // console.log('options now', JSON.stringify(options));
  // console.log('clone', clone(includeUIOptions));

  // get overrides
  options = mixinDeep(options, clone(this.options));
  // console.log('options now', JSON.stringify(options));

  if (!!this.newData && this.newData.length > 0) {
    options = mixinDeep(options, {
      includeUI: {
        loadImage: {
          path: this.newData,
          name: 'loaded image'
        }
      }
    });
  }

  // console.log('options now', JSON.stringify(options));
}

// if we have an existing image editor, destroy it
if (!!this.editorInstance) {
  console.log('destroying old image editor');
  this.editorInstance.destroy();
}

// console.log('creating image editor with options', options);
const editorInstance = new ImageEditor(
  this.editorRef.nativeElement,
  options
);
this.editorInstance = editorInstance;

// Patch the loadImageFromURL of our tui imageEditor instance:
this.editorInstance.loadImageFromURL = (function() {
  console.log('image editor loadImageFromURL');
  const cached_function = editorInstance.loadImageFromURL;
  function waitUntilImageEditorIsUnlocked(imageEditor) {
    return new Promise((resolve, reject) => {
      const interval = setInterval(() => {
        if (!imageEditor._invoker._isLocked) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
    });
  }
  return function() {
    return waitUntilImageEditorIsUnlocked(editorInstance).then(() => {
      console.log('loadImageFromURL', arguments);
      return cached_function.apply(this, arguments);
    });
  };
})();

this.addEventListeners();

}

ngOnDestroy() {
this.removeEventListeners();
this.editorInstance.destroy();
}

getImageData() {
const dataUrl = this.editorInstance.toDataURL({format: 'jpeg', quality: 0.9});
console.log('have data length', dataUrl.length);
return dataUrl;
}

async reset() {
this.newData = this.data;
await this.initImageEditor();
}

resize(maxWidth = 0, maxHeight = 0) {
console.log('resizing with maximum width and height', maxWidth, maxHeight);
const pica = Pica();

const srcCanvas = document.createElement('canvas');
const image = new Image();
image.src = this.getImageData();
image.onload = () => {
  const origAspectRatio = image.width / image.height;
  let targetWidth, targetHeight;

  console.log('original aspect ratio', origAspectRatio);

  // determine which dimension is going to be the limiting one (hit the max), so we can us it and scale the other by it
  const maxWidthRatio = image.width ? maxWidth / image.width : 0;
  const maxHeightRatio = image.height ? maxHeight / image.height : 0;

  if (maxWidthRatio >= 1 && maxHeightRatio >= 1) {
    console.log('image is already small enough, nothing to do');
    return;
  } else if (maxWidthRatio === 0 && maxHeightRatio === 0) {
    console.log('no resize dimensions specified, nothing to do');
    return;
  } else if (maxHeightRatio > maxWidthRatio) {
    targetWidth = maxWidth;
    targetHeight = maxWidth / origAspectRatio;
    console.log('clamping width to max, resizing dimensions to', targetWidth, targetHeight);
  } else if (maxWidthRatio > maxHeightRatio) {
    targetHeight = maxHeight;
    targetWidth = maxHeight * origAspectRatio;
    console.log('clamping height to max, resizing dimensions to', targetWidth, targetHeight);
  }

  srcCanvas.width = image.width;
  srcCanvas.height = image.height;

  const ctx = srcCanvas.getContext('2d');
  ctx.drawImage(image, 0, 0);

  // create the destination canvas
  const destCanvas = document.createElement('canvas');
  destCanvas.width = targetWidth;
  destCanvas.height = targetHeight;

  pica.resize(srcCanvas, destCanvas).then(async () => {
    console.log('resized');

    this.newData = destCanvas.toDataURL();
    await this.initImageEditor();
  });
};

// this.editorInstance.
//
// //scale the image to 600 (width) and keep aspect ratio
// var scaleFactor = resize_width / el.target.width;
// elem.width = resize_width;
// elem.height = el.target.height * scaleFactor;
//
// //draw in canvas
// var ctx = elem.getContext('2d');
// ctx.drawImage(el.target, 0, 0, elem.width, elem.height);
//
// //get the base64-encoded Data URI from the resize image
// var srcEncoded = ctx.canvas.toDataURL(el.target, 'image/jpeg', 0);
//
// //assign it to thumb src
// document.querySelector('#image').src = srcEncoded;
//
// /*Now you can send "srcEncoded" to the server and
// convert it to a png o jpg. Also can send
// "el.target.name" that is the file's name.*/

}

private addEventListeners() {
this.editorInstance.on(editorEvents.addText, event =>
this.addText.emit(event)
);
this.editorInstance.on(editorEvents.mousedown, (event, originPointer) =>
this.mousedown.emit({ event, originPointer })
);
this.editorInstance.on(editorEvents.objectActivated, event =>
this.objectActivated.emit(event)
);
this.editorInstance.on(editorEvents.objectMoved, event =>
this.objectMoved.emit(event)
);
this.editorInstance.on(editorEvents.objectScaled, event =>
this.objectScaled.emit(event)
);
this.editorInstance.on(editorEvents.redoStackChanged, event =>
this.redoStackChanged.emit(event)
);
this.editorInstance.on(editorEvents.textEditing, () =>
this.textEditing.emit()
);
this.editorInstance.on(editorEvents.undoStackChanged, event =>
this.undoStackChanged.emit(event)
);
}

private removeEventListeners() {
(this.editorInstance).off(editorEvents.addText);
(this.editorInstance).off(editorEvents.mousedown);
(this.editorInstance).off(editorEvents.objectActivated);
(this.editorInstance).off(editorEvents.objectMoved);
(this.editorInstance).off(editorEvents.objectScaled);
(this.editorInstance).off(editorEvents.redoStackChanged);
(this.editorInstance).off(editorEvents.textEditing);
(this.editorInstance).off(editorEvents.undoStackChanged);
}
}
```

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EABangalore picture EABangalore  路  5Comments

sj279811799 picture sj279811799  路  4Comments

sergeizhukov picture sergeizhukov  路  3Comments

Metalmi picture Metalmi  路  3Comments

marioviana picture marioviana  路  3Comments