Work in progress for release v1.7/1.8

This is a refactoring of the entire Inky-Calendar software and is work in progress. The reason for uploading is to test if everything works fine. Please do not attempt to use/install this software as it can potentially break your system. If you have any improvement ideas, you're most welcome to mention them in the Issues section. Thanks!
This commit is contained in:
Ace
2019-10-21 07:54:19 +02:00
committed by GitHub
parent 93426b3dea
commit e3a4997fdb
28 changed files with 1118 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,41 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Calibration module for the Black-White and Black-White-Red E-Paper display
Calibration refers to flushing all pixels in a single colour to prevent
ghosting.
"""
from __future__ import print_function
import time
from settings import display_colours
from image_data import black, white, red
def calibration():
"""Function for Calibration"""
import e_paper_drivers
epd = e_paper_drivers.EPD()
print('_________Calibration for E-Paper started_________'+'\n')
for i in range(2):
epd.init()
print('Calibrating black...')
epd.display_frame(epd.get_frame_buffer(black))
if display_colours == "bwr":
print('calibrating red...')
epd.display_frame(epd.get_frame_buffer(red))
print('Calibrating white...')
epd.display_frame(epd.get_frame_buffer(white))
epd.sleep()
print('Cycle', str(i+1)+'/2', 'complete'+'\n')
print('Calibration complete')
def main():
"""Added timer"""
start = time.time()
calibration()
end = time.time()
print('Calibration complete in', int(end - start), 'seconds')
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,171 @@
"""
Advanced configuration options for Inky-Calendar software.
Contains some useful functions for correctly rendering text,
calibrating (E-Paper display), checking internet connectivity
Copyright by aceisace
"""
from PIL import Image, ImageDraw, ImageFont
from urllib.request import urlopen
from settings import language
from pytz import timezone
import numpy as np
import os
"""Set the display height and width (in pixels)"""
display_height, display_width = 640, 384
"""Create 3 sections of the display, based on percentage"""
top_section_width = middle_section_width = bottom_section_width = display_width
top_section_height = int(display_height*0.10)
middle_section_height = int(display_height*0.65)
bottom_section_height = int(display_height - middle_section_height -
top_section_height)
top_section_offset = 0
middle_section_offset = top_section_height
bottom_section_offset = display_height - bottom_section_height
"""Get the relative path of the Inky-Calendar folder"""
path = os.path.dirname(os.path.abspath(__file__)).replace("\\", "/")
if path != "" and path[-1] != "/":
path += "/"
while not path.endswith('/Inky-Calendar/'):
path = ''.join(list(path)[:-1])
"""Fonts handling"""
fontpath = path+'fonts/'
NotoSansCJK = fontpath+'NotoSansCJK/NotoSansCJKsc-'
NotoSans = fontpath+'NotoSans/NotoSans-SemiCondensed'
weatherfont = fontpath+'WeatherFont/weathericons-regular-webfont.ttf'
if language in ['ja','zh','zh_tw','ko']:
default = ImageFont.truetype(NotoSansCJK+'Light.otf', 18)
semi = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 18)
bold = ImageFont.truetype(NotoSansCJK+'Regular.otf', 18)
month_font = ImageFont.truetype(NotoSansCJK+'DemiLight.otf', 40)
else:
default = ImageFont.truetype(NotoSans+'Light.ttf', 18)
semi = ImageFont.truetype(NotoSans+'.ttf', 18)
bold = ImageFont.truetype(NotoSans+'Medium.ttf', 18)
month_font = ImageFont.truetype(NotoSans+'Light.ttf', 40)
w_font = ImageFont.truetype(weatherfont, 10)
x_padding = int((display_width % 10) // 2)
line_height = default.getsize('hg')[1]
line_width = display_width- x_padding
image = Image.new('RGB', (display_width, display_height), 'white')
#def main():
def write_text(box_width, box_height, text, tuple,
font=default, alignment='middle', adapt_fontsize = False):
text_width, text_height = font.getsize(text)
if adapt_fontsize == True:
size = 10
while text_width < box_width and text_height < box_height:
size += 1
font = ImageFont.truetype(font.path, size)
text_width, text_height = font.getsize(text)
while (text_width, text_height) > (box_width, box_height):
text=text[0:-1]
text_width, text_height = font.getsize(text)
if alignment is "" or "middle" or None:
x = int((box_width / 2) - (text_width / 2))
if alignment is 'left':
x = 0
y = int((box_height / 2) - (text_height / 1.7))
space = Image.new('RGB', (box_width, box_height), color='white')
ImageDraw.Draw(space).text((x, y), text, fill='black', font=font)
image.paste(space, tuple)
"""Custom function to display longer text into multiple lines (wrapping)"""
def text_wrap(text, font=default, line_width = display_width):
counter, padding = 0, 60
lines = []
if font.getsize(text)[0] < line_width:
lines.append(text)
else:
for i in range(1, len(text.split())+1):
line = ' '.join(text.split()[counter:i])
if not font.getsize(line)[0] < line_width - padding:
lines.append(line)
line, counter = '', i
if i == len(text.split()) and line != '':
lines.append(line)
return lines
"""Check if internet is available by trying to reach google"""
def internet_available():
try:
urlopen('https://google.com',timeout=5)
return True
except URLError as err:
return False
'''Get system timezone and set timezone accordingly'''
def get_tz():
with open('/etc/timezone','r') as file:
lines = file.readlines()
system_tz = lines[0].rstrip()
local_tz = timezone(system_tz)
return local_tz
def fix_ical(ical_url):
ical = str(urlopen(ical_url).read().decode())
beginAlarmIndex = 0
while beginAlarmIndex >= 0:
beginAlarmIndex = ical.find('BEGIN:VALARM')
if beginAlarmIndex >= 0:
endAlarmIndex = ical.find('END:VALARM')
ical = ical[:beginAlarmIndex] + ical[endAlarmIndex+12:]
return ical
def reduce_colours(image):
buffer = np.array(image)
r,g,b = buffer[:,:,0], buffer[:,:,1], buffer[:,:,2]
if display_colours == "bwr":
buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white
buffer[np.logical_and(r > 245, g < 245)] = [255,0,0] #red
buffer[np.logical_and(r != 255, r == g )] = [0,0,0] #black
else:
buffer[np.logical_and(r > 245, g > 245)] = [255,255,255] #white
buffer[g < 255] = [0,0,0] #black
image = Image.fromarray(buffer).rotate(270, expand=True)
return image
def calibrate(cycles):
"""Function for Calibration"""
import e_paper_drivers
epd = e_paper_drivers.EPD()
print('----------Started calibration of E-Paper display----------')
for i in range(cycles):
epd.init()
print('Calibrating black...')
epd.display_frame(epd.get_frame_buffer(black))
if display_colours == "bwr":
print('calibrating red...')
epd.display_frame(epd.get_frame_buffer(red))
print('Calibrating white...')
epd.display_frame(epd.get_frame_buffer(white))
epd.sleep()
print('Cycle {0} of {1} complete'.format(i, cycle))
print('-----------Calibration complete----------')
#if __name__ == '__main__':
#main()

View File

@@ -0,0 +1,87 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Calendar module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
from inkycal_icalendar import upcoming_events
from configuration import *
from settings import *
import arrow
"""Find max number of lines that can fit in the middle section and allocate
a position for each line"""
lines = middle_section_height // line_height
line_pos = {}
for i in range(lines):
y = top_section_height + i * line_height
line_pos['pos'+str(i+1)] = (x_padding, y)
"""Create a list of dictionaries containing dates of the next days"""
now = arrow.now()
agenda_list = [{'date':now.replace(days=+i),
'date_str':now.replace(days=+i).format('ddd D MMM YY',locale=language),
'type':'date'} for i in range(lines)]
"""Copy the list from the icalendar module"""
filtered_events = upcoming_events.copy()
"""Print events with some styling"""
"""
style = 'D MMM YY HH:mm'
if filtered_events:
line_width = max(len(i.name) for i in filtered_events)
for events in filtered_events:
print('{0} {1} | {2} | {3} |'.format(events.name,
' '* (line_width - len(events.name)), events.begin.format(style),
events.end.format(style)), events.all_day)
"""
"""Convert the event-timings from utc to the specified locale's time
and create a ready-to-display list for the agenda view"""
for events in filtered_events:
if not events.all_day:
events.end = events.end.to(get_tz())
events.begin = events.begin.to(get_tz())
if hours == '24':
agenda_list.append({'date': events.begin,
'title':events.begin.format('HH:mm')+' '+ str(events.name),
'type':'timed_event'})
if hours == '12':
agenda_list.append({'date': events.begin,
'title':events.begin.format('hh:mm a')+' '+str(events.name),
'type':'timed_event'})
else:
if events.duration.days == 1:
agenda_list.append({'date': events.begin,'title':events.name, 'type':'full_day_event'})
else:
for days in range(events.duration.days):
agenda_list.append({'date': events.begin.replace(days=+i),'title':events.name, 'type':'full_day_event'})
"""Sort events and dates in chronological order"""
agenda_list = sorted(agenda_list, key = lambda i: i['date'])
"""Crop the agenda_list in case it's too long"""
if len(agenda_list) > len(line_pos):
del agenda_list[len(line_pos):]
"""Display all events and dates on the display"""
for i in range(len(agenda_list)):
if agenda_list[i]['type'] == 'date':
write_text(line_width, line_height, agenda_list[i]['date_str'],
line_pos['pos'+str(i+1)], alignment = 'left')
elif agenda_list[i]['type'] is 'timed_event':
write_text(line_width, line_height, agenda_list[i]['title'],
line_pos['pos'+str(i+1)], alignment = 'left')
else:
write_text(line_width, line_height, agenda_list[i]['title'],
line_pos['pos'+str(i+1)])
"""Crop the image to show only the middle section"""
image.crop((0, top_section_height, display_width,
display_height-bottom_section_height)).save('agenda.png')

View File

@@ -0,0 +1,98 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Calendar module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
import calendar
from configuration import *
from settings import *
import datetime
from PIL import Image, ImageDraw
"""Define some parameters for the grid"""
grid_width, grid_height = display_width, 324
grid_rows = 6
grid_coloums = 7
padding_left = int((display_width % grid_coloums) / 2)
padding_up = int((grid_height % grid_rows) / 2)
icon_width = grid_width // grid_coloums
icon_height = grid_height // grid_rows
weekdays_height = 22
#def main():
this = datetime.datetime.now()
"""Add grid-coordinates in the grid dictionary for a later lookup"""
grid = {}
counter = 0
for row in range(grid_rows):
y = middle_section_offset - grid_height + row*icon_height
for col in range(grid_coloums):
x = padding_left + col*icon_width
counter += 1
grid['pos'+str(counter)] = (x,y)
"""Set the Calendar to start on the day specified by the settings file """
if week_starts_on is "" or "Monday":
calendar.setfirstweekday(calendar.MONDAY)
else:
calendar.setfirstweekday(calendar.SUNDAY)
"""Create a scrolling calendar"""
cal = calendar.monthcalendar(this.year, this.month)
current_row = [cal.index(i) for i in cal if this.day in i][0]
if current_row > 1:
del cal[:current_row-1]
if len(cal) < grid_rows:
next_month = this + datetime.timedelta(days=30)
cal_next_month = calendar.monthcalendar(next_month.year, next_month.month)
cal.extend(cal_next_month[:grid_rows - len(cal)]
"""
flatten = lambda z: [x for y in z for x in y]
cal = flatten(cal)
cal_next_month = flatten(cal_next_month)
cal.extend(cal_next_month)
num_font= ImageFont.truetype(NotoSansCJK+'Light.otf', 30)
"""
#draw = ImageDraw.Draw(image) #
"""
counter = 0
for i in range(len(cal)):
counter += 1
if cal[i] != 0 and counter <= grid_rows*grid_coloums:
write_text(icon_width, icon_height, str(cal[i]), grid['pos'+str(counter)],
font = num_font)
##if this.day == cal[i]:
##pos = grid['pos'+str(counter)]
#x = pos[0] + int(icon_width/2)
#y = pos[1] + int(icon_height/2)
#r = int(icon_width * 0.75#coords = (x-r, y-r, x+r, y+r)
#draw.ellipse(coords, fill= 0, outline='black',
#width=3)
image.crop((0, top_section_height, display_width,
display_height-bottom_section_height)).save('cal.png')
#if __name__ == '__main__':
# main()
"""

View File

@@ -0,0 +1,53 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
iCalendar (parsing) module for Inky-Calendar Project
Copyright by aceisace
"""
from __future__ import print_function
from configuration import *
from settings import ical_urls
import arrow
from ics import Calendar
print_events = True
"""Set timelines for filtering upcoming events"""
now = arrow.now(tz=get_tz())
near_future = now.replace(days= 30)
further_future = now.replace(days=40)
"""Parse the iCalendars from the urls, fixing some known errors with ics"""
calendars = [Calendar(fix_ical(url)) for url in ical_urls]
"""Filter any upcoming events from all iCalendars and add them to a list"""
upcoming_events = []
upcoming_events += [events for ical in calendars for events in ical.events
if now <= events.end <= further_future or now <= events.begin <= near_future]
"""Sort events according to their beginning date"""
def sort_dates(event):
return event.begin
upcoming_events.sort(key=sort_dates)
"""Multiday events are displayed incorrectly; fix that"""
for events in upcoming_events:
if events.all_day and events.duration.days > 1:
events.end = events.end.replace(days=-2)
""" The list upcoming_events should not be modified. If you need the data from
this one, copy the list or the contents to another one."""
#print(upcoming_events) # Print all events. Might look a bit messy
"""Print upcoming events in a more appealing way"""
if print_events == True:
style = 'DD MMM YY HH:mm' #D MMM YY HH:mm
if upcoming_events:
line_width = max(len(i.name) for i in upcoming_events)
for events in upcoming_events:
print('{0} {1} | {2} | {3} |'.format(events.name,
' '* (line_width - len(events.name)), events.begin.format(style),
events.end.format(style)), events.all_day)

View File

@@ -0,0 +1,49 @@
"""Add rss-feeds at the bottom section of the Calendar"""
import feedparser
from random import shuffle
from settings import *
from configuration import *
"""Find out how many lines can fit at max in the bottom section"""
lines = bottom_section_height // line_height
"""Create and fill a dictionary of the positions of each line"""
line_pos = {}
for i in range(lines):
y = bottom_section_offset + i * line_height
line_pos['pos' + str(i+1)] = (x_padding, y)
if bottom_section == "RSS" and rss_feeds != []:
"""Parse the RSS-feed titles & summaries and save them to a list"""
rss_feed = []
for feeds in rss_feeds:
text = feedparser.parse(feeds)
for posts in text.entries:
rss_feed.append('{0}: {1}'.format(posts.title, posts.summary))
del rss_feed[lines:]
shuffle(rss_feed)
"""Check the lenght of each feed. Wrap the text if it doesn't fit on one line"""
flatten = lambda z: [x for y in z for x in y]
filtered, counter = [], 0
for posts in rss_feed:
wrapped = text_wrap(posts)
counter += len(filtered) + len(wrapped)
if counter < lines:
filtered.append(wrapped)
filtered = flatten(filtered)
## for i in lines: # Show line lenght and content of each line
## print(i, ' ' * (line-len(i)),'| height: ',default.getsize(i)[1])
"""Write the correctly formatted text on the display"""
for i in range(len(filtered)):
write_text(display_width, default.getsize('hg')[1],
' '+filtered[i], line_pos['pos'+str(i+1)], alignment= 'left')
image.crop((0,bottom_section_offset, display_width, display_height)).save(
'rss.png')
del filtered, rss_feed

View File

@@ -0,0 +1,161 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Weather module for Inky-Calendar software. In development...
To-do:
- make locations of icons and text dynamic
Copyright by aceisace
"""
from __future__ import print_function
import pyowm
from settings import *
from configuration import *
from PIL import Image, ImageDraw, ImageFont
import arrow
print('Initialising weather...', end=' ')
owm = pyowm.OWM(api_key, language=language)
print('Done')
"""Icon-code to unicode dictionary for weather-font"""
weathericons = {
'01d': '\uf00d', '02d': '\uf002', '03d': '\uf013',
'04d': '\uf012', '09d': '\uf01a', '10d': '\uf019',
'11d': '\uf01e', '13d': '\uf01b', '50d': '\uf014',
'01n': '\uf02e', '02n': '\uf013', '03n': '\uf013',
'04n': '\uf013', '09n': '\uf037', '10n': '\uf036',
'11n': '\uf03b', '13n': '\uf038', '50n': '\uf023'
}
"""Split top_section into to 2 rows"""
section_height = top_section_height // 2
section_width = (top_section_width - top_section_height) // 3
"""Allocate icon sizes"""
icon_small = section_height
icon_large = top_section_height
"""Split top section into 4 coloums"""
section1 = 0
section2 = icon_large + (top_section_width - icon_large) // 3 * 0
section3 = icon_large + (top_section_width - icon_large) // 3 * 1
section4 = icon_large + (top_section_width - icon_large) // 3 * 2
"""Allocate positions for icons"""
weather_icon_pos = (section1, 0)
wind_icon_pos = (section2, 0)
sun_icon_pos = (section3, 0)
temperature_icon_pos = (section4, 0)
weather_description_pos = (section2, section_height)
humidity_icon_pos = (section4, section_height)
"""Allocate positions for text"""
next_to = lambda x: (x[0]+ icon_small, x[1])
icon_offset = section_width - icon_small
wind_pos = next_to(wind_icon_pos)
temperature_pos = next_to(temperature_icon_pos)
sun_pos = next_to(sun_icon_pos)
humidity_pos = next_to(humidity_icon_pos)
weather_pos = (section2, section_height)
#def main():
"""Connect to Openweathermap API and fetch weather data"""
if top_section == "Weather" and api_key != "" and owm.is_API_online() is True:
try:
print("Fetching weather data from openweathermap...",end = ' ')
observation = owm.weather_at_place(location)
print("Done")
weather = observation.get_weather()
weathericon = weather.get_weather_icon_name()
Humidity = str(weather.get_humidity())
cloudstatus = str(weather.get_clouds())
weather_description = (str(weather.get_detailed_status()))
"""Add the icons at the correct positions"""
print('Adding weather info and icons to the image...', end = ' ')
write_text(icon_small, icon_small, '\uf055', temperature_icon_pos,
font = w_font, adapt_fontsize = True) # Temperature icon
write_text(icon_large, icon_large, weathericons[weathericon],
weather_icon_pos, font = w_font, adapt_fontsize = True) # Weather icon
write_text(icon_small, icon_small, '\uf07a', humidity_icon_pos, font = w_font,
adapt_fontsize = True) #Humidity icon
write_text(icon_small,icon_small, '\uf050', wind_icon_pos, font = w_font,
adapt_fontsize = True) #Wind icon
"""Format and write the temperature and windspeed"""
if units == "metric":
Temperature = str(int(weather.get_temperature(unit='celsius')['temp']))
windspeed = str(int(weather.get_wind()['speed']))
write_text(icon_offset, section_height, Temperature+'°C', temperature_pos)
write_text(icon_offset,section_height, windspeed+" km/h", wind_pos)
else:
Temperature = str(int(weather.get_temperature('fahrenheit')['temp']))
windspeed = str(int(weather.get_wind()['speed']*0.621))
write_text(icon_offset, section_height, Temperature+' F', temperature_pos)
write_text(icon_offset,section_height, windspeed+" mph", wind_pos)
"""write the humidity at the given position"""
write_text(icon_offset, section_height, Humidity+'%', humidity_pos)
now = arrow.now(tz=get_tz())
sunrise = arrow.get(weather.get_sunrise_time()).to(get_tz())
sunset = arrow.get(weather.get_sunset_time()).to(get_tz())
"""Add the sunrise/sunset icon and display the time"""
if (now <= sunrise and now <= sunset) or (now >= sunrise and now >= sunset):
write_text(icon_small, icon_small, '\uf051', sun_icon_pos, font = w_font,
adapt_fontsize = True)
if hours == "24":
write_text(icon_offset, section_height, sunrise.format('H:mm'), sun_pos)
else:
write_text(icon_offset, section_height, sunrise.format('h:mm'), sun_pos)
else:
write_text(icon_small, '\uf052', sun_icon_pos, font = w_font,
adapt_fontsize = True)
if hours == "24":
write_text(icon_offset, section_height, sunset.format('H:mm'), sun_pos)
else:
write_text(icon_offset, section_height, sunset.format('h:mm'), sun_pos)
"""Add a short weather description"""
write_text(section2+section3-icon_offset, section_height,
weather_description, weather_pos)
print('Done'+'\n')
"""Show the fetched weather data"""
print("Today's weather report: The current Temperature is {0}°C. The "
"relative humidity is {1} %. The current windspeed is {2} km/h. "
"The sunrise today was at {3}. The sunset is at {4}. The weather can "
"be described with: {5}".format(Temperature, Humidity, windspeed,
sunrise.format('H:mm'), sunset.format('H:mm'), weather_description))
image.crop((0,0, top_section_width, top_section_height)).save('weather.png')
except Exception as e:
"""If no response was received from the openweathermap
api server, add the cloud with question mark"""
print('__________OWM-ERROR!__________')
print('Reason: ',e)
write_text(icon_large, icon_large, '\uf07b', weather_icon_pos,
font = w_font, adapt_fontsize = True)
pass
#if __name__ == '__main__':
#main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -0,0 +1,18 @@
ical_urls = ["https://calendar.google.com/calendar/ical/en.usa%23holiday%40group.v.calendar.google.com/public/basic.ics",
"https://calendar.google.com/calendar/ical/ohcobp9hs097e9nnbppks7blv4%40group.calendar.google.com/private-55859b2165097102e0c061e978eb4926/basic.ics",
"https://calendar.yahoo.com/saadnaseer63/3ac8573af0f38b65367a4e3d287bf06d/ycal.ics?id=1407"]
rss_feeds = ["http://feeds.bbci.co.uk/news/world/rss.xml#"]
update_interval = "60"
api_key = "57c07b8f2ae09e348d32317f1bfe3f52"
location = "Stuttgart, DE"
week_starts_on = "Monday"
events_max_range = "60"
calibration_hours = [0,12,18]
display_colours = "bwr"
language = "en"
units = "metric"
hours = "24"
top_section = "Weather"
middle_section = "Agenda"
bottom_section = "RSS"
show_last_update_time = "False"

View File

@@ -0,0 +1,48 @@
from configuration import *
from settings import *
"""
def wrapper(text, font=default, max_width = display_width):
counter = 0
padding = 50
lines = []
if font.getsize(text)[0] < max_width:
lines.append(text)
else:
for i in range(1, len(text.split())+1):
line = ' '.join(text.split()[counter:i])
if not font.getsize(line)[0] < max_width- padding:
lines.append(line)
line = ''
counter = i
if i == len(text.split()) and line != '':
lines.append(line)
return lines
"""
def text_wrap(text, font=default, line_width = display_width):
counter, lines = 0, []
if font.getsize(text)[0] < line_width:
lines.append(text)
else:
while font.getsize(text)[0] < line_width:
"""
for i in range(1, len(text.split())+1):
line = ' '.join(text.split()[counter:i])
print(line, font.getsize(line)[0])
"""
#text = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.'
#text = 'Russia submersible fire was in battery compartment Fourteen crew died in the fire on board'
text = "Russian LGBT activist Yelena Grigoryeva murdered in St Petersburg: Yelena Grigoryeva, 41, was stabbed and strangled near her home in St Petersburg, relatives say."
lines = text_wrap(text, default, display_width)
line = len(max(lines, key=len))
for i in lines:
print(i, ' ' * (line-len(i)),'| width: ',default.getsize(i)[0])

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB