From c789d09b07575c8cba1fe93b14a46728b3d735a5 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Fri, 26 Apr 2024 17:33:02 -0700 Subject: [PATCH] Plaintext Thumbs (Proof of Concept) **BASIC** support for rendering thumbnails for certain plaintext types (txt, md, html, etc.) using PIL. Notable issues include: - Long draw times (entire files are read) - No text wrapping - Hardcoded style - Blurry text in preview pane images - No cached thumbnails (I call dibs on the thumbnail caching system) --- tagstudio/src/cli/ts_cli.py | 2 +- tagstudio/src/core/ts_core.py | 6 ++-- tagstudio/src/qt/ts_qt.py | 57 +++++++++++++++++------------------ 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index 560d22b2..4f78d750 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -191,7 +191,7 @@ class CliDriver: return WHITE_FG elif ext.lower().replace('.','',1) in VIDEO_TYPES: return BRIGHT_CYAN_FG - elif ext.lower().replace('.','',1) in TEXT_TYPES: + elif ext.lower().replace('.','',1) in DOC_TYPES: return BRIGHT_GREEN_FG else: return BRIGHT_WHITE_FG diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 0ea35039..cc5418bd 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -18,6 +18,7 @@ BACKUP_FOLDER_NAME: str = 'backups' COLLAGE_FOLDER_NAME: str = 'collages' LIBRARY_FILENAME: str = 'ts_library.json' +# TODO: Turn this whitelist into a user-configurable blacklist. IMAGE_TYPES: list[str] = ['png', 'jpg', 'jpeg', 'jpg_large', 'jpeg_large', 'jfif', 'gif', 'tif', 'tiff', 'heic', 'heif', 'webp', 'bmp', 'svg', 'avif', 'apng', 'jp2', 'j2k', 'jpg2'] @@ -25,8 +26,9 @@ VIDEO_TYPES: list[str] = ['mp4', 'webm', 'mov', 'hevc', 'mkv', 'avi', 'wmv', 'flv', 'gifv', 'm4p', 'm4v', '3gp'] AUDIO_TYPES: list[str] = ['mp3', 'mp4', 'mpeg4', 'm4a', 'aac', 'wav', 'flac', 'alac', 'wma', 'ogg', 'aiff'] -TEXT_TYPES: list[str] = ['txt', 'rtf', 'md', +DOC_TYPES: list[str] = ['txt', 'rtf', 'md', 'doc', 'docx', 'pdf', 'tex', 'odt', 'pages'] +PLAINTEXT_TYPES: list[str] = ['txt', 'md', 'css', 'html', 'xml', 'json', 'js', 'ts'] SPREADSHEET_TYPES: list[str] = ['csv', 'xls', 'xlsx', 'numbers', 'ods'] PRESENTATION_TYPES: list[str] = ['ppt', 'pptx', 'key', 'odp'] ARCHIVE_TYPES: list[str] = ['zip', 'rar', 'tar', 'tar.gz', 'tgz', '7z'] @@ -34,7 +36,7 @@ PROGRAM_TYPES: list[str] = ['exe', 'app'] SHORTCUT_TYPES: list[str] = ['lnk', 'desktop', 'url'] ALL_FILE_TYPES: list[str] = IMAGE_TYPES + VIDEO_TYPES + AUDIO_TYPES + \ - TEXT_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \ + DOC_TYPES + SPREADSHEET_TYPES + PRESENTATION_TYPES + \ ARCHIVE_TYPES + PROGRAM_TYPES + SHORTCUT_TYPES BOX_FIELDS = ['tag_box', 'text_box'] diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 7e397f90..83c8cd73 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -38,9 +38,9 @@ from humanfriendly import format_timespan, format_size from src.core.library import Collation, Entry, ItemType, Library, Tag from src.core.palette import ColorType, get_tag_color -from src.core.ts_core import (TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES, +from src.core.ts_core import (PLAINTEXT_TYPES, TagStudioCore, TAG_COLORS, DATE_FIELDS, TEXT_FIELDS, BOX_FIELDS, ALL_FILE_TYPES, SHORTCUT_TYPES, PROGRAM_TYPES, ARCHIVE_TYPES, PRESENTATION_TYPES, - SPREADSHEET_TYPES, TEXT_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES, + SPREADSHEET_TYPES, DOC_TYPES, AUDIO_TYPES, VIDEO_TYPES, IMAGE_TYPES, LIBRARY_FILENAME, COLLAGE_FOLDER_NAME, BACKUP_FOLDER_NAME, TS_FOLDER_NAME, VERSION_BRANCH, VERSION) from src.core.utils.web import strip_web_protocol @@ -3321,7 +3321,7 @@ class CollageIconRenderer(QObject): return '\033[37m' elif ext.lower().replace('.','',1) in VIDEO_TYPES: return '\033[96m' - elif ext.lower().replace('.','',1) in TEXT_TYPES: + elif ext.lower().replace('.','',1) in DOC_TYPES: return '\033[92m' else: return '\033[97m' @@ -3393,6 +3393,7 @@ class ThumbRenderer(QObject): extension = os.path.splitext(filepath)[1][1:].lower() try: + # Images ======================================================= if extension in IMAGE_TYPES: image = Image.open(filepath) # image = self.thumb_debug @@ -3403,11 +3404,8 @@ class ThumbRenderer(QObject): image = new_bg if image.mode != 'RGB': image = image.convert(mode='RGB') - # raise ValueError - # except (UnidentifiedImageError, FileNotFoundError): - # image = Image.open(os.path.normpath(f'{Path(__file__).parent.parent.parent}/resources/cli/images/no_preview.png')) - # image.thumbnail((adj_size,adj_size)) + # Videos ======================================================= elif extension in VIDEO_TYPES: video = cv2.VideoCapture(filepath) video.set(cv2.CAP_PROP_POS_FRAMES, @@ -3421,7 +3419,18 @@ class ThumbRenderer(QObject): success, frame = video.read() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image = Image.fromarray(frame) + + # Plain Text =================================================== + elif extension in ['txt', 'md']: + text: str = extension + with open(filepath, 'r', encoding='utf-8') as text_file: + text = text_file.read() + bg = Image.new('RGB',(256,256), color='#222222') + draw = ImageDraw.Draw(bg) + draw.text((16,16), text, file=(255,255,255)) + image = bg + # Other Files ================================================== # TODO: Create placeholder thumbnails for non-media files. # else: # image: Image.Image = ThumbRenderer.thumb_loading_512.resize( @@ -3440,9 +3449,7 @@ class ThumbRenderer(QObject): new_y = adj_size new_x = math.ceil(adj_size * (orig_x / orig_y)) - img_ratio = new_x / new_y - # logging.info(f'[TR] {(new_x / new_y)}') - # self.updated_ratio.emit(new_x / new_y) + # img_ratio = new_x / new_y image = image.resize( (new_x, new_y), resample=Image.Resampling.BILINEAR) @@ -3495,21 +3502,6 @@ class ThumbRenderer(QObject): final = ThumbRenderer.thumb_broken_512.resize( (adj_size, adj_size), resample=Image.Resampling.BILINEAR) - # if file_type in VIDEO_TYPES + ['gif', 'apng'] or broken_thumb: - # idk = ImageDraw.Draw(final) - # # idk.textlength(file_type) - # ext_offset_x = idk.textlength( - # text=file_type.upper(), font=ThumbRenderer.ext_font) / 2 - # ext_offset_x = math.floor(ext_offset_x * (1/pixelRatio)) - # x_margin = math.floor( - # (adj_size-((base_size[0]//6)+ext_offset_x) * pixelRatio)) - # y_margin = math.floor( - # (adj_size-((base_size[0]//8)) * pixelRatio)) - # stroke_width = round(2 * pixelRatio) - # fill = 'white' if not broken_thumb else '#E32B41' - # idk.text((x_margin, y_margin), file_type.upper( - # ), fill=fill, font=ThumbRenderer.ext_font, stroke_width=stroke_width, stroke_fill=(0, 0, 0)) - qim = ImageQt.ImageQt(final) if image: image.close() @@ -3559,6 +3551,7 @@ class ThumbRenderer(QObject): extension = os.path.splitext(filepath)[1][1:].lower() try: + # Images ======================================================= if extension in IMAGE_TYPES: image = Image.open(filepath) # image = self.thumb_debug @@ -3569,11 +3562,8 @@ class ThumbRenderer(QObject): image = new_bg if image.mode != 'RGB': image = image.convert(mode='RGB') - # raise ValueError - # except (UnidentifiedImageError, FileNotFoundError): - # image = Image.open(os.path.normpath(f'{Path(__file__).parent.parent.parent}/resources/cli/images/no_preview.png')) - # image.thumbnail((adj_size,adj_size)) + # Videos ======================================================= elif extension in VIDEO_TYPES: video = cv2.VideoCapture(filepath) video.set(cv2.CAP_PROP_POS_FRAMES, @@ -3587,6 +3577,15 @@ class ThumbRenderer(QObject): success, frame = video.read() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image = Image.fromarray(frame) + # Plain Text =================================================== + elif extension in PLAINTEXT_TYPES: + text: str = extension + with open(filepath, 'r', encoding='utf-8') as text_file: + text = text_file.read() + bg = Image.new('RGB',(256,256), color='#222222') + draw = ImageDraw.Draw(bg) + draw.text((16,16), text, file=(255,255,255)) + image = bg if not image: raise UnidentifiedImageError