radiorabe / nowplaying

Songticker 🦉
https://songticker.rabe.ch/
GNU Affero General Public License v3.0
2 stars 1 forks source link

Generate MOT and send to DAB provider (SMC) #155

Open hairmare opened 2 years ago

hairmare commented 2 years ago

This story is about implementing our internal wiki examples irl. The MOT we generate can be used for other vectors (ie. radioplayer) as well. The heavy lifting for this feature is probably something to add in nowplaypadgen.

Things EBU sez:

It is strongly recommended that images intended for SlideShow applications accompanying audio services are authored at a resolution 320 × 240 pixels in landscape format, to prevent rescaling distortion in receivers. Deviation from these dimensions may create significantly sub-optimal display.

For the simple profile:

Receivers shall be able to display an image at a resolution of 320 × 240 pixels at a colour/grey scale depth of 8 bits per pixel (¼-VGA). If a receiver cannot display an image natively at this resolution, it is permitted to rescale it provided the aspect ratio is maintained and the image is fully visible. If a slide is broadcast which is smaller than 320 × 240 pixels, then it shall be displayed in the centre of the screen surrounded by a black background if needed. Content providers need to be aware that if they broadcast an image larger than 320 × 240 pixels, it may be cropped by the receiver or not displayed at all. Receivers are only permitted to crop at the right hand side and at the bottom of the image.

All receivers shall be able to decode images up to a file size (JPEG or PNG) of 50 kbytes (51 200 bytes). The Holding Buffer shall be large enough for one image.

and for the enhanced profile:

Receivers are strongly recommended to implement a display equal to or larger than 320 × 240 pixels, at a colour depth of at least 15 bits per pixel. Receivers shall not implement SlideShow on displays smaller than 160 × 120 pixels. The SlideShow application display may be rotated to best fit the physical display aspect ratio (portrait or landscape), assuming that the majority of content will be formatted to fit a landscape display. However the orientation of the SlideShow application display shall be consistent across all services, and individual images received by the application shall not be rotated on a case-by-case basis. The original aspect ratio of the image shall always be preserved. Images may be scaled at factors of 150 % or greater in order to maximize the available physical display space. It is mandatory to implement a scale factor of 50 %, and this is the only downscaling factor permitted. The use of anti-aliasing and similar techniques is strongly recommended to optimize the quality of the scaled images.

All receivers shall be able to decode images up to an MOT object size (Body + Header) of 450 kbytes (460 800 bytes). The Holding Buffer shall be at least 450 kbytes (460 800 bytes) and be able to store between 1 and 64 images. When multiple images are stored, each image may be a different size and/or colour depth.

hairmare commented 1 year ago

here is some code that generates MOT images as svg and png:

from textwrap import dedent
from base64 import b64encode
from io import StringIO
from argparse import Namespace, BooleanOptionalAction
import logging
import subprocess
from pathlib import PosixPath

import cairosvg
import svg
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPM
from font_roboto import RobotoLight
import font_fjallaone as FjallaOne
from svglib.fonts import FontMap
from wand.image import Image

Options = Namespace

out = "out.png"

light = "#00e1d4"
dark = "#0d4b56"
white = "#ffffff"

width = 310
height = 240

def data_from_file(path: str) -> str:
    return b64encode(open(path, mode="rb").read()).decode("ascii")

def main(options: Options):
    format = options.format
    outfile = f"{options.name}.{format}"
    png_renderer = options.png_renderer

    style = None
    if format != "png":
        fjallaone_data = data_from_file(FjallaOne.font)
        roboto_data = data_from_file(RobotoLight)

        style = (
            svg.Style(
                type="text/css",
                text=dedent(
                    f"""
                    @font-face {{
                        font-family: "Fjalla-One";
                        src: url(data:font/ttf;charset=utf-8;base64,{fjallaone_data});
                    }}
                    @font-face {{
                        font-family: "Roboto-Light";
                        src: url(data:font/ttf;charset=utf-8;base64,{roboto_data});
                    }}
                    """
                ),
            ),
        )

    img_data = data_from_file("./monsters.jpg")

    # https://github.com/orsinium-labs/svg.py
    canvas = svg.SVG(
        width=width,
        height=height,
        viewBox=svg.ViewBoxSpec(0, 0, width, height),
        elements=[
            # add our fonts
            style,
            # background for top "title" part of image
            svg.Rect(width="100%", height="25%", fill=light),
            # background for remainder of image
            svg.Rect(x="0", y="25%", width="100%", height="77%", fill=dark),
            # station ident at top of image
            svg.Text(
                x="5",
                y=height * 0.25 - 5,
                text="RaBe",
                font_family="Fjalla-One",
                font_weight="bold",
                font_size=height * 0.25 - 10,
                fill=dark,
                text_rendering="optimizeLegibility",
            ),
            # "title" for things like show name
            svg.Text(
                x="5",
                y=height * 0.4 + 5,
                text="Klangbecken",
                font_family="Fjalla-One",
                font_weight="bold",
                font_size=height * 0.2 - 10,
                fill=light,
            ),
            # content part
            svg.Text(
                x="7",
                y=height * 0.5 + 10,
                text="Artist - Title",
                font_family="Roboto-Light",
                # font_style="Light",
                font_size=height * 0.125 - 10,
                fill=white,
            ),
            # smol text at bottom of image
            svg.Text(
                x="5",
                y=height - 5,
                text="Radio Bern RaBe | rabe.ch",
                font_family="Roboto-Light",
                font_size=height * 0.125 - 10,
                fill=light,
            ),
            svg.Image(
                x="70%",
                y="50%",
                width=width * 0.25,
                height=width * 0.25,
                preserveAspectRatio=True,
                #href=PosixPath("/home/hairmare/Documents/git.repos/rabe/nowplaying/monsters.jpg"),
                href=f"data:image/jpeg;base64,{img_data}",
            )
        ],
    )

    if format == "png":
        if png_renderer == "svglib":
            font_map = FontMap()
            font_map.register_font(
                "Fjalla One", font_path=FjallaOne.font, rlgFontName="Fjalla-One"
            )
            font_map.register_font(
                "Roboto Light", RobotoLight, style="light", rlgFontName="Roboto-Light"
            )

            f = StringIO(str(canvas))
            # https://clay-atlas.com/us/blog/2021/03/08/python-en-svglib-convert-svg-png/
            drawing = svg2rlg(f, font_map=font_map)
            configPIL = {}
            renderPM.drawToFile(drawing, outfile, fmt="PNG", configPIL=configPIL)
        elif png_renderer == "cairosvg":
            cairosvg.svg2png(
                bytestring=str(canvas).encode(),
                write_to=outfile,
                output_width=width,
                output_height=height,
            )
        elif png_renderer == "wand":
            with Image(
                blob=str(canvas).encode(), format="svg", width=width, height=height
            ) as img:
                with img.convert("png") as output_img:
                    output_img.save(filename=outfile)
        elif png_renderer == "inkscape":
            # svg string -> write png file
            inkscape = "/usr/bin/inkscape"
            subprocess.run(
                [
                    inkscape,
                    "--export-type=png",
                    f"--export-filename={outfile}",
                    f"--export-width={width}",
                    f"--export-height={height}",
                    "--export-png-use-dithering=false",
                    "--export-dpi=72",
                    "--pipe",
                ],
                input=str(canvas).encode(),
            )

    elif format == "svg":
        outfp = open(outfile, "w")
        outfp.write(str(canvas))

if __name__ == "__main__":
    from configargparse import ArgParser

    args = ArgParser()
    args.add_argument(
        "--format",
        default="png",
        required=False,
        choices=["png", "svg"],
        help="Format to output",
    )
    args.add_argument(
        "--name",
        default="out",
        required=False,
        help="basename of file to output, the default of 'out' creates a file called out.<format>",
    )
    args.add_argument(
        "--png-renderer",
        default="svglib",
        choices=["svglib", "cairosvg", "wand", "inkscape"],
    )
    args.add_argument("--debug", action=BooleanOptionalAction)

    options: Options = args.parse_args()

    if options.debug:
        logging.basicConfig(level=logging.DEBUG)

    main(options)

It looks like this as SVG (for some reason all my browsers are rendering the font wrong, see png for how it should look):

out

And sized for DAB it looks like this as PNG (rendered with svglib/reportlab, others options didn't improve the result):

out

The final PNG is 28K in size which in our limits. Most of the size is from embedding album art from a JPEG.