hoowl

Physicist, Emacser, Digitales Spielkind

Collecting images into one directory for self-contained Org mode exports
Published on Jun 17, 2023.

I often embed images into my Org mode documents. Especially attachments via org-download are convenient while composing notes or drafts. To share such documents, I usually export them. While HTML export allows to inline images, this is not an option for e.g. LaTeX (if I want to share the .tex file) or even Org mode.

To create a self-contained export including images, I have written an Elisp routine that extracts the file path from a link, copies the target file into a common directory and returns a modified link to the new location. The directory can be specified as a relative path, e.g. ./images/, which will result in relative links in the exported document as well. The exported file can then be easily compressed together with this directory into a zip file and send off.

This is the code that defines the variable that configures the output path and the filter function:

(defvar hanno/org-copy-linked-files-export-path "images/"
  "Sets the directory that linked local files are copied to on export.

The directory needs to already exist. Used by
`hanno/org-link-filter-copy-images-on-export' as target
directory.")

(defun hanno/org-copy-linked-files-on-export (text backend _info)
  "Copy linked files in exports into a common location.

Returns modified TEXT with path to the copied file for a given
export BACKEND. Supported backends are HTML, LaTeX and Org mode.
Intended to be added to `org-export-filter-link-functions'."
  (let (target source)
    (cond
     ((org-export-derived-backend-p backend 'html)
      (let* ((url
              (url-unhex-string
               (car (cl-remove-if
                     (lambda (link) (not (string-match-p "^file:/" link)))
                     (split-string-and-unquote text)))))
             (filename (file-name-nondirectory
                        (car (url-path-and-query
                              (url-generic-parse-url url))))))
        (unless (string-empty-p filename)
          (setq target (concat
                        (file-name-as-directory
                         hanno/org-copy-linked-files-export-path)
                        filename))
          (setq source url)
          (with-demoted-errors "Copy-files-link-filter error: %S"
            (url-copy-file url target)))))
     ((or (org-export-derived-backend-p backend 'latex)
          (org-export-derived-backend-p backend 'org))
      (let ((match (if (org-export-derived-backend-p backend 'latex)
                        "\\includegraphics.*?{\\(.*?\\)}"
                        "\\[\\[file:\\(.*?\\)\\]")))
        (when (string-match match text)
          (setq source (match-string 1 text))
          (let* ((filename (file-name-nondirectory source)))
            (setq target (concat
                          (file-name-as-directory
                           hanno/org-copy-linked-files-export-path)
                          filename))
            (with-demoted-errors "Copy-files-link-filter error: %S"
              (copy-file source target)))))))
    (when (and source target (file-exists-p target))
      (string-replace source target text))))

By adding this function to org-export-filter-link-functions it will be triggered once for each link when exporting an Org file:

(add-to-list 'org-export-filter-link-functions
             'hanno/org-copy-linked-files-on-export)

The filter will handle links such as

[[file:/path/to/some/file]]

and is technically not limited to images but would work with any linked file type.

Note that links with a description result in a \href{...} instead of an \includegraphics{...} in the LaTeX backend and are ignored by the filter to avoid affecting external links. Similarly, links without the file: prefix are not considered in the HTML backend.

Tags: emacs, org, lisp