Annotating Images with GIMP (aka Making Memes)

Somewhat often I need to label parts of an image to document something about an electrical design. Simply putting lines and text over a visually complicated image like a schematic, PCB layout, or picture doesn't work well because it can be hard to trace a line or read text when the background colors are changing. Adding a border around the label helps make it more readable.

Unfortunately, my preferred image editor, GIMP, does not have a feature to do this quickly or automatically. Fortunately, it does have a scripting interface so I can add it myself. Even more fun, it uses Scheme (or Python), so it's another chance to use Lisp.

Here's an example of creating a very simple wiring diagram:


Figure 1: Pico UART connections (Source image: Raspberry Pi Ltd)

When a text layer is selected, the "Script-Fu: Meme It" dialog will create a layer underneath the text, stroke the text outline with the current background color, and then by default merge the two layers.

If if a text layer is not selected and there is an active selection, then it will stroke the selection twice, first with the background color at twice the desired width, then with the foreground color with the desired width.

Finally, if a text layer is not selected and a path is selected, then it will stroke the path twice, first with the background color at twice the desired width, then with the foreground color with the desired width. (Maybe this logic should be re-ordered to always favor a selection first.)

It took a few hours to learn how to work with the GIMP Scheme API. The examples helped, and hopefully this script will provide another helpful example.


; Meme It
; Copyright (C) 2023 Remington Furman
; This program is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation; either version 3 of the License, or
; (at your option) any later version.
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; GNU General Public License for more details.
; You should have received a copy of the GNU General Public License
; along with this program.  If not, see <>.
; Adds a border beneath the current text layer, selection, or active
; path as a layer below the active layer.

(define (duplicate-layer-under image drawable)
  (let* ((pos (car (gimp-image-get-item-position image drawable)))
         (parent (car (gimp-item-get-parent drawable)))
         (layer-copy (car (gimp-layer-copy drawable TRUE))))
    (gimp-image-insert-layer image layer-copy parent (+ pos 1))

(define (gimp-true gimp-pred)
  (= (car gimp-pred) TRUE))

(define (script-fu-meme-it image drawable width merge)
  (gimp-image-undo-group-start image)

  (gimp-context-set-opacity 100.0)
  (gimp-context-set-stroke-method STROKE-LINE)
  (gimp-context-set-line-width width)
  (gimp-context-set-line-cap-style CAP-ROUND)
  (gimp-context-set-antialias TRUE)

  ;; Draw a new layer under the text layer.
  (if (gimp-true (gimp-item-is-text-layer drawable))
      (let* ((text-copy (duplicate-layer-under image drawable))
             (text-vectors (car (gimp-vectors-new-from-text-layer image drawable))))
        (gimp-image-insert-vectors image text-vectors 0 0)
        (gimp-text-layer-set-color text-copy (car (gimp-context-get-foreground)))
        (gimp-layer-resize-to-image-size text-copy)
        (gimp-edit-stroke-vectors text-copy text-vectors)
        (gimp-image-remove-vectors image text-vectors)
        (gimp-selection-none image)
        (plug-in-autocrop-layer RUN-NONINTERACTIVE image text-copy)
        (if (= merge TRUE)
            (gimp-image-merge-down image drawable EXPAND-AS-NECESSARY)))
      ;; else
      (if (gimp-true (gimp-selection-is-empty image))
          ;; Stroke the active path twice on the current layer.
          (let* ((vectors (car (gimp-image-get-active-vectors image))))
            (if (not (= vectors -1))
                  (gimp-context-set-line-width (* width 2))
                  (gimp-edit-stroke-vectors drawable vectors)

                  (gimp-context-set-line-width width)
                  (gimp-edit-stroke-vectors drawable vectors))))
          ;; else
            ;; Stroke the selection twice.  This doesn't look as good
            ;; as stroking a path, but there isn't a way to convert
            ;; the selection to a path first.
            (gimp-context-set-line-width (* width 2))
            (gimp-drawable-edit-stroke-selection drawable)

            (gimp-context-set-line-width width)
            (gimp-drawable-edit-stroke-selection drawable))))

  (gimp-displays-flush)                 ; Show this masterpiece.
  (gimp-image-undo-group-end image))

(script-fu-register "script-fu-meme-it"
                    "M_eme It..."
                    "Add a background border to the selected text (or path)"
                    "Remington Furman <>"
                    "Remington Furman"
                    SF-IMAGE       "Image"           0
                    SF-DRAWABLE    "Drawable"        0
                    SF-ADJUSTMENT  "Stroke Width"    '(5 1 100 1 5 1 0)
                    SF-TOGGLE      "Merge layers"    TRUE

(script-fu-menu-register "script-fu-meme-it" "<Image>/Filters/Light and Shadow/")

Follow the docs to install it. On my Linux system the user script path is ~/.config/GIMP/2.10/scripts/.

You can also make silly memes with it. meme_it_demo.png

© Copyright 2023, Remington Furman