aboutsummaryrefslogtreecommitdiff
path: root/epdify.py
diff options
context:
space:
mode:
authorMatt Singleton <matt@xcolour.net>2024-02-03 14:02:03 -0600
committerMatt Singleton <matt@xcolour.net>2024-02-03 14:02:03 -0600
commitbb06503bc6e5e27c4bda3bb4359ec82cfb3b5ed5 (patch)
treec771393004197b698c20b3778829054a1e9b3896 /epdify.py
initial checkin
Diffstat (limited to 'epdify.py')
-rw-r--r--epdify.py133
1 files changed, 133 insertions, 0 deletions
diff --git a/epdify.py b/epdify.py
new file mode 100644
index 0000000..3bf1326
--- /dev/null
+++ b/epdify.py
@@ -0,0 +1,133 @@
+import os
+import sys
+
+from PIL import Image, ImageDraw, ImageFont, ImagePalette, ImageOps
+
+_palettes = {
+ "perceived": (
+ 34, 30, 53, # black
+ 233, 235, 234, # white
+ 52, 102, 77, # green
+ 50, 51, 116, # blue
+ 205, 86, 82, # red
+ 236, 216, 101, # yellow
+ 209, 121, 97, # orange
+ ),
+ "native": (
+ 0, 0, 0, # black
+ 255, 255, 255, # white
+ 0, 255, 0, # green
+ 0, 0, 255, # blue
+ 255, 0, 0, # red
+ 255, 255, 0, # yellow
+ 255, 128, 0, # orange
+ ),
+}
+
+_font_stacks = {
+ "nimbus": {
+ "bold": "/usr/share/fonts/urw-base35/NimbusSansNarrow-Bold.t1",
+ "regular": "/usr/share/fonts/urw-base35/NimbusSansNarrow-Regular.t1",
+ },
+ "fira": {
+ "bold": "/usr/share/fonts/mozilla-fira/FiraSansCondensed-Medium.otf",
+ "regular": "/usr/share/fonts/mozilla-fira/FiraSansCondensed-Light.otf",
+ },
+ "liberation": {
+ "bold": "/usr/share/fonts/liberation-narrow/LiberationSansNarrow-Bold.ttf",
+ "regular": "/usr/share/fonts/liberation-narrow/LiberationSansNarrow.ttf",
+ },
+ "source": {
+ "bold": "/home/matt/Downloads/sourcesans/TTF/SourceSans3-Semibold.ttf",
+ "regular": "/home/matt/Downloads/sourcesans/TTF/SourceSans3-Medium.ttf",
+ },
+ "plex": {
+ "bold": "/usr/share/fonts/ibm-plex-sans-fonts/IBMPlexSansCondensed-SemiBold.otf",
+ "regular": "/usr/share/fonts/ibm-plex-sans-fonts/IBMPlexSansCondensed-Medium.otf",
+ },
+}
+
+def _get_palette_image(palette):
+ img = Image.new("P", (0, 0))
+ img.putpalette(palette)
+ return img
+
+
+# "public" methods
+
+
+def get_crop_box_and_orientation(width, height, ratio):
+ """
+ returns the orientation and bounding box to crop an image with
+ the given height and width to the given aspect ratio.
+ """
+ if width < height:
+ orientation = "portrait"
+ if width / height > ratio:
+ # trim width
+ px_to_trim = width - (height * ratio)
+ box = (
+ px_to_trim / 2,
+ 0,
+ width - (px_to_trim / 2),
+ height
+ )
+ else:
+ # trim height
+ px_to_trim = height - (width / ratio)
+ box = (
+ 0,
+ px_to_trim / 2,
+ width,
+ height - (px_to_trim / 2)
+ )
+ else:
+ orientation = "landscape"
+ if width / height > 1 / ratio:
+ # trim width
+ px_to_trim = width - (height * (1 / ratio))
+ box = (
+ px_to_trim / 2,
+ 0,
+ width - (px_to_trim / 2),
+ height
+ )
+ else:
+ # trim height
+ px_to_trim = height - (width / (1 / ratio))
+ box = (
+ 0,
+ px_to_trim / 2,
+ width,
+ height - (px_to_trim / 2)
+ )
+ return tuple(int(x) for x in box), orientation
+
+
+def get_bounded_font(font_name, font_weight, text, target_size, min_size, max_width):
+ font_filename = _font_stacks[font_name.lower()][font_weight.lower()]
+ size = target_size
+ font = ImageFont.truetype(font_filename, size=size)
+ while size > min_size and font.getlength(text) > max_width:
+ size -= 1
+ font = ImageFont.truetype(font_filename, size=size)
+ return font
+
+
+def epdify(image, dither_palette="perceived", final_palette="native"):
+ """
+ transforms the given input image into a scaled and dithered 7-color
+ image ready to be sent to the frame. expects image to be in the
+ correct aspect already (3x5 or 5x3), errors if not.
+ if 'simulate_perceived_palette' is True, returned image retains the
+ simulated paletted, suitable for reviewing images on a full-color
+ display, but not for loading on the frame.
+ """
+ img = image.quantize(
+ colors=7,
+ dither=Image.Dither.FLOYDSTEINBERG,
+ palette=_get_palette_image(_palettes[dither_palette.lower()]),
+ method=Image.Quantize.LIBIMAGEQUANT,
+ )
+ img.putpalette(_palettes[final_palette.lower()])
+ return img