covers.py

Configuration

Example configuration. Covers supports multiple covers to be configured. Here is an example from my blog where we have a template sized for dev.to and one sized for open graph. Each image takes it's own configuration.

[[markata.covers]]
name='-dev'
template = "static/cover-template.png"
font = "./static/JosefinSans-Regular.ttf"
text_font = "./static/JosefinSans-Regular.ttf"
font_color = "rgb(185,155,165)"
text_font_color = "rgb(255,255,255)"
text_key = 'description'
padding = [0, 40, 100, 300]
text_padding = [0,0]

[[markata.covers]]
name=''
template = "static/og-template.png"
font = "./static/JosefinSans-Regular.ttf"
font_color = "rgb(255,255,255)"
text_font = "./static/JosefinSans-Regular.ttf"
text_font_color = "rgb(200,200,200)"
text_key = 'description'
padding = [10, 10, 100, 300]
text_padding = [0,0]

_load_font function

_load_font source
def _load_font(path: Path, size: int) -> ImageFont.FreeTypeFont:
    return ImageFont.truetype(path, size=size)

get_font function

get_font source
def get_font(
    path: Path,
    draw: ImageDraw.Draw,
    title: str,
    size: int = 250,
    max_size: tuple = (800, 220),
) -> ImageFont.FreeTypeFont:
    title = title or ""
    font = _load_font(path, size)
    current_size = draw.textsize(title, font=font)

    if current_size[0] > max_size[0] or current_size[1] > max_size[1]:
        return get_font(path, draw, title, size - 10, max_size=max_size)
    return font

PaddingError class

PaddingError source
class PaddingError(BaseException):
    def __init__(
        self,
        msg: str = "",
    ) -> None:
        super().__init__(
            "Padding must be an iterable of length 1, 2, 3, or 4.\n" + msg,
        )

draw_text function

draw_text source
def draw_text(
    image: Image,
    font_path: Optional[Path],
    text: str,
    color: Union[str, None],
    padding: Tuple[int, ...],
    markata: "Markata",
) -> None:
    text = text or ""
    draw = ImageDraw.Draw(image)
    padding = resolve_padding(padding, markata)
    width = image.size[0]
    height = image.size[1]
    bounding_box = [padding[0], padding[1], width - padding[0], height - padding[1]]
    bounding_box = [padding[0], padding[1], width - padding[2], height - padding[3]]
    max_size = (bounding_box[2] - bounding_box[0], bounding_box[3] - bounding_box[1])
    x1, y1, x2, y2 = bounding_box
    if font_path:
        font = get_font(font_path, draw, text, max_size=max_size)
    else:
        font = None
    w, h = draw.textsize(text, font=font)
    x = (x2 - x1 - w) / 2 + x1
    y = (y2 - y1 - h) / 2 + y1
    draw.text((x, y), text, fill=color, font=font, align="center")

resolve_padding function

Convert padding to a len 4 tuple

resolve_padding source
def resolve_padding(padding: Tuple[int, ...], markata: "Markata") -> Tuple[int, ...]:
    """Convert padding to a len 4 tuple"""
    if len(padding) == 4:
        return padding
    if len(padding) == 3:
        return (*padding, padding[1])
    if len(padding) == 2:
        return padding * 2
    if len(padding) == 1:
        return padding * 4
    raise PaddingError(f"recieved padding: {padding}")

make_cover function

make_cover source
def make_cover(
    title: str,
    color: str,
    output_path: Path,
    template_path: Path,
    font_path: Optional[Path],
    padding: Tuple[int, ...],
    text_font: Path,
    text: str = None,
    text_font_color: str = None,
    text_padding: Tuple[int, ...] = None,
    resizes: List[int] = None,
    markata: "Markata" = None,
) -> None:
    if output_path.exists():
        return
    if template_path:
        image = Image.open(template_path)
    else:
        image = Image.new("RGB", (800, 450))

    draw_text(
        image=image,
        font_path=font_path,
        title=title,
        color=color,
        padding=padding,
        markata=markata,
    )
    if text is not None:
        if text_padding is None:
            text_padding = (
                image.size[1] - image.size[1] / 5,
                image.size[0] / 5,
                image.size[1] - image.size[1] / 10,
            )
        draw_text(image, text_font, text, text_font_color, text_padding)

    output_path.parent.mkdir(exist_ok=True)
    image.save(output_path)
    ratio = image.size[1] / image.size[0]

    covers = []
    if resizes:
        for width in resizes:
            re_img = image.resize((width, int(width * ratio)), Image.ANTIALIAS)
            filename = (
                f"{output_path.stem}_{width}x{int(width*ratio)}{output_path.suffix}"
            )
            covers.append(filename)

            filepath = Path(output_path.parent / filename)
            re_img.save(filepath)

save function

save source
def save(markata: "Markata") -> None:
    futures = []

    if "covers" not in markata.config.keys():
        return

    for article in markata.iter_articles("making covers"):
        for cover in markata.config["covers"]:
            try:
                padding = cover["padding"]
            except KeyError:
                padding = (
                    200,
                    100,
                )
            try:
                text_padding = cover["text_padding"]
            except KeyError:
                text_padding = (
                    200,
                    100,
                )
            if "text_key" in cover:
                try:
                    text = article.metadata[cover["text_key"]]
                except AttributeError:
                    text = article[cover["text_key"]]
                try:
                    text = text.replace("\n", "")
                    from more_itertools import chunked

                    text = "\n".join(["".join(c) for c in chunked(text, 60)])
                except AttributeError:
                    # text is likely None
                    pass

                text_font = cover["text_font"]
                text_font_color = cover["text_font_color"]
            else:
                text = None
                text_font = None
                text_font_color = None
            try:
                title = article.metadata["title"]
            except AttributeError:
                title = article["title"]
            futures.append(
                make_cover(
                    title=title,
                    color=cover["font_color"],
                    output_path=Path(markata.config["output_dir"])
                    / (article["slug"] + cover["name"] + ".png"),
                    template_path=cover.get("template", None),
                    font_path=cover.get("font", None),
                    padding=padding,
                    text_font=text_font,
                    text=text,
                    text_font_color=text_font_color,
                    text_padding=text_padding,
                    resizes=cover.get("resizes"),
                    markata=markata,
                )
            )

    progress = Progress(
        BarColumn(bar_width=None), transient=True, console=markata.console
    )
    task_id = progress.add_task("loading markdown")
    progress.update(task_id, total=len(futures))
    with progress:
        while not all([f.done() for f in futures]):
            time.sleep(0.1)
            progress.update(task_id, total=len([f for f in futures if f.done()]))
    [f.result() for f in futures]

init method

init source
def __init__(
        self,
        msg: str = "",
    ) -> None:
        super().__init__(
            "Padding must be an iterable of length 1, 2, 3, or 4.\n" + msg,
        )