From a28cf7eab8216a1688a97fad46a689ae9210f3f4 Mon Sep 17 00:00:00 2001 From: Joakim Soderlund Date: Wed, 27 Nov 2024 12:23:14 +0100 Subject: [PATCH] Use cover image only for advent calendar --- fimfarchive/commands/advent.py | 210 ++++++++++++++++++--------------- pyproject.toml | 1 + 2 files changed, 115 insertions(+), 96 deletions(-) diff --git a/fimfarchive/commands/advent.py b/fimfarchive/commands/advent.py index 5fb79c7..1f5520b 100644 --- a/fimfarchive/commands/advent.py +++ b/fimfarchive/commands/advent.py @@ -3,15 +3,20 @@ from copy import deepcopy from datetime import datetime from io import BytesIO +from math import ceil from pathlib import Path -from typing import Iterator, override +from typing import Iterator from xml.dom import minidom +from xml.dom.minidom import Document from zipfile import ZipFile +from PIL import Image, ImageDraw, ImageFont +from requests import get + from fimfarchive.converters import Converter from fimfarchive.fetchers import FimfarchiveFetcher -from fimfarchive.stories import Story from fimfarchive.mappers import StorySlugMapper +from fimfarchive.stories import Story from fimfarchive.writers import DirectoryWriter from .base import Command @@ -19,27 +24,10 @@ dt = datetime.fromisoformat -DATE_START = dt("2023-12-01 00:00:00Z") -DATE_STOP = dt("2024-01-01 00:00:00Z") +DATE_START = dt("2023-12-01T00:00:00+00:00") +DATE_STOP = dt("2024-01-01T00:00:00+00:00") TARGET_AUTHOR = 46322 -COVER_PAGE = """ - - - - - - Cover - - -
-

Title

- -
- - -""".lstrip() - COVER_IMAGES = [ "https://derpicdn.net/img/view/2015/12/1/1034522.png", "https://derpicdn.net/img/view/2015/12/2/1035253.png", @@ -71,77 +59,23 @@ ] -class CoverPage: - - def __init__(self, story: Story) -> None: - published = dt(story.meta["date_published"]) - self.day = published.day - - def get_cover(self) -> bytes: - if not (url := COVER_IMAGES[self.day - 1]): - raise ValueError("Missing cover") - - _, name = url.rsplit("/", 1) - path = Path(f"covers/{name}") - - return path.read_bytes() - - def get_title(self) -> bytes: - dom = minidom.parseString(COVER_PAGE) - (title,) = dom.getElementsByTagName("h1") - (text,) = title.childNodes - - text.replaceWholeText(f"Advent {self.day:02}") - - return dom.toprettyxml().encode() +def day(story: Story) -> int: + """ + Returns the day of publishing. + """ + return dt(story.meta["date_published"]).day class AdventConverter(Converter): """ - Replaces titles with advent dates. + Base class for modifying story meta and data. """ - def get_title(self, story: Story) -> str: - published = dt(story.meta["date_published"]) + def handle_opf(self, story: Story, dom: Document): + pass - return f"Advent {published.day:02}" - - def get_opf(self, story: Story, data: bytes) -> bytes: - dom = minidom.parseString(data) - (package,) = dom.getElementsByTagName("package") - (manifest,) = dom.getElementsByTagName("manifest") - (spine,) = dom.getElementsByTagName("spine") - (title,) = dom.getElementsByTagName("dc:title") - - (text,) = title.childNodes - text.replaceWholeText(self.get_title(story)) - - index = manifest.firstChild - item = dom.createElement("item") - item.setAttribute("id", "cover") - item.setAttribute("href", "cover.png") - item.setAttribute("media-type", "image/png") - manifest.insertBefore(item, index) - item = dom.createElement("item") - item.setAttribute("id", "title") - item.setAttribute("href", "title.xhtml") - item.setAttribute("media-type", "application/xhtml+xml") - manifest.insertBefore(item, index) - - index = spine.firstChild - item = dom.createElement("itemref") - item.setAttribute("idref", "title") - spine.insertBefore(item, index) - - guide = dom.createElement("guide") - reference = dom.createElement("reference") - reference.setAttribute("type", "cover") - reference.setAttribute("href", "title.xhtml") - reference.setAttribute("title", "title") - guide.appendChild(reference) - package.appendChild(guide) - - return dom.toprettyxml().encode() + def handle_zip(self, story: Story, arc: ZipFile): + pass def get_data(self, story: Story) -> bytes: buffer = BytesIO() @@ -153,23 +87,19 @@ def get_data(self, story: Story) -> bytes: data = source.read(info) if info.filename == "content.opf": - data = self.get_opf(story, data) + dom = minidom.parseString(data) + self.handle_opf(story, dom) + data = dom.toprettyxml().encode() target.writestr(info, data) - cover = CoverPage(story) - target.writestr("cover.png", cover.get_cover()) - target.writestr("title.xhtml", cover.get_title()) + self.handle_zip(story, target) return buffer.getvalue() def get_meta(self, story: Story) -> dict: - meta = deepcopy(story.meta) - meta["title"] = self.get_title(story) - - return meta + return deepcopy(story.meta) - @override def __call__(self, story: Story) -> Story: return story.merge( data=self.get_data(story), @@ -177,6 +107,90 @@ def __call__(self, story: Story) -> Story: ) +class CoverConverter(AdventConverter): + """ + Adds a fancy advent cover by dm29. + """ + + file_name = "cover.png" + + def fetch(self, story: Story) -> bytes: + if not (url := COVER_IMAGES[day(story) - 1]): + raise ValueError("Missing cover") + + _, name = url.rsplit("/", 1) + path = Path(f"covers/{name}") + + if not path.is_file(): + return get(url).content + + return path.read_bytes() + + def draw(self, story: Story) -> bytes: + # Load cover art + data = self.fetch(story) + art = Image.open(BytesIO(data)) + + # Create cover image + height = ceil(art.width * 1.6) + cover = Image.new("RGB", (art.width, height), "lightgray") + cover.paste(art, (0, height - art.height)) + + # Initialize draw tool + draw = ImageDraw.Draw(cover) + font = ImageFont.load_default(height // 12) + + # Draw story number + ident = f"{story.key}" + _, _, tw, th = draw.textbbox((0, 0), ident, font) + ts = ((art.width - tw) / 2, (height - art.height - th) / 2 - th / 8) + draw.text(ts, ident, "black", font) + + # Draw calendar date + title = f"Advent {day(story):02}" + _, _, tw, th = draw.textbbox((0, 0), title, font) + ts = ((art.width - tw) / 2, (ts[1] + th + th / 4)) + draw.text(ts, title, "black", font) + + # Render image + buffer = BytesIO() + cover.save(buffer, "png") + + return buffer.getvalue() + + def handle_opf(self, story: Story, dom: Document): + (manifest,) = dom.getElementsByTagName("manifest") + + cover = dom.createElement("item") + cover.setAttribute("id", "cover") + cover.setAttribute("href", self.file_name) + cover.setAttribute("media-type", "image/png") + + manifest.insertBefore(cover, manifest.firstChild) + + def handle_zip(self, story: Story, arc: ZipFile): + arc.writestr(self.file_name, self.draw(story)) + + +class TitleConverter(AdventConverter): + """ + Replaces titles with advent dates. + """ + + def handle_opf(self, story: Story, dom: Document): + title = f"Advent {day(story):02} - {story.key}" + (node,) = dom.getElementsByTagName("dc:title") + (text,) = node.childNodes + + text.replaceWholeText(title) + + def get_meta(self, story: Story) -> dict: + meta = super().get_meta(story) + meta["title"] = f"Advent {day(story):02}" + + return meta + + def filter_stories(fetcher: FimfarchiveFetcher) -> Iterator[Story]: for story in fetcher: if story.meta["author"]["id"] != TARGET_AUTHOR: @@ -194,11 +208,15 @@ def __call__(self, *args: str) -> int: (archive,) = args slug = StorySlugMapper() - convert = AdventConverter() fetcher = FimfarchiveFetcher(archive) writer = DirectoryWriter(data_path=slug) + convert_cover = CoverConverter() + convert_title = TitleConverter() + for story in filter_stories(fetcher): - writer.write(convert(story)) + story = convert_cover(story) + story = convert_title(story) + writer.write(story) return 0 diff --git a/pyproject.toml b/pyproject.toml index 17f5267..dc13a53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "jinja2~=3.1", "jmespath~=1.0", "jsonapi-client", + "pillow~=10.4.0", "requests~=2.32", "tqdm~=4.66", ]