#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Author: Hendrik Schutter, localhorst@mosad.xyz Date of creation: 2022/11/16 Date of last modification: 2022/11/23 """ import re import dataclasses import glob import datetime import json import qrcode from PIL import Image from PIL import ImageFont from PIL import ImageDraw @dataclasses.dataclass class DriveData: drive_index: int drive_state: str #none, deleted, shredded modelfamiliy: str modelname: str capacity: int #in bytes serialnumber: str power_on_hours: int #in hours power_cycle: int smart_error_count: int shred_timestamp: int #unix timestamp shred_duration: int #in seconds @dataclasses.dataclass class DriveDataPrintable: modelfamiliy: str #max lenght 25 modelname: str #max lenght 25 capacity: str #max lenght 25, in human-readable format with unit (GB/TB) serialnumber: str #max lenght 25 power_on_hours: str #max lenght 25, in hours and days and years power_cycle: str #max lenght 25 smart_error_count: str #max lenght 25 shred_timestamp: str #max lenght 25, human-readable shred_duration: str #max lenght 25, human-readable @dataclasses.dataclass class ReHddInfo: link: str version: str @dataclasses.dataclass class DriveDataJson: drive: DriveData 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(): path = "/usr/share/fonts" files = glob.glob(path + "/**/DejaVuSans-Bold.ttf", recursive = True) return files[0] 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): for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']: if size < 1000.0 or unit == 'PB': break size /= 1000.0 return f"{size:.{decimal_places}f} {unit}" 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): 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.modelname), "end"),\ cut_string(20, human_readable_capacity_1000(drive.capacity), "end"),\ 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(10, str(drive.power_cycle), "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(30, str(datetime.timedelta(seconds = drive.shred_duration)), "end")) def draw_text(drawable, printable_data, text_x_offset): try: font_file_regular = get_font_path_regular() 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_increment = 25 value_colum_x_offset = 120 drawable.text((text_x_offset, text_y_offset), printable_data.serialnumber,(0),font=ImageFont.truetype(font_file_bold, 30)) text_y_offset += 40 drawable.text((text_x_offset, text_y_offset), "Familiy: ",(0),font=ImageFont.truetype(font_file_bold, font_size)) drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelfamiliy,(0),font=ImageFont.truetype(font_file_regular, font_size)) text_y_offset += text_y_offset_increment drawable.text((text_x_offset, text_y_offset), "Model: ",(0),font=ImageFont.truetype(font_file_bold, font_size)) drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelname,(0),font=ImageFont.truetype(font_file_regular, font_size)) text_y_offset += text_y_offset_increment drawable.text((text_x_offset, text_y_offset), "Hours: " ,(0),font=ImageFont.truetype(font_file_bold, font_size)) 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)) 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)) 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): qr_img = qrcode.make(data) qr_img.thumbnail((291, 291), Image.Resampling.LANCZOS) image.paste(qr_img, (7, 7)) def generate_image(drive, rehdd_info, output_file): output_width = 696 #in px set by used paper 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: printable_data = format_to_printable(drive) except Exception as ex: print("unable to format data: " + str(ex)) return #print(printable_data.serialnumber) #create black and white (binary) image with white background output_image = Image.new('1', (output_width, output_height), "white") #create draw pane draw = ImageDraw.Draw(output_image) draw_outline(draw, 1, 4, output_width, output_height) draw_text(draw, printable_data, text_x_offset) draw_qr_code(output_image, str(json_qr_daten).replace(" ", "")) output_image.save(output_file) def main(): rehdd_info = ReHddInfo("https://git.mosad.xyz/localhorst/reHDD", "bV0.2.2") # read this from rehdd process temp_drive = DriveData( drive_index=0,\ drive_state="shredded",\ modelfamiliy="Toshiba 2.5\\ HDD MK..65GSSX",\ modelname="TOSHIBA MK3265GSDX",\ capacity=343597383680,\ serialnumber="YG6742U56UDRL123456789ABCDEFGJKL",\ power_on_hours=7074,\ power_cycle=4792,\ smart_error_count=1,\ shred_timestamp=1647937421,\ shred_duration=81718) generate_image(temp_drive, rehdd_info, "output.png") if __name__ == "__main__": main()