build question overlay svg

This commit is contained in:
Dominic 2024-05-25 13:53:15 +02:00
parent 7b1681d85d
commit 01e0758b6a
Signed by: msrd0
GPG key ID: AAF7C8430CA3345D
6 changed files with 184 additions and 5 deletions

View file

@ -11,6 +11,8 @@ license = "EPL-2.0"
anyhow = "1.0"
camino = "1.1"
clap = { version = "4.4", features = ["derive"] }
fontconfig = "0.8"
harfbuzz_rs = "2.0"
indexmap = "2.2"
rational = "1.5"
serde = { version = "1.0.188", features = ["derive"] }

BIN
question.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View file

@ -7,7 +7,7 @@ use std::{
str::FromStr
};
use svgwriter::{
tags::{Group, Rect, TagWithPresentationAttributes, Text},
tags::{Group, Rect, TagWithPresentationAttributes as _, Text},
Graphic
};
@ -134,14 +134,14 @@ impl Debug for Language<'_> {
}
#[repr(u16)]
enum FontSize {
pub(crate) enum FontSize {
Huge = 72,
Large = 56,
Big = 44
}
#[repr(u16)]
enum FontWeight {
pub(crate) enum FontWeight {
Normal = 400,
SemiBold = 500,
Bold = 700

View file

@ -3,16 +3,18 @@
#![forbid(elided_lifetimes_in_paths, unsafe_code)]
mod iotro;
mod question;
mod render;
mod time;
use crate::{
use self::{
iotro::Language,
question::Question,
render::{ffmpeg::FfmpegOutputFormat, Renderer},
time::{parse_date, parse_time, Date, Time}
};
use camino::Utf8PathBuf as PathBuf;
use clap::Parser;
use iotro::Language;
use rational::Rational;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};

160
src/question.rs Normal file
View file

@ -0,0 +1,160 @@
use crate::{iotro::Language, Resolution};
use fontconfig::Fontconfig;
use harfbuzz_rs::{Face, Font, Owned, UnicodeBuffer};
use std::sync::OnceLock;
use svgwriter::{
tags::{Group, Path, Rect, TSpan, TagWithPresentationAttributes as _, Text},
Data, Graphic, Transform
};
pub(crate) struct Question {
res: Resolution,
g: Group
}
impl Question {
pub(crate) fn new(res: Resolution, lang: &Language<'_>, str: String) -> Self {
static FONT: OnceLock<Owned<Font<'static>>> = OnceLock::new();
let font = FONT.get_or_init(|| {
let fc = Fontconfig::new().unwrap();
let font_path = fc.find("Noto Sans", None).unwrap().path;
let face = Face::from_file(font_path, 0).unwrap();
Font::new(face)
});
let upem = font.face().upem();
// constants
let border_r = 12;
let font_size = 44;
let line_height = font_size * 6 / 5;
let padding = font_size / 2;
let margin_x = 240;
let margin_y = padding * 3 /2;
let question_offset = 64;
let question_width = 240;
// calculated
let box_width = 1920 - 2 * margin_x;
let text_width = box_width - 2 * padding;
// calculates the width of the given string
let width_of = |s: &str| {
let width: i32 =
harfbuzz_rs::shape(font, UnicodeBuffer::new().add_str(s), &[])
.get_glyph_positions()
.iter()
.map(|glyph_pos| glyph_pos.x_advance)
.sum();
(width * font_size) / upem as i32
};
let space_width = width_of(" ");
// lay out the text
let mut text = Text::new()
.with_dominant_baseline("hanging")
.with_transform(
Transform::new().translate(padding, padding + font_size / 2 + border_r)
);
let words = str.split_whitespace();
let mut text_height = 0;
let mut text_x = 0;
for word in words {
let word_width = width_of(word);
if text_x + word_width > text_width {
text_x = 0;
text_height += line_height;
}
text.push(
TSpan::new()
.with_x(text_x)
.with_y(text_height)
.append(word.to_owned())
);
text_x += word_width + space_width;
}
text_height += font_size;
// calculated
let box_height = text_height + 2 * padding + font_size / 2 + border_r;
let mut g = Group::new()
.with_fill("white")
.with_font_family("Noto Sans")
.with_font_size(font_size)
.with_transform(
Transform::new().translate(margin_x, 1080 - margin_y - box_height)
);
let mut outline = Data::new();
outline.move_by(border_r, 0).horiz_line_to(question_offset);
outline
.vert_line_by(-font_size / 2)
.arc_by(border_r, border_r, 0, false, true, border_r, -border_r)
.horiz_line_by(question_width)
.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
.vert_line_by(font_size)
.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
.horiz_line_by(-question_width)
.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
.vert_line_by(-font_size / 2)
.move_by(question_width + 2 * border_r, 0);
outline
.horiz_line_to(box_width - border_r)
.arc_by(border_r, border_r, 0, false, true, border_r, border_r)
.vert_line_by(box_height - 2 * border_r)
.arc_by(border_r, border_r, 0, false, true, -border_r, border_r)
.horiz_line_to(border_r)
.arc_by(border_r, border_r, 0, false, true, -border_r, -border_r)
.vert_line_to(border_r)
.arc_by(border_r, border_r, 0, false, true, border_r, -border_r);
g.push(
Path::new()
.with_stroke("#fff")
.with_stroke_width(3)
.with_fill("#000")
.with_fill_opacity(".3")
.with_d(outline)
);
g.push(
Text::new()
.with_x(question_offset + question_width / 2 + border_r)
.with_y(0)
.with_dominant_baseline("middle")
.with_text_anchor("middle")
.with_font_weight(600)
.append("Question")
);
g.push(text);
Self { res, g }
}
pub(crate) fn finish(self) -> Graphic {
let mut svg = Graphic::new();
svg.set_width(self.res.width());
svg.set_height(self.res.height());
svg.set_view_box("0 0 1920 1080");
svg.push(
Rect::new()
.with_fill("#040")
.with_x(0)
.with_y(0)
.with_width(1920)
.with_height(1080)
);
svg.push(self.g);
svg
}
}
#[cfg(test)]
#[test]
fn question() {
let svg = Question::new(
Resolution::FullHD,
&Language::default(),
"Hallo Welt! Dies ist eine sehr kluge Frage aus dem Publikum. Die Frage ist nämlich: Was ist eigentlich die Frage?".into()
)
.finish();
std::fs::write("question.svg", svg.to_string_pretty()).unwrap();
}

15
tmp.sh Executable file
View file

@ -0,0 +1,15 @@
#!/bin/busybox ash
set -euo pipefail
rm tmp.mkv || true
ffmpeg -hide_banner \
-loop 1 -r 25 -t 4 -i question.png \
-filter_complex "
gradients=s=2560x1440:d=4:c0=#000055:c1=#005500:x0=480:y0=540:x1=1440:y1=540[input];
[0]fade=t=in:st=0:d=1:alpha=1,fade=t=out:st=3:d=1:alpha=1[overlay];
[input][overlay]overlay=eval=frame:x=0:y=0[v]
" \
-map "[v]" \
-c:v libsvtav1 -preset 1 -crf 18 \
"tmp.mkv"