浏览代码

Merge pull request #4000 from cmprince/i3908-image_rotation

I3908 image rotation, flip and invert
Jason Grout 7 年之前
父节点
当前提交
8de8cf6d36

+ 4 - 1
docs/source/user/file_formats.rst

@@ -85,7 +85,10 @@ Images
 
 JupyterLab supports image data in cell output and as files in the above
 formats. In the image file viewer, you can use keyboard shortcuts such
-as ``+`` and ``-`` to zoom the image and ``0`` to reset the zoom level.
+as ``+`` and ``-`` to zoom the image, ``[`` and ``]`` to rotate the image, 
+and ``H`` and ``V`` to flip the image horizontally and vertically. Use 
+``I`` to invert the colors, and use ``0`` to reset the image.
+
 To edit an SVG image as a text file, right-click on the SVG filename in
 the file browser and select the “Editor” item in the “Open With”
 submenu:

+ 94 - 6
packages/imageviewer-extension/src/index.ts

@@ -18,13 +18,28 @@ import {
  */
 namespace CommandIDs {
   export
-  const resetZoom = 'imageviewer:reset-zoom';
+  const resetImage = 'imageviewer:reset-image';
 
   export
   const zoomIn = 'imageviewer:zoom-in';
 
   export
   const zoomOut = 'imageviewer:zoom-out';
+
+  export
+  const flipHorizontal = 'imageviewer:flip-horizontal';
+
+  export
+  const flipVertical = 'imageviewer:flip-vertical';
+
+  export
+  const rotateClockwise = 'imageviewer:rotate-clockwise';
+
+  export
+  const rotateCounterclockwise = 'imageviewer:rotate-counterclockwise';
+
+  export
+  const invertColors = 'imageviewer:invert-colors';
 }
 
 
@@ -98,7 +113,7 @@ function activate(app: JupyterLab, palette: ICommandPalette, restorer: ILayoutRe
 
   const category = 'Image Viewer';
 
-  [CommandIDs.zoomIn, CommandIDs.zoomOut, CommandIDs.resetZoom]
+  [CommandIDs.zoomIn, CommandIDs.zoomOut, CommandIDs.resetImage, CommandIDs.rotateClockwise, CommandIDs.rotateCounterclockwise, CommandIDs.flipHorizontal, CommandIDs.flipVertical, CommandIDs.invertColors]
     .forEach(command => { palette.addItem({ command, category }); });
 
   return tracker;
@@ -132,9 +147,39 @@ function addCommands(app: JupyterLab, tracker: IImageTracker) {
     isEnabled
   });
 
-  commands.addCommand('imageviewer:reset-zoom', {
-    execute: resetZoom,
-    label: 'Reset Zoom',
+  commands.addCommand('imageviewer:reset-image', {
+    execute: resetImage,
+    label: 'Reset Image',
+    isEnabled
+  });
+
+  commands.addCommand('imageviewer:rotate-clockwise', {
+    execute: rotateClockwise,
+    label: 'Rotate Clockwise',
+    isEnabled
+  });
+
+  commands.addCommand('imageviewer:rotate-counterclockwise', {
+    execute: rotateCounterclockwise,
+    label: 'Rotate Counterclockwise',
+    isEnabled
+  });
+
+  commands.addCommand('imageviewer:flip-horizontal', {
+    execute: flipHorizontal,
+    label: 'Flip image horizontally',
+    isEnabled
+  });
+
+  commands.addCommand('imageviewer:flip-vertical', {
+    execute: flipVertical,
+    label: 'Flip image vertically',
+    isEnabled
+  });
+
+  commands.addCommand('imageviewer:invert-colors', {
+    execute: invertColors,
+    label: 'Invert Colors',
     isEnabled
   });
 
@@ -154,11 +199,54 @@ function addCommands(app: JupyterLab, tracker: IImageTracker) {
     }
   }
 
-  function resetZoom(): void {
+  function resetImage(): void {
     const widget = tracker.currentWidget;
 
     if (widget) {
       widget.scale = 1;
+      widget.colorinversion = 0;
+      widget.resetRotationFlip();
+    }
+  }
+
+  function rotateClockwise(): void {
+    const widget = tracker.currentWidget;
+
+    if (widget) {
+      widget.rotateClockwise();
+    }
+  }
+
+  function rotateCounterclockwise(): void {
+    const widget = tracker.currentWidget;
+
+    if (widget) {
+      widget.rotateCounterclockwise();
+    }
+  }
+
+  function flipHorizontal(): void {
+    const widget = tracker.currentWidget;
+
+    if (widget) {
+      widget.flipHorizontal();
+    }
+  }
+
+  function flipVertical(): void {
+    const widget = tracker.currentWidget;
+
+    if (widget) {
+      widget.flipVertical();
+    }
+  }
+
+  function invertColors(): void {
+    const widget = tracker.currentWidget;
+
+    if (widget) {
+      widget.colorinversion += 1;
+      widget.colorinversion %= 2;
     }
   }
 }

+ 110 - 16
packages/imageviewer/src/widget.ts

@@ -37,11 +37,14 @@ class ImageViewer extends Widget implements DocumentRegistry.IReadyWidget {
    * Construct a new image widget.
    */
   constructor(context: DocumentRegistry.Context) {
-    super({ node: Private.createNode() });
+    super();
     this.context = context;
     this.node.tabIndex = -1;
     this.addClass(IMAGE_CLASS);
 
+    this._img = document.createElement('img');
+    this.node.appendChild(this._img);
+
     this._onTitleChanged();
     context.pathChanged.connect(this._onTitleChanged, this);
 
@@ -79,10 +82,61 @@ class ImageViewer extends Widget implements DocumentRegistry.IReadyWidget {
       return;
     }
     this._scale = value;
-    let scaleNode = this.node.querySelector('div') as HTMLElement;
-    let transform: string;
-    transform = `scale(${value})`;
-    scaleNode.style.transform = transform;
+    this._updateStyle();
+  }
+
+  /**
+   * The color inversion of the image.
+   */
+  get colorinversion(): number {
+    return this._colorinversion;
+  }
+  set colorinversion(value: number) {
+    if (value === this._colorinversion) {
+        return;
+    }
+    this._colorinversion = value;
+    this._updateStyle();
+  }
+
+  /**
+   * Reset rotation and flip transformations.
+   */
+  resetRotationFlip(): void {
+    this._matrix = [1, 0, 0, 1];
+    this._updateStyle();
+  }
+
+  /**
+   * Rotate the image counter-clockwise (left).
+   */
+  rotateCounterclockwise(): void {
+    this._matrix = Private.prod(this._matrix, Private.rotateCounterclockwiseMatrix);
+    this._updateStyle();
+  }
+
+  /**
+   * Rotate the image clockwise (right).
+   */
+  rotateClockwise(): void {
+    this._matrix = Private.prod(this._matrix, Private.rotateClockwiseMatrix);
+    this._updateStyle();
+  }
+
+  /**
+   * Flip the image horizontally.
+   */
+  flipHorizontal(): void {
+    this._matrix = Private.prod(this._matrix, Private.flipHMatrix);
+    this._updateStyle();
+  }
+
+  /**
+   * Flip the image vertically.
+   */
+  flipVertical(): void {
+    this._matrix = Private.prod(this._matrix, Private.flipVMatrix);
+    this._updateStyle();
   }
 
   /**
@@ -119,13 +173,25 @@ class ImageViewer extends Widget implements DocumentRegistry.IReadyWidget {
       return;
     }
     let content = context.model.toString();
-    let src = `data:${cm.mimetype};${cm.format},${content}`;
-    let node = this.node.querySelector('img') as HTMLImageElement;
-    node.setAttribute('src', src);
+    this._img.src = `data:${cm.mimetype};${cm.format},${content}`;
+  }
+
+  /**
+   * Update the image CSS style, including the transform and filter.
+   */
+  private _updateStyle(): void {
+    let [a, b, c, d] = this._matrix;
+    let [tX, tY] = Private.prodVec(this._matrix, [1, 1]);
+    let transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0) translate(${tX < 0 ? -100 : 0}%, ${tY < 0 ? -100 : 0}%) `;
+    this._img.style.transform = `scale(${this._scale}) ${transform}`;
+    this._img.style.filter = `invert(${this._colorinversion})`;
   }
 
   private _scale = 1;
+  private _matrix = [1, 0, 0, 1];
+  private _colorinversion = 0;
   private _ready = new PromiseDelegate<void>();
+  private _img: HTMLImageElement;
 }
 
 
@@ -147,15 +213,43 @@ class ImageViewerFactory extends ABCWidgetFactory<ImageViewer, DocumentRegistry.
  */
 namespace Private {
   /**
-   * Create the node for the image widget.
+   * Multiply 2x2 matrices.
    */
   export
-  function createNode(): HTMLElement {
-    let node = document.createElement('div');
-    let innerNode = document.createElement('div');
-    let image = document.createElement('img');
-    node.appendChild(innerNode);
-    innerNode.appendChild(image);
-    return node;
+  function prod([a11, a12, a21, a22]: number[], [b11, b12, b21, b22]: number[]): number[] {
+    return [a11 * b11 + a12 * b21, a11 * b12 + a12 * b22,
+            a21 * b11 + a22 * b21, a21 * b12 + a22 * b22];
   }
+
+  /**
+   * Multiply a 2x2 matrix and a 2x1 vector.
+   */
+  export
+  function prodVec([a11, a12, a21, a22]: number[], [b1, b2]: number[]): number[] {
+    return [a11 * b1 + a12 * b2, a21 * b1 + a22 * b2];
+  }
+
+  /**
+   * Clockwise rotation transformation matrix.
+   */
+  export
+  const rotateClockwiseMatrix = [0, 1, -1, 0];
+
+  /**
+   * Counter-clockwise rotation transformation matrix.
+   */
+  export
+  const rotateCounterclockwiseMatrix = [0, -1, 1, 0];
+
+  /**
+   * Horizontal flip transformation matrix.
+   */
+  export
+  const flipHMatrix = [-1, 0, 0, 1];
+
+  /**
+   * Vertical flip transformation matrix.
+   */
+  export
+  const flipVMatrix = [1, 0, 0, -1];
 }

+ 2 - 10
packages/imageviewer/style/index.css

@@ -8,19 +8,11 @@
   overflow: auto;
 }
 
-
-.jp-ImageViewer > div {
+.jp-ImageViewer > img {
+  box-sizing: border-box;
   transform-origin: top left;
 }
 
-
-.jp-ImageViewer img {
-  max-width: 100%;
-  max-height: 100%;
-  margin: auto;
-}
-
-
 .jp-ImageViewer::before {
     content: '';
     display: block;

+ 47 - 2
packages/shortcuts-extension/schema/plugin.json

@@ -148,10 +148,10 @@
       },
       "type": "object"
     },
-    "imageviewer:reset-zoom": {
+    "imageviewer:reset-image": {
       "default": { },
       "properties": {
-        "command": { "default": "imageviewer:reset-zoom" },
+        "command": { "default": "imageviewer:reset-image" },
         "keys": { "default": ["0"] },
         "selector": { "default": ".jp-ImageViewer" }
       },
@@ -175,6 +175,51 @@
       },
       "type": "object"
     },
+    "imageviewer:rotate-clockwise": {
+      "default": { },
+      "properties": {
+        "command": { "default": "imageviewer:rotate-clockwise" },
+        "keys": { "default": ["]"] },
+        "selector": { "default": ".jp-ImageViewer" }
+      },
+      "type": "object"
+    },
+    "imageviewer:rotate-counterclockwise": {
+      "default": { },
+      "properties": {
+        "command": { "default": "imageviewer:rotate-counterclockwise" },
+        "keys": { "default": ["["] },
+        "selector": { "default": ".jp-ImageViewer" }
+      },
+      "type": "object"
+    },
+    "imageviewer:flip-vertical": {
+      "default": { },
+      "properties": {
+        "command": { "default": "imageviewer:flip-vertical" },
+        "keys": { "default": ["V"] },
+        "selector": { "default": ".jp-ImageViewer" }
+      },
+      "type": "object"
+    },
+    "imageviewer:flip-horizontal": {
+      "default": { },
+      "properties": {
+        "command": { "default": "imageviewer:flip-horizontal" },
+        "keys": { "default": ["H"] },
+        "selector": { "default": ".jp-ImageViewer" }
+      },
+      "type": "object"
+    },
+    "imageviewer:invert-colors": {
+      "default": { },
+      "properties": {
+        "command": { "default": "imageviewer:invert-colors" },
+        "keys": { "default": ["I"] },
+        "selector": { "default": ".jp-ImageViewer" }
+      },
+      "type": "object"
+    },
     "inspector:open": {
       "default": { },
       "properties": {