<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/assets/rss.xsl" type="text/xsl"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
>
<channel>
	<title>Sacha Chua - category - elisp</title>
	<atom:link href="https://sachachua.com/blog/category/elisp/feed/index.xml" rel="self" type="application/rss+xml" />
	<atom:link href="https://sachachua.com/blog/category/elisp" rel="alternate" type="text/html" />
	<link>https://sachachua.com/blog/category/elisp/feed/index.xml</link>
	<description>Emacs, sketches, and life</description>
	<lastBuildDate>Mon, 13 Apr 2026 13:43:00 GMT</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>daily</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>11ty</generator>
  <item>
		<title>YE11: Fix find-function for Emacs Lisp from org-babel or scratch</title>
		<link>https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sun, 05 Apr 2026 21:03:48 GMT</pubDate>
    <category>org</category>
<category>emacs</category>
<category>elisp</category>
<category>stream</category>
<category>yay-emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/</guid>
		<description><![CDATA[<p>
<video controls="1" src="https://archive.org/download/yay-emacs-11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/ye11-find-function.mp4" poster="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/2026-04-05-19-25-03.png" type="video/mp4"><track kind="subtitles" label="Captions" src="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/Yay%20Emacs%2011:%20Fix%20find-function%20for%20Emacs%20Lisp%20from%20org-babel%20or%20scratch.vtt" srclang="en" default=""><span>Video not supported. Thumbnail:<br><img src="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/2026-04-05-19-25-03.png" alt="Thumbnail"></span></video>
</p>

<p>
<a href="https://archive.org/details/yay-emacs-11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch">Watch on Internet Archive</a>, <a href="https://youtube.com/live/PKkV1Tbev_Y">watch/comment on YouTube</a>, <a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/Yay%20Emacs%2011:%20Fix%20find-function%20for%20Emacs%20Lisp%20from%20org-babel%20or%20scratch.vtt">download captions</a>, or <a href="mailto:sacha@sachachua.com">email me</a>
</p>


<p>
Where can you define an Emacs Lisp function so
that you can use <code>find-function</code> to jump to it
again later?
</p>

<ul class="org-ul">
<li><b>A: In an indirect buffer</b> from Org Mode source
block with your favorite eval function like
<code>eval-defun</code> <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>

<ul class="org-ul">
<li><p>
<code>C-c '</code> (<code>org-edit-special</code>) inside the block; execute the defun with <code>C-M-x</code> (<code>eval-defun</code>), <code>C-x C-e</code> (<code>eval-last-sexp</code>), or <code>eval-buffer</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>    (<span class="org-keyword">defun</span> <span class="org-function-name">my-test-1</span> () (message <span class="org-string">"Hello"</span>))
</code></pre>
</div>
</li>
</ul></li>

<li><p>
<b>B: In an Org Mode file</b> by executing the block
with C-c C-c <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>  (<span class="org-keyword">defun</span> <span class="org-function-name">my-test-2</span> () (message <span class="org-string">"Hello"</span>))
</code></pre>
</div>
</li>

<li><p>
<b>C: In a .el file</b> <label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">yup</span></label>
</p>

<p>
<a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/test-search-function.el">file:///tmp/test-search-function.el</a> : execute the defun with <code>C-M-x</code> (<code>eval-defun</code>), <code>C-x C-e</code> (<code>eval-last-sexp</code>), or <code>eval-buffer</code>
</p></li>

<li><p>
<b>D: In a scratch buffer,</b> other temporary buffer,
or really any buffer thanks to eval-last-sexp
<label class="hint"><input type="checkbox"> <span class="hint-desc">(hint)</span><span class="hint-text">nope</span></label>
</p>

<p>
<code>(defun my-test-4 () (message "Hello"))</code>
</p></li>
</ul>

<p>
Only option C works - it's gotta be in an .el file for
<code>find-function</code> to find it. But I love jumping to
function definitions using <code>find-function</code> or
<code>lispy-goto-symbol</code> (which is bound to <code>M-.</code> if
you use <a target="_blank" href="https://melpa.org/#/lispy">lispy</a> and set up <code>lispy-mode</code>) so
that I can look at or change how something works.
It can be a little frustrating when I try to jump
to a definition and it says, "Don't know where
blahblahblah is defined." I just defined it five
minutes ago! It's there in one of my other
buffers, don't make me look for it myself.
Probably this will get fixed in Emacs core
someday, but no worries, we can work around it
today with a little bit of advice.
</p>

<p>
I did some digging around in the source code.
Turns out that <code>symbol-file</code> can't find the
function definition in the <code>load-history</code> variable
if you're not in a .el file, so
<code>find-function-search-for-symbol</code> gets called with
<code>nil</code> for the library, which causes the error.
(<a href="https://github.com/emacs-mirror/emacs/blob/master/lisp/subr.el">emacs:subr.el</a>)
</p>

<p>
I wrote some advice that searches in any open
<code>emacs-lisp-mode</code> buffers or in a list of other
files, like my Emacs configuration.
This is how I activate it:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">setq</span> sacha-elisp-find-function-search-extra <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"~/sync/emacs/Sacha.org"</span>))
(advice-add <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">find-function-search-for-symbol</span> <span class="org-builtin">:around</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">sacha-elisp-find-function-search-for-symbol</span>)
</code></pre>
</div>


<p>
Now I should be able to jump to all those
functions wherever they're defined.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(my-test-1)
(my-test-2)
(my-test-3)
(my-test-4)
</code></pre>
</div>


<p>
Note that by default, <code>M-.</code> in <code>emacs-lisp-mode</code> uses <code>xref-find-definitions</code>, which seems to really want files. I haven't figured out a good workaround for that yet, but <a target="_blank" href="https://melpa.org/#/lispy">lispy-mode</a> makes <code>M-.</code> work and gives me a bunch of other great shortcuts, so I'd recommend checking that out.
</p>

<p>
Here's the source code for the find function thing:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defvar</span> <span class="org-variable-name">sacha-elisp-find-function-search-extra</span>
  nil
  <span class="org-doc">"List of filenames to search for functions."</span>)

<span class="org-comment-delimiter">;;;</span><span class="org-comment">###</span><span class="org-comment"><span class="org-warning">autoload</span></span>
(<span class="org-keyword">defun</span> <span class="org-function-name">sacha-elisp-find-function-search-for-symbol</span> (fn symbol type library <span class="org-type">&amp;rest</span> _)
  <span class="org-doc">"Find SYMBOL with TYPE in Emacs Lisp buffers or `</span><span class="org-doc"><span class="org-constant">sacha-find-function-search-extra</span></span><span class="org-doc">'.</span>
<span class="org-doc">Prioritize buffers that do not have associated files, such as Org Src</span>
<span class="org-doc">buffers or *scratch*. Note that the fallback search uses \"^([</span><span class="org-doc"><span class="org-negation-char">^</span></span><span class="org-doc"> )]+\" so that</span>
<span class="org-doc">it isn't confused by preceding forms.</span>

<span class="org-doc">If LIBRARY is specified, fall back to FN.</span>

<span class="org-doc">Activate this with:</span>

<span class="org-doc">(advice-add 'find-function-search-for-symbol</span>
<span class="org-doc"> :around #'sacha-org-babel-find-function-search-for-symbol-in-dotemacs)"</span>
  (<span class="org-keyword">if</span> (null library)
      <span class="org-comment-delimiter">;; </span><span class="org-comment">Could not find library; search my-dotemacs-file just in case</span>
      (<span class="org-keyword">progn</span>
        (<span class="org-keyword">while</span> (<span class="org-keyword">and</span> (symbolp symbol) (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">definition-name</span>))
          (<span class="org-keyword">setq</span> symbol (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">definition-name</span>)))
        (<span class="org-keyword">catch</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
          (mapc
           (<span class="org-keyword">lambda</span> (buffer-or-file)
             (<span class="org-keyword">with-current-buffer</span> (<span class="org-keyword">if</span> (bufferp buffer-or-file)
                                      buffer-or-file
                                    (find-file-noselect buffer-or-file))
               (<span class="org-keyword">let*</span> ((regexp-symbol
                       (<span class="org-keyword">or</span> (<span class="org-keyword">and</span> (symbolp symbol)
                                (alist-get type (get symbol <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">find-function-type-alist</span>)))
                           (alist-get type find-function-regexp-alist)))
                      (form-matcher-factory
                       (<span class="org-keyword">and</span> (functionp (cdr-safe regexp-symbol))
                            (cdr regexp-symbol)))
                      (regexp-symbol (<span class="org-keyword">if</span> form-matcher-factory
                                         (car regexp-symbol)
                                       regexp-symbol))

                      (case-fold-search)
                      (regexp (<span class="org-keyword">if</span> (functionp regexp-symbol) regexp-symbol
                                (format (symbol-value regexp-symbol)
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">Entry for ` (backquote) macro in loaddefs.el,</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">(defalias (quote \`)..., has a \ but</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">(symbol-name symbol) doesn't.  Add an</span>
                                        <span class="org-comment-delimiter">;; </span><span class="org-comment">optional \ to catch this.</span>
                                        (concat <span class="org-string">"\\\\?"</span>
                                                (regexp-quote (symbol-name symbol)))))))
                 (<span class="org-keyword">save-restriction</span>
                   (widen)
                   (<span class="org-keyword">with-syntax-table</span> emacs-lisp-mode-syntax-table
                     (goto-char (point-min))
                     (<span class="org-keyword">if</span> (<span class="org-keyword">if</span> (functionp regexp)
                             (funcall regexp symbol)
                           (<span class="org-keyword">or</span> (re-search-forward regexp nil t)
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">`</span><span class="org-comment"><span class="org-constant">regexp</span></span><span class="org-comment">' matches definitions using known forms like</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">`</span><span class="org-comment"><span class="org-constant">defun</span></span><span class="org-comment">', or `</span><span class="org-comment"><span class="org-constant">defvar</span></span><span class="org-comment">'.  But some functions/variables</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">are defined using special macros (or functions), so</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">if `</span><span class="org-comment"><span class="org-constant">regexp</span></span><span class="org-comment">' can't find the definition, we look for</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">something of the form "(SOMETHING &lt;symbol&gt; ...)".</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">This fails to distinguish function definitions from</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">variable declarations (or even uses thereof), but is</span>
                               <span class="org-comment-delimiter">;; </span><span class="org-comment">a good pragmatic fallback.</span>
                               (re-search-forward
                                (concat <span class="org-string">"^([</span><span class="org-string"><span class="org-negation-char">^</span></span><span class="org-string"> )]+"</span> find-function-space-re <span class="org-string">"['(]?"</span>
                                        (regexp-quote (symbol-name symbol))
                                        <span class="org-string">"\\_&gt;"</span>)
                                nil t)))
                         (<span class="org-keyword">progn</span>
                           (beginning-of-line)
                           (<span class="org-keyword">throw</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
                                   (cons (current-buffer) (point))))
                       (<span class="org-keyword">when-let*</span> ((find-expanded
                                    (<span class="org-keyword">when</span> (trusted-content-p)
                                      (find-function&#45;&#45;search-by-expanding-macros
                                       (current-buffer) symbol type
                                       form-matcher-factory))))
                         (<span class="org-keyword">throw</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">found</span>
                                 (cons (current-buffer)
                                       find-expanded)))))))))
           (delq nil
                 (append
                  (sort
                   (match-buffers <span class="org-highlight-quoted-quote">'</span>(derived-mode . emacs-lisp-mode))
                   <span class="org-builtin">:key</span> (<span class="org-keyword">lambda</span> (o) (<span class="org-keyword">or</span> (buffer-file-name o) <span class="org-string">""</span>)))
                  sacha-elisp-find-function-search-extra)))))
    (funcall fn symbol type library)))
</code></pre>
</div>


<p>
I even figured out how to <span title="(ignore (ert &quot;sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;.*&quot;))">write tests for it</span>:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;in-buffer</span> ()
  (<span class="org-keyword">let</span> ((sym (make-temp-name <span class="org-string">"&#45;&#45;test-fn"</span>))
        buffer)
    (<span class="org-keyword">unwind-protect</span>
        (<span class="org-keyword">with-temp-buffer</span>
          (emacs-lisp-mode)
          (insert (format <span class="org-string">";; Comment\n(defun %s () (message \"Hello\"))"</span> sym))
          (eval-last-sexp nil)
          (<span class="org-keyword">setq</span> buffer (current-buffer))
          (<span class="org-keyword">with-temp-buffer</span>
            (<span class="org-keyword">let</span> ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
              (<span class="org-keyword">should</span> (equal (car pos) buffer))
              (<span class="org-keyword">should</span> (equal (cdr pos) 12)))))
      (fmakunbound (intern sym)))))

(<span class="org-keyword">ert-deftest</span> <span class="org-function-name">sacha-elisp&#45;&#45;find-function-search-for-symbol&#45;&#45;in-file</span> ()
  (<span class="org-keyword">let*</span> ((sym (make-temp-name <span class="org-string">"&#45;&#45;test-fn"</span>))
         (temp-file (make-temp-file
                     <span class="org-string">"test-"</span> nil <span class="org-string">".org"</span>
                     (format
                      <span class="org-string">"#+begin_src emacs-lisp\n;; Comment\n(defun %s () (message \"Hello\"))\n#+end_src"</span>
                      sym)))
         (sacha-elisp-find-function-search-extra (list temp-file))
         buffer)
    (<span class="org-keyword">unwind-protect</span>
        (<span class="org-keyword">with-temp-buffer</span>
          (<span class="org-keyword">let</span> ((pos (sacha-elisp-find-function-search-for-symbol nil (intern sym) nil nil)))
            (<span class="org-keyword">should</span> (equal (buffer-file-name (car pos)) temp-file))
            (<span class="org-keyword">should</span> (equal (cdr pos) 35))))
      (delete-file temp-file))))
</code></pre>
</div>


<div class="note">This is part of my <a href="https://sachachua.com/dotemacs#org-mode-org-babel-fix-find-function-when-i-ve-evaluated-something-from-org-babel">Emacs configuration.</a></div><div><a href="https://sachachua.com/blog/2026/04/ye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch/index.org">View Org source for this post</a></div>
<p>You can <a href="https://social.sachachua.com/@sacha/statuses/01KNFTFSC3D0K7XH1JW1XTF6QG" target="_blank" rel="noopener noreferrer">comment on Mastodon</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2026%2F04%2Fye11-fix-find-function-for-emacs-lisp-from-org-babel-or-scratch%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Org Mode: calculating table sums using tag hierarchies</title>
		<link>https://sachachua.com/blog/2025/09/org-mode-calculating-table-sums-using-tag-hierarchies/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Tue, 30 Sep 2025 14:08:45 GMT</pubDate>
    <category>org</category>
<category>elisp</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/09/org-mode-calculating-table-sums-using-tag-hierarchies/</guid>
		<description><![CDATA[<p>
While collecting posts for Emacs News, I came
across this question about <a href="https://lemmy.ml/post/36200781">adding up Org Mode
table data by tag hierarchy</a>, which might be
interesting if you want to add things up in
different combinations. I haven't needed to do
something like that myself, but I got curious
about it. It turns out that you can define a tag
hierarchy like this:
</p>


<div class="org-src-container">
<pre class="src src-org"><code><span class="org-org-meta-line">#+STARTUP: noptag</span>
<span class="org-org-meta-line">#+TAGS:</span>
<span class="org-org-meta-line">#+TAGS: [ GT1 : tagA tagC tagD ]</span>
<span class="org-org-meta-line">#+TAGS: [ GT2 : tagB tagE ]</span>
<span class="org-org-meta-line">#+TAGS: [ GT3 : tagB tagC tagD ]</span>
</code></pre>
</div>


<p>
The first two lines remove any other tags you've
defined in your config aside from those in
<code>org-tag-persistent-alist</code>, but can be omitted if
you want to also include other tags you've defined
in <code>org-tag-alist</code>. Note that it doesn't have to
be a strict tree. Tags can belong to more than one
tag group.
</p>

<p>
EduMerco wanted to know how to use those tag
groups to sum up rows in a table. I added a
<code>#+NAME</code> header to the table so that I could refer
to it with <code>:var source=source</code> later on.
</p>


<div class="org-src-container">
<pre class="src src-org"><code><span class="org-org-meta-line">#+NAME: source</span>
<span class="org-org-table">| tag  | Q1 | Q2 |</span>
<span class="org-org-table">|&#45;&#45;&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;+&#45;&#45;&#45;&#45;|</span>
<span class="org-org-table">| tagA |  9 |    |</span>
<span class="org-org-table">| tagB |  4 |  2 |</span>
<span class="org-org-table">| tagC |  1 |  4 |</span>
<span class="org-org-table">| tagD |    |  5 |</span>
<span class="org-org-table">| tagE |    |  6 |</span>
</code></pre>
</div>



<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">defun</span> <span class="org-function-name">my-sum-tag-groups</span> (source <span class="org-type">&amp;optional</span> groups)
  <span class="org-doc">"Sum up the rows in SOURCE by GROUPS.</span>
<span class="org-doc">If GROUPS is nil, use `</span><span class="org-doc"><span class="org-constant">org-tag-groups-alist</span></span><span class="org-doc">'."</span>
  (<span class="org-keyword">setq</span> groups (<span class="org-keyword">or</span> groups org-tag-groups-alist))
  (cons
   (car source)
   (mapcar
    (<span class="org-keyword">lambda</span> (tag-group)
      (<span class="org-keyword">let</span> ((tags (org&#45;&#45;tags-expand-group (list (car tag-group))
                                          groups nil)))
        (cons (car tag-group)
              (seq-map-indexed
               (<span class="org-keyword">lambda</span> (colname i)
                 (apply <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">+</span>
                        (mapcar (<span class="org-keyword">lambda</span> (tag)
                                  (<span class="org-keyword">let</span> ((val (<span class="org-keyword">or</span> (elt (assoc-default tag source) i) <span class="org-string">"0"</span>)))
                                    (<span class="org-keyword">if</span> (stringp val)
                                        (string-to-number val)
                                      (<span class="org-keyword">or</span> val 0))))
                                tags)))
               (cdr (car source))))))
    groups)))
</code></pre>
</div>


<p>
Then that can be used with the following code:
</p>


<div class="org-src-container">
<pre class="src src-org"><code><span class="org-org-block-begin-line">#+begin_src emacs-lisp :var source=source :colnames no :results table</span>
<span class="org-org-block">(my-sum-tag-groups source)</span>
<span class="org-org-block-end-line">#+end_src</span>
</code></pre>
</div>


<p>
to result in:
</p>

<table>


<colgroup>
<col class="org-left">

<col class="org-right">

<col class="org-right">
</colgroup>
<tbody>
<tr>
<td class="org-left">tag</td>
<td class="org-right">Q1</td>
<td class="org-right">Q2</td>
</tr>

<tr>
<td class="org-left">GT1</td>
<td class="org-right">10</td>
<td class="org-right">9</td>
</tr>

<tr>
<td class="org-left">GT2</td>
<td class="org-right">4</td>
<td class="org-right">8</td>
</tr>

<tr>
<td class="org-left">GT3</td>
<td class="org-right">5</td>
<td class="org-right">11</td>
</tr>
</tbody>
</table>

<p>
Because <code>org&#45;&#45;tags-expand-group</code> takes the groups
as a parameter, you could use it to sum things by
different groups. The <code>#+TAGS:</code> directives above set
<code>org-tag-groups-alist</code> to:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>((<span class="org-string">"GT1"</span> <span class="org-string">"tagA"</span> <span class="org-string">"tagC"</span> <span class="org-string">"tagD"</span>)
 (<span class="org-string">"GT2"</span> <span class="org-string">"tagB"</span> <span class="org-string">"tagE"</span>)
 (<span class="org-string">"GT3"</span> <span class="org-string">"tagB"</span> <span class="org-string">"tagC"</span> <span class="org-string">"tagD"</span>))
</code></pre>
</div>


<p>
Following the same format, we could do something like this:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(my-sum-tag-groups source <span class="org-highlight-quoted-quote">'</span>((<span class="org-string">"Main"</span> <span class="org-string">"- Subgroup 1"</span> <span class="org-string">"- Subgroup 2"</span>)
                            (<span class="org-string">"- Subgroup 1"</span> <span class="org-string">"tagA"</span> <span class="org-string">"tagB"</span>)
                            (<span class="org-string">"- Subgroup 2"</span> <span class="org-string">"tagC"</span> <span class="org-string">"tagD"</span>)
                            ))
</code></pre>
</div>


<table>


<colgroup>
<col class="org-left">

<col class="org-right">

<col class="org-right">
</colgroup>
<tbody>
<tr>
<td class="org-left">tag</td>
<td class="org-right">Q1</td>
<td class="org-right">Q2</td>
</tr>

<tr>
<td class="org-left">Main</td>
<td class="org-right">14</td>
<td class="org-right">11</td>
</tr>

<tr>
<td class="org-left">- Subgroup 1</td>
<td class="org-right">13</td>
<td class="org-right">2</td>
</tr>

<tr>
<td class="org-left">- Subgroup 2</td>
<td class="org-right">1</td>
<td class="org-right">9</td>
</tr>
</tbody>
</table>

<p>
I haven't specifically needed to add tag groups in tables myself, but I suspect the recursive expansion in <code>org&#45;&#45;tags-expand-group</code> might come in handy even in a non-Org context. Hmm&hellip;
</p>
<div><a href="https://sachachua.com/blog/2025/09/org-mode-calculating-table-sums-using-tag-hierarchies/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F09%2Forg-mode-calculating-table-sums-using-tag-hierarchies%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Emacs and dom.el: quick notes on parsing HTML and turning DOMs back into HTML</title>
		<link>https://sachachua.com/blog/2025/09/emacs-and-dom-el-quick-notes-on-parsing-html-and-turning-doms-back-into-html/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 13 Sep 2025 15:12:57 GMT</pubDate>
    <category>elisp</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/09/emacs-and-dom-el-quick-notes-on-parsing-html-and-turning-doms-back-into-html/</guid>
		<description><![CDATA[<p>
<code>libxml-parse-html-region</code> turns HTML into a DOM
(document object model). There's also
<code>xml-parse-file</code> and <code>xml-parse-region</code>.
<code>xml-parse-string</code> actually parses the character
data at point and returns it as a string instead
of parsing a string as a parameter. If you have a
string and you want to parse it, insert it into a
temporary buffer and use
<code>libxml-parse-html-region</code> or <code>xml-parse-region</code>.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">let</span> ((s <span class="org-string">"&lt;span&gt;Hello world&lt;/span&gt;"</span>)
      dom)
  (<span class="org-keyword">setq</span> dom
        (<span class="org-keyword">with-temp-buffer</span>
          (insert s)
          (libxml-parse-html-region))))
</code></pre>
</div>


<pre class="example" id="orgf0cc8dc">
(html nil (body nil (span nil Hello world)))
</pre>

<p>
Then you can use functions like <code>dom-by-tag</code>,
<code>dom-search</code>, <code>dom-attr</code>, <code>dom-children</code>, etc. If
you need to make a deep copy of the DOM, you can
use <code>copy-tree</code>.
</p>

<p>
Turning the DOM back into HTML can be a little
tricky. By default, <code>dom-print</code> escapes &amp; in
attributes, which could mess up things like href:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>  (<span class="org-keyword">with-temp-buffer</span>
    (dom-print (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">a</span> <span class="org-highlight-quoted-quote">'</span>((href . <span class="org-string">"https://example.com?a=b&amp;c=d"</span>))))
     (buffer-string))
</code></pre>
</div>


<pre class="example" id="orgffdb4ab">
  &lt;a href="https://example.com?a=b&amp;amp;c=d" /&gt;
</pre>


<p>
<code>shr-dom-print</code> handles &amp; correctly, but it adds spaces in between elements. Also, you need to escape HTML entities in text, maybe with <code>org-html-encode-plain-text</code>.
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>  (<span class="org-keyword">with-temp-buffer</span>
    (shr-dom-print
      (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">p</span> nil
                (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">span</span> nil <span class="org-string">"hello"</span>)
                (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">span</span> nil <span class="org-string">"world"</span>)
                (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">a</span> <span class="org-highlight-quoted-quote">'</span>((href . <span class="org-string">"https://example.com?a=b&amp;c=d"</span>))
                          (org-html-encode-plain-text <span class="org-string">"text &amp; stuff"</span>))))
    (buffer-string))
</code></pre>
</div>


<pre style="white-space: wrap" class="example" id="org95a1079">
  &lt;p&gt; &lt;span&gt;hello&lt;/span&gt; &lt;span&gt;world&lt;/span&gt; &lt;a href="https://example.com?a=b&amp;c=d"&gt;text &amp;amp; stuff&lt;/a&gt;&lt;/p&gt;
</pre>

<p>
<code>svg-print</code> does the right thing when it comes to href and tags, but you need to escape HTML entities yourself as usual.
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp"><code>(<span class="org-keyword">with-temp-buffer</span>
  (svg-print
   (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">p</span> nil
             (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">span</span> nil <span class="org-string">"hello"</span>)
             (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">span</span> nil <span class="org-string">"world"</span>)
             (dom-node <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">a</span> <span class="org-highlight-quoted-quote">'</span>((href . <span class="org-string">"https://example.com?a=b&amp;c=d"</span>))
                       (org-html-encode-plain-text <span class="org-string">"text &amp; stuff"</span>))))
  (buffer-string))
</code></pre>
</div>


<pre style="white-space: wrap" class="example" id="org3e3d0ec">
  &lt;p&gt;&lt;span&gt;hello&lt;/span&gt;&lt;span&gt;world&lt;/span&gt;&lt;a href="https://example.com?a=b&amp;c=d"&gt;text &amp;amp; stuff&lt;/a&gt;&lt;/p&gt;
</pre>

<p>
Looks like I'll be using <code>svg-print</code> for more than just <abbr title="Scalable Vector Graphics" tabindex="0">SVG</abbr>s.
</p>

<p>
Relevant Emacs info pages:
</p>

<ul class="org-ul">
<li><a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Document-Object-Model.html">Document Object Model (GNU Emacs Lisp Reference Manual)</a></li>
<li><a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-HTML_002fXML.html">Parsing HTML/XML (GNU Emacs Lisp Reference Manual)</a></li>
</ul>
<div><a href="https://sachachua.com/blog/2025/09/emacs-and-dom-el-quick-notes-on-parsing-html-and-turning-doms-back-into-html/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F09%2Femacs-and-dom-el-quick-notes-on-parsing-html-and-turning-doms-back-into-html%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Getting an Org link URL from a string; debugging regex groups</title>
		<link>https://sachachua.com/blog/2025/03/getting-an-org-link-url-from-a-string-debugging-regex-groups/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 06 Mar 2025 18:17:05 GMT</pubDate>
    <category>elisp</category>
<category>org</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2025/03/getting-an-org-link-url-from-a-string-debugging-regex-groups/</guid>
		<description><![CDATA[<p>
Sometimes I want to get the URL from a string
whether the string contains a bare URL
(<code>https://example.com</code>) or an Org bracketed link
(<code>[[https://example.com]]</code> or
<code>[[https://example.com][Example]]</code>, ignoring any
extra non-link text (<code>blah https://example.com
blah blah</code>). <code>org-link-any-re</code> seemed like the
right regular expression to use, but I started to
get a little dizzy looking at all the parenthesis
and I couldn't figure out which matching group to
use. I tried using <code>re-builder</code>. That highlighted
the groups in different colours, but I didn't know
what the colours meant. All the matching
information is in <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Entire-Match-Data.html">(match-data)</a>, but integer pairs
can be a little hard to translate back to
substrings. So I wrote an Emacs Lisp function to
gave me the matching groups:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-match-groups</span> (<span class="org-type">&amp;optional</span> object)
  <span class="org-doc">"Return the matching groups, good for debugging regexps."</span>
  (seq-map-indexed (<span class="org-keyword">lambda</span> (entry i)
                     (list i entry
                           (<span class="org-keyword">and</span> (car entry)
                                (<span class="org-keyword">if</span> object
                                    (substring object (car entry) (cadr entry))
                                  (buffer-substring (car entry) (cadr entry))))))
                   (seq-partition
                    (match-data t)
                    2)))
</pre>
</div>


<p>
There's probably a standard way to do this, but I
couldn't figure out how to find it.
</p>

<p>
Anyway, if I give it a string with a bracketed
link, I can tell that the URL ends up in group 2:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((text <span class="org-string">"blah [[https://example.com][example]] blah blah"</span>))
  (<span class="org-keyword">when</span> (string-match org-link-any-re text)
    (pp-to-string (my-match-groups text))))
</pre>
</div>


<pre class="example" id="org6de0ae1">
((0 (5 37) "[[https://example.com][example]]")
 (1 (5 37) "[[https://example.com][example]]")
 (2 (7 26) "https://example.com")
 (3 (28 35) "example"))
</pre>

<p>
When I use a string with a bare link, I can see
that the URL ends up in group 7:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((text <span class="org-string">"blah https://example.com blah blah"</span>))
  (<span class="org-keyword">when</span> (string-match org-link-any-re text)
    (pp-to-string (my-match-groups text))))
</pre>
</div>


<pre class="example" id="org139be00">
((0 (5 24) "https://example.com")
 (1 (nil nil) nil) (2 (nil nil) nil)
 (3 (nil nil) nil) (4 (nil nil) nil)
 (5 (nil nil) nil) (6 (nil nil) nil)
 (7 (5 24) "https://example.com")
 (8 (5 10) "https") (9 (11 24) "//example.com"))
</pre>

<p>
This makes it so much easier to refer to the right
capture group. So now I can use those groups to
extract the URL from a string:
</p>


<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-org-link-url-from-string</span> (s)
  <span class="org-doc">"Return the link URL from S."</span>
  (<span class="org-keyword">when</span> (string-match org-link-any-re s)
    (<span class="org-keyword">or</span>
     (match-string 7 s)
       (match-string 2 s))))
</pre>
</div>


<p>
This is handy when I <a href="https://sachachua.com/blog/2024/09/collecting-emacs-news-from-mastodon/">summarize Emacs News links
from Mastodon</a> or from my inbox. Sometimes I add
extra text after a link that I've captured from my
phone, and I don't want that included in the URL.
Sometimes I have a bracketed link that I've copied
from org-capture note. Now I don't have to worry
about the format. I can just grab the link I want.
</p>
<div><a href="https://sachachua.com/blog/2025/03/getting-an-org-link-url-from-a-string-debugging-regex-groups/index.org">View org source for this post</a></div>
<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2025%2F03%2Fgetting-an-org-link-url-from-a-string-debugging-regex-groups%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Updating YouTube videos via the YouTube Data API using Emacs Lisp and url-http-oauth</title>
		<link>https://sachachua.com/blog/2023/12/updating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Sat, 09 Dec 2023 16:23:48 GMT</pubDate>
    <category>elisp</category>
<category>emacs</category>
<category>emacsconf</category>
<category>youtube</category>
<category>video</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/12/updating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth/</guid>
		<description><![CDATA[<p>
We upload EmacsConf videos to both YouTube and Toobnix, which is a
PeerTube instance. This makes it easier for people to come across them
after the conference.
</p>

<p>
I can upload to Toobnix and set titles and descriptions using the
peertube-cli tool. I tried a Python script for uploading to YouTube,
but it was a bit annoying due to quota restrictions. Instead, I
uploaded the videos by dragging and dropping them into YouTube Studio.
This allowed me to upload 15 at a time.
</p>

<p>
The videos on YouTube had just the filenames. I wanted to rename the
videos and set the descriptions. In 2022, I used xdotool, simulating
mouse clicks and pasting in text for larger text blocks.
</p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>Xdotool script</strong></summary>
<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-xdotool-insert-mouse-location</span>
    (interactive)
  (<span class="org-keyword">let</span> ((pos (shell-command-to-string <span class="org-string">"xdotool getmouselocation"</span>)))
    (<span class="org-keyword">when</span> (string-match <span class="org-string">"x:</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">[0-9]+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string"> y:</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">(</span></span><span class="org-string">[0-9]+</span><span class="org-string"><span class="org-regexp-grouping-backslash">\\</span></span><span class="org-string"><span class="org-regexp-grouping-construct">)</span></span><span class="org-string">"</span> pos)
      (insert (format <span class="org-string">"(shell-command \"xdotool mousemove %s %s click 1\")\n"</span> (match-string 1 pos) (match-string 2 pos))))))

(<span class="org-keyword">setq</span> list (seq-filter (<span class="org-keyword">lambda</span> (o)
                         (<span class="org-keyword">and</span>
                          (file-exists-p
                           (expand-file-name
                            (concat (plist-get o <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;final.webm"</span>)
                            emacsconf-cache-dir))
                          (null (plist-get o <span class="org-builtin">:youtube-url</span>))))
            (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))

(<span class="org-keyword">while</span> list
  (<span class="org-keyword">progn</span>
    (shell-command <span class="org-string">"xdotool mousemove 707 812 click 1 sleep 2"</span>)

    (<span class="org-keyword">setq</span> talk (<span class="org-keyword">pop</span> list))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">click create</span>
    (shell-command <span class="org-string">"xdotool mousemove 843 187 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">video</span>
    (shell-command <span class="org-string">"xdotool mousemove 833 217 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">select files</span>
    (shell-command (concat <span class="org-string">"xdotool mousemove 491 760 click 1 sleep 4 type "</span>
                           (shell-quote-argument (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;final.webm"</span>))))
    <span class="org-comment-delimiter">;; </span><span class="org-comment">open</span>
    (shell-command <span class="org-string">"xdotool mousemove 1318 847 click 1 sleep 5"</span>)

    (kill-new (concat
               emacsconf-name <span class="org-string">" "</span>
               emacsconf-year <span class="org-string">": "</span>
               (plist-get talk <span class="org-builtin">:title</span>)
               <span class="org-string">" - "</span>
               (plist-get talk <span class="org-builtin">:speakers-with-pronouns</span>)))
    (shell-command <span class="org-string">"xdotool sleep 1 mousemove 331 440 click :1 key Ctrl+a Delete sleep 1 key Ctrl+Shift+v sleep 2"</span>)

    (kill-new (emacsconf-publish-video-description talk t))
    (shell-command <span class="org-string">"xdotool mousemove 474 632 click 1 sleep 1 key Ctrl+a sleep 1 key Delete sleep 1 key Ctrl+Shift+v"</span>))
  (read-string <span class="org-string">"Press a key once you've pasted in the description"</span>)

  <span class="org-comment-delimiter">;; </span><span class="org-comment">next</span>
  (<span class="org-keyword">when</span> (emacsconf-captions-edited-p (expand-file-name (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;main.vtt"</span>) emacsconf-cache-dir))
    (shell-command <span class="org-string">"xdotool mousemove 352 285 click 1 sleep 1"</span>)

    <span class="org-comment-delimiter">;; </span><span class="org-comment">add captions</span>
    (shell-command <span class="org-string">"xdotool mousemove 877 474 click 1 sleep 3"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 165 408 click 1 sleep 1"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 633 740 click 1 sleep 2"</span>)
    (shell-command (concat <span class="org-string">"xdotool mousemove 914 755  click 1 sleep 4 type "</span>
                           (shell-quote-argument (concat (plist-get talk <span class="org-builtin">:video-slug</span>) <span class="org-string">"&#45;&#45;main.vtt"</span>))))
    (read-string <span class="org-string">"Press a key once you've loaded the VTT"</span>)
    (shell-command <span class="org-string">"xdotool mousemove 910 1037 sleep 1 click 1 sleep 4"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">done</span>
    (shell-command <span class="org-string">"xdotool mousemove 890 297 click 1 sleep 3"</span>)
    )


  (<span class="org-keyword">progn</span>
    <span class="org-comment-delimiter">;; </span><span class="org-comment">visibility</span>
    (shell-command <span class="org-string">"xdotool mousemove 810 303 click 1 sleep 2"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">public</span>
    (shell-command <span class="org-string">"xdotool mousemove 119 614 click 1 sleep 2"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">copy</span>
    (shell-command <span class="org-string">"xdotool mousemove 882 669 click 1 sleep 1"</span>)
    <span class="org-comment-delimiter">;; </span><span class="org-comment">done</span>
    (shell-command <span class="org-string">"xdotool mousemove 908 1089 click 1 sleep 5 key Alt+Tab"</span>)

    (<span class="org-keyword">emacsconf-with-talk-heading</span> talk
      (org-entry-put (point) <span class="org-string">"YOUTUBE_URL"</span> (read-string <span class="org-string">"URL: "</span>))
      ))
  )
</pre>
</div>


</details>

<p>
Using xdotool wasn't very elegant, since I needed to figure out the
coordinates for each click. I tried using Spookfox to control Mozilla
Firefox from Emacs, but Youtube's editing interface didn't seem to
have any textboxes that I could set. I decided to use EmacsConf 2023
as an excuse to learn how to talk to the Youtube Data API, which
required figuring out OAuth. Even though it was easy to find examples
in Python and NodeJS, I wanted to see if I could stick with using
Emacs Lisp so that I could add the code to the <a href="https://git.emacsconf.org/emacsconf-el/">emacsconf-el</a> repository.
</p>

<p>
After a quick search, I picked <a href="https://elpa.gnu.org/packages/url-http-oauth.html">url-http-oauth</a> as the library that I'd
try first. I used the url-http-oauth-demo.el included in the package
to figure out what to set for the YouTube Data API. I wrote a function
to make getting the redirect URL easier
(<code>emacsconf-extract-oauth-browse-and-prompt</code>). Once I authenticated
successfully, I explored using alphapapa's plz library. It can handle
finding the JSON object and parsing it out for me. With it, I updated
videos to include titles and descriptions from my Emacs code, and I
copied the video IDs into my Org properties.
</p>

<details class="code-details" style="padding: 1em;
                 border-radius: 15px;
                 font-size: 0.9em;
                 box-shadow: 0.05em 0.1em 5px 0.01em  #00000057;">
                  <summary><strong>emacsconf-extract.el code for Youtube renaming</strong></summary>
<p>
</p><div class="org-src-container">
<pre class="src src-emacs-lisp"><span class="org-comment-delimiter">;;; </span><span class="org-comment">YouTube</span>

<span class="org-comment-delimiter">;; </span><span class="org-comment">When the token needs refreshing, delete the associated lines from</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">~/.authinfo This code just sets the title and description. Still</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">need to figure out how to properly set the license, visibility,</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">recording date, and captions.</span>
<span class="org-comment-delimiter">;;</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">To avoid being prompted for the client secret, it's helpful to have a line in ~/.authinfo or ~/.authinfo.gpg with</span>
<span class="org-comment-delimiter">;; </span><span class="org-comment">machine https://oauth2.googleapis.com/token username CLIENT_ID password CLIENT_SECRET</span>

(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-google-client-identifier</span> nil)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-api-channels</span> nil)
(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-api-categories</span> nil)

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-oauth-browse-and-prompt</span> (url)
  <span class="org-doc">"Open URL and wait for the redirected code URL."</span>
  (browse-url url)
  (read-from-minibuffer <span class="org-string">"Paste the redirected code URL: "</span>))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-setup</span> ()
  (<span class="org-keyword">interactive</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">plz</span>)
  (<span class="org-keyword">require</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">url-http-oauth</span>)
  (<span class="org-keyword">when</span> (getenv <span class="org-string">"GOOGLE_APPLICATION_CREDENTIALS"</span>)
    (<span class="org-keyword">let-alist</span> (json-read-file (getenv <span class="org-string">"GOOGLE_APPLICATION_CREDENTIALS"</span>))
      (<span class="org-keyword">setq</span> emacsconf-extract-google-client-identifier .web.client_id)))
  (<span class="org-keyword">unless</span> (url-http-oauth-interposed-p <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)
    (url-http-oauth-interpose
     <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"client-identifier"</span> . ,emacsconf-extract-google-client-identifier)
       (<span class="org-string">"resource-url"</span> . <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)
       (<span class="org-string">"authorization-code-function"</span> . emacsconf-extract-oauth-browse-and-prompt)
       (<span class="org-string">"authorization-endpoint"</span> . <span class="org-string">"https://accounts.google.com/o/oauth2/v2/auth"</span>)
       (<span class="org-string">"authorization-extra-arguments"</span> .
        ((<span class="org-string">"redirect_uri"</span> . <span class="org-string">"http://localhost:8080"</span>)))
       (<span class="org-string">"access-token-endpoint"</span> . <span class="org-string">"https://oauth2.googleapis.com/token"</span>)
       (<span class="org-string">"scope"</span> . <span class="org-string">"https://www.googleapis.com/auth/youtube"</span>)
       (<span class="org-string">"client-secret-method"</span> . prompt))))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-channels
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> <span class="org-string">"https://youtube.googleapis.com/youtube/v3/channels?part=contentDetails&amp;mine=true"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-categories
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> <span class="org-string">"https://youtube.googleapis.com/youtube/v3/videoCategories?part=snippet&amp;regionCode=CA"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>))
  (<span class="org-keyword">setq</span> emacsconf-extract-youtube-api-videos
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">get</span> (concat <span class="org-string">"https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails,status&amp;forMine=true&amp;order=date&amp;maxResults=50&amp;playlistId="</span>
                          (url-hexify-string
                           (<span class="org-keyword">let-alist</span> (elt (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> emacsconf-extract-youtube-api-channels) 0)
                             .contentDetails.relatedPlaylists.uploads)
                           ))
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>)))
          <span class="org-builtin">:as</span> <span class="org-highlight-quoted-quote">#'</span><span class="org-highlight-quoted-symbol">json-read</span>)))

(<span class="org-keyword">defvar</span> <span class="org-variable-name">emacsconf-extract-youtube-tags</span> <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"emacs"</span> <span class="org-string">"emacsconf"</span>))
(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-object</span> (video-id talk <span class="org-type">&amp;optional</span> privacy-status)
  <span class="org-doc">"Format the video object for VIDEO-ID using TALK details."</span>
  (<span class="org-keyword">setq</span> privacy-status (<span class="org-keyword">or</span> privacy-status <span class="org-string">"unlisted"</span>))
  (<span class="org-keyword">let</span> ((properties (emacsconf-publish-talk-video-properties talk <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">youtube</span>)))
    <span class="org-highlight-quoted-quote">`</span>((id . ,video-id)
      (kind . <span class="org-string">"youtube#video"</span>)
      (snippet
       (categoryId . <span class="org-string">"28"</span>)
       (title . ,(plist-get properties <span class="org-builtin">:title</span>))
       (tags . ,emacsconf-extract-youtube-tags)
       (description . ,(plist-get properties <span class="org-builtin">:description</span>))
       <span class="org-comment-delimiter">;; </span><span class="org-comment">Even though I set recordingDetails and status, it doesn't seem to stick.</span>
       <span class="org-comment-delimiter">;; </span><span class="org-comment">I'll leave this in here in case someone else can figure it out.</span>
       (recordingDetails (recordingDate . ,(format-time-string <span class="org-string">"%Y-%m-%dT%TZ"</span> (plist-get talk <span class="org-builtin">:start-time</span>) t))))
      (status (privacyStatus . <span class="org-string">"unlisted"</span>)
              (license . <span class="org-string">"creativeCommon"</span>)))))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-api-update-video</span> (video-object)
  <span class="org-doc">"Update VIDEO-OBJECT."</span>
  (<span class="org-keyword">let-alist</span> video-object
    (<span class="org-keyword">let*</span> ((slug (<span class="org-keyword">cond</span>
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">not yet renamed</span>
                  ((string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year) <span class="org-string">" "</span>
                                     (group (1+ (<span class="org-keyword">or</span> (syntax word) <span class="org-string">"-"</span>)))
                                     <span class="org-string">"  "</span>)
                                 .snippet.title)
                   (match-string 1 .snippet.title))
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">renamed, match the description instead</span>
                  ((string-match (<span class="org-keyword">rx</span> (literal emacsconf-base-url) (literal emacsconf-year) <span class="org-string">"/talks/"</span>
                                     (group (1+ (<span class="org-keyword">or</span> (syntax word) <span class="org-string">"-"</span>))))
                                 .snippet.description)
                   (match-string 1 .snippet.description))
                  <span class="org-comment-delimiter">;; </span><span class="org-comment">can't find, prompt</span>
                  (t
                   (<span class="org-keyword">when</span> (string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year))
                                       .snippet.title)
                     (completing-read (format <span class="org-string">"Slug for %s: "</span>
                                              .snippet.title)
                                      (seq-map (<span class="org-keyword">lambda</span> (o) (plist-get o <span class="org-builtin">:slug</span>))
                                               (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))))))
           (video-id .snippet.resourceId.videoId)
           (id .id)
           result)
      (<span class="org-keyword">when</span> slug
        <span class="org-comment-delimiter">;; </span><span class="org-comment">set the YOUTUBE_URL property</span>
        (<span class="org-keyword">emacsconf-with-talk-heading</span> slug
          (org-entry-put (point) <span class="org-string">"YOUTUBE_URL"</span> (concat <span class="org-string">"https://www.youtube.com/watch?v="</span> video-id))
          (org-entry-put (point) <span class="org-string">"YOUTUBE_ID"</span> id))
        (plz <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">put</span> <span class="org-string">"https://www.googleapis.com/youtube/v3/videos?part=snippet,recordingDetails,status"</span>
          <span class="org-builtin">:headers</span> <span class="org-highlight-quoted-quote">`</span>((<span class="org-string">"Authorization"</span> . ,(url-oauth-auth <span class="org-string">"https://youtube.googleapis.com/youtube/v3/"</span>))
                     (<span class="org-string">"Accept"</span> . <span class="org-string">"application/json"</span>)
                     (<span class="org-string">"Content-Type"</span> . <span class="org-string">"application/json"</span>))
          <span class="org-builtin">:body</span> (json-encode (emacsconf-extract-youtube-object video-id (emacsconf-resolve-talk slug))))))))

(<span class="org-keyword">defun</span> <span class="org-function-name">emacsconf-extract-youtube-rename-videos</span> (<span class="org-type">&amp;optional</span> videos)
  <span class="org-doc">"Rename videos and set the YOUTUBE_URL property in the Org heading."</span>
  (<span class="org-keyword">let</span> ((info (emacsconf-get-talk-info)))
    (mapc
     (<span class="org-keyword">lambda</span> (video)
       (<span class="org-keyword">when</span> (string-match (<span class="org-keyword">rx</span> (literal emacsconf-id) <span class="org-string">" "</span> (literal emacsconf-year)))
         (emacsconf-extract-youtube-api-update-video video)))
     (assoc-default <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">items</span> (<span class="org-keyword">or</span> videos emacsconf-extract-youtube-api-videos)))))

(<span class="org-keyword">provide</span> <span class="org-highlight-quoted-quote">'</span><span class="org-constant">emacsconf-extract</span>)

</pre>
</div>

<p></p>


</details>

<p>
I haven't quite figured out how to set <code>status</code> and <code>recordingDetails</code>
properly. The code sets them, but they don't stick. That's okay. I
think I can set those as a batch operation. <a href="https://www.reddit.com/r/youtube/comments/1289op8/how_do_i_get_the_bulk_edit_feature_in_studio_to/">It looks like I need to
change visibility one by one, though</a>, which might be a good
opportunity to check the end of the video for anything that needs to
be trimmed off.
</p>

<p>
I also want to figure out how to upload captions. I'm not entirely
sure how to do multipart form data yet with the <code>url</code> library or
<code>plz</code>. It might be nice to someday set up an HTTP server so that Emacs
can handle OAuth redirects itself. I'll save that for another blog
post and share my notes for now.
</p>

<p>
This code is in <a href="https://git.emacsconf.org/emacsconf-el/tree/emacsconf-extract.el">emacsconf-extract.el</a>.
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F12%2Fupdating-youtube-videos-via-the-youtube-data-api-using-emacs-lisp-and-url-http-oauth%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Checking image sizes and aspect ratios in Emacs Lisp so that I can automatically smartcrop them</title>
		<link>https://sachachua.com/blog/2023/01/checking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Mon, 30 Jan 2023 00:29:43 GMT</pubDate>
    <category>elisp</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/01/checking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them/</guid>
		<description><![CDATA[<p>
A+ occasionally likes to flip through pictures in a photo album. I
want to print another batch of 4x6 photos, and I'd like to crop them
before labeling them with the date from the EXIF info. Most of the
pictures are from my phone, so I have a 4:3 aspect ratio instead of
the 3:2 aspect ratio I want for prints.
</p>

<p>
First step: figuring out how to get the size of an image. I could
either use Emacs's built-in <code>image-size</code> function or call
ImageMagick's <code>identify</code> command. Which one's faster? First, I define
the functions:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-image-size</span> (filename)
  (<span class="org-keyword">let</span> ((img (create-image filename)))
    (<span class="org-keyword">prog1</span> (image-size img t) (image-flush img))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-identify-image-size</span> (filename)
  (<span class="org-keyword">let</span> ((result
         (seq-map <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">string-to-number</span>
                  (split-string
                   (shell-command-to-string
                    (concat <span class="org-string">"identify -format \"%w %h\" "</span> (shell-quote-argument filename)))))))
    (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> result (&gt; (car result) 0))
      result)))
</pre>
</div>

<p>
and then benchmark them:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((filename <span class="org-string">"~/Downloads/Other prints/20230102_135059.MP.jpg"</span>)
      (times 10))
  (list (benchmark times <span class="org-highlight-quoted-quote">`</span>(my-image-size ,filename))
        (benchmark times <span class="org-highlight-quoted-quote">`</span>(my-identify-image-size ,filename))))
</pre>
</div>

<p>
Looks like ImageMagick's <code>identify</code> command is a lot faster.
Now I can define a filter:
</p>

<details><summary>Code for aspect ratios</summary><div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defun</span> <span class="org-function-name">my-aspect-ratio</span> (normalize <span class="org-type">&amp;rest</span> args)
  <span class="org-doc">"Return the aspect ratio of ARGS.</span>
<span class="org-doc">If NORMALIZE is non-nil, return an aspect ratio &gt;= 1 (width is greater than height).</span>
<span class="org-doc">ARGS can be:</span>
<span class="org-doc">- width height</span>
<span class="org-doc">- a filename</span>
<span class="org-doc">- a list of (width height)"</span>
  (<span class="org-keyword">let</span> (size width height result)
    (<span class="org-keyword">cond</span>
     ((stringp (car args))
      (<span class="org-keyword">setq</span> size (my-identify-image-size (car args)))
      (<span class="org-keyword">setq</span> width (car size) height (cadr size)))
     ((listp (car args))
      (<span class="org-keyword">setq</span> width (car (car args)) height (cadr (car args))))
     (t
      (<span class="org-keyword">setq</span> width (car args) height (cadr args))))
    (<span class="org-keyword">when</span> (<span class="org-keyword">and</span> width height)
      (<span class="org-keyword">setq</span> result (/ (* 1.0 width) height))
      (<span class="org-keyword">if</span> (<span class="org-keyword">and</span> normalize (&lt; result 1))
          (/ 1 result)
        result))))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-files-not-matching-aspect-ratio</span> (print-width print-height file-list)
  (<span class="org-keyword">let</span> ((target-aspect-ratio (my-aspect-ratio t print-width print-height)))
    (seq-filter
     (<span class="org-keyword">lambda</span> (filename)
       (<span class="org-keyword">let</span> ((image-ratio (my-aspect-ratio t filename)))
         (<span class="org-keyword">when</span> image-ratio
           (&gt; (abs (- image-ratio
                      target-aspect-ratio))
              0.001))))
     file-list)))
</pre>
</div></details>

<p>
and I could use it like this to get a list of files that need to be cropped:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(my-files-not-matching-aspect-ratio 4 6 (directory-files <span class="org-string">"~/Downloads/Other prints"</span> t))
</pre>
</div>

<p>
&#x2026; which is most of the pictures, so let's see if I can get <a href="https://github.com/jwagner/smartcrop-cli">smartcrop</a>
to automatically crop them as a starting point. I used <code>npm install -g
smartcrop-cli node-opencv</code> to install the Node packages I needed, and
then I defined these functions:
</p>

<details><summary>Code for cropping</summary><div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">defvar</span> <span class="org-variable-name">my-smartcrop-image-command</span> <span class="org-highlight-quoted-quote">'</span>(<span class="org-string">"smartcrop"</span> <span class="org-string">"&#45;&#45;faceDetection"</span>))

(<span class="org-keyword">defun</span> <span class="org-function-name">my-smartcrop-image</span> (filename aspect-ratio output-file <span class="org-type">&amp;optional</span> do-copy)
  <span class="org-doc">"Call smartcrop command to crop FILENAME to ASPECT-RATIO if needed.</span>
<span class="org-doc">Write the result to OUTPUT-FILE.</span>
<span class="org-doc">If DO-COPY is non-nil, copy files if they already have the correct aspect ratio."</span>
  (<span class="org-keyword">when</span> (file-directory-p output-file)
    (<span class="org-keyword">setq</span> output-file (expand-file-name (file-name-nondirectory filename)
                                        output-file)))
  (<span class="org-keyword">let*</span> ((size (my-identify-image-size filename))
         (image-ratio (my-aspect-ratio t size))
         new-height new-width
         buf)
    (<span class="org-keyword">when</span> image-ratio
      (<span class="org-keyword">if</span> (&lt; (abs (- image-ratio aspect-ratio)) 0.01)
          (<span class="org-keyword">when</span> do-copy (copy-file filename output-file t))
        (<span class="org-keyword">with-current-buffer</span> (get-buffer-create <span class="org-string">"*smartcrop*"</span>)
          (erase-buffer)
          (<span class="org-keyword">setq</span> new-width
                (number-to-string
                 (floor (min
                         (car size)
                         (*
                          (cadr size)
                          (<span class="org-keyword">if</span> (&gt; (car size) (cadr size))
                              aspect-ratio
                            (/ 1.0 aspect-ratio))))))
                new-height
                (number-to-string
                 (floor (min
                         (cadr size)
                         (/
                          (car size)
                          (<span class="org-keyword">if</span> (&gt; (car size) (cadr size))
                              aspect-ratio
                            (/ 1.0 aspect-ratio)))))))
          (message <span class="org-string">"%d %d -&gt; %s %s: %s"</span> (car size) (cadr size) new-width new-height filename)
          (apply <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">call-process</span>
           (car
            my-smartcrop-image-command)
           nil t t
           (append
            (cdr my-smartcrop-image-command)
            (list
             <span class="org-string">"&#45;&#45;width"</span>
             new-width
             <span class="org-string">"&#45;&#45;height"</span>
             new-height
             filename
             output-file))))))))
</pre>
</div></details>

<p>
so that I could use this code to process the files:
</p>

<div class="org-src-container">
<pre class="src src-emacs-lisp">(<span class="org-keyword">let</span> ((aspect-ratio (my-aspect-ratio t 4 6))
      (output-dir <span class="org-string">"~/Downloads/Other prints/cropped"</span>))
  (mapc (<span class="org-keyword">lambda</span> (file)
          (<span class="org-keyword">unless</span> (file-exists-p (expand-file-name (file-name-nondirectory file) output-dir))
            (my-smartcrop-image file  aspect-ratio output-dir t)))
        (directory-files <span class="org-string">"~/Downloads/Other prints"</span> t)))
</pre>
</div>

<p>
Then I can use <a href="https://geeqie.org">Geeqie</a> to review the cropped images and straighten or re-crop specific ones with <a href="https://shotwell-project.org/doc/html/">Shotwell</a>.
</p>

<p>
It looks like smartcrop removes the exif information (including
original date), so I want to copy that info again.
</p>

<div class="org-src-container">
<pre class="src src-sh"><span class="org-keyword">for</span> FILE<span class="org-keyword"> in</span> *; <span class="org-keyword">do</span> exiftool -TagsFromFile <span class="org-string">"../$FILE"</span> <span class="org-string">"-all:all&gt;all:all"</span> <span class="org-string">"exif/$FILE"</span>; <span class="org-keyword">done</span>
</pre>
</div>

<p>
And then finally, I can add the labels with this <a href="https://sachachua.com/blog/2023/01/checking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them/add-labels.py">add-labels.py</a> script, which I call with <code>add-labels.py output-dir file1 file2 file3...</code>.
</p>

<details><summary>add-labels.py: add the date to the lower left corner</summary><div class="org-src-container">
<pre class="src src-python"><span class="org-comment-delimiter">#</span><span class="org-comment">!/usr/bin/python3</span>
<span class="org-keyword">import</span> sys
<span class="org-keyword">import</span> PIL
<span class="org-keyword">import</span> PIL.Image <span class="org-keyword">as</span> Image
<span class="org-keyword">import</span> PIL.ImageDraw <span class="org-keyword">as</span> ImageDraw
<span class="org-keyword">import</span> PIL.ImageFont <span class="org-keyword">as</span> ImageFont
<span class="org-keyword">from</span> PIL <span class="org-keyword">import</span> Image, ExifTags
<span class="org-keyword">import</span> re
<span class="org-keyword">import</span> os

<span class="org-comment-delimiter"># </span><span class="org-comment">use: add-labels.py output-dir photo1 photo2 photo3</span>
<span class="org-variable-name">PHOTO_DIR</span> <span class="org-operator">=</span> <span class="org-string">"/home/sacha/photos/"</span>
<span class="org-variable-name">OUTPUT_DIR</span> <span class="org-operator">=</span> sys.argv[1]
<span class="org-variable-name">OUTPUT_WIDTH</span> <span class="org-operator">=</span> 6
<span class="org-variable-name">OUTPUT_HEIGHT</span> <span class="org-operator">=</span> 4
<span class="org-variable-name">OUTPUT_RATIO</span> <span class="org-operator">=</span> OUTPUT_WIDTH <span class="org-operator">*</span> 1.0 <span class="org-operator">/</span> OUTPUT_HEIGHT
<span class="org-variable-name">font_fname</span> <span class="org-operator">=</span> <span class="org-string">"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"</span>
<span class="org-variable-name">ALWAYS</span> <span class="org-operator">=</span> <span class="org-constant">True</span>
<span class="org-variable-name">DO_ROTATE</span> <span class="org-operator">=</span> <span class="org-constant">False</span>

<span class="org-keyword">def</span> <span class="org-function-name">label_image</span>(filename):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">numbers</span> <span class="org-operator">=</span> re.sub(r<span class="org-string">'[^0-9]'</span>, <span class="org-string">''</span>, filename)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">img</span> <span class="org-operator">=</span> Image.<span class="org-builtin">open</span>(filename)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">exif</span> <span class="org-operator">=</span> {
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   PIL.ExifTags.TAGS[k]: v
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> k, v <span class="org-keyword">in</span> img._getexif().items()        <span class="org-keyword">if</span> k <span class="org-keyword">in</span> PIL.ExifTags.TAGS
<span class="org-highlight-indentation"> </span>   }
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> DO_ROTATE:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Rotate image</span>
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> exif[<span class="org-string">'Orientation'</span>] <span class="org-operator">==</span> 3:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">img</span> <span class="org-operator">=</span> img.rotate(180, expand<span class="org-operator">=</span><span class="org-constant">True</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">elif</span> exif[<span class="org-string">'Orientation'</span>] <span class="org-operator">==</span> 6:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">img</span> <span class="org-operator">=</span> img.rotate(270, expand<span class="org-operator">=</span><span class="org-constant">True</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">elif</span> exif[<span class="org-string">'Orientation'</span>] <span class="org-operator">==</span> 8:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">img</span> <span class="org-operator">=</span> img.rotate(90, expand<span class="org-operator">=</span><span class="org-constant">True</span>)    
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">Label</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">time</span> <span class="org-operator">=</span> exif[<span class="org-string">'DateTimeOriginal'</span>][0:10].replace(<span class="org-string">':'</span>, <span class="org-string">'-'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-keyword">not</span> time:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> <span class="org-builtin">len</span>(numbers) <span class="org-operator">&gt;=</span> 10 <span class="org-keyword">and</span> numbers[0:4] <span class="org-operator">&gt;=</span> <span class="org-string">'2016'</span> <span class="org-keyword">and</span> numbers[0:4] <span class="org-operator">&lt;</span> <span class="org-string">'2025'</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">time</span> <span class="org-operator">=</span> <span class="org-string">'%s-%s-%s'</span> <span class="org-operator">%</span> (numbers[0:4], numbers[4:6], numbers[6:8])
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">new_filename</span> <span class="org-operator">=</span> os.path.join(OUTPUT_DIR, time <span class="org-operator">+</span> <span class="org-string">' '</span> <span class="org-operator">+</span> os.path.basename(filename))
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> ALWAYS <span class="org-keyword">or</span> <span class="org-keyword">not</span> os.path.isfile(new_filename):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> <span class="org-operator">=</span> add_label(img, time)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(filename, time)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   out.save(new_filename)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> new_filename

<span class="org-keyword">def</span> <span class="org-function-name">add_label</span>(img, caption):
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">draw</span> <span class="org-operator">=</span> ImageDraw.Draw(img)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">w</span>, <span class="org-variable-name">h</span> <span class="org-operator">=</span> img.size
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">border</span> <span class="org-operator">=</span> <span class="org-builtin">int</span>(<span class="org-builtin">min</span>(w, h) <span class="org-operator">*</span> 0.02)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">font_size</span> <span class="org-operator">=</span> <span class="org-builtin">int</span>(<span class="org-builtin">min</span>(w, h) <span class="org-operator">*</span> 0.04)
<span class="org-highlight-indentation"> </span>   <span class="org-comment-delimiter"># </span><span class="org-comment">print(w, h, font_size)</span>
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">font</span> <span class="org-operator">=</span> ImageFont.truetype(font_fname, font_size)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">_</span>, <span class="org-variable-name">_</span>, <span class="org-variable-name">text_w</span>, <span class="org-variable-name">text_h</span> <span class="org-operator">=</span> draw.textbbox((0, 0), caption, font)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">overlay</span> <span class="org-operator">=</span> Image.new(<span class="org-string">'RGBA'</span>, (w, h))
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">draw</span> <span class="org-operator">=</span> ImageDraw.Draw(overlay)
<span class="org-highlight-indentation"> </span>   draw.rectangle([(border, h <span class="org-operator">-</span> text_h <span class="org-operator">-</span> 2 <span class="org-operator">*</span> border),
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   (text_w <span class="org-operator">+</span> 3 <span class="org-operator">*</span> border, h <span class="org-operator">-</span> border)],
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>  fill<span class="org-operator">=</span>(255, 255, 255, 128))
<span class="org-highlight-indentation"> </span>   draw.text((border <span class="org-operator">*</span> 2, h <span class="org-operator">-</span> text_h <span class="org-operator">-</span> 2 <span class="org-operator">*</span> border), caption, (0, 0, 0), font<span class="org-operator">=</span>font)
<span class="org-highlight-indentation"> </span>   <span class="org-variable-name">out</span> <span class="org-operator">=</span> Image.alpha_composite(img.convert(<span class="org-string">'RGBA'</span>), overlay).convert(<span class="org-string">'RGB'</span>)
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">return</span> out

<span class="org-keyword">if</span> <span class="org-builtin">len</span>(sys.argv) <span class="org-operator">&gt;=</span> 2:
<span class="org-highlight-indentation"> </span>   <span class="org-keyword">for</span> a <span class="org-keyword">in</span> sys.argv[2:]:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">if</span> ALWAYS <span class="org-keyword">or</span> <span class="org-keyword">not</span> os.path.exists(a):
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(a)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">try</span>:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   label_image(a)
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-keyword">except</span> <span class="org-type">Exception</span> <span class="org-keyword">as</span> e:
<span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-highlight-indentation"> </span>   <span class="org-builtin">print</span>(<span class="org-string">"Error"</span>, a, e)
</pre>
</div></details>

<p>
I hope it all works out, since I've just ordered 120 4x6 prints
covering the past three years or so&#x2026;
</p>

<p>You can <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F01%2Fchecking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item><item>
		<title>Making highlight-sexp follow modus-themes-toggle</title>
		<link>https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/</link>
		<dc:creator><![CDATA[Sacha Chua]]></dc:creator>
		<pubDate>Thu, 26 Jan 2023 15:25:38 GMT</pubDate>
    <category>elisp</category>
<category>emacs</category>
		<guid isPermaLink="false">https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/</guid>
		<description><![CDATA[<div class="update" id="orgc9d7be3">
<p>
<span class="timestamp-wrapper"><span class="timestamp">[2023-01-27 Fri] </span></span> Prot just added a <a href="https://github.com/protesilaos/modus-themes/commit/0ca79257ef941ff5f9ec34f5d76eed2ff35d7752">modus-themes-get-color-value</a>
function. Yay! Also, it turns out that I need to update the overlay in
all the buffers.
</p>

</div>

<p>
I'm experimenting with using the <code>highlight-sexp</code> minor mode to
highlight my current s-expression, since I sometimes get confused
about what I'm modifying with smartparens. The highlight-sexp
background colour is hardcoded in the variable
<code>hl-sexp-background-color</code>, and will probably look terrible if you use
a light background. I wanted it to adapt when I use
<code>modus-themes-toggle</code>. Here's how that works:
</p>

<div class="org-src-container">
<label class="org-src-name"><span class="listing-number">Listing 1: </span>highlight-sexp demonstration</label><pre class="src src-emacs-lisp">(<span class="org-keyword">use-package</span> <span class="org-constant">highlight-sexp</span>
  <span class="org-builtin">:quelpa</span>
  (highlight-sexp <span class="org-builtin">:repo</span> <span class="org-string">"daimrod/highlight-sexp"</span> <span class="org-builtin">:fetcher</span> github <span class="org-builtin">:version</span> original)
  <span class="org-builtin">:hook</span>
  (emacs-lisp-mode . highlight-sexp-mode)
  <span class="org-builtin">:config</span>
  (<span class="org-keyword">defun</span> <span class="org-function-name">my-hl-sexp-update-overlay</span> ()
    (<span class="org-keyword">when</span> (overlayp hl-sexp-overlay)
      (overlay-put
       hl-sexp-overlay
       <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">face</span>
       <span class="org-highlight-quoted-quote">`</span>(<span class="org-builtin">:background</span>        
         ,(<span class="org-keyword">if</span> (fboundp <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-themes-get-color-value</span>)
              (modus-themes-get-color-value <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">bg-inactive</span>)
            (car
             (assoc-default
              <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">bg-inactive</span>
              (modus-themes&#45;&#45;current-theme-palette))))))))
  (<span class="org-keyword">defun</span> <span class="org-function-name">my-hl-sexp-update-all-overlays</span> ()
    (<span class="org-keyword">dolist</span> (buf (buffer-list))
      (<span class="org-keyword">with-current-buffer</span> buf
        (<span class="org-keyword">when</span> highlight-sexp-mode
          (my-hl-sexp-update-overlay)))))
  (advice-add <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">hl-sexp-create-overlay</span> <span class="org-builtin">:after</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-hl-sexp-update-overlay</span>)
  (advice-add <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">modus-themes-toggle</span> <span class="org-builtin">:after</span> <span class="org-highlight-quoted-quote">'</span><span class="org-highlight-quoted-symbol">my-hl-sexp-update-all-overlays</span>))
</pre>
</div>

<p>
This is what it looks like:
</p>


<figure id="org2c1dcf4">
<img src="https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/highlight-sexp.gif" alt="highlight-sexp.gif">

<figcaption><span class="figure-number">Figure 1: </span>Animation of highlight-sexp toggling along with modus-themes-toggle</figcaption>
</figure>

<div class="note">This is part of my <a href="https://sachachua.com/dotemacs">Emacs configuration.</a></div>
<p>You can <a href="https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/#comment">view 2 comments</a> or <a href="mailto:sacha@sachachua.com?subject=Comment%20on%20https%3A%2F%2Fsachachua.com%2Fblog%2F2023%2F01%2Fmaking-highlight-sexp-follow-modus-themes-toggle%2F&body=Name%20you%20want%20to%20be%20credited%20by%20(if%20any)%3A%20%0AMessage%3A%20%0ACan%20I%20share%20your%20comment%20so%20other%20people%20can%20learn%20from%20it%3F%20Yes%2FNo%0A">e-mail me at sacha@sachachua.com</a>.</p>]]></description>
		</item>
	</channel>
</rss>