Browse Source

Merge pull request #772 from threema-ch/770-resize-avatar

Actually resize avatars
Lennart Grahl 7 years ago
parent
commit
e3d996bc53
5 changed files with 151 additions and 96 deletions
  1. 3 3
      package-lock.json
  2. 1 1
      package.json
  3. 55 81
      src/directives/avatar_editor.ts
  4. 1 1
      src/helpers.ts
  5. 91 10
      src/types/croppie.d.ts

+ 3 - 3
package-lock.json

@@ -2610,9 +2610,9 @@
       }
     },
     "croppie": {
-      "version": "2.6.2",
-      "resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.2.tgz",
-      "integrity": "sha512-uvQI8XliWNWiquFCgIjwNoWCjbnQ/qvyitwIj8F4PjwrH40vniPmKSXmXaIbiWNojiraCi08PovKXfs8YXjMGg=="
+      "version": "2.6.3",
+      "resolved": "https://registry.npmjs.org/croppie/-/croppie-2.6.3.tgz",
+      "integrity": "sha512-x0EGQF5GwUTzW7igxFu0eP9UM5JsK/3y0hIiFIVjV6zLd4EMsGZDIKhh7XwppQoZMDyEun3Ge7fWlu1jzp+iuA=="
     },
     "cross-spawn": {
       "version": "6.0.5",

+ 1 - 1
package.json

@@ -63,7 +63,7 @@
     "babelify": "~10.0.0",
     "browserify": "^16.2.3",
     "browserify-header": "^1.0.0",
-    "croppie": "~2.6.0",
+    "croppie": "^2.6.3",
     "file-saver": "2.0.0",
     "messageformat": "^2.0.5",
     "msgpack-lite": "~0.1.26",

+ 55 - 81
src/directives/avatar_editor.ts

@@ -15,6 +15,9 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
+// tslint:disable:no-reference
+/// <reference path="../types/croppie.d.ts" />
+
 // tslint:disable:max-line-length
 
 import {bufferToUrl, logAdapter} from '../helpers';
@@ -41,47 +44,41 @@ export default [
                 const editorArea: any = angular.element(element[0].querySelector('.avatar-editor'));
                 const fileTrigger: any = angular.element(element[0].querySelector('.file-trigger'));
                 const fileInput: any = angular.element(element[0].querySelector('input.file-input'));
-                // const avatarRemove: any = angular.element(element[0].querySelector('.avatar-remove'));
-                // const navigation: any = angular.element(element[0].querySelector('.avatar-area-navigation'));
-                const enabled = scope.enabled === undefined || scope.enabled === true;
-
-                let croppieInstance = null;
-                const initCroppie = () => {
-                    if (croppieInstance !== null) {
-                        return croppieInstance;
-                    }
-                    croppieInstance = new Croppie(element[0].querySelector('.croppie-target'), {
-                        viewport: {
-                            type: 'square',
-                            width: VIEWPORT_SIZE,
-                            height: VIEWPORT_SIZE,
-                        },
-                        customClass: 'has-image',
-                        showZoomer: true,
-                        update() {
-                            if (updateTimeout !== undefined) {
-                                clearTimeout(updateTimeout);
-                            }
 
-                            updateTimeout = self.setTimeout(() => {
-                                croppieInstance.result({
-                                    type: 'blob',
-                                    // max allowed size on device
-                                    size: [512, 512],
-                                    circle: false,
-                                    format: 'png',
-                                })
-                                    .then((blob: Blob) => {
-                                        const fileReader = new FileReader();
-                                        fileReader.onload = function() {
-                                            scope.onChange(this.result);
-                                        };
-                                        fileReader.readAsArrayBuffer(blob);
+                let croppieInstance: Croppie = null;
+                const initCroppie = (): Croppie => {
+                    if (croppieInstance === null) {
+                        // Create croppie
+                        const croppieTarget: HTMLElement = element[0].querySelector('.croppie-target');
+                        croppieInstance = new Croppie(croppieTarget, {
+                            viewport: {
+                                type: 'square',
+                                width: VIEWPORT_SIZE,
+                                height: VIEWPORT_SIZE,
+                            },
+                            customClass: 'has-image',
+                            showZoomer: true,
+                            update: (): void => {
+                                if (updateTimeout !== undefined) {
+                                    clearTimeout(updateTimeout);
+                                }
+                                updateTimeout = self.setTimeout(async () => {
+                                    const image: Blob = await croppieInstance.result({
+                                        type: 'blob',
+                                        // TODO: Should be retrieved from clientInfo once available
+                                        size: { width: 512, height: 512 },
+                                        circle: false,
+                                        format: 'png',
                                     });
-                            }, 500);
-                        },
-                    });
-
+                                    const fileReader = new FileReader();
+                                    fileReader.onload = function() {
+                                        scope.onChange(fileReader.result);
+                                    };
+                                    fileReader.readAsArrayBuffer(image);
+                                }, 500);
+                            },
+                        });
+                    }
                     return croppieInstance;
                 };
 
@@ -92,6 +89,7 @@ export default [
                         editorArea.removeClass('loading');
                     }
                 }
+
                 // set after default avatar is set
                 let updateTimeout;
 
@@ -172,50 +170,36 @@ export default [
                     uploadFiles(this.files);
                 }
 
-                function setImage(newImage: any) {
+                function setImage(imageBase64Url: string) {
                     const croppie = initCroppie();
-
-                    if (newImage === null) {
-                        // set a none image
-                        // TODO
-                        croppie.bind({
-                            url: null,
-                        });
-
-                        scope.onChange(null);
-                        return;
-                    }
-
                     loading(true);
+
                     // load image to calculate size
                     const img = new Image();
-                    img.addEventListener('load', function() {
+                    img.addEventListener('load', async () => {
                         $log.debug(logTag, 'Image loaded');
 
-                        const w = this.naturalWidth;
-                        const h = this.naturalHeight;
+                        const w = img.naturalWidth;
+                        const h = img.naturalHeight;
                         const size = Math.min(w, h);
 
                         // set to center
-                        const imageSize = [
+                        const imageSize: [number, number, number, number] = [
                             (w - size) / 2,
                             (h - size) / 2,
                             size,
-                            size];
-
-                        croppie.bind({
-                            url: newImage,
-                            points: imageSize,
-                        }).then(() => {
-                            loading(false);
-                        }).catch((e) => {
-                            $log.error(logTag, 'Could not bind avatar preview:', e);
-                            loading(false);
-                        });
-
-                        if (newImage === null) {
-                            scope.onChange(null);
+                            size,
+                        ];
+
+                        try {
+                            await croppie.bind({
+                                url: imageBase64Url,
+                                points: imageSize,
+                            });
+                        } catch (error) {
+                            $log.error(logTag, 'Could not bind avatar preview:', error);
                         }
+                        loading(false);
                     });
 
                     img.addEventListener('error', function(e) {
@@ -224,7 +208,7 @@ export default [
                         loading(false);
                     });
 
-                    img.src = newImage;
+                    img.src = imageBase64Url;
 
                 }
 
@@ -241,16 +225,6 @@ export default [
                 // Handle file uploads
                 fileInput.on('change', onFileUploaded);
 
-                // Handle remove
-                if (scope.enableClear !== undefined && scope.enableClear === true) {
-                    // avatarRemove.on('click', () => {
-                    //     setImage(null);
-                    // });
-                } else {
-                    // remove element if clear disabled
-                    // avatarRemove.remove();
-                }
-
             },
             template: `
                 <div class="avatar-editor">

+ 1 - 1
src/helpers.ts

@@ -286,7 +286,7 @@ export function hasFeature(contactReceiver: threema.ContactReceiver,
 /**
  * Convert an ArrayBuffer to a data URL.
  */
-export function bufferToUrl(buffer: ArrayBuffer, mimeType: string, logWarning: (msg: string) => void) {
+export function bufferToUrl(buffer: ArrayBuffer, mimeType: string, logWarning: (msg: string) => void): string {
     if (buffer === null || buffer === undefined) {
         throw new Error('Called bufferToUrl on null or undefined');
     }

+ 91 - 10
src/types/croppie.d.ts

@@ -15,19 +15,100 @@
  * along with Threema Web. If not, see <http://www.gnu.org/licenses/>.
  */
 
-declare namespace __Croppie {
-    interface Croppie {
-        bind(options: any): Promise<any>;
-        result(options: any): Promise<any>;
+declare namespace croppie {
+    type WheelZoomType = 'ctrl';
+
+    type ViewportType = 'square' | 'circle';
+
+    type ImageUrl = string;
+
+    // String representation as returned by Number.prototype.toFixed()
+    type Point = string | number;
+
+    // [topLeftX, topLeftY, bottomRightX, bottomRightY]
+    type Points = [Point, Point, Point, Point];
+
+    type Orientation = number;
+
+    type Type = 'canvas' | 'base64' | 'html' | 'blob' | 'rawcanvas';
+
+    type Size = 'viewport' | 'original' | { width: number, height: number };
+
+    type Format = 'jpeg' | 'png' | 'webp';
+
+    type Degrees = 90 | 180 | 270 | -90 | -180 | -270;
+
+    // Dispatched on element constructed on as 'update.croppie'
+    type UpdateEvent = CustomEvent<State>;
+
+    interface Viewport {
+        width?: number,
+        height?: number,
+        type?: ViewportType,
     }
 
-    interface Static {
-        new(element: any, cfg?: any): Croppie
+    interface Boundary {
+        width?: number,
+        height: number,
+    }
+
+    interface ResizeControls {
+        width?: boolean,
+        height?: boolean,
     }
-}
 
-declare var Croppie: __Croppie.Static;
+    interface CreateOptions {
+        viewport?: Viewport,
+        boundary?: Boundary,
+        resizeControls?: ResizeControls,
+        customClass?: string,
+        showZoomer?: boolean,
+        enableZoom?: boolean,
+        enableResize?: boolean,
+        mouseWheelZoom?: boolean | WheelZoomType,
+        enableExif?: boolean,
+        enforceBoundary?: boolean,
+        enableOrientation?: boolean,
+        enableKeyMovement?: boolean,
+        url?: ImageUrl,
+        points?: Points,
+        update?: (UpdateEvent) => void,
+    }
+
+    interface State {
+        points: Points;
+        zoom: number,
+        orientation?: Orientation,
+    }
+
+    interface BindOptions {
+        url?: ImageUrl,
+        points?: Points,
+        zoom?: number,
+        orientation?: Orientation,
+    }
+
+    interface ResultOptions {
+        type?: Type;
+        size?: Size;
+        format?: Format;
+        quality?: number; // Range: [0..1]
+        backgroundColor?: string,
+        circle?: boolean;
+    }
+}
 
-declare module "croppie" {
-    export = Croppie;
+declare class Croppie {
+    constructor(element: HTMLElement, options?: croppie.CreateOptions);
+    get(): croppie.State;
+    bind(options?: croppie.BindOptions | croppie.ImageUrl | croppie.Points): Promise<void>;
+    destroy(): void;
+    result(options: croppie.ResultOptions & { type: 'base64' | 'canvas' }): Promise<string>;
+    result(options: croppie.ResultOptions & { type: 'html' }): Promise<HTMLElement>;
+    result(options: croppie.ResultOptions & { type: 'blob' }): Promise<Blob>;
+    result(options: croppie.ResultOptions & { type: 'rawcanvas' }): Promise<HTMLCanvasElement>;
+    result(options?: croppie.ResultOptions): Promise<HTMLCanvasElement>;
+    rotate(degrees: croppie.Degrees): void;
+    setZoom(zoom: number): void;
+    destroy(): void;
 }