cleanup
This commit is contained in:
parent
63ba1e8d1d
commit
7b4dfebbdc
364
layouter.py
364
layouter.py
@ -1,215 +1,279 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
""" Author: Hendrik Schutter, localhorst@mosad.xyz
|
"""
|
||||||
Date of creation: 2022/11/16
|
Author: Hendrik Schutter, localhorst@mosad.xyz
|
||||||
Date of last modification: 2022/11/23
|
Date of creation: 2022/11/16
|
||||||
|
Date of last modification: 2025/06/08
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import glob
|
import glob
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import qrcode
|
import qrcode
|
||||||
from PIL import Image
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
from PIL import ImageFont
|
|
||||||
from PIL import ImageDraw
|
# Constants
|
||||||
|
FONT_PATH = "/usr/share/fonts"
|
||||||
|
DEFAULT_FONT_REGULAR = "DejaVuSans.ttf"
|
||||||
|
DEFAULT_FONT_BOLD = "DejaVuSans-Bold.ttf"
|
||||||
|
OUTPUT_WIDTH = 696 # px
|
||||||
|
OUTPUT_HEIGHT = 300 # px
|
||||||
|
TEXT_X_OFFSET = 300 # px
|
||||||
|
QR_CODE_SIZE = 289 # px
|
||||||
|
|
||||||
|
# Configure logging
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class DriveData:
|
class DriveData:
|
||||||
drive_index: int
|
drive_index: int
|
||||||
drive_state: str #none, deleted, shredded
|
drive_state: str
|
||||||
modelfamiliy: str
|
modelfamily: str
|
||||||
modelname: str
|
modelname: str
|
||||||
capacity: int #in bytes
|
capacity: int
|
||||||
serialnumber: str
|
serialnumber: str
|
||||||
power_on_hours: int #in hours
|
power_on_hours: int
|
||||||
power_cycle: int
|
power_cycle: int
|
||||||
smart_error_count: int
|
smart_error_count: int
|
||||||
shred_timestamp: int #unix timestamp
|
shred_timestamp: int
|
||||||
shred_duration: int #in seconds
|
shred_duration: int
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class DriveDataPrintable:
|
class DriveDataPrintable:
|
||||||
modelfamiliy: str #max lenght 25
|
modelfamily: str
|
||||||
modelname: str #max lenght 25
|
modelname: str
|
||||||
capacity: str #max lenght 25, in human-readable format with unit (GB/TB)
|
capacity: str
|
||||||
serialnumber: str #max lenght 25
|
serialnumber: str
|
||||||
power_on_hours: str #max lenght 25, in hours and days and years
|
power_on_hours: str
|
||||||
power_cycle: str #max lenght 25
|
power_cycle: str
|
||||||
smart_error_count: str #max lenght 25
|
smart_error_count: str
|
||||||
shred_timestamp: str #max lenght 25, human-readable
|
shred_timestamp: str
|
||||||
shred_duration: str #max lenght 25, human-readable
|
shred_duration: str
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class ReHddInfo:
|
class ReHddInfo:
|
||||||
link: str
|
link: str
|
||||||
version: str
|
version: str
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class DriveDataJson:
|
class DriveDataJson:
|
||||||
drive: DriveData
|
drive: DriveData
|
||||||
rehdd: ReHddInfo
|
rehdd: ReHddInfo
|
||||||
|
|
||||||
def get_font_path_regular():
|
|
||||||
path = "/usr/share/fonts"
|
|
||||||
files = glob.glob(path + "/**/DejaVuSans.ttf", recursive = True)
|
|
||||||
return files[0]
|
|
||||||
|
|
||||||
def get_font_path_bold():
|
def find_font_path(font_name):
|
||||||
path = "/usr/share/fonts"
|
"""Finds the full path of the specified font."""
|
||||||
files = glob.glob(path + "/**/DejaVuSans-Bold.ttf", recursive = True)
|
try:
|
||||||
return files[0]
|
files = glob.glob(f"{FONT_PATH}/**/{font_name}", recursive=True)
|
||||||
|
return files[0] if files else None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error locating font {font_name}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def human_readable_capacity_1024(size, decimal_places=0):
|
|
||||||
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']:
|
|
||||||
if size < 1024.0 or unit == 'PiB':
|
|
||||||
break
|
|
||||||
size /= 1024.0
|
|
||||||
return f"{size:.{decimal_places}f} {unit}"
|
|
||||||
|
|
||||||
def human_readable_capacity_1000(size, decimal_places=0):
|
def human_readable_capacity(size, decimal_places=0, base=1000):
|
||||||
for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']:
|
"""Converts bytes to a human-readable string."""
|
||||||
if size < 1000.0 or unit == 'PB':
|
units = (
|
||||||
break
|
["B", "KB", "MB", "GB", "TB", "PB"]
|
||||||
size /= 1000.0
|
if base == 1000
|
||||||
return f"{size:.{decimal_places}f} {unit}"
|
else ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]
|
||||||
|
)
|
||||||
|
for unit in units:
|
||||||
|
if size < base or unit == units[-1]:
|
||||||
|
return f"{size:.{decimal_places}f} {unit}"
|
||||||
|
size /= base
|
||||||
|
|
||||||
|
|
||||||
|
def human_readable_power_on_hours(hours):
|
||||||
|
"""Converts hours to human-readable string in hours, days, and years."""
|
||||||
|
return (
|
||||||
|
str(hours)
|
||||||
|
+ "h or "
|
||||||
|
+ str(int(hours / 24))
|
||||||
|
+ "d or "
|
||||||
|
+ str("{:.2f}".format(float(hours / 24 / 365)))
|
||||||
|
+ "y"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cut_string(max_length, data, direction="end"):
|
||||||
|
"""Trims a string to the maximum length, adding ellipses if necessary."""
|
||||||
|
if len(data) > max_length:
|
||||||
|
return (
|
||||||
|
f"{data[:max_length-4]} ..."
|
||||||
|
if direction == "end"
|
||||||
|
else f"... {data[-(max_length-4):]}"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
def human_readable_power_on_hours(hours, decimal_places=2):
|
|
||||||
return str(hours) + "h or " + str(int(hours/24)) + "d or " + str("{:.2f}".format(float(hours/24/365))) + "y"
|
|
||||||
|
|
||||||
def cut_string(max_lenght, data, direction):
|
|
||||||
if (len(data) > max_lenght):
|
|
||||||
if (direction == "end"):
|
|
||||||
return data[0:(max_lenght-4)] + " ..."
|
|
||||||
elif (direction == "start"):
|
|
||||||
return "... " + data[(len(data)-max_lenght+4):]
|
|
||||||
else:
|
|
||||||
return cut_string(max_lenght, data, "end")
|
|
||||||
else:
|
|
||||||
return data
|
|
||||||
|
|
||||||
def format_to_printable(drive):
|
def format_to_printable(drive):
|
||||||
return DriveDataPrintable(
|
return DriveDataPrintable(
|
||||||
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelfamiliy), "end"),\
|
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelfamily), "end"),
|
||||||
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelname), "end"),\
|
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelname), "end"),
|
||||||
cut_string(20, human_readable_capacity_1000(drive.capacity), "end"),\
|
cut_string(20, human_readable_capacity(drive.capacity), "end"),
|
||||||
cut_string(16, re.sub(r"[^a-zA-Z0-9.-_]", "", drive.serialnumber), "start"),\
|
cut_string(16, re.sub(r"[^a-zA-Z0-9.-_]", "", drive.serialnumber), "start"),
|
||||||
cut_string(30, human_readable_power_on_hours(drive.power_on_hours), "end"),\
|
cut_string(30, human_readable_power_on_hours(drive.power_on_hours), "end"),
|
||||||
cut_string(10, str(drive.power_cycle), "end"),\
|
cut_string(10, str(drive.power_cycle), "end"),
|
||||||
cut_string(10, str(drive.smart_error_count), "end"),\
|
cut_string(10, str(drive.smart_error_count), "end"),
|
||||||
cut_string(30, datetime.datetime.utcfromtimestamp(drive.shred_timestamp).strftime('%Y-%m-%d %H:%M:%S'), "end"),\
|
cut_string(
|
||||||
cut_string(30, str(datetime.timedelta(seconds = drive.shred_duration)), "end"))
|
30,
|
||||||
|
datetime.datetime.fromtimestamp(
|
||||||
|
drive.shred_timestamp, datetime.UTC
|
||||||
|
).strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"end",
|
||||||
|
),
|
||||||
|
cut_string(30, str(datetime.timedelta(seconds=drive.shred_duration)), "end"),
|
||||||
|
)
|
||||||
|
|
||||||
def draw_text(drawable, printable_data, text_x_offset):
|
|
||||||
try:
|
def draw_text(drawable, printable_data, font_regular, font_bold, font_bold_bigger):
|
||||||
font_file_regular = get_font_path_regular()
|
"""Draws the formatted text onto the image."""
|
||||||
font_file_bold = get_font_path_bold()
|
|
||||||
except Exception as ex:
|
|
||||||
print("unable to find font: " + str(ex))
|
|
||||||
return
|
|
||||||
|
|
||||||
font_size = 20
|
|
||||||
text_y_offset = 10
|
text_y_offset = 10
|
||||||
text_y_offset_increment = 25
|
value_column_x_offset = 120
|
||||||
value_colum_x_offset = 120
|
font_size = 20
|
||||||
|
|
||||||
drawable.text((text_x_offset, text_y_offset), printable_data.serialnumber,(0),font=ImageFont.truetype(font_file_bold, 30))
|
drawable.text(
|
||||||
text_y_offset += 40
|
(TEXT_X_OFFSET, text_y_offset), printable_data.serialnumber, (0), font=font_bold
|
||||||
|
)
|
||||||
|
text_y_offset += 25
|
||||||
|
|
||||||
drawable.text((text_x_offset, text_y_offset), "Familiy: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
|
fields = [
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelfamiliy,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
("Family:", printable_data.modelfamily, font_regular),
|
||||||
text_y_offset += text_y_offset_increment
|
("Model:", printable_data.modelname, font_regular),
|
||||||
drawable.text((text_x_offset, text_y_offset), "Model: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
|
("Hours:", printable_data.power_on_hours, font_regular),
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelname,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
("Cycles:", printable_data.power_cycle, font_regular),
|
||||||
text_y_offset += text_y_offset_increment
|
("Errors:", printable_data.smart_error_count, font_regular),
|
||||||
drawable.text((text_x_offset, text_y_offset), "Hours: " ,(0),font=ImageFont.truetype(font_file_bold, font_size))
|
("Shred on:", printable_data.shred_timestamp, font_regular),
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.power_on_hours,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
("Duration:", printable_data.shred_duration, font_regular),
|
||||||
text_y_offset += text_y_offset_increment
|
]
|
||||||
drawable.text((text_x_offset, text_y_offset), "Cycles: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
|
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.power_cycle,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
|
||||||
text_y_offset += text_y_offset_increment
|
|
||||||
drawable.text((text_x_offset, text_y_offset), "Errors: ", (0),font=ImageFont.truetype(font_file_bold, font_size))
|
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.smart_error_count,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
|
||||||
text_y_offset += text_y_offset_increment
|
|
||||||
drawable.text((text_x_offset, text_y_offset), "Shred on: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
|
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.shred_timestamp,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
|
||||||
text_y_offset += text_y_offset_increment
|
|
||||||
drawable.text((text_x_offset, text_y_offset), "Duration: " ,(0),font=ImageFont.truetype(font_file_bold, font_size))
|
|
||||||
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.shred_duration,(0),font=ImageFont.truetype(font_file_regular, font_size))
|
|
||||||
text_y_offset += text_y_offset_increment
|
|
||||||
|
|
||||||
drawable.text((text_x_offset, text_y_offset), printable_data.capacity,(0),font=ImageFont.truetype(font_file_bold, font_size*3))
|
for label, value, font in fields:
|
||||||
|
drawable.text((TEXT_X_OFFSET, text_y_offset), label, fill=0, font=font_bold)
|
||||||
|
drawable.text(
|
||||||
|
(TEXT_X_OFFSET + value_column_x_offset, text_y_offset),
|
||||||
|
value,
|
||||||
|
fill=0,
|
||||||
|
font=font,
|
||||||
|
)
|
||||||
|
text_y_offset += 25
|
||||||
|
|
||||||
|
drawable.text(
|
||||||
|
(TEXT_X_OFFSET, text_y_offset),
|
||||||
|
printable_data.capacity,
|
||||||
|
(0),
|
||||||
|
font=font_bold_bigger,
|
||||||
|
)
|
||||||
|
|
||||||
def draw_outline(drawable, margin, width, output_width, output_height):
|
|
||||||
#upper
|
|
||||||
drawable.line([(margin,margin), (output_width -margin ,margin)], fill=None, width=width, joint=None)
|
|
||||||
#left
|
|
||||||
drawable.line([(margin,margin), (margin ,output_height-margin)], fill=None, width=width, joint=None)
|
|
||||||
#right
|
|
||||||
drawable.line([(output_width-margin,margin), (output_width-margin ,output_height-margin)], fill=None, width=width, joint=None)
|
|
||||||
#lower
|
|
||||||
drawable.line([(margin,output_height - margin), (output_width-margin,output_height -margin)], fill=None, width=width, joint=None)
|
|
||||||
|
|
||||||
def draw_qr_code(image, data):
|
def draw_qr_code(image, data):
|
||||||
|
"""
|
||||||
|
Draws a QR code on the provided image at a specific location.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
image (Image.Image): The target image.
|
||||||
|
data (str): The data to encode in the QR code.
|
||||||
|
"""
|
||||||
|
# Generate the QR code
|
||||||
qr_img = qrcode.make(data)
|
qr_img = qrcode.make(data)
|
||||||
qr_img.thumbnail((291, 291), Image.Resampling.LANCZOS)
|
qr_img = qr_img.convert("1") # Ensure QR code is in binary (black/white)
|
||||||
image.paste(qr_img, (7, 7))
|
|
||||||
|
# Remove white border
|
||||||
|
bbox = qr_img.getbbox() # Get the bounding box of the non-white area
|
||||||
|
qr_img = qr_img.crop(bbox)
|
||||||
|
|
||||||
|
# Resize to desired size
|
||||||
|
qr_img = qr_img.resize((QR_CODE_SIZE, QR_CODE_SIZE), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
|
# Paste the QR code onto the image
|
||||||
|
region = (5, 5, 5 + QR_CODE_SIZE, 5 + QR_CODE_SIZE)
|
||||||
|
image.paste(qr_img, box=region)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_outline(drawable, margin, width, output_width, output_height):
|
||||||
|
"""
|
||||||
|
Draws a rectangular outline on the drawable canvas.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
drawable (ImageDraw.Draw): The drawable canvas.
|
||||||
|
margin (int): The margin from the edges of the image.
|
||||||
|
width (int): The width of the outline.
|
||||||
|
output_width (int): The total width of the image.
|
||||||
|
output_height (int): The total height of the image.
|
||||||
|
"""
|
||||||
|
# Define the four corners of the rectangle, adjusting for line width
|
||||||
|
top_left = (margin, margin)
|
||||||
|
top_right = (output_width - margin - width, margin)
|
||||||
|
bottom_left = (margin, output_height - margin - width)
|
||||||
|
bottom_right = (output_width - margin - width, output_height - margin - width)
|
||||||
|
|
||||||
|
# Draw the outline lines with adjusted coordinates
|
||||||
|
lines = [
|
||||||
|
(top_left, top_right), # Top edge
|
||||||
|
(top_left, bottom_left), # Left edge
|
||||||
|
(top_right, bottom_right), # Right edge
|
||||||
|
(bottom_left, bottom_right), # Bottom edge
|
||||||
|
]
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
drawable.line(line, fill=0, width=width)
|
||||||
|
|
||||||
|
|
||||||
def generate_image(drive, rehdd_info, output_file):
|
def generate_image(drive, rehdd_info, output_file):
|
||||||
output_width = 696 #in px set by used paper
|
"""Generates an image containing drive data and a QR code."""
|
||||||
output_height = 300 #in px
|
|
||||||
text_x_offset= 300 #in px
|
|
||||||
|
|
||||||
qr_data = DriveDataJson(drive, rehdd_info)
|
|
||||||
try:
|
|
||||||
json_qr_daten = json.dumps(dataclasses.asdict(qr_data))
|
|
||||||
except Exception as ex:
|
|
||||||
print("unable to generate json: " + str(ex))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
qr_data = json.dumps(dataclasses.asdict(DriveDataJson(drive, rehdd_info)))
|
||||||
printable_data = format_to_printable(drive)
|
printable_data = format_to_printable(drive)
|
||||||
except Exception as ex:
|
except Exception as e:
|
||||||
print("unable to format data: " + str(ex))
|
logging.error(f"Error preparing data: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
#print(printable_data.serialnumber)
|
|
||||||
|
|
||||||
#create black and white (binary) image with white background
|
output_image = Image.new("1", (OUTPUT_WIDTH, OUTPUT_HEIGHT), "white")
|
||||||
output_image = Image.new('1', (output_width, output_height), "white")
|
|
||||||
|
|
||||||
#create draw pane
|
|
||||||
draw = ImageDraw.Draw(output_image)
|
draw = ImageDraw.Draw(output_image)
|
||||||
|
|
||||||
draw_outline(draw, 1, 4, output_width, output_height)
|
font_regular = ImageFont.truetype(find_font_path(DEFAULT_FONT_REGULAR), 20)
|
||||||
draw_text(draw, printable_data, text_x_offset)
|
font_bold = ImageFont.truetype(find_font_path(DEFAULT_FONT_BOLD), 20)
|
||||||
draw_qr_code(output_image, str(json_qr_daten).replace(" ", ""))
|
font_bold_bigger = ImageFont.truetype(find_font_path(DEFAULT_FONT_BOLD), 60)
|
||||||
|
|
||||||
output_image.save(output_file)
|
draw_outline(draw, 1, 4, OUTPUT_WIDTH, OUTPUT_HEIGHT)
|
||||||
|
draw_text(draw, printable_data, font_regular, font_bold, font_bold_bigger)
|
||||||
|
draw_qr_code(output_image, qr_data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
output_image.save(output_file)
|
||||||
|
logging.info(f"Image saved to {output_file}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error saving image: {e}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
rehdd_info = ReHddInfo("https://git.mosad.xyz/localhorst/reHDD", "bV0.2.2")
|
||||||
rehdd_info = ReHddInfo("https://git.mosad.xyz/localhorst/reHDD", "bV0.2.2") # read this from rehdd process
|
|
||||||
|
|
||||||
temp_drive = DriveData(
|
temp_drive = DriveData(
|
||||||
drive_index=0,\
|
drive_index=0,
|
||||||
drive_state="shredded",\
|
drive_state="shredded",
|
||||||
modelfamiliy="Toshiba 2.5\\ HDD MK..65GSSX",\
|
modelfamily='Toshiba 2.5" HDD MK..65GSSX',
|
||||||
modelname="TOSHIBA MK3265GSDX",\
|
modelname="TOSHIBA MK3265GSDX",
|
||||||
capacity=343597383680,\
|
capacity=343597383680,
|
||||||
serialnumber="YG6742U56UDRL123456789ABCDEFGJKL",\
|
serialnumber="YG6742U56UDRL123456789ABCDEFGJKL",
|
||||||
power_on_hours=7074,\
|
power_on_hours=7074,
|
||||||
power_cycle=4792,\
|
power_cycle=4792,
|
||||||
smart_error_count=1,\
|
smart_error_count=1,
|
||||||
shred_timestamp=1647937421,\
|
shred_timestamp=datetime.datetime.now(datetime.timezone.utc).timestamp(),
|
||||||
shred_duration=81718)
|
shred_duration=81718,
|
||||||
|
)
|
||||||
|
|
||||||
generate_image(temp_drive, rehdd_info, "output.png")
|
generate_image(temp_drive, rehdd_info, "output.png")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
BIN
output.png
BIN
output.png
Binary file not shown.
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.5 KiB |
Loading…
Reference in New Issue
Block a user