[2024-11-14 Thu]: stefanvdwalt suggested using
hue-rotate in the filter, ooooh. I tweaked my CSS
to do hue-rotate to get back to the original
colours and boosted the brightness slightly so
that the yellow feels more like a highlighter. I
also changed my dark red colour to a medium-gray
colour, which is more flexible for shading and for
layout cues.
The Supernote A5X is an e-ink notebook that lets
me draw in black, white, and two shades of gray.
It has a drawing app that supports other shades of
gray, but the main notebook app and the PDF
annotation is limited to those two shades of gray.
I like to use a dotted grid in order to write in
neat lines. I used to manually change this
template to a white one before exporting. Then it
occurred to me to make a coloured template:
Using colour lets me use a darker grid, which is
more visible on the Supernote, while still letting
that grid blend into the background if I export
without processing. Screen mirroring shares the
grayscale version, though.
I use my recoloring script to change #a6d2ff (light blue) to #ffffff (white).
Here's the SVG source in case you want to
customize it. When I exported the PNG from
Inkscape, I needed to make sure that antialiasing
was turned off. This involved unchecking the "Hide
export settings" checkbox in the Export dialog,
then setting Antialias to 0. source
My current color scheme is
9d9d9d,c2c2c2,c9c9c9,f6f396,cacaca,f6f396,a6d2ff,ffffff',
which maps light gray to a highlighter sort of
yellow and dark gray to a light gray. I used to
map the dark gray to a dark red like the links on
my site, but light gray is more flexible for
shading and layout.
Anyway, here's an example of the export from my
Supernote and the result after processing:
which is not fine-tuned or amazing, but it reduces
the glare from the white background when I browse
on my phone at night.
Sometimes I switch things around and
use blue/dark blue instead. I now have some Emacs
Lisp code to let me somewhat interactively
recolour a sketch from the Emacs text editor so
that I can change the colours in a sketch as I'm
writing a post about it.
Using a coloured template and a script to change
the colours around has made my Supernote workflow
more convenient. I don't need to change the
template on new pages. I just export the image,
sync with Dropbox or use the Browse & Access
feature, and run my processing script. My
processing script also uses Google Cloud Vision to
recognize the text, rename the sketch, and file it
in the appropriate directory, so it's pretty
smooth. It's pretty idiosyncratic, but maybe you
might be able to adapt the ideas to your own
setup. Hope this helps!
I've been experimenting with the Supernote's
Browse and Access feature because I want to be
able to upload files quickly instead of waiting
for Dropbox to synchronize. First, I want to store
the IP address in a variable:
HTML isn't supported. Text works, but it doesn't support annotation. PDF or EPUB could work.
It would make sense to register this as an export backend so that I can call it as part of the usual export process.
(defunmy-supernote-org-upload-as-text (&optional async subtree visible-only body-only ext-plist)
"Export Org format, but save it with a .txt extension."
(interactive (list nil current-prefix-arg))
(let ((filename (org-export-output-file-name ".txt" subtree))
(text (org-export-as 'org subtree visible-only body-only ext-plist)))
;; consider copying instead of exporting so that #+begin_export html etc. is preserved
(with-temp-file filename
(insert text))
(my-supernote-upload filename)))
(defunmy-supernote-org-upload-as-pdf (&optional async subtree visible-only body-only ext-plist)
(interactive (list nil current-prefix-arg))
(my-supernote-upload (org-latex-export-to-pdf async subtree visible-only body-only ext-plist)))
(defunmy-supernote-org-upload-as-epub (&optional async subtree visible-only body-only ext-plist)
(interactive (list nil current-prefix-arg))
(my-supernote-upload (org-epub-export-to-epub async subtree visible-only ext-plist)))
(org-export-define-backend
'supernote nil
:menu-entry'(?s "Supernote"
((?s "as PDF" my-supernote-org-upload-as-pdf)
(?e "as EPUB" my-supernote-org-upload-as-epub)
(?o "as Org" my-supernote-org-upload-as-text))))
Adding this line to my Org file allows me to use \spacing{1.5} for 1.5 line spacing, so I can write in more annotations..
#+LATEX_HEADER+: \usepackage{setspace}
Sometimes I use custom blocks for HTML classes. When LaTeX complains about undefined environments, I can define them like this:
Bonus: Autocropping encourages me to just get stuff out there even if I haven't filled a page
ideas: remove template automatically? I wonder if I can use another color…
2024-09-26-01
I want to quickly get drawings from my Supernote A5X into Emacs so that I can include them in blog posts. Dropbox/Google Drive sync is slow because it synchronizes all the files. The Supernote can mirror its screen as an .mjpeg stream. I couldn't figure out how to grab a frame from that, but I did find out how to use Puppeteer to take an screenshot of the Supernote's screen mirror. Still, the resulting image is a little pixelated. If I turn on Browse and Access, the Supernote can serve directories and files as webpages. This lets me grab the latest file and process it. I don't often have time to fill a full A5 page with thoughts, so autocropping the image encourages me to get stuff out there instead of holding on to things.
(defvarmy-supernote-ip-address"192.168.1.221")
(defunmy-supernote-get-exported-files ()
(let ((data (plz 'get (format "http://%s:8089/EXPORT" my-supernote-ip-address)))
(list))
(when (string-match "const json = '\\(.*\\)'" data)
(sort
(alist-get 'fileList (json-parse-string (match-string 1 data) :object-type'alist:array-type'list))
:key (lambda (o) (alist-get 'date o))
:lessp'string<:reverse t))))
(defunmy-supernote-org-attach-latest-exported-file ()
(interactive)
;; save the file to the screenshot directory
(let ((info (car (my-supernote-get-exported-files)))
new-file
renamed)
;; delete matching files
(setq new-file (expand-file-name
(replace-regexp-in-string " ""%20" (alist-get 'name info) (org-attach-dir))))
(when (file-exists-p new-file)
(delete-file new-file))
(org-attach-attach
(format "http://%s:8089%s" my-supernote-ip-address
(alist-get 'uri info))
nil
'url)
(setq new-file (my-latest-file (org-attach-dir)))
;; recolor
(my-sketch-recolor-png new-file)
;; autocrop that image
(my-image-autocrop new-file)
;; possibly rename
(setq renamed (my-image-recognize-get-new-filename new-file))
(when renamed
(setq renamed (expand-file-name renamed (org-attach-dir)))
(rename-file new-file renamed t)
(my-image-store renamed) ; file it in my archive
(setq new-file renamed))
;; use a sketch link if it has an ID
(if (string-match "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9] "
(file-name-base renamed))
(org-insert-link nil (concat "sketchFull:" (file-name-base renamed)))
;; insert the link
(org-insert-link nil (concat "attachment:" (replace-regexp-in-string "#""%23" (file-name-nondirectory new-file)))))
(org-redisplay-inline-images)))
Krita might work, but it's awkward to draw on my tablet PC's screen
when it's in laptop mode because of the angle. Flipping it to
tablet mode is a bit disruptive.
I can draw on my Supernote, which feels a bit more natural. I
have a good workflow for recoloring and renaming exported sketches,
but exporting via Dropbox is a little slow since it synchronizes all
the folders. The SuperNote has a built-in screen mirroring mode with
an MJPEG that I can open in a web browser. Saving it to an image is a
little complicated, though. ffmpeg doesn't work with the MJPEG that it
streams, and I can't figure out how to get stuff out aside from using
a browser. I can work around this by using Puppeteer and getting a
screenshot. Here's a NodeJS snippet that saves that screenshot to a file.
/* This file is tangled to ~/bin/supernote-screenshot.js from my config at https://sachachua.com/dotemacsUsage: supernote-screenshot.js [filename]Set SUPERNOTE_URL to the URL.*/constprocess = require('process');
constpuppeteer = require('puppeteer');
consturl = process.env['SUPERNOTE_URL'] || 'http://192.168.1.221:8080/screencast.mjpeg';
constscale = 0.5;
constdelay = 2000;
asyncfunction takeSupernoteScreenshot() {
constbrowser = await puppeteer.launch({headless: 'new'});
constpage = await browser.newPage();
await page.setViewport({width: 2808 * scale, height: 3744 * scale, deviceScaleFactor: 1});
page.goto(url);
awaitnewPromise((resolve, reject) => setTimeout(resolve, delay));
letfilename = process.argv[2] || 'screenshot.png';
await page.screenshot({type: 'png', path: filename, fullPage: true});
await browser.close();
}
takeSupernoteScreenshot();
(defunmy-org-insert-supernote-screenshot-from-mirror ()
"Copy the current image from the SuperNote mirror."
(interactive)
(let ((filename (expand-file-name (format-time-string "%Y-%m-%d-%H-%M-%S.png") "~/recordings")))
(shell-command-to-string (concat "NODE_PATH=/usr/lib/node_modules node ~/bin/supernote-screenshot.js " (shell-quote-argument filename)))
;; trim it
(call-process "mogrify" nil nil nil "-trim""+repage" filename)
(shell-command-to-string (concat "~/bin/recolor.py --colors c0c0c0,f6f396 " (shell-quote-argument filename)))
(call-interactively 'my-org-insert-screenshot)))
[2024-09-13 Fri] I already have some code elsewhere for using the Google Cloud Vision API to extract text from an image, so I should hook that up sometime.
Also, OpenAI supports multimodal requests, so I've been thinking about using AI to recognize text and diagrams like in this Supernote to Markdown example. Fun fun fun!
Both Google Cloud Vision and SuperNote's new handwriting recognition
handle my print fine. Neither handle columns the way I'd like, but to
be fair, I'm not really sure how I want columns and wrapping handled
anyway. I can always experiment with the standard use-case: one column
of text, to export as text (with perhaps the occasional sketch, which
I can crop and include).
If I can get the hang of writing my thoughts, then it turns some of
those bedtime hours into writing hours. Writing by hand feels slow and
linear, but it's better than nothing, and thinking takes most of the
time anyway. While speech recognition feels like it might be faster in
short bursts, I don't have a lot of "talking to myself" time (aside
from sleepy brain dumps), and my workflow for processing audio is
still slow and disjointed. I can't type on my phone because then A-
will want to be on a screen too. I'm glad e-ink devices are different
enough not to trigger her sense of unfairness, although sometimes she
does ask if she can do mazes or connect-the-dots. Then I switch to
knitting until it's really really time to go to bed.
I'm slowly figuring out my workflows for experimenting with and
writing about code. Naturally, that's a little more challenging to
write about by hand, but I could draft the context. I can think
through life stuff too, and maybe look into saving more notes in my
Org files.
I've experimented with handwritten blog posts before. Now that I have
a little more time to tweak my workflow and think thoughts, maybe I'll
get the hang of them!
It looks like the Supernote's real-time recognition is pretty accurate
for my handwriting, getting the text out of multiple pages is pretty
straightforward.
Here's what it took to edit it into the first part of this post - just adding line-breaks and fixing up some words:
Source images
[[
If I add more lines between paragraphs when writing, I might be able
to skip adding them in the text export.
For comparison, here's the text output from Google Cloud Vision.
Tweaking my handwriting workflow
Both Google Cloud Vision and Super Note's new
handwriting recognition handle my print fine. Neither
handle columns the way I'd like, but to be fair,
I'm not really sure how I want columns and wrapping
handled anyway I can always experiment with the
standard use-case
use-case: One column of text, to export
as Text (with perhaps the occasional sketch, which
can crop and include).
If I can get the hang of writing my thoughts,
then it turns some of those bedtime hours into writi
writing
hours. Writing by hand feels slow and linear, but it's
better than nothing, and thinking takes most of the time
anyway while speech recognition feels like it might be
faster in short bursts, don't have a lot of "talking to
myself" time (aside from sleepy braindumps), and my workflow
for processing audio is still slow and disjointed. I can't
type on my phone because then A- will want to be on
I'm glad e-ink devices are different enough
not to trigger her sense of unfairness, although sometimes
she does ask if she can do mazes or connect-the-dots
a screen too
Then I switch to Knitting until it's really really time to
go to bed.
I'm slowly figuring out my workflows for experimenting
with and writing about code. Naturally, that's a little
more challenging to write about by hand, but I could
draft the context. I can think through life stuff too, and
maybe look into saving more notes in my org files
I've experimented with handwritten blog posts before
Now that I have a little more time to tweak my workflow
and think thoughts, maybe I'll get the hang of them!
I'm leaning towards SuperNote's recognition results for long text,
although I don't get access to the confidence data so I'll probably
just have to delete the misrecognized text if I include sketches.
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 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.
#!/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
from dotenv import load_dotenv
# Import the Google Cloud client librariesfrom google.cloud import vision
from google.cloud.vision_v1 import AnnotateImageResponse
import sys
sys.path.append("/home/sacha/proj/supernote/")
import recolor # noqa: E402 # muffles flake8 error about import
load_dotenv()
# Set the folder path where the png files are locatedfolder_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 clientclient = vision.ImageAnnotatorClient()
refresh_counter = 0
defextract_text(client, file):
json_file = file[:-3] + 'json'# TODO Preprocess to keep only black textwithopen(file, 'rb') as image_file:
content = image_file.read()
# Convert the png file to a Google Cloud Vision image objectimage = vision.Image(content=content)
# Extract handwriting from the image using the Google Cloud Vision APIresponse = client.document_text_detection(image=image)
response_json = AnnotateImageResponse.to_json(response)
json_response = json.loads(response_json)
# Save the response to a json file with the same name as the png filewithopen(json_file, "w") as f:
json.dump(json_response, f)
defmaybe_rename(file):
# TODO Match on IDjson_file = file[:-3] + 'json'withopen(json_file, 'r') as f:
data = json.load(f)
# Extract the text from the json filetext = data['fullTextAnnotation']['text']
# Check if the text contains a string matching the regex patternpattern = r'(?<!ref:)[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}'match = re.search(pattern, text)
ifmatch:
# Get the matched stringmatched_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 stringnew_filename = os.path.join(os.path.dirname(file), new_name + '.png')
rename_set(file, new_filename)
return new_filename
defget_tags(filename, text):
tags = re.findall(r'(^|\W)#[ \n\t]+', text)
return' '.join(filter(lambda x: x notin filename, tags))
defget_references(text):
refs = re.findall(r'!ref:[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}', text)
return' '.join(refs)
defget_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 andnot re.search('^I thought about', j['Note']):
return j['Note']
defget_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 purplishelif re.search(r'#(emacs|geek|tech|blue)', together):
return {'9d9d9d': '2b64a9', 'c9c9c9': 'b3e3f1'} # geeky stuff in light/dark blueelse:
return {'9d9d9d': '884636', 'c9c9c9': 'f6f396'} # yellow highlighter, dark browndefrename_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)
defrecolor_based_on_filename(filename):
color_map = get_color_map(filename)
recolored = recolor.map_colors(filename, color_map)
# possibly rename based on the filenamenew_filename = re.sub(' #(purple|blue)', '', filename)
rename_set(filename, new_filename)
recolored.save(new_filename)
defmove_processed_sketch(file):
global refresh_counter
if'#private'infile:
output_dir = private_sketch_dir
elif'#'infile:
output_dir = public_sketch_dir
refresh_counter = 3
else:
returnfilenew_filename = os.path.join(output_dir, os.path.basename(file))
rename_set(file, new_filename)
return new_filename
defprocess_file(file):
json_file = file[:-3] + 'json'# Check if a corresponding json file already existsifnot os.path.exists(json_file):
extract_text(client, file)
ifnot 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)
defprocess_dir(folder_path):
global processed_files
# Iterate through all png files in the specified folderfiles = sorted(os.listdir(folder_path))
forfilein files:
iffile.endswith('.png') and'_'infile:
print("Processing ", file)
process_file(os.path.join(folder_path, file))
defdaemon(folder_path, wait):
global refresh_counter
whileTrue:
process_dir(folder_path)
time.sleep(wait)
if refresh_counter > 0:
refresh_counter = refresh_counter - 1
if refresh_counter == 0:
print("Reloading sketches")
requests.get('https://' + os.environ['JOURNAL_USER'] + ':'+ os.environ['JOURNAL_PASS']
+ '@sketches.sachachua.com/reload?python=1')
if__name__ == '__main__':
# Create a set to store the names of processed filesprocessed_files = set()
iflen(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)
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!