## Rename, recolor, and file my sketches automatically

|

I want to make it easier to process the sketchnotes I make on my Supernote. I write IDs of the form yyyy-mm-dd-nn to identify my sketches. To avoid duplicates, I get these IDs from the web-based journaling system I wrote. I've started putting the titles and tags into those journal entries as well so that I can reuse them in scripts. When I export a sketch to PNG and synchronize it, the file appears in my ~/Dropbox/Supernote/EXPORT directory on my laptop. Then it goes through this process:

• I use Google Cloud Vision to detect handwriting so that I can find the ID.
• I retrieve the matching entry from my journal system and rename the file based on the title and tags.
• If there's no matching entry, I rename the file based on the ID.
• If there are other tags or references in the sketch, I add those to the filename as well.
• I recolor it based on the tags, so parenting-related posts are a little purple, tech/Emacs-related posts are blue, and things are generally highlighted in yellow otherwise.
• I move it to a directory based on the tags.
• If it's a private sketch, I move it to the directory for my private sketches.
• If it's a public sketch, I move it to the directory that will eventually get synchronized to sketches.sachachua.com, and I reload the list of sketches after some delay.

The following code does that processing.

supernote-daemon source code
#!/usr/bin/python3
# -*- mode: python -*-

# (c) 2022-2023 Sacha Chua (sacha@sachachua.com) - MIT License

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import os
import json
import re
import requests
import time
# Import the Google Cloud client libraries
import sys
sys.path.append("/home/sacha/proj/supernote/")
import recolor   # noqa: E402  # muffles flake8 error about import

# Set the folder path where the png files are located
folder_path = '/home/sacha/Dropbox/Supernote/EXPORT/'
public_sketch_dir = '/home/sacha/sync/sketches/'
private_sketch_dir = '/home/sacha/sync/private-sketches/'

# Initialize the Google Cloud Vision client
client = vision.ImageAnnotatorClient()
refresh_counter = 0

def extract_text(client, file):
json_file = file[:-3] + 'json'
# TODO Preprocess to keep only black text
with open(file, 'rb') as image_file:
# Convert the png file to a Google Cloud Vision image object
image = vision.Image(content=content)

# Extract handwriting from the image using the Google Cloud Vision API
response = client.document_text_detection(image=image)
response_json = AnnotateImageResponse.to_json(response)
# Save the response to a json file with the same name as the png file
with open(json_file, "w") as f:
json.dump(json_response, f)

def maybe_rename(file):
# TODO Match on ID
json_file = file[:-3] + 'json'
with open(json_file, 'r') as f:

# Extract the text from the json file
text = data['fullTextAnnotation']['text']

# Check if the text contains a string matching the regex pattern
pattern = r'(?<!ref:)[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}'
match = re.search(pattern, text)
if match:
# Get the matched string
matched_string = match.group(0)
new_name = matched_string
from_zid = get_journal_entry(matched_string).strip()
if from_zid:
new_name = matched_string + ' ' + from_zid
tags = get_tags(new_name, text)
if tags:
new_name = new_name + ' ' + tags
ref = get_references(text)
if ref:
new_name = new_name + ' ' + ref
print('Renaming ' + file + ' to ' + new_name)
# Rename the png and json files to the matched string
new_filename = os.path.join(os.path.dirname(file), new_name + '.png')
rename_set(file, new_filename)
return new_filename

def get_tags(filename, text):
tags = re.findall(r'(^|\W)#[ \n\t]+', text)
return ' '.join(filter(lambda x: x not in filename, tags))

def get_references(text):
refs = re.findall(r'!ref:[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}', text)
return ' '.join(refs)

def get_journal_entry(zid):
resp = requests.get('https://' + os.environ['JOURNAL_USER']
+ ':' + os.environ['JOURNAL_PASS']
+ '@journal.sachachua.com/api/entries/' + zid)
j = resp.json()
if j and not re.search('^I thought about', j['Note']):
return j['Note']

def get_color_map(filename, text=None):
if text:
together = filename + ' ' + text
else:
together = filename
if re.search('r#(parenting|purple|life)', together):
return {'9d9d9d': '8754a1', 'c9c9c9': 'e4c1d9'}  # parenting is purplish
elif re.search(r'#(emacs|geek|tech|blue)', together):
return {'9d9d9d': '2b64a9', 'c9c9c9': 'b3e3f1'}  # geeky stuff in light/dark blue
else:
return {'9d9d9d': '884636', 'c9c9c9': 'f6f396'}  # yellow highlighter, dark brown

def rename_set(old_name, new_name):
if old_name != new_name:
old_json = old_name[:-3] + 'json'
new_json = new_name[:-3] + 'json'
os.rename(old_name, new_name)
os.rename(old_json, new_json)

def recolor_based_on_filename(filename):
color_map = get_color_map(filename)
recolored = recolor.map_colors(filename, color_map)
# possibly rename based on the filename
new_filename = re.sub(' #(purple|blue)', '', filename)
rename_set(filename, new_filename)
recolored.save(new_filename)

def move_processed_sketch(file):
global refresh_counter
if '#private' in file:
output_dir = private_sketch_dir
elif '#' in file:
output_dir = public_sketch_dir
refresh_counter = 3
else:
return file
new_filename = os.path.join(output_dir, os.path.basename(file))
rename_set(file, new_filename)
return new_filename

def process_file(file):
json_file = file[:-3] + 'json'
# Check if a corresponding json file already exists
if not os.path.exists(json_file):
extract_text(client, file)
if not re.search('[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2} ', file):
file = maybe_rename(file)
recolor_based_on_filename(file)
move_processed_sketch(file)

def process_dir(folder_path):
global processed_files
# Iterate through all png files in the specified folder
files = sorted(os.listdir(folder_path))
for file in files:
if file.endswith('.png') and '_' in file:
print("Processing ", file)
process_file(os.path.join(folder_path, file))

def daemon(folder_path, wait):
global refresh_counter
while True:
process_dir(folder_path)
time.sleep(wait)
if refresh_counter > 0:
refresh_counter = refresh_counter - 1
if refresh_counter == 0:
requests.get('https://' + os.environ['JOURNAL_USER'] + ':'
+ os.environ['JOURNAL_PASS']

if __name__ == '__main__':
# Create a set to store the names of processed files
processed_files = set()
if len(sys.argv) > 1:
if os.path.isdir(sys.argv[1]):
folder_path = sys.argv[1]
daemon(folder_path, 300)
else:
for f in sys.argv[1:]:
process_file(f)
else:
daemon(folder_path, 300)


It uses this script I wrote to recolor my sketches with Python.

I'm contemplating writing some annotation tools to make it easier to turn the detected text into useful text for searching or writing about because the sketches throw off the recognition (misrecognized text, low confidence) and the columns mess up the line wrapping. Low priority, though.

My handwriting (at least for numbers) is probably simple enough that I might be able to train Tesseract OCR to process that someday. And who knows, maybe some organization will release a pre-trained model for offline handwriting recognition that'll be as useful as OpenAI Whisper is for audio files. That would be neat!

## Recoloring my sketches with Python

|

The SuperNote lets me draw with black, dark gray (0x9d), gray (0xc9), or white. I wanted to make it easy to recolor them, since a little splash of colour makes sketches more fun and also makes them easier to pick out from thumbnails. Here's the Python script I wrote:

#!/usr/bin/python3
# Recolor PNGs
#
# (c) 2022 Sacha Chua (sacha@sachachua.com) - MIT License
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:

# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

import numpy as np
import os
import csv
import argparse
from PIL import Image

DARK_GRAY = 0x9d
GRAY = 0xc9
WHITE = 0xfe

color_dict = {}

def color_to_tuple(color_dict, s):
if s in color_dict:
s = color_dict[s]
s = s.lstrip('#')
if (s == '.'):
return (None, None, None)
elif (len(s) == 2):
return (int(s, 16), int(s, 16), int(s, 16))
else:
return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4))

dict = {}
with open(os.path.expanduser(filename), newline='') as csvfile:
dict[row[0]] = row[1]
return dict

def map_colors(input, color_map):
if isinstance(input, str):
im = Image.open(input).convert('RGB')
else:
im = input
data = np.array(im)
red, green, blue = data[:, :, 0], data[:, :, 1], data[:, :, 2]
for from_c, to_c in color_map.items():
from_r, from_b, from_g = color_to_tuple(color_dict, from_c)
to_r, to_b, to_g = color_to_tuple(color_dict, to_c)
mask = (red == from_r) & (green == from_g) & (blue == from_b)
data[:, :, :3][mask] = [to_r, to_b, to_g]
return Image.fromarray(data)

def set_colors_by_freq(input, color_list):
if isinstance(input, str):
im = Image.open(input).convert('RGB')
else:
im = input
data = np.array(im)
red, green, blue = data[:, :, 0], data[:, :, 1], data[:, :, 2]
sorted_colors = get_colors_by_freq(input)
freq = iter(color_list.split(','))
for i, f in enumerate(freq):
if f != '.':
to_r, to_b, to_g = color_to_tuple(color_dict, f)
by_freq = sorted_colors[i][1]
if isinstance(by_freq, np.uint8):
mask = (red == by_freq) & (green == by_freq) & (blue == by_freq)
else:
mask = (red == by_freq[0]) & (green == by_freq[1]) & (blue == by_freq[2])
data[:, :, :3][mask] = [to_r, to_b, to_g]
return Image.fromarray(data)

def color_string_to_map(s):
color_map = {}
colors = iter(args.colors.split(','))
for from_c in colors:
to_c = next(colors)
color_map[from_c] = to_c
return color_map

def get_colors_by_freq(input):
if isinstance(input, str):
im = Image.open(input).convert('RGB')
else:
im = input
colors = im.getcolors(im.size[0] * im.size[1])
return sorted(colors, key=lambda x: x[0], reverse=True)

def print_colors(input):
sorted_colors = get_colors_by_freq(input)
for x in sorted_colors:
if x[0] > 10:
if isinstance(x[1], np.uint8):
print('%02x %d' % (x[1], x[0]))
else:
print(''.join(['%02x' % c for c in x[1]]) + ' %d' % x[0])

def process_file(input):
print(input)
if args.preview:
output = None
else:
output = args.output if args.output else input
if os.path.isdir(output):
output = os.path.join(output, os.path.basename(input))
im = Image.open(input).convert('RGB')
if args.colors:
im = map_colors(im, color_string_to_map(args.colors))
elif args.freq:
im = set_colors_by_freq(im, args.freq)
else:
print_colors(im)
exit(0)
if args.preview:
im.thumbnail((700, 700), Image.ANTIALIAS)
im.show()
elif output:
im.save(output)

if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='Recolor a PNG.',
formatter_class=argparse.RawTextHelpFormatter,
epilog="If neither --colors nor --freq are specified, "
+ "display the most frequent colours in the image.")
parser.add_argument('--colors', help="""Comma-separated list of RGB hex values in the form of old,new,old,new
Examples:
9d,ffaaaa,c9,ffd2d2 - reddish
c9,ffea96 - yellow highlighter
c9,d2d2ff - light blue
""")
parser.add_argument('--freq', help="Color replacements in order of descending frequency (ex: .,ffea96). .: use original color")
parser.add_argument('--csv', help="CSV of color names to use in the form of colorname,hex")
parser.add_argument('--output', help="Output file. If not specified, overwrite input file.")

args = parser.parse_args()
color_dict = load_color_dict(args.csv) if args.csv else {}
for input in args.input:
process_file(os.path.join(os.getcwd(), input))


I don't think in hex colours, so I added a way to refer to colours by names. I converted this list of Copic CSS colours to a CSV by copying the text, pasting it into a file, and doing a little replacement. It's not complete, but I can copy selected colours from this longer list. I can also add my own. The CSV looks a little like this:

lightgreen,cfe8d3
lightyellow,f6f396
lightblue,b3e3f1
y02,f6f396
w2,ddddd5
b02,b3e3f1
...


It doesn't do any fuzzing or clustering of similar colours, so it won't work well on antialiased images. For the simple sketches I make with the SuperNote, though, it seems to work well enough.

I can preview my changes with something like ./recolor.py ~/sketches/"2022-08-02-01 Playing with my drawing workflow #supernote #drawing #workflow #sketching #kaizen.png" --csv colors.csv --freq .,lightyellow --preview , and then I can take the --preview flag off to overwrite the PNG.

Here's what the output looks like:

## One month with the SuperNote A5X

|

I've had my SuperNote A5X for a month now, and I really like it.

Text from my sketch

I use it for:

• untangling thoughts
• sketchnoting books
• planning
• drafting blog posts
• drawing

A- uses it for: (she's 6 years old)

• practising cursive
• doing mazes and dot-to-dots
• drawing

Things I'm learning:

• Exporting PNGs at 200% works well for my workflow. I rename them in Dropbox and upload them to sketches.sachachua.com.
• Carefully copying & deleting pages lets me preserve page numbers. I use lassoed titles for active thoughts and maintain a manual index for other things.
• Layouts:
• Landscape: only easier to review on my laptop
• Portrait columns: lots of scrolling up and down
• Portrait rows: a little harder to plan, but easier to review
• Many books fit into one page each.
• Google Lens does a decent job of converting my handwriting to text (print or cursive, even with a background). Dropbox → Google Photos → Orgzly → Org
• Draft blog posts go into new notebooks so that I can delete them once converted.
• The Super Note helps me reclaim a lot of the time I spend waiting for A-. A digital notebook is really nice. Easy to erase, rearrange, export… It works well for me.
• Part of my everyday carry kit

Ideas for growth:

• Settle into monthly pages, bullet journaling techniques
• Practise drawing; use larger graphic elements & organizers, different shades
• Integrate into Zettelkasten

I put my visual book notes and visual library notes into a Dropbox shared folder so that you can check them out if you have a Supernote. If you don't have a Supernote, you can find my visual book notes at sketches.sachachua.com. Enjoy!

## Trying out the SuperNote A5X

|

W- was happy with his SuperNote A5X, so I ordered one for myself on July 18. The company was still doing pre-orders because of the lockdowns in China, but it shipped out on July 20 and arrived on July 25, which was pretty fast.

I noticed that the org-epub export makes verse blocks look double-spaced on the SuperNote, probably because <br> tags are getting extra spacing. I couldn't figure out how to fix it with CSS, so I've been hacking around it by exporting it as a different class without the <br> tags and just using { white-space: pre }. I also ended up redoing the templates I made in Inkscape, since the gray I used was too light to see on the SuperNote.

It was very tempting to dive into the rabbithole of interesting layouts on /r/supernote and various journaling resources, but I still don't have much time, so there's no point in getting all fancy about to-do lists or trackers at the moment. I wanted to focus on just a couple of things: untangling my thoughts and sketching. Sketchnoting books would be a nice bonus (and I actually managed to do one on paper during a recent playdate), but that can also wait until I have more focused time.

I've had the A5X for five days and I really like it. Writing with the Lamy pen feels like less work than writing with a pencil or regular pen. It's smooth but not rubbery. I've still been drawing in landscape form because that feels a little handier for reviewing on my tablet or writing about on my blog, but I should probably experiment with portrait form at some point.

So far, I've:

sketched out my thoughts
I used to use folded-over 8x14" to sketch out two thoughts, but scanning them was a bit of a pain. Sometimes I used the backs of our writing practice sheets in order to reduce paper waste, but then scanning wasn't always as clean. I really like using the SuperNote to sketch out thoughts like this one. It's neat, and I can get the note into my archive pretty easily.
sketched stuff from life
This is easier if I take a quick reference picture on my phone. I could probably even figure out some kind of workflow for making that available as a template for tracing.
A- loves being able to use the eraser and lasso to modify her drawings. Novelty's probably another key attraction, too. She's made quite a few drawings for me, even experimenting with drawing faces from the side like the way she's been seeing me practice doing.
A- likes to ask me to draw things. She enjoys tracing over them in another layer. More drawing practice for both of us!
used it to help A- practise coding, etc.
A- wanted to do some coding puzzles with her favourite characters. I enjoyed being able to quickly sketch it up, drawing large versions and then scaling down as needed.
played a game of chess
I drew chess pieces just to see if I could, and we ended up using those to play chess. I should share these and maybe add other games as well.
referred to EPUBs and PDFs
I put our favourite songs and poems on it. I've also started using org-chef to keep a cookbook.
doodled sketch elements
boxes, borders, little icons, people… Probably should organize these and share them too.

I've figured out how to publish sketches by using my phone to rotate them and sync them with my online sketches. Now I'm playing around with my writing workflow to see if I can easily post them to my blog. At some point, I think I'll experiment with using my phone to record and automatically transcribe some commentary, which I can pull into the blog post via some other Emacs Lisp code I've written. Whee!

## Making a menu of activities

|

A- wants to be with me almost all the time. This can be challenging.

A multiple-choice question is easier than a fill-in-the-blank one, especially when it comes to "What do we do now?" A- seems less grumpy throughout the day when she can go from one activity to another of her choosing. I like letting her take the lead. I also like not having to come up with stuff. During bedtime, I sketched this menu:

## Kindergarten means I get to learn how to write, too

| drawing

A- wants to learn cursive, probably because it's extra-fancy and the sort of thing Elizabeth Bennet would have done. There's some support for teaching cursive in kindergarten, so it's not totally crazy. It's a good opportunity for me to improve my lettering skills, too. She usually likes it when we do the same thing at the same time, so working on letters together is a good way to nudge her to practise fine motor skills. We did a brush lettering worksheet for "Aa" from Amy Latta Creations. This one is my worksheet.

I've got lots to learn about controlling a brush pen. Doing lots of drills will probably help me get my up-strokes to be as thin as the samples.

A- often asks me to connect my letters. I think I'll make our own worksheets so that she can connect letters too.

At bedtime, I drew in my sketchbook while she read independently. When she noticed what I was doing, she said she liked the 3D letters and encouraged me to do more. She pointed to blank spaces on the page and suggested things to add.

Not that different compared to my lettering experiments from 2013:

But hey, I'm learning stuff!

|

## Story

• It's time to make a smoothie!
• I pour blueberries into the blender.
• Mama blends it all with some water.
• I peel and add a banana.
• Mama blends it again.
• Yum yum!

## Process

• Prerequisites

• ImageMagick
• Texlive (probably)
• latex-beamer
• Org Mode and Emacs
• Set up Org Mode export to Beamer

(eval-after-load "ox-latex"
;; update the list of LaTeX classes and associated header (encoding, etc.)
;; and structure
("beamer"
,(concat "\\documentclass[presentation]{beamer}\n"
"[DEFAULT-PACKAGES]"
"[PACKAGES]"
"[EXTRA]\n")
("\\section{%s}" . "\\section*{%s}")
("\\subsection{%s}" . "\\subsection*{%s}")
("\\subsubsection{%s}" . "\\subsubsection*{%s}"))))


• Set up image directories

mkdir text-pages blank-spreads drawn drawn-pages


 text-pages Will contain one image per page of just the plain text. blank-spreads Will contain text spreads ready for drawing drawn Export one image per spread (without the text layers) from your drawing program drawn-pages Will contain one image per page combining text and drawing

This file gets included in the LaTeX file for the children's book. Tweak it to change the appearance. In this example, I use black serif text at the bottom of the page.

\geometry{paperwidth=7in,paperheight=8.5in,left=0.5in,top=0.5in,right=0.5in,bottom=0.5in}
\setbeamercolor{normal text}{fg=black,bg=white}
\setbeamercolor{structure}{fg=black,bg=white}
\usefonttheme{serif}
\setbeamertemplate{frametitle}
{
\begin{center}
\vspace{0.7\textheight}
\noindent
\insertframetitle
\end{center}
}
\usepackage[noframe]{showframe}
\renewcommand{\maketitle}{}


• Write the story

I used Org Mode to make it easy to write the story.

Some considerations:

• Because we're printing this as a saddle-stitched booklet, the number of lines should be a multiple of four. Sixteen is probably a good maximum.
• The first heading is actually for the last page.
• The second heading is for the cover page.
• The third heading is for the first inner page, the fourth heading is for the second inner page, and so on.
#+OPTIONS:   TeX:t LaTeX:t skip:nil d:nil todo:t pri:nil tags:not-in-toc author:nil date:nil
#+OPTIONS: H:1
#+startup: beamer
#+LaTeX_CLASS: beamer
#+LaTeX_CLASS_OPTIONS: [20pt]
#+BEAMER_FRAME_LEVEL: 1

* Story

**
**
**
** It's time to make a smoothie!
** I pour blueberries into the blender.
** Mama blends it all with some water.
** I peel and add a banana.
** Mama blends it again.
** Yum yum!


• Make the tex, PDF, page PNGs, and spread PNGs

1. Go to the subtree for the story and use M-x org-export-dispatch (C-c C-e) with the subtree option (C-s) to export it as a Beamer file (option l b).
2. Use pdflatex to convert the .tex to PDF.

pdflatex index.tex


3. Create one PNG per text page with:

convert -density 300 index.pdf -quality 100 text-pages/page%02d.png


4. Create spreads to draw on with:

montage text-pages/page*.png -tile 2x1 -mode Concatenate blank-spreads/spread%d.png


5. Optionally, create a layered PSD with:

convert blank-spreads/spread*.png $$-clone 1,0 -background white -flatten -alpha off$$ -reverse spreads-for-drawing.psd


• Draw

I imported the PNG layers into MediBang Paint on a Samsung Note 8 Android phone, and then:

• imported photos
• traced them
• hid the text layers
• exported one PNG per spread to QuickPic, renamed them, and uploaded them to Dropbox, because I couldn't figure out how to export to Dropbox directly

Layer folders were handy for organizing spread-related images. I couldn't seem to move all of the layers in a layer folder together on Android, but the iPad was able to do so. If I didn't have the iPad handy, I combined the layers by exporting a PNG and then importeing it back into MediBang Paint.

This was a decent setup that allowed me to draw and paint even when I was in bed nursing A- and waiting for her to fall asleep. I held the phone with one hand and rotated the canvas as needed so that it was easier for me to draw lines with my right. Because of the awkward position and the small screen size, the lines are not as smooth as I might like, but the important thing is that they're there. Whee! =)

It turns out to be possible to use the free MediBang Pro drawing program under Wine on Linux to import the PSD and save it to the cloud. I was also sometimes able to switch to drawing with iPad Pro with Pencil, but it was harder to find time to do that because that usually made A- want to draw too.

Anyway, after I drew and exported the PNGs, the next step was to…

• Convert the drawn spreads back to pages and combine them with the text

Here's some code that combines the drawing and the text. Keeping the drawing and the text separate until this stage (instead of exporting the PNGs with the text) makes it easier to change the text later by recreating the text PNGs and running this step.

(defun my/combine-spread-drawing-and-text (page num-pages)
(let ((gravity (if (= (% page 2) 1) "West" "East"))
(spread (/ (% page num-pages) 2)))
(shell-command
(format
(concat "convert \$$" "drawn/spread%d.png -gravity %s " "-chop 50%%x0 +repage \$$ "
"text-pages/page%02d.png -compose darken "
"-composite drawn-pages/page%02d.png")

(cl-loop for i from 0 to (1- pages) do


This code pairs up the drawn pages into a PDF that can be printed duplex. Make sure to choose the option to flip along the short edge. I hard-coded the page orders for 4-, 8-, 12-, and 16-page booklets.

(let* ((page-order
'((0 1 2 3)   ; hard-coded page sequences
(0 1 2 7 6 3 4 5)
(0 1 2 11 10 3 4 9 8 5 6 7)
(0 1 2 15 14 3 4 13 12 5 6 11 10 7 8 9)))
(sequence
(mapconcat (lambda (d) (format "drawn-pages/page%02d.png" d))
(elt page-order (1- (/ pages 4))) " ")))
(shell-command
(format
"montage %s -tile 2x1 -mode Concatenate print-duplex-short-edge-flip.pdf"
sequence)))


• Print and bind

After printing and folding the book, I used tape to make the book hold together. Tada!

• Create on-screen PDF for reading

A little bit of manipulation so that the last page is in the right place:

(shell-command
(format "convert %s onscreen.pdf"
(mapconcat 'identity (cl-loop for i from 1 to pages
collect (format "drawn-pages/page%02d.png" (% i pages))) " ")))


(cl-loop
for i from 0 to (1- (/ pages 2)) do
(shell-command
(format
(concat "convert "
"\$$blank-spreads/spread%d.png " "drawn/spread%d.png " "-compose darken " "-resize %dx -flatten \$$ "
"\$$+clone -background black -shadow 50x1+%d+%d \$$ "
"+swap -compose src-over -composite "
`