Groesse in vier Stufen, Schrift und Hintergrund als fuenf feste Paare — Weiss, Weiss auf Schwarz, Schwarz auf Weiss, Gelb, Gelb auf Schwarz. Feste Paare statt zweier Regler, weil unabhaengige Achsen unlesbare Mischungen zulassen: Schwarz auf durchsichtig ueber einer Nachtszene ist eine gueltige Einstellung und eine wertlose. Die Uebersetzung nach AVFoundation ist eine Tabelle aus Schluesseln, bei der jeder Fehler lautlos ist — ein falscher Schluessel laesst den Initialisierer `nil` zurueckgeben, und dann gibt es gar keine Regel. Auf dem Bildschirm sieht „keine Regel\" genauso aus wie „Regel ohne Wirkung\". Zehn Tests decken das ab, darunter die drei Fallen: Alpha zuerst in der Farbe, Prozentwert statt Punktgroesse, und der Hintergrund auch fuer „durchsichtig\" ausdruecklich gesetzt statt weggelassen — weggelassen gilt sonst, was Jellyfins WebVTT selbst mitbringt, und das ist oft ein schwarzer Kasten. Als Protokollmethode, nicht als Feld an `PlaybackSource`: die wird einmal beim Oeffnen gelesen, und eine Einstellung, die erst beim naechsten Film wirkt, haelt jeder fuer kaputt. Angewandt wird an zwei Stellen — am laufenden Gegenstand und am frisch angelegten. Nur das erste zu tun hiesse, dass die naechste Folge wieder in der Vorgabe erscheint. VLC erbt die leere Vorgabe. Die Einstellungsseite sagt beides: dass es fuer den Standardplayer gilt, und dass eingebrannte Bilduntertitel von Blu-rays sich nicht gestalten lassen, weil es dort keinen Text gibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
3.3 KiB
Swift
98 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
/// Wie Untertitel aussehen sollen.
|
|
///
|
|
/// **Warum hier und nicht im App-Ziel.** Weil es prüfbar sein muss: die
|
|
/// Übersetzung nach AVFoundation ist eine Tabelle aus Schlüsseln, bei der ein
|
|
/// Tippfehler lautlos dazu führt, dass **gar nichts** gesetzt wird. Ein Test
|
|
/// findet das, ein Blick auf den Bildschirm nicht — dort sieht „keine Regel"
|
|
/// genauso aus wie „Regel ohne Wirkung".
|
|
public struct SubtitleStyle: Sendable, Equatable {
|
|
/// Eine Farbe, wie CoreMedia sie versteht: vier Anteile von 0 bis 1.
|
|
public struct RGBA: Sendable, Equatable {
|
|
public let red: Double
|
|
public let green: Double
|
|
public let blue: Double
|
|
public let alpha: Double
|
|
|
|
public init(red: Double, green: Double, blue: Double, alpha: Double = 1) {
|
|
self.red = red
|
|
self.green = green
|
|
self.blue = blue
|
|
self.alpha = alpha
|
|
}
|
|
|
|
static let white = RGBA(red: 1, green: 1, blue: 1)
|
|
static let black = RGBA(red: 0, green: 0, blue: 0)
|
|
static let yellow = RGBA(red: 1, green: 0.85, blue: 0.2)
|
|
static let clear = RGBA(red: 0, green: 0, blue: 0, alpha: 0)
|
|
/// Nicht ganz deckend: ein voll schwarzer Kasten wirkt wie ein Loch
|
|
/// im Bild.
|
|
static let box = RGBA(red: 0, green: 0, blue: 0, alpha: 0.75)
|
|
}
|
|
|
|
/// Die Grösse, **relativ** zur Vorgabe des Players.
|
|
///
|
|
/// Ein Prozentwert, keine Punktgrösse: hundert heisst unverändert. So
|
|
/// bleibt die Einstellung richtig, egal wie gross das Bild ist.
|
|
public enum Size: Int, CaseIterable, Identifiable, Sendable, Codable {
|
|
case small = 75
|
|
case normal = 100
|
|
case large = 130
|
|
case huge = 175
|
|
|
|
public var id: Int { rawValue }
|
|
}
|
|
|
|
/// Schrift und Kasten als **Paar**, nicht als zwei Regler.
|
|
///
|
|
/// Unabhängige Achsen liessen unlesbare Mischungen zu — Schwarz auf
|
|
/// durchsichtig über einer Nachtszene ist eine gültige Einstellung und
|
|
/// eine wertlose. Diese fünf decken ab, was man tatsächlich will.
|
|
public enum Palette: String, CaseIterable, Identifiable, Sendable, Codable {
|
|
case whiteOnClear
|
|
case whiteOnBlack
|
|
case blackOnWhite
|
|
case yellowOnClear
|
|
case yellowOnBlack
|
|
|
|
public var id: String { rawValue }
|
|
}
|
|
|
|
public var size: Size
|
|
public var palette: Palette
|
|
|
|
public init(size: Size = .normal, palette: Palette = .whiteOnClear) {
|
|
self.size = size
|
|
self.palette = palette
|
|
}
|
|
|
|
public var foreground: RGBA {
|
|
switch palette {
|
|
case .whiteOnClear, .whiteOnBlack: .white
|
|
case .blackOnWhite: .black
|
|
case .yellowOnClear, .yellowOnBlack: .yellow
|
|
}
|
|
}
|
|
|
|
public var background: RGBA {
|
|
switch palette {
|
|
case .whiteOnClear, .yellowOnClear: .clear
|
|
case .whiteOnBlack, .yellowOnBlack: .box
|
|
case .blackOnWhite: RGBA(red: 1, green: 1, blue: 1, alpha: 0.85)
|
|
}
|
|
}
|
|
|
|
/// Ob ein Schlagschatten nötig ist.
|
|
///
|
|
/// Ohne Kasten steht die Schrift unmittelbar im Bild, und Weiss auf einer
|
|
/// hellen Szene ist dort unlesbar. Mit Kasten wäre der Schatten nur
|
|
/// Unruhe.
|
|
public var wantsEdgeShadow: Bool {
|
|
switch palette {
|
|
case .whiteOnClear, .yellowOnClear: true
|
|
case .whiteOnBlack, .blackOnWhite, .yellowOnBlack: false
|
|
}
|
|
}
|
|
}
|