Categories: geek » emacs

RSS - Atom - Subscribe via email

Using Spookfox to scroll Firefox up and down from Emacs

| web, emacs

I open lots of pages in the process of making Emacs News. I like to open the pages in Mozilla Firefox, but I want the keyboard focus to stay with Emacs so that I can quickly categorize the links. I also sometimes want to scroll the page up or down. While reading the Reading, and not forgetting post, I came across Spookfox, which bridges Emacs and Firefox using an Firefox add-on and websockets. After I started spookfox and connected to it by clicking on the extension in Firefox, I was able to interact with it from Emacs Lisp. I feel a little nervous about it security-wise, but at least it's only listening on the local port. There might be another way to do it with the Marionette support in Firefox, but I haven't looked into it yet.

(use-package spookfox
  :quelpa (spookfox :fetcher github :repo "bitspook/spookfox"
                    :files ("lisp/*.el" "lisp/apps/*.el"))
  :when my-laptop-p
  :config
  (require 'spookfox-tabs)
  (require 'spookfox-org-tabs)
  (require 'spookfox-js-injection)
  (add-to-list 'spookfox-enabled-apps 'spookfox-tabs)
  (add-to-list 'spookfox-enabled-apps 'spookfox-org-tabs)
  (add-to-list 'spookfox-enabled-apps 'spookfox-js-injection)
  ;; (spookfox-init) ; don't automatically enable it; run (spookfox-init) to manually enable
  )

Anyway, this code seems to do the job of scrolling my Firefox window:

(defun my-spookfox-scroll-down ()
  (interactive)
  (spookfox-eval-js-in-active-tab "window.scrollBy(0, document.documentElement.clientHeight);"))

(defun my-spookfox-scroll-up ()
  (interactive)
  (spookfox-eval-js-in-active-tab "window.scrollBy(0, -document.documentElement.clientHeight);"))

(global-set-key (kbd "C-s-v") 'my-spookfox-scroll-down)
(global-set-key (kbd "C-s-S-v") 'my-spookfox-scroll-up)

This code opens a tab without switching keyboard focus away from Emacs:

(defun my-spookfox-background-tab (url &rest args)
  "Open URL as a background tab."
  (if spookfox--connected-clients
      (spookfox-tabs--request (cl-first spookfox--connected-clients) "OPEN_TAB" `(:url ,url))
    (browse-url url)))

My Emacs News code for processing my upvoted Reddit posts can automatically grab the links from Reddit link posts, but sometimes people post Reddit text or image posts and then include the link to the actual project in the post body or a comment instead.

(defun my-spookfox-get-links ()
  (seq-uniq
   (spookfox-eval-js-in-active-tab "[...(document.querySelector('[data-testid=post-container]')?.parentElement || document).querySelectorAll('a')].map(a => a.href).filter(a => a && !a.match(/redd\.?it/) && !a.match(window.location.host))" t)))
;;https://emacs.stackexchange.com/questions/41801/how-to-stop-completing-read-ivy-completing-read-from-sorting
(defun my-presorted-completion-table (completions)
  (lambda (string pred action)
    (if (eq action 'metadata)
        '(metadata
         (cycle-sort-function . identity)
         (display-sort-function . identity))
      (complete-with-action action completions string pred))))

(defun my-spookfox-insert-link-from-page ()
  (interactive)
  (let* ((links (my-spookfox-get-links))
         (link (completing-read
                "Link: "
                (my-presorted-completion-table
                 links))))
    (insert (org-link-make-string link (my-page-title link)))))
This is part of my Emacs configuration.

2023-01-30 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

Checking image sizes and aspect ratios in Emacs Lisp so that I can automatically smartcrop them

| emacs

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.

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

(defun my-image-size (filename)
  (let ((img (create-image filename)))
    (prog1 (image-size img t) (image-flush img))))

(defun my-identify-image-size (filename)
  (let ((result
         (seq-map 'string-to-number
                  (split-string
                   (shell-command-to-string
                    (concat "identify -format \"%w %h\" " (shell-quote-argument filename)))))))
    (when (and result (> (car result) 0))
      result)))

and then benchmark them:

(let ((filename "~/Downloads/Other prints/20230102_135059.MP.jpg")
      (times 10))
  (list (benchmark times `(my-image-size ,filename))
        (benchmark times `(my-identify-image-size ,filename))))

Looks like ImageMagick's identify command is a lot faster. Now I can define a filter:

Code for aspect ratios
(defun my-aspect-ratio (normalize &rest args)
  "Return the aspect ratio of ARGS.
If NORMALIZE is non-nil, return an aspect ratio >= 1 (width is greater than height).
ARGS can be:
- width height
- a filename
- a list of (width height)"
  (let (size width height result)
    (cond
     ((stringp (car args))
      (setq size (my-identify-image-size (car args)))
      (setq width (car size) height (cadr size)))
     ((listp (car args))
      (setq width (car (car args)) height (cadr (car args))))
     (t
      (setq width (car args) height (cadr args))))
    (when (and width height)
      (setq result (/ (* 1.0 width) height))
      (if (and normalize (< result 1))
          (/ 1 result)
        result))))

(defun my-files-not-matching-aspect-ratio (print-width print-height file-list)
  (let ((target-aspect-ratio (my-aspect-ratio t print-width print-height)))
    (seq-filter
     (lambda (filename)
       (let ((image-ratio (my-aspect-ratio t filename)))
         (when image-ratio
           (> (abs (- image-ratio
                      target-aspect-ratio))
              0.001))))
     file-list)))

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

(my-files-not-matching-aspect-ratio 4 6 (directory-files "~/Downloads/Other prints" t))

… which is most of the pictures, so let's see if I can get smartcrop to automatically crop them as a starting point. I used npm install -g smartcrop-cli node-opencv to install the Node packages I needed, and then I defined these functions:

Code for cropping
(defvar my-smartcrop-image-command '("smartcrop" "--faceDetection"))

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

so that I could use this code to process the files:

(let ((aspect-ratio (my-aspect-ratio t 4 6))
      (output-dir "~/Downloads/Other prints/cropped"))
  (mapc (lambda (file)
          (unless (file-exists-p (expand-file-name (file-name-nondirectory file) output-dir))
            (my-smartcrop-image file  aspect-ratio output-dir t)))
        (directory-files "~/Downloads/Other prints" t)))

Then I can use Geeqie to review the cropped images and straighten or re-crop specific ones with Shotwell.

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

for FILE in *; do exiftool -TagsFromFile "../$FILE" "-all:all>all:all" "exif/$FILE"; done

And then finally, I can add the labels with this add-labels.py script, which I call with add-labels.py output-dir file1 file2 file3....

add-labels.py: add the date to the lower left corner
#!/usr/bin/python3
import sys
import PIL
import PIL.Image as Image
import PIL.ImageDraw as ImageDraw
import PIL.ImageFont as ImageFont
from PIL import Image, ExifTags
import re
import os

# use: add-labels.py output-dir photo1 photo2 photo3
PHOTO_DIR = "/home/sacha/photos/"
OUTPUT_DIR = sys.argv[1]
OUTPUT_WIDTH = 6
OUTPUT_HEIGHT = 4
OUTPUT_RATIO = OUTPUT_WIDTH * 1.0 / OUTPUT_HEIGHT
font_fname = "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
ALWAYS = True
DO_ROTATE = False

def label_image(filename):
    numbers = re.sub(r'[^0-9]', '', filename)
    img = Image.open(filename)
    exif = {
        PIL.ExifTags.TAGS[k]: v
        for k, v in img._getexif().items()        if k in PIL.ExifTags.TAGS
    }
    if DO_ROTATE:
        # Rotate image
        if exif['Orientation'] == 3:
            img = img.rotate(180, expand=True)
        elif exif['Orientation'] == 6:
            img = img.rotate(270, expand=True)
        elif exif['Orientation'] == 8:
            img = img.rotate(90, expand=True)    
    # Label
    time = exif['DateTimeOriginal'][0:10].replace(':', '-')
    if not time:
        if len(numbers) >= 10 and numbers[0:4] >= '2016' and numbers[0:4] < '2025':
            time = '%s-%s-%s' % (numbers[0:4], numbers[4:6], numbers[6:8])
    new_filename = os.path.join(OUTPUT_DIR, time + ' ' + os.path.basename(filename))
    if ALWAYS or not os.path.isfile(new_filename):
        out = add_label(img, time)
        print(filename, time)
        out.save(new_filename)
        return new_filename

def add_label(img, caption):
    draw = ImageDraw.Draw(img)
    w, h = img.size
    border = int(min(w, h) * 0.02)
    font_size = int(min(w, h) * 0.04)
    # print(w, h, font_size)
    font = ImageFont.truetype(font_fname, font_size)
    _, _, text_w, text_h = draw.textbbox((0, 0), caption, font)
    overlay = Image.new('RGBA', (w, h))
    draw = ImageDraw.Draw(overlay)
    draw.rectangle([(border, h - text_h - 2 * border),
                    (text_w + 3 * border, h - border)],
                   fill=(255, 255, 255, 128))
    draw.text((border * 2, h - text_h - 2 * border), caption, (0, 0, 0), font=font)
    out = Image.alpha_composite(img.convert('RGBA'), overlay).convert('RGB')
    return out

if len(sys.argv) >= 2:
    for a in sys.argv[2:]:
        if ALWAYS or not os.path.exists(a):
            print(a)
            try:
                label_image(a)
            except Exception as e:
                print("Error", a, e)

I hope it all works out, since I've just ordered 120 4x6 prints covering the past three years or so…

Adding a custom header argument to Org Mode source blocks and using that argument during export

| org, emacs

I sometimes want to put long source blocks in a <details><summary>...</summary>...</details> block when I export to HTML, so that they're tucked away in a collapsible block. I tried using https://github.com/alhassy/org-special-block-extras to define my own #+begin_my_details "summary text" ... #+end_my_details block, but source blocks inside my_details doesn't get fontlocked properly while in the Org file. I wanted to add a :summary attribute to the regular src blocks, and to change the HTML export to wrap the code in details if the summary was specified.

Code for adding a :summary argument and using it during export
(setq org-babel-exp-code-template "#+begin_src %lang%switches%flags :summary %summary\n%body\n#+end_src")
(defun my-org-html-src-block (src-block _contents info)
  (let* ((result (org-html-src-block src-block _contents info))
         (block-info
          (org-with-point-at (org-element-property :begin src-block)
            (org-babel-get-src-block-info)))         
         (summary (assoc-default :summary (elt block-info 2))))
    (if (member summary '("%summary" ""))
        result
      (format "<details><summary>%s</summary>%s</details>"
              summary
              result))))
(with-eval-after-load 'ox-html
  (map-put! 
   (org-export-backend-transcoders (org-export-get-backend 'html))
   'src-block 'my-org-html-src-block))

So now I can use it by specifying blocks like this:

#+begin_src emacs-lisp :summary "Code for adding a :summary argument and using it during export"
;; code goes here
#+end_src

It took me a bit of digging around to figure this out. When I added the :summary attribute, org-babel-get-src-block-info found it when I was in the Org file, but by the time my-org-html-src-block was called, the block had been replaced with a copy that didn't have the header argument. I dug around using edebug's d command for displaying the backtrace, stepping through various functions. I found out that in the process for exporting source code blocks, org-babel-exp-code replaces the source block with the value of org-babel-exp-code-template, substituting certain values. Adding the summary flag to that and retrieving the summary information using org-babel-get-src-block-info worked. I originally used advice-add to override org-html-src-block, but I think I'll try replacing the transcoder.

Adding custom header arguments could be useful for different export-related tweaks (someone wanted to create an argument for highlighting certain lines but hadn't figured it out in that thread). If there's a more elegant way to do this, I'd love to find out!

This is part of my Emacs configuration.

Making highlight-sexp follow modus-themes-toggle

| elisp, emacs

[2023-01-27 Fri] Prot just added a modus-themes-get-color-value function. Yay! Also, it turns out that I need to update the overlay in all the buffers.

I'm experimenting with using the highlight-sexp 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 hl-sexp-background-color, and will probably look terrible if you use a light background. I wanted it to adapt when I use modus-themes-toggle. Here's how that works:

(use-package highlight-sexp
  :quelpa
  (highlight-sexp :repo "daimrod/highlight-sexp" :fetcher github :version original)
  :hook
  (emacs-lisp-mode . highlight-sexp-mode)
  :config
  (defun my-hl-sexp-update-overlay ()
    (when (overlayp hl-sexp-overlay)
      (overlay-put
       hl-sexp-overlay
       'face
       `(:background        
         ,(if (fboundp 'modus-themes-get-color-value)
              (modus-themes-get-color-value 'bg-inactive)
            (car
             (assoc-default
              'bg-inactive
              (modus-themes--current-theme-palette))))))))
  (defun my-hl-sexp-update-all-overlays ()
    (dolist (buf (buffer-list))
      (with-current-buffer buf
        (when highlight-sexp-mode
          (my-hl-sexp-update-overlay)))))
  (advice-add 'hl-sexp-create-overlay :after 'my-hl-sexp-update-overlay)
  (advice-add 'modus-themes-toggle :after 'my-hl-sexp-update-all-overlays))

This is what it looks like:

highlight-sexp.gif
Figure 1: Animation of highlight-sexp toggling along with modus-themes-toggle
This is part of my Emacs configuration.

2023-01-23 Emacs news

| emacs, emacs-news

[2023-01-23 Mon]: Added emacs.si meetup

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

2023-01-16 Emacs news

| emacs, emacs-news

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!