Skip to content
Snippets Groups Projects
Verified Commit 01e0758b authored by Dominic Meiser's avatar Dominic Meiser
Browse files

build question overlay svg

parent 7b1681d8
No related branches found
No related tags found
No related merge requests found
...@@ -11,6 +11,8 @@ license = "EPL-2.0" ...@@ -11,6 +11,8 @@ license = "EPL-2.0"
anyhow = "1.0" anyhow = "1.0"
camino = "1.1" camino = "1.1"
clap = { version = "4.4", features = ["derive"] } clap = { version = "4.4", features = ["derive"] }
fontconfig = "0.8"
harfbuzz_rs = "2.0"
indexmap = "2.2" indexmap = "2.2"
rational = "1.5" rational = "1.5"
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.188", features = ["derive"] }
......
question.png

105 KiB

...@@ -7,7 +7,7 @@ use std::{ ...@@ -7,7 +7,7 @@ use std::{
str::FromStr str::FromStr
}; };
use svgwriter::{ use svgwriter::{
tags::{Group, Rect, TagWithPresentationAttributes, Text}, tags::{Group, Rect, TagWithPresentationAttributes as _, Text},
Graphic Graphic
}; };
...@@ -134,14 +134,14 @@ impl Debug for Language<'_> { ...@@ -134,14 +134,14 @@ impl Debug for Language<'_> {
} }
#[repr(u16)] #[repr(u16)]
enum FontSize { pub(crate) enum FontSize {
Huge = 72, Huge = 72,
Large = 56, Large = 56,
Big = 44 Big = 44
} }
#[repr(u16)] #[repr(u16)]
enum FontWeight { pub(crate) enum FontWeight {
Normal = 400, Normal = 400,
SemiBold = 500, SemiBold = 500,
Bold = 700 Bold = 700
......
...@@ -3,16 +3,18 @@ ...@@ -3,16 +3,18 @@
#![forbid(elided_lifetimes_in_paths, unsafe_code)] #![forbid(elided_lifetimes_in_paths, unsafe_code)]
mod iotro; mod iotro;
mod question;
mod render; mod render;
mod time; mod time;
use crate::{ use self::{
iotro::Language,
question::Question,
render::{ffmpeg::FfmpegOutputFormat, Renderer}, render::{ffmpeg::FfmpegOutputFormat, Renderer},
time::{parse_date, parse_time, Date, Time} time::{parse_date, parse_time, Date, Time}
}; };
use camino::Utf8PathBuf as PathBuf; use camino::Utf8PathBuf as PathBuf;
use clap::Parser; use clap::Parser;
use iotro::Language;
use rational::Rational; use rational::Rational;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr}; use serde_with::{serde_as, DisplayFromStr};
......
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();
}
tmp.sh 0 → 100755
#!/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"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment