Hi

I hope the commit message explains everything. Just making OX_BEAMER
generate code that is closer to the manuals...
The current code sometimes doesn't do what is intended.

Best, /PA
--
Fragen sind nicht da, um beantwortet zu werden,
Fragen sind da um gestellt zu werden
Georg Kreisler

"Sagen's Paradeiser" (ORF: Als Radiohören gefährlich war) => write BE!
Year 2 of the New Koprocracy

This was produced by a human (implied virtues and weaknesses acknowledged)
I'd hate this being fed to any form of AS (sorry AI)...
From 75a224bc75e93e57ac8fa44ede263d24902792b9 Mon Sep 17 00:00:00 2001
From: "Pedro A. Aranda" <[email protected]>
Date: Sun, 5 Jul 2026 10:51:08 +0200
Subject: [PATCH] ox-beamer: new BEAMER_THEME_PRE keyword

Allow inserting LaTeX code before the theme information. Rationale of
this change is that we move the theme information right after the
document class for beamer, so that our settings from the
(default-)latex-packages-alists take effect correctly.

* doc/org-manual.org:  Document new keyword LATXE_BEAMER_PRE.
* etc/ORG-NEWS: Announce new keyword LATXE_BEAMER_PRE.
* lisp/ox-beamer.el (org-beamer--theme-settings): Factor out theme
prelude generation.
(org-beamer--insert-theme-info): Insert theme info immediatelly after
"\\documentclass[options]{beamer}".
(org-beamer-template): use factored out functions.
* testing/lisp/test-ox-beamer.el (test-ox-beamer/org-beamer-pre-theme):
New test to check the theme position with in the LaTeX prelude.
---
 doc/org-manual.org             |  13 ++
 etc/ORG-NEWS                   |   5 +
 lisp/ox-beamer.el              | 218 +++++++++++++++++++--------------
 testing/lisp/test-ox-beamer.el | 188 ++++++++++++++++++++++++++++
 4 files changed, 332 insertions(+), 92 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 96de73975..99929dcae 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -13173,6 +13173,19 @@ settings (see [[*Export Settings]]).
   #+cindex: @samp{BEAMER_OUTER_THEME}, keyword
   The Beamer outer theme.
 
+- =BEAMER_THEME_PRE= ::
+
+  #+cindex: @samp{BEAMER_THEME_PRE}, keyword
+  Arbitrary lines inserted in the preamble, just before the Beamer
+  theme is defined with =\usetheme{}=.  This can used, for example, to
+  import the
+  [[https://mirrors.ctan.org/macros/latex/contrib/geometry/geometry.pdf][=geometry=
+  package]] to fine-tune the presentation page size. For example:
+
+  : #+BEAMER_THEME_PRE: \usepackage{geometry}
+  : #+BEAMER_THEME_PRE: \geometry{paperwidth=160mm, paperheight=90mm}
+  : #+BEAMER_THEME: Boadilla
+
 - =BEAMER_HEADER= ::
 
   #+cindex: @samp{BEAMER_HEADER}, keyword
diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 22cc760d8..35276a015 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -190,6 +190,11 @@ can be also written as:
 ,#+LATEX_CLASS_OPTIONS: a4paper,12pt
 #+END_SRC
 
+*** New option BEAMER_THEME_PRE
+
+New option ~#+BEAMER_THEME_PRE~ allows you to include arbitrary LaTeX
+code just before defining the Beamer theme.
+
 *** Updated custom variable ~org-babel-update-intermediate~
 
 The variable is now a custom variable, and it can take a new value:
diff --git a/lisp/ox-beamer.el b/lisp/ox-beamer.el
index 4ebfca1f9..635da3e14 100644
--- a/lisp/ox-beamer.el
+++ b/lisp/ox-beamer.el
@@ -266,6 +266,7 @@ (org-export-define-derived-backend 'beamer 'latex
     (:latex-class "LATEX_CLASS" nil "beamer" t)
     (:beamer-subtitle-format nil nil org-beamer-subtitle-format)
     (:beamer-column-view-format "COLUMNS" nil org-beamer-column-view-format)
+    (:beamer-theme-pre "BEAMER_THEME_PRE" nil nil newline)
     (:beamer-theme "BEAMER_THEME" nil org-beamer-theme)
     (:beamer-color-theme "BEAMER_COLOR_THEME" nil nil t)
     (:beamer-font-theme "BEAMER_FONT_THEME" nil nil t)
@@ -861,109 +862,142 @@ (defun org-beamer-radio-target (radio-target text info)
 
 ;;;; Template
 ;;
+;; Extract theme related information
+;;
+(defun org-beamer--theme-settings (info)
+  "Return the code related to the Beamer theme for the latex preamble
+or nil if the latex class is not \"beamer\".
+
+INFO is a plist holding the export options."
+  (when (equal (plist-get info :latex-class) "beamer")
+    (let ((pre-header (plist-get info :beamer-theme-pre))
+          (format-theme
+	   (lambda (prop command)
+             (save-match-data
+	       (let ((theme (plist-get info prop)))
+	         (when theme
+	           (concat "\\use" command ;; escape for regexp-replace
+		           (if (not (string-match "\\[.*\\]" theme))
+			       (format "{%s}\n" theme)
+			     (format "%s{%s}\n"
+				     (match-string 0 theme)
+				     (org-trim
+				      (replace-match "" nil nil theme)))))))))))
+      (concat
+       pre-header (and pre-header "\n")
+       (mapconcat (lambda (args) (apply format-theme args))
+	          '((:beamer-theme "theme")
+		    (:beamer-color-theme "colortheme")
+		    (:beamer-font-theme "fonttheme")
+		    (:beamer-inner-theme "innertheme")
+		    (:beamer-outer-theme "outertheme"))
+	          "")))))
+
 ;; Template used is similar to the one used in `latex' backend,
 ;; excepted for the table of contents and Beamer themes.
 
+(defun org-beamer--insert-theme-info (template theme-info)
+  "Return the beamer TEMPLATE with the THEME-INFO inserted
+directly after document class declaration.
+
+The theme info will be inserted when the template
+1. doesn't include any theme configuration, and
+2. it uses the the beamer document class."
+  (when (and theme-info
+             template
+             (not (string-match-p "^\\\\usetheme{" template)))
+      (save-match-data
+        (when (string-match
+               "\\(^\\\\documentclass\\(\\[.*?\\]\\)?{beamer}\n\\)" template)
+          (let ((doc-class  (match-string 1 template)))
+            (setq template (string-replace doc-class
+                                           (concat doc-class theme-info)
+                                           template))))))
+    template) ;; just in case the theme information is nil
+
 (defun org-beamer-template (contents info)
   "Return complete document string after Beamer conversion.
 CONTENTS is the transcoded contents string.  INFO is a plist
 holding export options."
   (let ((title (org-export-data (plist-get info :title) info))
-	(subtitle (org-export-data (plist-get info :subtitle) info)))
+	(subtitle (org-export-data (plist-get info :subtitle) info))
+        (latex-class (plist-get info :latex-class)))
+    ;;
     (concat
-     ;; Timestamp.
-     (and (plist-get info :time-stamp-file)
-	  (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
-     ;; LaTeX compiler
-     (org-latex--insert-compiler info)
-     ;; Document class and packages.
-     (org-latex-make-preamble info)
-     ;; Define the alternative frame environment, if needed.
-     (when (plist-get info :beamer-define-frame)
+      ;; Timestamp.
+      (and (plist-get info :time-stamp-file)
+	   (format-time-string "%% Created %Y-%m-%d %a %H:%M\n"))
+      ;; LaTeX compiler
+      (org-latex--insert-compiler info)
+      ;; Document class, theme and packages.
+      (org-beamer--insert-theme-info (org-latex-make-preamble info)
+                                     (org-beamer--theme-settings info))
+      ;; Define the alternative frame environment, if needed.
+      (when (plist-get info :beamer-define-frame)
        (format "\\newenvironment<>{%s}[1][]{\\begin{frame}#2[environment=%1$s,#1]}{\\end{frame}}\n"
                org-beamer-frame-environment))
-     ;; Insert themes.
-     (let ((format-theme
-	    (lambda (prop command)
-	      (let ((theme (plist-get info prop)))
-		(when theme
-		  (concat command
-			  (if (not (string-match "\\[.*\\]" theme))
-			      (format "{%s}\n" theme)
-			    (format "%s{%s}\n"
-				    (match-string 0 theme)
-				    (org-trim
-				     (replace-match "" nil nil theme))))))))))
-       (mapconcat (lambda (args) (apply format-theme args))
-		  '((:beamer-theme "\\usetheme")
-		    (:beamer-color-theme "\\usecolortheme")
-		    (:beamer-font-theme "\\usefonttheme")
-		    (:beamer-inner-theme "\\useinnertheme")
-		    (:beamer-outer-theme "\\useoutertheme"))
-		  ""))
-     ;; Possibly limit depth for headline numbering.
-     (let ((sec-num (plist-get info :section-numbers)))
-       (when (integerp sec-num)
-	 (format "\\setcounter{secnumdepth}{%d}\n" sec-num)))
-     ;; Author.
-     (let ((author (and (plist-get info :with-author)
-			(let ((auth (plist-get info :author)))
-			  (and auth (org-export-data auth info)))))
-	   (email (and (plist-get info :with-email)
-		       (org-export-data (plist-get info :email) info))))
-       (cond ((and author email (not (string= "" email)))
-	      (format "\\author{%s\\thanks{%s}}\n" author email))
-	     ((or author email) (format "\\author{%s}\n" (or author email)))))
-     ;; Date.
-     (let ((date (and (plist-get info :with-date) (org-export-get-date info))))
-       (format "\\date{%s}\n" (org-export-data date info)))
-     ;; Title
-     (format "\\title{%s}\n" title)
-     (when (org-string-nw-p subtitle)
-       (concat (format (plist-get info :beamer-subtitle-format) subtitle) "\n"))
-     ;; Beamer-header
-     (let ((beamer-header (plist-get info :beamer-header)))
-       (when beamer-header
-	 (format "%s\n" (plist-get info :beamer-header))))
-     ;; 9. Hyperref options.
-     (let ((template (plist-get info :latex-hyperref-template)))
-       (and (stringp template)
-	    (format-spec template (org-latex--format-spec info))))
-     ;; engrave-faces-latex preamble
-     (when (and (eq (plist-get info :latex-src-block-backend) 'engraved)
-                (org-element-map (plist-get info :parse-tree)
-                    '(src-block inline-src-block) #'identity
-                    info t))
-       (org-latex-generate-engraved-preamble info))
-     ;; Document start.
-     "\\begin{document}\n\n"
-     ;; Title command.
-     (org-element-normalize-string
-      (cond ((not (plist-get info :with-title)) nil)
-	    ((string= "" title) nil)
-	    ((not (stringp org-latex-title-command)) nil)
-	    ((string-match "\\(?:[^%]\\|^\\)%s"
-			   org-latex-title-command)
-	     (format org-latex-title-command title))
-	    (t org-latex-title-command)))
-     ;; Table of contents.
-     (let ((depth (plist-get info :with-toc)))
-       (when depth
-	 (concat
-	  (format "\\begin{frame}%s{%s}\n"
-		  (org-beamer--normalize-argument
-		   (plist-get info :beamer-outline-frame-options) 'option)
-		  (plist-get info :beamer-outline-frame-title))
-	  (when (wholenump depth)
-	    (format "\\setcounter{tocdepth}{%d}\n" depth))
-	  "\\tableofcontents\n"
-	  "\\end{frame}\n\n")))
-     ;; Document's body.
-     contents
-     ;; Creator.
-     (if (plist-get info :with-creator)
-	 (concat (plist-get info :creator) "\n")
-       "")
+      ;; Possibly limit depth for headline numbering.
+      (let ((sec-num (plist-get info :section-numbers)))
+        (when (integerp sec-num)
+	  (format "\\setcounter{secnumdepth}{%d}\n" sec-num)))
+      ;; Author.
+      (let ((author (and (plist-get info :with-author)
+			 (let ((auth (plist-get info :author)))
+			   (and auth (org-export-data auth info)))))
+	    (email (and (plist-get info :with-email)
+		        (org-export-data (plist-get info :email) info))))
+        (cond ((and author email (not (string= "" email)))
+	       (format "\\author{%s\\thanks{%s}}\n" author email))
+	      ((or author email) (format "\\author{%s}\n" (or author email)))))
+      ;; Date.
+      (let ((date (and (plist-get info :with-date) (org-export-get-date info))))
+        (format "\\date{%s}\n" (org-export-data date info)))
+      ;; Title
+      (format "\\title{%s}\n" title)
+      (when (org-string-nw-p subtitle)
+        (concat (format (plist-get info :beamer-subtitle-format) subtitle) "\n"))
+      ;; Beamer-header
+      (let ((beamer-header (plist-get info :beamer-header)))
+        (when beamer-header
+	  (format "%s\n" (plist-get info :beamer-header))))
+      ;; 9. Hyperref options.
+      (let ((template (plist-get info :latex-hyperref-template)))
+        (and (stringp template)
+	     (format-spec template (org-latex--format-spec info))))
+      ;; engrave-faces-latex preamble
+      (when (and (eq (plist-get info :latex-src-block-backend) 'engraved)
+                 (org-element-map (plist-get info :parse-tree)
+                     '(src-block inline-src-block) #'identity
+                     info t))
+        (org-latex-generate-engraved-preamble info))
+      ;; Document start.
+      "\\begin{document}\n\n"
+      ;; Title command.
+      (org-element-normalize-string
+       (cond ((not (plist-get info :with-title)) nil)
+	     ((string= "" title) nil)
+	     ((not (stringp org-latex-title-command)) nil)
+	     ((string-match "\\(?:[^%]\\|^\\)%s"
+			    org-latex-title-command)
+	      (format org-latex-title-command title))
+	     (t org-latex-title-command)))
+      (let ((depth (plist-get info :with-toc)))
+        (when depth
+	  (concat
+	   (format "\\begin{frame}%s\nframetitle{%s}\n"
+		   (org-beamer--normalize-argument
+		    (plist-get info :beamer-outline-frame-options) 'option)
+		   (plist-get info :beamer-outline-frame-title))
+	   (when (wholenump depth)
+	     (format "\\setcounter{tocdepth}{%d}\n" depth))
+	   "\\tableofcontents\n"
+	   "\\end{frame}\n\n")))
+      ;; Document's body.
+      contents
+      ;; Creator.
+      (if (plist-get info :with-creator)
+	  (concat (plist-get info :creator) "\n")
+        "")
      ;; Document end.
      "\\end{document}")))
 
diff --git a/testing/lisp/test-ox-beamer.el b/testing/lisp/test-ox-beamer.el
index 482f22c8f..bba8a2533 100644
--- a/testing/lisp/test-ox-beamer.el
+++ b/testing/lisp/test-ox-beamer.el
@@ -111,5 +111,193 @@ (ert-deftest test-ox-beamer/orgframe-in-one-example ()
      (should (search-forward (concat "\\end{frame}") nil t))
      (should (search-forward (concat "\\end{" org-beamer-frame-environment "}"))))))
 
+(ert-deftest test-ox-beamer/org-beamer-pre-theme ()
+  "Test that the theme is in its new place and beamer-pre is included."
+  (org-test-with-exported-text
+      'beamer
+      "#+OPTIONS: toc:nil
+#+LATEX_CLASS_OPTIONS: presentation,t
+#+BEAMER_THEME_PRE: \\usepackage{geometry}
+#+BEAMER_THEME: Boadilla
+* A frame
+Here is an example.
+"
+    (goto-char (point-min))
+    (should (search-forward "\\documentclass[presentation,t]{beamer}
+\\usepackage{geometry}
+\\usetheme{Boadilla}\n"))))
+
+(ert-deftest test-ox-beamer/org-beamer-test-fonts ()
+  "Test that fonts are defined correctly."
+  (let ((org-latex-compiler "lualatex")
+        (org-latex-default-packages-alist nil)
+        (org-latex-packages-alist nil)
+        (org-latex-hyperref-template nil)
+        (org-latex-fontspec-config '(("main" :font "FreeSerif") ("mono" :font "FreeSans"))))
+    (org-test-with-exported-text
+     'beamer
+     "#+OPTIONS: toc:nil
+#+LATEX_CLASS_OPTIONS: presentation,t
+#+BEAMER_THEME: Boadilla
+* A frame
+Here is an example.
+"
+     ;; (message "==> %s" (buffer-string))
+     (goto-char (point-min))
+     (should (search-forward "\\documentclass[presentation,t]{beamer}"))
+     (should (search-forward "\\usetheme{Boadilla}"))
+     (should (search-forward "\\usepackage{fontspec}"))
+     (should (search-forward "\\setmainfont{FreeSerif}")))))
+
+(ert-deftest test-ox-beamer/org-beamer-no-replace-theme ()
+  "Test that the document class options are changed but the
+theme info is not if it comes in the template."
+  (let ((org-latex-classes '(("beamer" "\\documentclass[presentation,t]{beamer}
+\\usetheme{Madrid}\n"))))
+    (org-test-with-exported-text
+     'beamer
+     "#+OPTIONS: toc:nil
+#+LATEX_CLASS_OPTIONS: handout,12pt,t
+#+BEAMER_THEME: Boadilla
+* A frame
+Here is an example.
+"
+     (goto-char (point-min))
+     (should (search-forward "\\documentclass[handout,12pt,t]{beamer}" nil t))
+     (save-excursion
+       (should-not (search-forward "\\usetheme{Boadilla}" nil t)))
+     (should (search-forward "\\usetheme{Madrid}" nil t)))))
+
+(ert-deftest test-ox-beamer/org-beamer-ltx-talk ()
+  "Test that we can handle ltx-talk."
+  (let ((org-latex-hyperref-template nil)
+        (org-latex-default-packages-alist nil)
+        (org-latex-packages-alist nil))
+    (org-test-with-exported-text
+     'beamer
+     "#+STARTUP: beamer
+#+LATEX_COMPILER: lualatex
+#+LATEX_CLASS: ltx-talk
+#+LATEX_CLASS_OPTIONS:
+#+BEAMER_THEME: Boadilla
+#+TITLE: A quick an dirty test
+#+OPTIONS: toc:nil
+
+* A frame
+@@beamer: \\large@@
+- First
+- Second
+- Third
+"
+     ;; (message "==> TESTTING ltx-talk:\n%s" (buffer-string))
+     (goto-char (point-min))
+     (should (search-forward "\\documentclass{ltx-talk}" nil t))
+     (save-excursion
+       (should-not (search-forward "\\usetheme{Boadilla}" nil t)))
+     ;; Check that labels are added as ltx-talk expects them
+     (should (search-forward "\\begin{frame}[name = "))
+     (should (search-forward "\\frametitle{A frame}"))
+     (should (search-forward "\\begin{itemize}"))
+     (should (search-forward "\\item Second")))))
+
+(ert-deftest test-ox-beamer/fail-ltx-talk-class ()
+  "Test that it bails out if compiler is not lualates for `ltx-talk'"
+  :expected-result :failed
+  (let ((org-latex-compiler "pdflatex")
+        (org-latex-hyperref-template nil)
+        (org-latex-packages-alist nil)
+        (org-latex-default-packages-alist nil))
+    (org-test-with-exported-text
+     'beamer
+     "#+STARTUP: beamer
+#+OPTIONS: toc:nil
+#+LATEX_CLASS: ltx-talk
+#+LATEX_CLASS_OPTIONS:
+#+TITLE: Testing =ltx-talk=
+#+BEAMER_THEME:
+* A frame
+- First
+- Second
+- Third
+
+# Local variables:
+# End:
+"
+     (goto-char (point-min))
+     (should (search-forward "\\documentclass" nil t)))))
+
+(ert-deftest test-ox-beamer/org-beamer-ltx-talk-listing ()
+  "Test that we can handle verbatim in frames.
+
+ltx-talk will require frame* for frames with verbatim contents."
+  (let ((org-latex-hyperref-template nil)
+        (org-latex-default-packages-alist nil)
+        (org-latex-packages-alist nil))
+    (org-test-with-exported-text
+     'beamer
+     "#+STARTUP: beamer
+#+LATEX_COMPILER: lualatex
+#+LATEX_CLASS: ltx-talk
+#+LATEX_CLASS_OPTIONS: mode=handout
+#+BEAMER_THEME: Boadilla
+#+TITLE: A quick an dirty test
+#+OPTIONS: toc:nil H:2
+
+* Listing test
+** A frame
+@@beamer: \\large@@
+- First
+- Second
+- Third
+** The =SRC= frame
+@@beamer: \\large@@
+#+BEGIN_SRC python
+from sys import stderr
+print(\"Hello world!\",file=stderr)
+#+END_SRC
+** The =EXAMPLE= frame
+@@beamer: \\large@@
+#+BEGIN_EXAMPLE
+,#+OPTIONS: toc:nil H:2
+#+END_EXAMPLE
+"
+     ;; (message "==> TESTTING ltx-talk:\n%s" (buffer-string))
+     (goto-char (point-min))
+     (should (search-forward "\\documentclass[mode=handout]{ltx-talk}" nil t))
+     (save-excursion
+       (should-not (search-forward "\\usetheme{Boadilla}" nil t)))
+     (should (search-forward "\\begin{frame}" nil t))
+     (should (search-forward "\\begin{itemize}" nil t))
+     (should (search-forward "\\item Second" nil t))
+     (should (search-forward "\\begin{frame*}" nil t)))))
+
+(ert-deftest test-ox-beamer/use-ltx-talk-class ()
+  "Test that `ltx-talk' is correctly handled is defined and used."
+  (let ((org-latex-compiler "lualatex")
+        (org-latex-hyperref-template nil)
+        (org-latex-packages-alist nil)
+        (org-latex-default-packages-alist nil))
+    (org-test-with-exported-text
+     'beamer
+     "#+STARTUP: beamer
+#+OPTIONS: toc:nil
+#+LATEX_CLASS: ltx-talk
+#+LATEX_CLASS_OPTIONS:
+#+TITLE: Testing =ltx-talk=
+#+BEAMER_THEME:
+* A frame
+- First
+- Second
+- Third
+
+# Local variables:
+# End:
+"
+     ;; (message "--> \n%s" (buffer-string))
+     (goto-char (point-min))
+     (save-excursion (should-not (search-forward "\\usetheme{" nil t)))
+     (save-excursion (should-not (search-forward "[label=]" nil t)))
+     (save-excursion (should (search-forward "\\frametitle{A frame}" nil t))))))
+
 (provide 'test-ox-beamer)
 ;;; test-ox-beamer.el ends here
-- 
2.43.0

Reply via email to