Skip to content

Module app.api.images

View Source
import asyncio

from contextlib import suppress

from typing import List

from sanic import Blueprint, response

from sanic.log import logger

from sanic_openapi import doc

from .. import helpers, models, settings, utils

blueprint = Blueprint("images", url_prefix="/images")

@blueprint.get("/")

@doc.summary("List sample memes")

@doc.operation("images.list")

@doc.produces(

    doc.List({"url": str, "template": str}),

    description="Successfully returned a list of sample memes",

    content_type="application/json",

)

async def index(request):

    samples = await asyncio.to_thread(helpers.get_sample_images, request)

    return response.json(

        [{"url": url, "template": template} for url, template in samples]

    )

@blueprint.post("/")

@doc.summary("Create a meme from a template")

@doc.operation("images.create")

@doc.consumes(

    doc.JsonBody(

        {"template_key": str, "text_lines": [str], "extension": str, "redirect": bool}

    ),

    content_type="application/json",

    location="body",

)

@doc.response(201, {"url": str}, description="Successfully created a meme")

@doc.response(

    400, {"error": str}, description='Required "template_key" missing in request body'

)

async def create(request):

    if request.form:

        payload = dict(request.form)

        with suppress(KeyError):

            payload["template_key"] = payload.pop("template_key")[0]

        with suppress(KeyError):

            payload["text_lines"] = payload.pop("text_lines[]")

    else:

        payload = request.json

    try:

        template_key = payload["template_key"]

    except KeyError:

        return response.json({"error": '"template_key" is required'}, status=400)

    template = models.Template.objects.get(template_key)

    url = template.build_custom_url(

        request.app,

        payload.get("text_lines") or [],

        extension=payload.get("extension"),

    )

    if payload.get("redirect", False):

        return response.redirect(url)

    return response.json({"url": url}, status=201)

@blueprint.get("/preview.jpg")

@doc.summary("Display a preview of a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/jpeg",

)

async def preview(request):

    key = request.args.get("template", "_error")

    lines = request.args.getlist("lines[]", [])

    style = request.args.get("style")

    return await preview_image(request, key, lines, style)

@blueprint.get("/<template_key>.png")

@doc.summary("Display a template background")

@doc.produces(

    doc.File(),

    description="Successfully displayed a template background",

    content_type="image/png",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def blank_png(request, template_key):

    return await render_image(request, template_key, ext="png")

@blueprint.get("/<template_key>.jpg")

@doc.summary("Display a template background")

@doc.produces(

    doc.File(),

    description="Successfully displayed a template background",

    content_type="image/jpeg",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def blank_jpg(request, template_key):

    return await render_image(request, template_key, ext="jpg")

@blueprint.get("/<template_key>/<text_paths:[\s\S]+>.png")

@doc.summary("Display a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/png",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(414, doc.File(), description="Custom text too long (length >200)")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def text_png(request, template_key, text_paths):

    slug, updated = utils.text.normalize(text_paths)

    if updated:

        url = request.app.url_for(

            "images.text_png",

            template_key=template_key,

            text_paths=slug,

            **request.args,

        ).replace("%3A%2F%2F", "://")

        return response.redirect(url, status=301)

    return await render_image(request, template_key, slug)

@blueprint.get("/<template_key>/<text_paths:[\s\S]+>.jpg")

@doc.summary("Display a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/jpeg",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(414, doc.File(), description="Custom text too long (length >200)")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def text_jpg(request, template_key, text_paths):

    slug, updated = utils.text.normalize(text_paths)

    if updated:

        url = request.app.url_for(

            "images.text_jpg",

            template_key=template_key,

            text_paths=slug,

            **request.args,

        ).replace("%3A%2F%2F", "://")

        return response.redirect(url, status=301)

    return await render_image(request, template_key, slug, ext="jpg")

async def preview_image(request, key: str, lines: List[str], style: str):

    if "://" in key:

        template = await models.Template.create(key)

    else:

        template = models.Template.objects.get(key)

    data, content_type = await asyncio.to_thread(

        utils.images.preview, template, lines, style

    )

    return response.raw(data, content_type=content_type)

async def render_image(

    request, key: str, slug: str = "", ext: str = settings.DEFAULT_EXT

):

    status = 200

    if len(slug.encode()) > 200:

        logger.error(f"Slug too long: {slug}")

        slug = slug[:50] + "..."

        template = models.Template.objects.get("_error")

        style = settings.DEFAULT_STYLE

        status = 414

    elif key == "custom":

        style = settings.DEFAULT_STYLE

        url = request.args.get("background") or request.args.get("alt")

        if url:

            template = await models.Template.create(url)

            if not template.image.exists():

                logger.error(f"Unable to download image URL: {url}")

                template = models.Template.objects.get("_error")

                status = 415

        else:

            logger.error("No image URL specified for custom template")

            template = models.Template.objects.get("_error")

            status = 422

    else:

        template = models.Template.objects.get_or_none(key)

        if not template:

            logger.error(f"No such template: {key}")

            template = models.Template.objects.get("_error")

            status = 404

        style = request.args.get("style") or request.args.get("alt")

        if style and style not in template.styles:

            logger.error(f"Invalid style for template: {style}")

            status = 422

    lines = utils.text.decode(slug)

    size = int(request.args.get("width", 0)), int(request.args.get("height", 0))

    await helpers.track(request, lines)

    path = await asyncio.to_thread(utils.images.save, template, lines, ext, style, size)

    return await response.file(path, status)

Variables

blueprint

Functions

blank_jpg

def blank_jpg(
    request,
    template_key
)
View Source
@blueprint.get("/<template_key>.jpg")

@doc.summary("Display a template background")

@doc.produces(

    doc.File(),

    description="Successfully displayed a template background",

    content_type="image/jpeg",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def blank_jpg(request, template_key):

    return await render_image(request, template_key, ext="jpg")

blank_png

def blank_png(
    request,
    template_key
)
View Source
@blueprint.get("/<template_key>.png")

@doc.summary("Display a template background")

@doc.produces(

    doc.File(),

    description="Successfully displayed a template background",

    content_type="image/png",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def blank_png(request, template_key):

    return await render_image(request, template_key, ext="png")

create

def create(
    request
)
View Source
@blueprint.post("/")

@doc.summary("Create a meme from a template")

@doc.operation("images.create")

@doc.consumes(

    doc.JsonBody(

        {"template_key": str, "text_lines": [str], "extension": str, "redirect": bool}

    ),

    content_type="application/json",

    location="body",

)

@doc.response(201, {"url": str}, description="Successfully created a meme")

@doc.response(

    400, {"error": str}, description='Required "template_key" missing in request body'

)

async def create(request):

    if request.form:

        payload = dict(request.form)

        with suppress(KeyError):

            payload["template_key"] = payload.pop("template_key")[0]

        with suppress(KeyError):

            payload["text_lines"] = payload.pop("text_lines[]")

    else:

        payload = request.json

    try:

        template_key = payload["template_key"]

    except KeyError:

        return response.json({"error": '"template_key" is required'}, status=400)

    template = models.Template.objects.get(template_key)

    url = template.build_custom_url(

        request.app,

        payload.get("text_lines") or [],

        extension=payload.get("extension"),

    )

    if payload.get("redirect", False):

        return response.redirect(url)

    return response.json({"url": url}, status=201)

index

def index(
    request
)
View Source
@blueprint.get("/")

@doc.summary("List sample memes")

@doc.operation("images.list")

@doc.produces(

    doc.List({"url": str, "template": str}),

    description="Successfully returned a list of sample memes",

    content_type="application/json",

)

async def index(request):

    samples = await asyncio.to_thread(helpers.get_sample_images, request)

    return response.json(

        [{"url": url, "template": template} for url, template in samples]

    )

preview

def preview(
    request
)
View Source
@blueprint.get("/preview.jpg")

@doc.summary("Display a preview of a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/jpeg",

)

async def preview(request):

    key = request.args.get("template", "_error")

    lines = request.args.getlist("lines[]", [])

    style = request.args.get("style")

    return await preview_image(request, key, lines, style)

preview_image

def preview_image(
    request,
    key: str,
    lines: List[str],
    style: str
)
View Source
async def preview_image(request, key: str, lines: List[str], style: str):

    if "://" in key:

        template = await models.Template.create(key)

    else:

        template = models.Template.objects.get(key)

    data, content_type = await asyncio.to_thread(

        utils.images.preview, template, lines, style

    )

    return response.raw(data, content_type=content_type)

render_image

def render_image(
    request,
    key: str,
    slug: str = '',
    ext: str = 'png'
)
View Source
async def render_image(

    request, key: str, slug: str = "", ext: str = settings.DEFAULT_EXT

):

    status = 200

    if len(slug.encode()) > 200:

        logger.error(f"Slug too long: {slug}")

        slug = slug[:50] + "..."

        template = models.Template.objects.get("_error")

        style = settings.DEFAULT_STYLE

        status = 414

    elif key == "custom":

        style = settings.DEFAULT_STYLE

        url = request.args.get("background") or request.args.get("alt")

        if url:

            template = await models.Template.create(url)

            if not template.image.exists():

                logger.error(f"Unable to download image URL: {url}")

                template = models.Template.objects.get("_error")

                status = 415

        else:

            logger.error("No image URL specified for custom template")

            template = models.Template.objects.get("_error")

            status = 422

    else:

        template = models.Template.objects.get_or_none(key)

        if not template:

            logger.error(f"No such template: {key}")

            template = models.Template.objects.get("_error")

            status = 404

        style = request.args.get("style") or request.args.get("alt")

        if style and style not in template.styles:

            logger.error(f"Invalid style for template: {style}")

            status = 422

    lines = utils.text.decode(slug)

    size = int(request.args.get("width", 0)), int(request.args.get("height", 0))

    await helpers.track(request, lines)

    path = await asyncio.to_thread(utils.images.save, template, lines, ext, style, size)

    return await response.file(path, status)

text_jpg

def text_jpg(
    request,
    template_key,
    text_paths
)
View Source
@blueprint.get("/<template_key>/<text_paths:[\s\S]+>.jpg")

@doc.summary("Display a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/jpeg",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(414, doc.File(), description="Custom text too long (length >200)")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def text_jpg(request, template_key, text_paths):

    slug, updated = utils.text.normalize(text_paths)

    if updated:

        url = request.app.url_for(

            "images.text_jpg",

            template_key=template_key,

            text_paths=slug,

            **request.args,

        ).replace("%3A%2F%2F", "://")

        return response.redirect(url, status=301)

    return await render_image(request, template_key, slug, ext="jpg")

text_png

def text_png(
    request,
    template_key,
    text_paths
)
View Source
@blueprint.get("/<template_key>/<text_paths:[\s\S]+>.png")

@doc.summary("Display a custom meme")

@doc.produces(

    doc.File(),

    description="Successfully displayed a custom meme",

    content_type="image/png",

)

@doc.response(404, doc.File(), description="Template not found")

@doc.response(414, doc.File(), description="Custom text too long (length >200)")

@doc.response(415, doc.File(), description="Unable to download image URL")

@doc.response(

    422,

    doc.File(),

    description="Invalid style for template or no image URL specified for custom template",

)

async def text_png(request, template_key, text_paths):

    slug, updated = utils.text.normalize(text_paths)

    if updated:

        url = request.app.url_for(

            "images.text_png",

            template_key=template_key,

            text_paths=slug,

            **request.args,

        ).replace("%3A%2F%2F", "://")

        return response.redirect(url, status=301)

    return await render_image(request, template_key, slug)