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

Implement Image Resize #100

Closed
emyl3 opened this issue Nov 28, 2018 · 9 comments
Closed

Implement Image Resize #100

emyl3 opened this issue Nov 28, 2018 · 9 comments
Labels
Enhancement Enhance performance or improve usability of original features.

Comments

@emyl3
Copy link

emyl3 commented Nov 28, 2018

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. 😸

@beppe34
Copy link

beppe34 commented Dec 15, 2018

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.

@jinwoo-kim-nhn jinwoo-kim-nhn added the Enhancement Enhance performance or improve usability of original features. label Dec 27, 2018
@junghwan-park
Copy link
Member

junghwan-park commented Jan 10, 2019

@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.

@bspilove
Copy link

bspilove commented Feb 5, 2019

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!!

@kakusharma
Copy link

Any update on this resize feature?

2 similar comments
@abozhinov
Copy link

Any update on this resize feature?

@GraemeFulton
Copy link

Any update on this resize feature?

@arimus
Copy link

arimus commented Aug 15, 2019

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();
      });
    };
  }

@ravizeewee
Copy link

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?

@arimus
Copy link

arimus commented Mar 5, 2020

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.

 * Original code from https://www.npmjs.com/package/ngx-tui-image-editor, MIT license
 */

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: `
    <div style="position: absolute; top: 20px; left: 300px; z-index: 1000;">
      Resize:
      <a href="javascript:void(0)" (click)="resize(800, 800)" style="padding-left: 20px; color: grey;">1x</a>
      <a href="javascript:void(0)" (click)="resize(1200, 1200)" style="padding-left: 20px; color: grey;">1.5x</a>
      <a href="javascript:void(0)" (click)="resize(1600, 1600)" style="padding-left: 20px; color: grey;">2x</a>
    </div>

    <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<IAddTextEvent>();
  @Output() mousedown = new EventEmitter<IMousedownEvent>();
  @Output() objectActivated = new EventEmitter<IGraphicObjectProps>();
  @Output() objectMoved = new EventEmitter<IGraphicObjectProps>();
  @Output() objectScaled = new EventEmitter<IGraphicObjectProps>();
  @Output() redoStackChanged = new EventEmitter<number>();
  @Output() textEditing = new EventEmitter<void>();
  @Output() undoStackChanged = new EventEmitter<number>();

  @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() {
    (<IImageEditor>this.editorInstance).off(editorEvents.addText);
    (<IImageEditor>this.editorInstance).off(editorEvents.mousedown);
    (<IImageEditor>this.editorInstance).off(editorEvents.objectActivated);
    (<IImageEditor>this.editorInstance).off(editorEvents.objectMoved);
    (<IImageEditor>this.editorInstance).off(editorEvents.objectScaled);
    (<IImageEditor>this.editorInstance).off(editorEvents.redoStackChanged);
    (<IImageEditor>this.editorInstance).off(editorEvents.textEditing);
    (<IImageEditor>this.editorInstance).off(editorEvents.undoStackChanged);
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement Enhance performance or improve usability of original features.
Projects
None yet
Development

No branches or pull requests

10 participants