6. Images

We do not have a widget to show images, although Emacs has the intrinsic ability to do so. In this lesson we are going to review some resources to work with graphical elements.

When inserting an image into a buffer, we can use the insert-image function. However, in our case we will use put-image. It is an equivalent, but with the ability to insert images on whatever lines we want (and not where we are invoking the function) and because of how easy it makes removing the images present. Both are fundamental features when we work with a user interface that will change as the user interacts with it.

For all the examples we will use the image https://www.gnu.org/software/emacs/images/emacs.png. You can download it and save it in your working directory or wherever your .el file is.

Inserting an image

First we must create the image from a file. For that we will use the create-image function. This function receives as arguments:

(create-image FILENAME TYPE DATA &rest PROPS)
  • FILENAME: path to the image file.
  • TYPE: Type of the file (png, jpeg, gif, etc).
  • DATA: Image data. For example if it is an svg file, the data will be the svg code. Otherwise it will be nil.
  • PROPS: Properties of the image, such as the width or the height.

To load the example image, we will use:

(setq imagen-emacs (create-image "emacs.png" 'png nil :width 100))

We are not showing it yet.

Now that we have the image, we can insert it into the buffer. For that we will use the put-image function:

(put-image IMAGE &optional POSITION)
  • IMAGE: Image to insert. It must be an image created with create-image. We have the imagen-emacs variable that we created earlier.
  • POSITION: Position at which to insert the image (the buffer line). If not specified, it will be inserted at the current cursor position.
(put-image imagen-emacs 2)

With all this we can now display the image.

(setq imagen-emacs (create-image "emacs.png" 'png nil :width 100))
(put-image imagen-emacs 2)

Removing an image

For that we have a function called remove-images. It is a function that removes all the images it finds in a range of lines.

(remove-images &optional START END)
  • START: Start line.
  • END: End line.

In the following example the images on lines 2 and 3 are removed:

(remove-images 2 3)

To remove all the images of the buffer, we can use:

(remove-images (point-min) (point-max))

Inserting an image from a URL

Emacs does not have a function to download an image from a URL. However, we can create it ourselves.

(require 'url)

(defun put-image-from-url (url pos &optional width)
  "Put an image from an URL in the buffer at position."
  (unless url (setq url (url-get-url-at-point)))
  (unless url
    (error "Couldn't find URL."))
  (let ((buffer (url-retrieve-synchronously url)))
    (unwind-protect
        (let ((data (with-current-buffer buffer
                      (goto-char (point-min))
                      (search-forward "\n\n")
                      (buffer-substring (point) (point-max)))))
      (save-excursion
            (goto-char (point-min))
            (forward-line (1- pos)) ; Go to the beginning of the specified line
            (setq pos (line-beginning-position)))
      (put-image (create-image data nil t :width width) pos))
      (kill-buffer buffer))))

It would be equivalent to using put-image, but we save ourselves the step of creating the image with create-image.

(put-image-from-url "https://www.gnu.org/software/emacs/images/emacs.png" 3)

If the line does not exist, the image will not be inserted in the right place. Keep it in mind.

Optionally we can specify the width of the image. For example 100 pixels.

(put-image-from-url "https://www.gnu.org/software/emacs/images/emacs.png" 3 100)

You must be aware that this function blocks the main thread of Emacs until the image is downloaded. If the image is very large, it can take a considerable amount of time. I recommend launching it in a secondary thread.

(make-thread (lambda ()
               (put-image-from-url "https://www.gnu.org/software/emacs/images/emacs.png" 3 100)))

svg-lib

Of the few libraries we can find to create graphics, svg-lib is one of the most interesting. It lets us create vector graphics, as its name indicates, in svg format.

If you want to view some examples, you can visit its repository.

You can install it directly from MELPA.

M-x package-install RET svg-lib RET

Or in your init.el file:

(use-package svg-lib
  :ensure t)

To create an svg graphic, we will use the svg-lib* function.

(require 'svg-lib)

;; Tag
(put-image (svg-lib-tag "TODO" nil) 1)
;; Icon
(put-image (svg-lib-icon "star" nil) 2)
;; Icon with tag
(put-image (svg-lib-icon+tag "star" "Emacs" nil) 3)
;; Progress bar
(put-image (svg-lib-progress-bar 0.5 nil :width 5) 4)
;; Pie chart
(put-image (svg-lib-progress-pie 0.75 nil) 5)
;; Date
(put-image (svg-lib-date nil nil) 6)

You will have seen that I have used :width to modify the width of the progress bar. If you want to customize the properties, you can do so through the arguments.

  • :foreground: Text color.
  • :background: Background color.
  • :padding: Inner spacing. Only for tag and icon.
  • :margin: Outer spacing. For char.
  • :stroke-width: Border thickness. In pixels.
  • :corner-radius: Corner radius. In pixels.
  • :align: Horizontal alignment. From 0 to 1.
  • :width: Width. In characters.
  • :height: Height. As a scale of the line height.
  • :scale: Scale. Only for icons.
  • :ascent: Ascent. Only for text.
  • :crop-left: Crop on the left.
  • :crop-right: Crop on the right.
  • :collection: Icon collection.
  • :font-family: Font family.
  • :font-size: Font size.
  • :font-weight: Font weight.

I leave the possibilities of this library in your hands.

Example

Below I am going to show you a simple application to search and display free images from Wikimedia.

Read the code carefully and try to understand it. The concepts we have seen in the lesson have been used.

;; -*- coding: utf-8 -*-
;; Imports
(require 'widget)
(require 'cl-lib)
(require 'url)
(require 'svg-lib)


(eval-when-compile
  (require 'wid-edit))

;; Variables
(defvar buffer-name "*Wikimedia image search*")
(defvar endpoint "https://commons.wikimedia.org/w/api.php")
(defvar results-width 300)
(defvar limit-results 10)
(defvar start-line-results 8)
(defvar list-urls '())
(defvar input-text nil)
(defvar button-search nil)
(defvar button-search-value "Search")
(defvar loading-value "⏳ Searching... Please wait")

;; Functions

(defun process-response (pages)
  "Convert the response to a list of urls."
  ;; Add loading message
  (widget-value-set button-search loading-value)
  ;; Get the urls
  (setq list-urls '())
  (dolist (page pages)
    (setq list-urls (cons (assoc-default 'url (aref (assoc-default 'imageinfo page) 0)) list-urls)))
  (make-thread #'render-results "Render results"))

(defun search (widget &rest ignore)
  "Action for the search button. It will search for the text in the input field."
  (request endpoint
    :params `(("titles" . ,(widget-value input-text))
          ("gimlimit" . ,limit-results)
              ("action" . "query")
          ("format" . "json")
          ("generator" . "images")
          ("prop" . "imageinfo")
          ("redirects" . 1)
          ("iiprop" . "url"))
    :parser 'json-read
    :sync nil
    :success (cl-function
              (lambda (&key data &allow-other-keys)
        (process-response (assoc-default 'pages (assoc-default 'query data)))))
    :error (lambda (&rest _) (message "Error fetching data"))))


(defun put-image-from-url (url pos &optional width)
  "Put an image from an URL in the buffer at position."
  (unless url (setq url (url-get-url-at-point)))
  (unless url
    (error "Couldn't find URL."))
  (let ((buffer (url-retrieve-synchronously url)))
    (unwind-protect
        (let ((data (with-current-buffer buffer
                      (goto-char (point-min))
                      (search-forward "\n\n")
                      (buffer-substring (point) (point-max)))))
      (save-excursion
            (goto-char (point-min))
            (forward-line (1- pos)) ; Go to the beginning of the specified line
            (setq pos (line-beginning-position)))
      (put-image (create-image data nil t :width nil) pos))
      (kill-buffer buffer))))

(defun render-results ()
  "Render the results of the search."
  ;; Clear the previous results
  (remove-images (point-min) (point-max))
  ;; Add images
  (dotimes (i (length list-urls))
    (put-image-from-url (nth i list-urls) (+ start-line-results i) results-width))
  ;; Remove loading message
  (widget-value-set button-search button-search-value))

(defun main-layout ()
  "Make widgets for the main layout."
  (setq list-products '())
  ;; Create the buffer
  (switch-to-buffer buffer-name)
  ;; Clear the buffer
  (kill-all-local-variables)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (remove-overlays)
  ;; Create the widgets
  ;; Title
  (widget-insert "\nWikimedia image search\n\n")
  ;; Form
  (setq input-text (widget-create 'editable-field
                  :format "%v"
                  :size 20
                  :help-echo "What do you want to search?"
                  ""))
  (widget-insert "\n\n")
  (setq button-search (widget-create 'push-button
                     :notify #'search
                     :help-echo "Search"
                     "Search"))
  ;; Empty lines for the results
  (dotimes (i (1+ limit-results))
    (widget-insert "\n"))

  ;; End widgets
  ;; Display the buffer
  (use-local-map widget-keymap)
  (widget-setup)
  ;; Go to the first input field
  (widget-forward 1))

;; Initialization
(main-layout)
Activity 1

Add a button that, when pressed, shows an image from your computer.

Activity 2

Build an image carousel in a buffer. Every 5 seconds it must change to the next one; 3 will be enough. Remember to remove the previous image before inserting the next one so only one is visible.

Activity 3

Create a button that, when pressed, shows a random image from Unsplash. You can use the put-image-from-url function. You will need to research the Unsplash API.

This work is under a Attribution-NonCommercial-NoDerivatives 4.0 International license.

Desafíos de programación atemporales y multiparadigmáticos

Desafíos de programación atemporales y multiparadigmáticos

Te encuentras ante un librillo de actividades, divididas en 2 niveles de dificultad. Te enfrentarás a los casos más comunes que te puedes encontrar en pruebas técnicas o aprender conceptos elementales de programación.

Buy the book

Will you buy me a coffee?

This is how I keep writing without ads or paywalls.

Comments

There are no comments yet.