summary refs log tree commit diff stats
path: root/ranger
diff options
context:
space:
mode:
authorhut <hut@lepus.uberspace.de>2015-03-18 22:11:01 +0100
committerhut <hut@lepus.uberspace.de>2015-03-18 22:11:01 +0100
commit7993ed4b19d57cfda0a22426cb0c3bcf88232bc3 (patch)
treee89c6830699621306e70ddbfb988efde43c842f8 /ranger
parent9a879332d96e36fc348053cc0d23f03de73a2ed2 (diff)
parent7e85001052e4398f71e50e731899800106d2eefd (diff)
downloadranger-7993ed4b19d57cfda0a22426cb0c3bcf88232bc3.tar.gz
Merge branch 'add-iterm2-image-preview-support' of git://github.com/kattrali/ranger
Diffstat (limited to 'ranger')
-rw-r--r--ranger/config/rc.conf17
-rw-r--r--ranger/container/settings.py1
-rw-r--r--ranger/core/fm.py12
-rw-r--r--ranger/ext/img_display.py145
4 files changed, 159 insertions, 16 deletions
diff --git a/ranger/config/rc.conf b/ranger/config/rc.conf
index 21c685d5..ca67c389 100644
--- a/ranger/config/rc.conf
+++ b/ranger/config/rc.conf
@@ -58,11 +58,22 @@ set vcs_backend_git enabled
 set vcs_backend_hg disabled
 set vcs_backend_bzr disabled
 
-# Preview images in full color with the external command "w3mimgpreview"?
-# This requires the console web browser "w3m" and a supported terminal.
-# It has been successfully tested with "xterm" and "urxvt" without tmux.
+# Use one of the supported image preview protocols
 set preview_images false
 
+# Set the preview image method. Supported methods:
+#
+# * w3m (default):
+#   Preview images in full color with the external command "w3mimgpreview"?
+#   This requires the console web browser "w3m" and a supported terminal.
+#   It has been successfully tested with "xterm" and "urxvt" without tmux.
+#
+# * iterm2:
+#   Preview images in full color using iTerm2 image previews
+#   (http://iterm2.com/images.html). This requires using iTerm2 compiled
+#   with image preview support.
+set preview_images_method w3m
+
 # Use a unicode "..." character to mark cut-off filenames?
 set unicode_ellipsis false
 
diff --git a/ranger/container/settings.py b/ranger/container/settings.py
index ddcc7d14..27737eb1 100644
--- a/ranger/container/settings.py
+++ b/ranger/container/settings.py
@@ -35,6 +35,7 @@ ALLOWED_SETTINGS = {
     'preview_directories': bool,
     'preview_files': bool,
     'preview_images': bool,
+    'preview_images_method': str,
     'preview_max_size': int,
     'preview_script': (str, type(None)),
     'save_console_history': bool,
diff --git a/ranger/core/fm.py b/ranger/core/fm.py
index 22908f46..b32a2c33 100644
--- a/ranger/core/fm.py
+++ b/ranger/core/fm.py
@@ -19,7 +19,7 @@ from ranger.container.tags import Tags
 from ranger.gui.ui import UI
 from ranger.container.bookmarks import Bookmarks
 from ranger.core.runner import Runner
-from ranger.ext.img_display import ImageDisplayer
+from ranger.ext.img_display import *
 from ranger.core.metadata import MetadataManager
 from ranger.ext.rifle import Rifle
 from ranger.container.directory import Directory
@@ -49,7 +49,6 @@ class FM(Actions, SignalDispatcher):
         self.start_paths = paths
         self.directories = dict()
         self.log = deque(maxlen=20)
-        self.image_displayer = ImageDisplayer()
         self.bookmarks = bookmarks
         self.current_tab = 1
         self.tabs = {}
@@ -97,6 +96,7 @@ class FM(Actions, SignalDispatcher):
             rifleconf = self.relpath('config/rifle.conf')
         self.rifle = Rifle(rifleconf)
         self.rifle.reload_config()
+        self.image_displayer = self._get_image_displayer()
 
         if not ranger.arg.clean and self.tags is None:
             self.tags = Tags(self.confpath('tagged'))
@@ -184,6 +184,14 @@ class FM(Actions, SignalDispatcher):
                 if debug:
                     raise
 
+    def _get_image_displayer(self):
+        if self.settings.preview_images_method == "w3m":
+            return W3MImageDisplayer()
+        elif self.settings.preview_images_method == "iterm2":
+            return ITerm2ImageDisplayer()
+        else:
+            return ImageDisplayer()
+
     def _get_thisfile(self):
         return self.thistab.thisfile
 
diff --git a/ranger/ext/img_display.py b/ranger/ext/img_display.py
index a70fe366..1f5be2be 100644
--- a/ranger/ext/img_display.py
+++ b/ranger/ext/img_display.py
@@ -1,21 +1,20 @@
 # This software is distributed under the terms of the GNU GPL version 3.
+"""Interface for drawing images into the console
 
-"""Interface for w3mimgdisplay to draw images into the console
-
-This module provides functions to draw images in the terminal using
-w3mimgdisplay, an utilitary program from w3m (a text-based web browser).
-w3mimgdisplay can display images either in virtual tty (using linux
-framebuffer) or in a Xorg session.
-
-w3m need to be installed for this to work.
+This module provides functions to draw images in the terminal using supported
+implementations, which are currently w3m and iTerm2.
 """
 
+import base64
+import curses
 import fcntl
+import imghdr
 import os
 import select
 import struct
 import sys
 import termios
+from ranger.core.shared import FileManagerAware
 from subprocess import Popen, PIPE
 
 W3MIMGDISPLAY_PATH = '/usr/lib/w3m/w3mimgdisplay'
@@ -24,8 +23,28 @@ W3MIMGDISPLAY_OPTIONS = []
 class ImgDisplayUnsupportedException(Exception):
     pass
 
-
 class ImageDisplayer(object):
+    """Image display provider functions for drawing images in the terminal"""
+    def draw(self, path, start_x, start_y, width, height):
+        """Draw an image at the given coordinates."""
+        pass
+
+    def clear(self, start_x, start_y, width, height):
+        """Clear a part of terminal display."""
+        pass
+
+    def quit(self):
+        """Cleanup and close"""
+        pass
+
+class W3MImageDisplayer(ImageDisplayer):
+    """Implementation of ImageDisplayer using w3mimgdisplay, an utilitary
+    program from w3m (a text-based web browser). w3mimgdisplay can display
+    images either in virtual tty (using linux framebuffer) or in a Xorg session.
+    Does not work over ssh.
+
+    w3m need to be installed for this to work.
+    """
     is_initialized = False
 
     def initialize(self):
@@ -38,7 +57,6 @@ class ImageDisplayer(object):
         self.is_initialized = True
 
     def draw(self, path, start_x, start_y, width, height):
-        """Draw an image at the given coordinates."""
         if not self.is_initialized or self.process.poll() is not None:
             self.initialize()
         self.process.stdin.write(self._generate_w3m_input(path, start_x,
@@ -47,7 +65,6 @@ class ImageDisplayer(object):
         self.process.stdout.readline()
 
     def clear(self, start_x, start_y, width, height):
-        """Clear a part of terminal display."""
         if not self.is_initialized or self.process.poll() is not None:
             self.initialize()
 
@@ -108,6 +125,112 @@ class ImageDisplayer(object):
         if self.is_initialized:
             self.process.kill()
 
+class ITerm2ImageDisplayer(ImageDisplayer, FileManagerAware):
+    """Implementation of ImageDisplayer using iTerm2 image display support
+    (http://iterm2.com/images.html).
+
+    Ranger must be running in iTerm2 for this to work.
+    """
+    _minimum_font_width = 8
+    _minimum_font_height = 11
+
+    def draw(self, path, start_x, start_y, width, height):
+        curses.putp(curses.tigetstr("sc"))
+        sys.stdout.write(curses.tparm(curses.tigetstr("cup"), start_y, start_x))
+        sys.stdout.write(self._generate_iterm2_input(path, width, height))
+        curses.putp(curses.tigetstr("rc"))
+        sys.stdout.flush()
+
+    def clear(self, start_x, start_y, width, height):
+        self.fm.ui.win.redrawwin()
+        self.fm.ui.win.refresh()
+
+    def _generate_iterm2_input(self, path, max_cols, max_rows):
+        """Prepare the image content of path for image display in iTerm2"""
+        image_width, image_height = self._get_image_dimensions(path)
+        if max_cols == 0 or max_rows == 0 or image_width == 0 or image_height == 0:
+            return ""
+        image_width = self._fit_width(
+            image_width, image_height, max_cols, max_rows)
+        content = self._encode_image_content(path)
+        text = "\033]1337;File=inline=1;preserveAspectRatio=0;"
+        text += "size={0};width={1}px:{2}\a\n".format(
+            str(len(content)),
+            str(int(image_width)),
+            content)
+        return text
+
+    def _fit_width(self, width, height, max_cols, max_rows):
+        max_width = self._minimum_font_width * max_cols
+        max_height = self._minimum_font_height * max_rows
+        if height > max_height:
+            if width > max_width:
+                width_scale = max_width/float(width)
+                height_scale = max_height/float(height)
+                min_scale = min(width_scale, height_scale)
+                max_scale = max(width_scale, height_scale)
+                if width * max_scale <= max_width and height * max_scale <= max_height:
+                    return (width * max_scale)
+                else:
+                    return (width * min_scale)
+            else:
+                scale = max_height/float(height)
+                return (width * scale)
+        elif width > max_width:
+            scale = max_width/float(width)
+            return (width * scale)
+        else:
+            return width
+
+
+    def _encode_image_content(self, path):
+        """Read and encode the contents of path"""
+        file = open(path, 'rb')
+        try:
+            return base64.b64encode(file.read())
+        except:
+            return ""
+        finally:
+            file.close()
+
+    def _get_image_dimensions(self, path):
+        """Determine image size using imghdr"""
+        file_handle = open(path, 'rb')
+        file_header = file_handle.read(24)
+        image_type  = imghdr.what(path)
+        if len(file_header) != 24:
+            file_handle.close()
+            return 0, 0
+        if image_type == 'png':
+            check = struct.unpack('>i', file_header[4:8])[0]
+            if check != 0x0d0a1a0a:
+                file_handle.close()
+                return 0, 0
+            width, height = struct.unpack('>ii', file_header[16:24])
+        elif image_type == 'gif':
+            width, height = struct.unpack('<HH', file_header[6:10])
+        elif image_type == 'jpeg':
+            try:
+                file_handle.seek(0)
+                size = 2
+                ftype = 0
+                while not 0xc0 <= ftype <= 0xcf:
+                    file_handle.seek(size, 1)
+                    byte = file_handle.read(1)
+                    while ord(byte) == 0xff:
+                        byte = file_handle.read(1)
+                    ftype = ord(byte)
+                    size = struct.unpack('>H', file_handle.read(2))[0] - 2
+                file_handle.seek(1, 1)
+                height, width = struct.unpack('>HH', file_handle.read(4))
+            except:
+                file_handle.close()
+                return 0, 0
+        else:
+            file_handle.close()
+            return 0, 0
+        file_handle.close()
+        return width, height
 
 def _get_font_dimensions():
     # Get the height and width of a character displayed in the terminal in