├── .gitignore ├── Eask ├── LICENSE ├── README.md ├── media └── example.gif └── org-sliced-images.el /.gitignore: -------------------------------------------------------------------------------- 1 | .eask 2 | .elsa 3 | 4 | *.elc 5 | 6 | -------------------------------------------------------------------------------- /Eask: -------------------------------------------------------------------------------- 1 | (package "org-sliced-images" 2 | "0.1" 3 | "Sliced inline images in org-mode") 4 | 5 | (website-url "https://github.com/jcfk/org-sliced-images") 6 | (keywords "convenience") 7 | 8 | (package-file "org-sliced-images.el") 9 | (files "*.el") 10 | 11 | (script "test" "echo \"Error: no test specified\" && exit 1") 12 | 13 | (source "gnu") 14 | (source "melpa") 15 | 16 | (depends-on "emacs" "28.1") 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jacob Fong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org-sliced-images [![MELPA](https://melpa.org/packages/org-sliced-images-badge.svg)](https://melpa.org/#/org-sliced-images) 2 | 3 | This package displays org-mode inline images in a sliced manner, like the 4 | built-in `insert-sliced-image`. This improves the image scrolling experience. 5 | 6 | ![](media/example.gif) 7 | 8 | ## Installation 9 | 10 | Get me on MELPA! `use-package` example: 11 | 12 | ```elisp 13 | (use-package org-sliced-images 14 | :ensure t 15 | :config 16 | (org-sliced-images-mode 1)) 17 | ``` 18 | 19 | ## Usage 20 | 21 | Toggle the global minor mode `org-sliced-images-mode` to enable sliced images. 22 | The mode advises the following functions: 23 | 24 | - `org-remove-inline-images` 25 | - `org-toggle-inline-images` 26 | - `org-display-inline-images` 27 | 28 | Concerning BEG and END arguments to the some of the functions, the beginning of 29 | the link to the image is the point considered. 30 | 31 | ### Customization 32 | 33 | - `org-sliced-images-consume-dummies` Dummy lines are used to support slice 34 | overlays. If non-nil, lines matching dummy lines coming directly after a link 35 | will be overlaid, instead of adding new ones. 36 | - `org-sliced-images-round-image-height` If non-nil, round the height of images 37 | to be a multiple of the font height. This should be used with things like 38 | `org-indent-mode` or line numbering that add prefixes to slice lines, or else 39 | you will typically have a visible gap above the final slice. 40 | 41 | ## Comparisons 42 | 43 | Here's how this package compares to other attempts to make image scrolling nice. 44 | 45 | - [iscroll](https://github.com/casouri/iscroll): iscroll works by making you 46 | rebind your movement keys to a function that calculates and adds a vertical 47 | scroll. org-sliced-images works by inserting images as slices when they are 48 | toggled; you can then move over these slices however you want, resulting in a 49 | faster and more "native" experience. 50 | - pixel-scroll -based methods: These only work with the mouse wheel and are very 51 | resource intensive. 52 | 53 | ## Todo 54 | 55 | -------------------------------------------------------------------------------- /media/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jcfk/org-sliced-images/cbe25ca63bb4c3979396834f279308cf87923f71/media/example.gif -------------------------------------------------------------------------------- /org-sliced-images.el: -------------------------------------------------------------------------------- 1 | ;;; org-sliced-images.el --- Sliced inline images in org-mode -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2024 Jacob Fong 4 | ;; Author: Jacob Fong 5 | ;; Version: 0.1 6 | ;; Homepage: https://github.com/jcfk/org-sliced-images 7 | ;; Keywords: convenience 8 | 9 | ;; Package-Requires: ((emacs "28.1")) 10 | 11 | ;; Permission is hereby granted, free of charge, to any person obtaining a copy 12 | ;; of this software and associated documentation files (the “Software”), to deal 13 | ;; in the Software without restriction, including without limitation the rights 14 | ;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | ;; copies of the Software, and to permit persons to whom the Software is 16 | ;; furnished to do so, subject to the following conditions: 17 | 18 | ;; The above copyright notice and this permission notice shall be included in 19 | ;; all copies or substantial portions of the Software. 20 | 21 | ;; THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | ;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | ;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | ;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | ;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | ;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | ;; SOFTWARE. 28 | 29 | ;;; Commentary: 30 | 31 | ;; Display images as sliced images in org-mode, like insert-sliced-image. This 32 | ;; makes scrolling nicer. 33 | 34 | ;; See https://github.com/jcfk/org-sliced-images for more information. 35 | 36 | ;;; Code: 37 | 38 | (require 'org) 39 | (require 'org-element) 40 | (require 'org-attach) 41 | 42 | ;;;; Configuration 43 | 44 | (defgroup org-sliced-images nil 45 | "Configure org-sliced-images." 46 | :group 'org) 47 | 48 | (defcustom org-sliced-images-consume-dummies t 49 | "If non-nil, overlay existing dummy lines instead of adding new ones." 50 | :type 'boolean 51 | :group 'org-sliced-images) 52 | 53 | (defcustom org-sliced-images-round-image-height nil 54 | "If non-nil, resize images to make height a multiple of the font height. 55 | 56 | This is useful for avoiding gaps when prefixing display lines with extra 57 | characters. The image height is rounded to the nearest multiple of the 58 | `default-font-height'." 59 | :type 'boolean 60 | :group 'org-sliced-images) 61 | 62 | ;;;; Buffer variables 63 | 64 | (defvar-local org-sliced-images--image-overlay-families nil 65 | "A list of elements corresponding to displayed inline images. 66 | Each element is a list of overlays making up the displayed image. 67 | 68 | The first element in each list is an overlay over all the dummy lines 69 | inserted to support the slices. The remaining elements are the slices 70 | themselves; the last element is the topmost slice.") 71 | (put 'org-sliced-images--image-overlay-families 'permanent-local t) 72 | 73 | ;;;; Function overrides 74 | 75 | (defmacro org-sliced-images--without-undo (&rest body) 76 | "Evaluate BODY with current buffer undo recording disabled." 77 | `(let ((saved-undo-list buffer-undo-list)) 78 | (unwind-protect 79 | (progn 80 | (buffer-disable-undo) 81 | ,@body) 82 | (buffer-enable-undo) 83 | (setq buffer-undo-list saved-undo-list)))) 84 | 85 | (defun org-sliced-images--remove-overlay-family (ovfam) 86 | "Delete the overlay family OVFAM." 87 | (let ((container-ov (car ovfam)) 88 | (line-ovs (cdr ovfam))) 89 | (mapc #'delete-overlay line-ovs) 90 | (let ((inhibit-modification-hooks nil)) ;; ??? 91 | (delete-region (overlay-start container-ov) (overlay-end container-ov))) 92 | (delete-overlay container-ov))) 93 | 94 | (defun org-sliced-images--get-image-overlay-families (&optional beg end) 95 | "Return image overlay families which start between BEG and END." 96 | (let* ((beg (or beg (point-min))) 97 | (end (or end (point-max))) 98 | result) 99 | (mapc 100 | (lambda (ovfam) 101 | (let ((ovfam-start (overlay-start (car (last ovfam))))) 102 | (when (and (<= beg ovfam-start) (< ovfam-start end)) 103 | (add-to-list 'result ovfam)))) 104 | org-sliced-images--image-overlay-families) 105 | result)) 106 | 107 | ;;;###autoload 108 | (defun org-sliced-images-remove-inline-images (&optional beg end) 109 | "Remove inline display of images starting between BEG and END." 110 | (interactive) 111 | (org-sliced-images--without-undo 112 | (mapc 113 | (lambda (ovfam) 114 | (setq org-sliced-images--image-overlay-families 115 | (delq ovfam org-sliced-images--image-overlay-families)) 116 | (org-sliced-images--remove-overlay-family ovfam)) 117 | (org-sliced-images--get-image-overlay-families beg end)))) 118 | 119 | (defun org-sliced-images--create-inline-image (file width) 120 | "Create image located at FILE, or return nil. 121 | WIDTH is the width of the image. The image may not be created 122 | according to the value of `org-display-remote-inline-images'." 123 | (let* ((remote? (file-remote-p file)) 124 | (file-or-data 125 | (pcase org-display-remote-inline-images 126 | ((guard (not remote?)) file) 127 | (`download (with-temp-buffer 128 | (set-buffer-multibyte nil) 129 | (insert-file-contents-literally file) 130 | (buffer-string))) 131 | (`cache (let ((revert-without-query '("."))) 132 | (with-current-buffer (find-file-noselect file) 133 | (buffer-string)))) 134 | (`skip nil) 135 | (other 136 | (message "Invalid value of `org-display-remote-inline-images': %S" 137 | other) 138 | nil)))) 139 | (when file-or-data 140 | (create-image file-or-data 141 | (and (image-type-available-p 'imagemagick) 142 | width 143 | 'imagemagick) 144 | remote? 145 | :width width :scale 1 :ascent 'center)))) 146 | 147 | (defun org-sliced-images--handle-modified-overlay-family (ov after _beg _end &optional _len) 148 | "Remove inline display overlay family if the area is modified. 149 | This function is to be used as an overlay modification hook; OV, AFTER, 150 | BEG, END, LEN will be passed by the overlay." 151 | (when (and ov after) 152 | (when (overlay-get ov 'org-image-overlay) 153 | (image-flush (cadr (overlay-get ov 'display)))) 154 | (catch 'break 155 | (dolist (ovfam org-sliced-images--image-overlay-families) 156 | (when (memq ov ovfam) 157 | (setq org-sliced-images--image-overlay-families 158 | (delq ovfam org-sliced-images--image-overlay-families)) 159 | (org-sliced-images--remove-overlay-family ovfam) 160 | (throw 'break nil)))))) 161 | 162 | (defun org-sliced-images--make-inline-image-overlay (start end spec) 163 | "Make overlay from START to END with display value SPEC. 164 | The overlay is returned." 165 | (let ((ov (make-overlay start end))) 166 | (overlay-put ov 'display spec) 167 | (overlay-put ov 'face 'default) 168 | (overlay-put ov 'org-image-overlay t) 169 | (overlay-put ov 'modification-hooks 170 | (list 'org-sliced-images--handle-modified-overlay-family)) 171 | (when (boundp 'image-map) 172 | (overlay-put ov 'keymap image-map)) 173 | ov)) 174 | 175 | ;;;###autoload 176 | (defun org-sliced-images-display-inline-images (&optional include-linked refresh beg end) 177 | "Display sliced inline images. 178 | 179 | See the docstring of `org-display-inline-images' for more info. This advice is 180 | an amalgamation of different versions of that function, with the goal of being 181 | compatible with Emacs 28+." 182 | (interactive "P") 183 | (when (display-graphic-p) 184 | (when refresh 185 | (org-sliced-images-remove-inline-images beg end) 186 | (when (fboundp 'clear-image-cache) (clear-image-cache))) 187 | (let ((end (or end (point-max)))) 188 | (org-with-point-at (or beg (point-min)) 189 | (let* ((case-fold-search t) 190 | (file-extension-re (image-file-name-regexp)) 191 | (link-abbrevs (mapcar #'car 192 | (append org-link-abbrev-alist-local 193 | org-link-abbrev-alist))) 194 | ;; Check absolute, relative file names and explicit 195 | ;; "file:" links. Also check link abbreviations since 196 | ;; some might expand to "file" links. 197 | (file-types-re 198 | (format "\\[\\[\\(?:file%s:\\|attachment:\\|[./~]\\)\\|\\]\\[\\( (current-column) 0) 279 | (setq left-margin (current-column))) 280 | (while (< y 1.0) 281 | (let (slice-start slice-end) 282 | (if (= y 0.0) 283 | ;; Overlay link 284 | (progn 285 | (setq slice-start (org-element-property :begin link)) 286 | (setq slice-end (org-element-property :end link)) 287 | (end-of-line) 288 | (delete-char 1) 289 | (insert (propertize "\n" 'line-height t))) 290 | (setq slice-start (line-beginning-position)) 291 | (setq slice-end (1+ (line-beginning-position))) 292 | (if (and org-sliced-images-consume-dummies 293 | (equal 294 | (buffer-substring-no-properties 295 | (line-beginning-position) 296 | (line-end-position)) 297 | " ")) 298 | ;; Consume next line as dummy 299 | (progn 300 | (when left-margin 301 | (put-text-property 302 | slice-start 303 | slice-end 304 | 'line-prefix 305 | `(space :width ,left-margin))) 306 | (put-text-property 307 | (line-end-position) (1+ (line-end-position)) 'line-height t) 308 | (forward-line)) 309 | ;; Create dummy line 310 | (insert (if left-margin 311 | (propertize " " 'line-prefix `(space :width ,left-margin)) 312 | " ")) 313 | (insert (propertize "\n" 'line-height t))) 314 | (when (not dummy-zone-start) 315 | (setq dummy-zone-start slice-start)) 316 | (setq dummy-zone-end (1+ slice-end))) 317 | (push 318 | (org-sliced-images--make-inline-image-overlay 319 | slice-start 320 | slice-end 321 | (list (list 'slice 0 y 1.0 dy) image)) 322 | ovfam)) 323 | (setq y (+ y dy)))) 324 | (setq end (+ end (* 2 (- (ceiling image-line-h) 1)))) 325 | (push (make-overlay dummy-zone-start dummy-zone-end) ovfam) 326 | (push ovfam org-sliced-images--image-overlay-families)))))))))))))))))) 327 | 328 | ;;;###autoload 329 | (defun org-sliced-images-toggle-inline-images (&optional include-linked beg end) 330 | "Toggle the display of inline images starting between BEG and END. 331 | INCLUDE-LINKED is passed to `org-sliced-images-display-inline-images'." 332 | (interactive "P") 333 | (if (org-sliced-images--get-image-overlay-families beg end) 334 | (progn 335 | (org-sliced-images-remove-inline-images beg end) 336 | (when (called-interactively-p 'interactive) 337 | (message "Inline image display turned off"))) 338 | (org-sliced-images-display-inline-images include-linked nil beg end) 339 | (when (called-interactively-p 'interactive) 340 | (let ((new (org-sliced-images--get-image-overlay-families beg end))) 341 | (message 342 | (if new 343 | (format "%d images displayed inline" (length new)) 344 | "No images to display inline")))))) 345 | 346 | ;;;; Minor mode 347 | 348 | ;;;###autoload 349 | (define-minor-mode org-sliced-images-mode 350 | "Toggle org-sliced-images-mode to display sliced images in `org-mode'." 351 | :group 'org-sliced-images 352 | :global t 353 | 354 | ;; Always start by removing existing images upon toggle 355 | (mapc 356 | (lambda (buf) 357 | (with-current-buffer buf 358 | (when (derived-mode-p 'org-mode) 359 | (org-remove-inline-images)))) 360 | (buffer-list)) 361 | 362 | (if org-sliced-images-mode 363 | (progn 364 | (advice-add 365 | #'org-toggle-inline-images :override #'org-sliced-images-toggle-inline-images) 366 | (advice-add 367 | #'org-display-inline-images :override #'org-sliced-images-display-inline-images) 368 | (advice-add 369 | #'org-remove-inline-images :override #'org-sliced-images-remove-inline-images)) 370 | (advice-remove 371 | #'org-toggle-inline-images #'org-sliced-images-toggle-inline-images) 372 | (advice-remove 373 | #'org-display-inline-images #'org-sliced-images-display-inline-images) 374 | (advice-remove 375 | #'org-remove-inline-images #'org-sliced-images-remove-inline-images))) 376 | 377 | (provide 'org-sliced-images) 378 | 379 | ;;; org-sliced-images.el ends here 380 | --------------------------------------------------------------------------------