Exporting several variants from an org-beamer file

This is a follow up on my org beamer post, since I've changed the way I manage to export three variants of the same document. Recall what I want to do is to be able to export:

  1. pres: the slides with speaker notes (obviously),
  2. handout: a handout version (no animations, several slides per page),
  3. slides: the slides without speaker notes (so that students might be able to re-see the slides exactly as they were shown during the lecture, including animations).

In the above post, I explained I wasn't really satisfied with the need to maintain three setup files. At some point, I actually managed to merge them in a single one by using org macros of the (eval form to select amongst several values, but that was quite cumbersome.

I rethought about that and came up with the following solution:

  • use a "setup" file in which settings to be used for each variant are defined,
  • define a new export backend that will offer several export actions in the export dispatcher, in order to select the variant to output,
  • use a org-export-before-parsing-functions hook that will copy the appropriate settings from the setup file to the export buffer just before actual export is performed.

Here's the definition of the my-beamer export backend:

(require 'ox-beamer)

(org-export-define-derived-backend 'my-beamer 'beamer
  :menu-entry '(?s "My beamer export"
		   ((?p "Teacher presentation" my--beamer-export-pres)
		    (?s "Student slides"       my--beamer-export-slides)
		    (?h "Student handout"      my--beamer-export-handout)
		    (?a "All"                  my--beamer-export-all))))

The setup file is defined by the my-beamer-setup-file variable. Here it's defined as a plain file name to it will be looked for in the current directory, but a full pathname could be used to have a global setup file:

(defvar my-beamer-setup-file "my-beamer-setup.org"
  "The default setup file for the my-beamer exporter.  This will be used if
the MY_BEAMER_SETUP key is not set in the file.")

Moreover the setup file to use can also be set in the org file with the MY_BEAMER_SETUP option.

(defun my--beamer-get-setup-name ()
  "Get the name of the setup file to use (and delete line from buffer)."
  (or (and (search-forward "#+MY_BEAMER_SETUP: " nil t)
	   (prog1 (thing-at-point 'filename) (delete-line)))
      my-beamer-setup-file))

The export action functions are defined as

(defun my--beamer-export-pres (&rest options)
  (let ((basename (file-name-sans-extension (buffer-name))))
    (my--org-beamer-do-export basename "pres")))

(defun my--beamer-export-slides (&rest options)
  (let ((basename (file-name-sans-extension (buffer-name))))
    (my--org-beamer-do-export basename "slides")))

(defun my--beamer-export-handout (&rest options)
  (let ((basename (file-name-sans-extension (buffer-name))))
    (my--org-beamer-do-export basename "handout")))

(defun my--beamer-export-all (&rest options)
  (my--beamer-export-pres)
  (my--beamer-export-slides)
  (my--beamer-export-handout))

As we can see, they all call my--org-beamer-do-export with the file basename and the desired variant. This function sets a dynamic variable to the desired variant, calls the org-beamer-export-to-pdf function and then renames the output file by suffixing it with a dash and the variant name (so that exporting eg lecture1.org as the handout variant creates the lecture1-handout.pdf file).

(defvar my--beamer-variant nil
  "Dynamic variable for the export hook to know the output variant (will be
nil if we don't use the my-beamer backend).")

(defun my--org-beamer-do-export (basename variant)
  "Export, rename."
  (setq my--beamer-variant variant)
  (org-beamer-export-to-pdf)
  (rename-file (concat basename ".pdf")
               (format "%s-%s.pdf" basename variant) t))

The actual work is done by the my--beamer-hook function. It will open the setup file and, according to the value of my--beamer-variant, iterate on the headlines of the setup file. It will copy the contents in the export buffer if the selected variant name appears in the heading tags.

(defun my--beamer-hook (backend)
  ;; note: the backend here is 'beamer since we call org-beamer-export-to-pdf
  (when my--beamer-variant  ;; nil when another backend is used
    (let ((buf (current-buffer))
	  (setup (my--beamer-get-setup-name)))
      (with-temp-buffer
	(insert-file-contents setup)
	(goto-char (point-min))
	;; org-next... returns 0 if found or point if no more (undocumented)
	(while (= 0 (org-next-visible-heading 1))
	  (let ((elt (org-element-at-point)) text)
	    (when (member my--beamer-variant
			  (org-element-property :tags elt))
	      (setq text (buffer-substring-no-properties
			  (org-element-property :contents-begin elt)
			  (org-element-property :contents-end   elt)))
	      (with-current-buffer buf (insert text)))))
	))))

(add-hook 'org-export-before-parsing-functions #'my--beamer-hook)

Finally, here's the setup file I currently use. Using the tags to select parts allows to easily factor parts that are common to several variants.

Enjoy!

setup file for my-beamer org exporter.

Heading content is inserted if the selected output variant appears
in the heading tags.

* This part is common to all output variants            :pres:slides:handout:

#+latex_header: \usepackage[]{babel}\usepackage{alltt}\usepackage{version}
#+latex_header: \usepackage{beamer-mfig}
#+latex_header: \graphicspath{{figs}}

# in header for it to also apply to title and toc
#+latex_header: \setbeamertemplate{navigation symbols}{}
#+latex_header: \setbeamertemplate{footline}[frame number]{}
#+latex: \setbeamercolor{block title}{fg=black,bg=white}
#+latex: \newif\ifhandout

* This part is for pres and slides                              :pres:slides:

# - select a 16/9 format for slides (default 4/3 is better for printed handout)
# - include toc slides at each section and subsection
# - include poll slides

#+BEAMER_THEME: Rochester
#+BEAMER_COLOR_THEME: spruce
#+LATEX_CLASS_OPTIONS: [aspectratio=169]
#+LATEX_HEADER: \AtBeginSection[]{\begin{frame}<beamer>[noframenumbering]{Outline}\tableofcontents[currentsection]\end{frame}}
#+LATEX_HEADER: \AtBeginSubsection[]{\begin{frame}<beamer>[noframenumbering]{Outline}\tableofcontents[currentsection\,currentsubsection]\end{frame}}
#+LATEX_HEADER: \newenvironment{poll}[2][]{\begin{frame}[environment=poll\,noframenumbering\,#1]{Poll -- #2}  \renewcommand{\theenumi}{\textbf{\Alph{enumi}}}\setbeamertemplate{enumerate item}[square]}{\end{frame}}
#+latex: \handoutfalse

* For pres only, show notes on second screen                           :pres: 
#+LATEX_HEADER: \setbeameroption{show notes on second screen}

* For slides only, hide notes                                        :slides:
#+LATEX_HEADER: \setbeameroption{hide notes}

* For handout                                                       :handout:

# What differs in handout:
#  - sets the handout beamer class option (so no overlays)
#  - select a different color theme
#  - exclude slides with tag nohandout
#  - use light footline with just the slide number
#  - use pgfpages to print 8 slides per page
#  - exclude polls

#+LATEX_CLASS_OPTIONS: [handout]
#+BEAMER_THEME: Rochester
#+BEAMER_COLOR_THEME: dove
#+EXCLUDE_TAGS: nohandout noexport
#+LATEX_HEADER: \setbeamertemplate{footline}{\hfill\insertframenumber}
#+LATEX_HEADER: \usepackage{pgfpages}\pgfpagesuselayout{8 on 1}[a4paper, border shrink=5mm]
#+LATEX_HEADER: \excludeversion{poll}
#+latex: \handouttrue