Files
ComicDownload/comic_download/existential_comics.py

223 lines
9.9 KiB
Python

#!/usr/bin/env python3
# Copyright (c) 2025 Markil 3
import logging
import datetime
import textwrap
import json
import re
from urllib.parse import urlparse
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 Comic, ComicStrip, register_comic_class
class ExistentialComic(Comic):
"""
The Existential Comics strip
"""
def __init__(self):
super().__init__("existential_comics")
@classmethod
def create_from_url(cls, url):
return cls()
def _load_data(self):
self.title = "Existential Comics"
self.author = "Corey Mohler"
self.image_urls = {
"header": "https://static.existentialcomics.com/title.jpg",
"safety": "https://static.existentialcomics.com/safety.png"
}
self.description = "A webcomic of romance, sarcasm, math, and language."
result = requests.get("https://existentialcomics.com/")
result.raise_for_status()
content = BeautifulSoup(result.content, features="lxml")
# The home page automatically fetches the latest comic, and we
# can grab the regular URL (and thus the index) from there.
index = content.find("meta", property="og:url")
if index and index["content"]:
self.latest_identifier = int(index["content"].split('/')[-1])
else:
raise ValueError("Invalid comic data")
def get_first_strip(self):
return 1
def get_latest_strip(self):
return self.latest_identifier
def get_all_strips(self) -> list:
return list(range(self.get_first_strip(), self.get_latest_strip() + 1))
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(ExistentialComic, cls).__new__(cls)
return cls.instance
class ExistentialComicStrip(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://existentialcomics.com/comic/{index}"
self.title = None
@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_="comicImg"):
self.image_urls.append(f'http:{img["src"]}')
if img.get("title"):
self.captions.append(img["title"])
else:
self.captions.append("")
final_caption = ""
explanation = content.find(id="explanation")
if explanation:
self.captions.extend(explanation.get_text().split("\n"))
philosophers = content.find(id="philosophers-comic")
if philosophers:
self.captions.extend(philosophers.get_text().split("\n"))
if final_caption:
self.captions.append(final_caption)
self.date = datetime.date(2013, 11, 11) + datetime.timedelta(weeks=self.index - 2)
if self.date >= datetime.date(2023, 11, 13):
self.safe = (self.date - datetime.date(2023, 11, 13)).days
else:
self.safe = (self.date - datetime.date(2013, 11, 12)).days
def _transform_images(self, base_path: Path):
"""
Takes the raw image data from #download_data and transforms it to add a title, captions, etc. This function is not idempotent.
"""
# Marks out various bounding boxes of strip-global texts (i.e. how much space the title takes up)
title_size = 18
safety_size = 24
caption_size = 15
f_size = (1000, 1500)
title_font = ImageFont.truetype(Path(Path(__file__).parent, "Lucida Sans Bold.ttf"), title_size)
safety_font = ImageFont.truetype(Path(Path(__file__).parent, "Lucida Sans Bold.ttf"), safety_size)
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])
caption_spacing = 4
safe_box = safety_font.getbbox(str(self.safe))
safe_box = (safe_box[2] - safe_box[0], safe_box[3] - safe_box[1])
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
img_size = (0, 0)
self.transformed_images = []
header_img = Image.open(self.comic.images["header"].name)
safety_img = Image.open(self.comic.images["safety"].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 + safety_img.height + 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 safety text
draw.text(((safety_img.width - safe_box[0]) // 2, header_img.height + 20), str(self.safe), (0, 0, 0), font=safety_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)
if len(self.captions) > len(self.image_urls):
# Add the explanation to a new page
explanation = []
# Pillow's bbox functions do not handle newline characters properly, so we have to
# split the text, find the widest line, and use that.
for paragraph in self.captions[len(self.image_urls):]:
explanation.extend(textwrap.wrap(paragraph.strip(), width = img_size[0] // caption_size))
explanation_box = [caption_font.getbbox(ex) for ex in explanation]
explanation_box = [(box[2] - box[0], box[3] - box[1]) for box in explanation_box]
# Using a comparison function (i.e. max) on a tuple just compares the first value of each
explanation_box = [max(explanation_box)[0], (explanation_box[0][1] + caption_spacing) * len(explanation_box)]
explanation = "\n".join(explanation)
f_img = Image.new("RGBA", (img_size[0], explanation_box[1] + text_margin * 4), (255, 255, 255, 255))
draw = ImageDraw.Draw(f_img)
# Adds a background rectangle, like on the website
draw.rounded_rectangle(((img_size[0] - explanation_box[0] - text_margin * 2) // 2, text_margin, (img_size[0] + explanation_box[0] + text_margin * 2) // 2, explanation_box[1] + text_margin * 3), 20, (200, 200, 200), (0, 0, 0), 2)
draw.multiline_text(((img_size[0] - explanation_box[0]) // 2, text_margin * 2), explanation, (0, 0, 0), align="center", font=caption_font, spacing=caption_spacing)
# Save the image
f_image_path = NamedTemporaryFile(mode='wb', suffix=f"f_ex.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)
ExistentialComic.strip_cls = ExistentialComicStrip
register_comic_class("existentialcomics.com", ExistentialComic)