diff --git a/src/comic_download/dinosaur_comics.py b/src/comic_download/dinosaur_comics.py new file mode 100644 index 0000000..a1f0b2e --- /dev/null +++ b/src/comic_download/dinosaur_comics.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +# This CLI tool allows for the download and packaging of various internet comics into .cbz files. +# Copyright (C) 2025 Markil 3 +# http://www.singlepilot.net +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging +import datetime +import textwrap +import json +import re +from urllib.parse import urlparse, parse_qs + +from pathlib import Path +from tempfile import NamedTemporaryFile + +import requests +from PIL import Image, ImageDraw, ImageFont +from bs4 import BeautifulSoup + +from comic_download.comic_strip import ComicCollection as Comic, ComicStrip, register_comic_class + +class DinosaurComic(Comic): + """ + The Dinosaur Comics strip + """ + + def __init__(self): + super().__init__("dinosaur_comics") + + @classmethod + def create_from_url(cls, url): + return cls() + + def _load_data(self): + self.title = "Dinosaur Comics!" + self.author = "Ryan North" + self.image_urls = { + "header": "https://www.qwantz.com/logo5.png", + } + self.description = "this comic... might be the best comic?" + + result = requests.get("https://www.qwantz.com/archive.php") + result.raise_for_status() + + content = BeautifulSoup(result.content, features="lxml") + archives = content.find_all("ul", class_="archive") + self.strips = [] + self.strip_index = {} + date_pat = re.compile(r'(\d+)(st|nd|rd|th)') + for archive_month in archives: + link_el = archive_month.find("a") + if link_el: + print(archive_month) + parsed_index = urlparse(link_el["href"]) + identifier = int(parse_qs(parsed_index.query)["comic"][0]) + date_str = link_el.get_text() + date = datetime.datetime.strptime(date_pat.sub(r'\1', date_str), '%B %d, %Y').date() + self.strips.append({ + "identifier": identifier, + "date": date, + "description": link_el.next_sibling.get_text() + }) + self.strips.sort(key=lambda x: x["date"]) + for i in range(len(self.strips)): + self.strip_index[self.strips[i]["identifier"]] = i + + def get_first_strip(self): + return self.strips[0]["identifier"] + + def get_latest_strip(self): + return self.strips[-1]["identifier"] + + def get_all_strips(self) -> list: + return [strip["indentifier"] for strip in self.strips] + + def __new__(cls): + if not hasattr(cls, 'instance'): + cls.instance = super(DinosaurComic, cls).__new__(cls) + return cls.instance + + +class DinosaurComicStrip(ComicStrip): + """ + A comic strip object that represents a single Existential Comic strip. + """ + + def __init__(self, comic, index: int): + super().__init__(comic, index) + self.url = f"https://qwantz.com/index.php?comic={index}" + self.title = None + strip_data = comic.strips[comic.strip_index[index]] + self.date = strip_data["date"] + self.description = strip_data["description"] + + @property + def index(self): + # Alias + return self.identifier + + def _load_data(self): + logging.info("Loading data for comic %s from %s", self.index, self.url) + result = requests.get(self.url) + result.raise_for_status() + + content = BeautifulSoup(result.content, features="lxml") + title = content.find("meta", property="og:title") + if title and title["content"]: + self.title = title["content"] + for img in content.find_all("img", class_="comic"): + self.image_urls.append(f'https://qwantz.com/{img["src"]}') + if img.get("title"): + self.captions.append(img["title"]) + else: + self.captions.append("") + + + def _transform_images(self): + """ + Takes the raw image data from #download_data and transforms it to add a title, captions, etc. This function is not idempotent. + """ + f_width = 780 + title_size = 21 + description_spacing = 10 + description_size = 18 + caption_size = 12 + + with resources.path("comic_download", "Lucida Sans Bold.ttf") as luc_sans_bold, resources.path("comic_download", "Lucida Sans.ttf") as luc_sans: + title_font = ImageFont.truetype(Path(Path(__file__).parent, "Lucida Sans Bold.ttf"), title_size, layout_engine=ImageFont.Layout.RAQM) + description_font = ImageFont.truetype(Path(Path(__file__).parent, "Lucida Sans Bold.ttf"), description_size, layout_engine=ImageFont.Layout.RAQM) + caption_font = ImageFont.truetype(Path(Path(__file__).parent, "Lucida Sans.ttf"), caption_size) + + title_box = title_font.getbbox(self.title) + title_box = (title_box[2] - title_box[0], title_box[3] - title_box[1]) + + description = textwrap.wrap(self.description, width=img.width // description_size) + description_lines = len(description) + description_box = description_font.getbbox(max(description, key=len)) + description_box = (description_box[2] - description_box[0], (description_box[3] - description_box[1] + description_spacing) * len(description)) + description = "\n".join(description) + + url_box = caption_font.getbbox(self.url) + url_box = (url_box[2] - url_box[0], url_box[3] - url_box[1]) + date_box = caption_font.getbbox(self.date.isoformat()) + date_box = (date_box[2] - date_box[0], date_box[3] - date_box[1]) + text_margin = 10 + + self.transformed_images = [] + header_img = Image.open(self.comic.images["header"].name) + for i in range(len(self.images)): + img = Image.open(self.images[i].name) + if self.captions[i]: + # Take measurements of the page caption, if one is present. + # Pillow's bbox functions do not handle newline characters properly, so we have to + # split the text, find the widest line, and use that. + caption = textwrap.wrap(self.captions[i], width = img.width // caption_size) + caption_lines = len(caption) + caption_box = caption_font.getbbox(max(caption, key=len)) + caption_box = (caption_box[2] - caption_box[0], (caption_box[3] - caption_box[1] + caption_spacing) * len(caption)) + caption = "\n".join(caption) + else: + caption_box = (0, 0) + # The first page requires extra vertical space for the headers + if i == 0: + f_offset = header_img.height + description_box[1] + max(date_box[1], url_box[1]) + text_margin + # Grab the image size while we are at it, for use outside of the loop + img_size = img.size + else: + f_offset = 0 + f_img_size = (img.width, f_offset + img.height + caption_box[1] + text_margin) + f_img = Image.new("RGBA", f_img_size, (255, 255, 255, 255)) + # Draw the headers as needed + if i == 0: + f_img.paste(header_img, (0, 0)) + f_img.paste(safety_img, (0, header_img.height)) + + # Draw the actual comic + f_img.paste(img, (0, f_offset)) + + draw = ImageDraw.Draw(f_img) + if i == 0: + # Draw the title + draw.text(((img.width - title_box[0]) // 2, header_img.height + 20), self.title, (0, 0, 0), font=title_font) + # Draw the URL + draw.text((text_margin, header_img.height + safety_img.height + text_margin), self.url, (0, 0, 0), font=caption_font) + # Draw the date + draw.text((img.width - date_box[0] - text_margin, header_img.height + safety_img.height + text_margin), self.date.isoformat(), (0, 0, 0), align="right", font=caption_font) + # Draw the caption + if self.captions[i]: + draw.multiline_text(((img.width - caption_box[0]) // 2, f_offset + img.height), caption, (0, 0, 0), spacing=caption_spacing, font=caption_font, align="center") + # Save the image + f_image_path = NamedTemporaryFile(mode='wb', suffix=f"f_{i}.png", prefix=self.get_identifier_string(), delete = False) + f_image_path.close() + f_img.save(f_image_path.name) + self.transformed_images.append(f_image_path) + +DinosaurComicStrip.collection_cls = DinosaurComic +register_comic_class("qwantz.com", DinosaurComicStrip) +register_comic_class("dinosaurcomics.com", DinosaurComicStrip) +register_comic_class("exotica.ca", DinosaurComicStrip) +register_comic_class("chewbac.ca", DinosaurComicStrip)