"""Tests for kinetic-typography block layout (fcpxml/text_layout.py). The reference values come from "Exemplo Letra.fcpxmld", a Final Cut export the user built by hand: the sentence "Toda a minha vida, assim," with each word positioned in the Inspector. Those hand-placed coordinates are the only ground truth for the template's coordinate space, so the calibration tests below pin the layout to them — if a change drifts from the user's own framing, they fail. """ import pytest from fcpxml.text_layout import _SAFETY_MARGIN as SAFETY_MARGIN from fcpxml.text_layout import ( LayoutBox, PlacedWord, layout_sentence, measure_text, metrics_for, rhythm_font_size, rhythm_italic, ) # Hand-placed positions from the calibration export, in canvas points. REFERENCE = { "Toda": (-251.828, -109.0), "a": (2.236, -101.135), "minha": (267.319, -95.0), "vida,": (71.0395, -233.65), "assim,": (210.88, -100.0), } REFERENCE_SIZES = (170, 128, 151, 128, 128) class Style: """Stands in for WordStyle, carrying the reference export's values.""" bold = False kerning = 2.048 font_size = 128 size_pattern = REFERENCE_SIZES italic_pattern = (False, False, False, False, True) @pytest.fixture def sentence(): return [{"word": w} for w in REFERENCE] @pytest.fixture def box(): return LayoutBox.for_frame(2160, 3840) class TestMeasureText: def test_empty_string_has_no_width(self): assert measure_text("", 128) == 0.0 def test_width_scales_with_font_size(self): assert measure_text("minha", 256) > measure_text("minha", 128) def test_narrow_letters_measure_less_than_wide_ones(self): assert measure_text("lli", 128) < measure_text("mmm", 128) def test_accented_letters_measure_like_their_base(self): assert measure_text("ção", 128) == pytest.approx( measure_text("cao", 128), rel=1e-9 ) def test_kerning_widens_by_one_step_per_character(self): plain = measure_text("abc", 100, kerning=0) kerned = measure_text("abc", 100, kerning=10) # 3 characters x 10 units, carried through the safety margin. assert kerned - plain == pytest.approx(30 * SAFETY_MARGIN, rel=1e-6) def test_uses_the_real_font_metrics_when_available(self): """Generic Helvetica advances differ from the actual macOS variants by up to 21% of the em. Measuring "Helvetica Neue" against the generic table once made words run narrow, which is what puts two of them on top of each other.""" generic = measure_text("minha", 151) neue = measure_text("minha", 151, font="Helvetica Neue") assert generic != neue def test_never_underestimates_real_advances(self): """The whole point of the embedded metrics: a width that runs low by even a few points lets neighbouring words touch. Measured width must always be the true advance plus the safety margin, never less.""" cases = [ ("Toda", "Helvetica", "Light", 170, 2.72, 398.3), ("minha", "Helvetica Neue", None, 151, 2.416, 423.4), ("assim,", "Helvetica Neue", "Light Italic", 128, 2.048, 367.7), ("dificuldade", "Helvetica", "Light", 170, 2.72, 880.1), ("encontrar", "Helvetica Neue", None, 151, 2.416, 667.7), ] for text, font, face, size, kerning, truth in cases: got = measure_text(text, size, kerning=kerning, font=font, face=face) assert got >= truth, f"{text!r} measured {got:.1f} < real {truth}" assert got == pytest.approx(truth * SAFETY_MARGIN, rel=0.001) def test_metrics_cover_every_font_the_rhythm_uses(self): for font, face in ( ("Helvetica", "Light"), ("Helvetica Neue", None), ("Helvetica Neue", "Light Italic"), ): assert metrics_for(font, face) is not None, f"{font} {face}" def test_unknown_font_falls_back_instead_of_raising(self): assert measure_text("oi", 100, font="No Such Font") > 0 def test_matches_reference_word_widths(self): """Half-width sums must reproduce the center-to-center distances the user's hand-placed words show: 254.06 (Toda→a), 265.08 (a→minha).""" w_toda = measure_text("Toda", 170, kerning=2.72) w_a = measure_text("a", 128, kerning=2.048) w_minha = measure_text("minha", 151, kerning=2.416) assert (w_toda + w_a) / 2 == pytest.approx(254.06, abs=25) assert (w_a + w_minha) / 2 == pytest.approx(265.08, abs=25) class TestRhythm: def test_font_size_cycles_through_the_pattern(self): sizes = [rhythm_font_size(i, Style()) for i in range(5)] assert sizes == [float(s) for s in REFERENCE_SIZES] def test_font_size_wraps_past_the_pattern(self): assert rhythm_font_size(5, Style()) == rhythm_font_size(0, Style()) def test_italic_follows_its_own_pattern(self): flags = [rhythm_italic(i, Style()) for i in range(5)] assert flags == [False, False, False, False, True] def test_no_pattern_falls_back_to_plain_style(self): class Plain: font_size = 90 assert rhythm_font_size(3, Plain()) == 90.0 assert rhythm_italic(3, Plain()) is False class TestLayoutSentence: def test_places_every_word(self, sentence, box): out = layout_sentence(sentence, Style(), box) assert [p.text for p in out.placed] == list(REFERENCE) assert out.overflow == [] def test_no_two_words_overlap(self, sentence, box): out = layout_sentence(sentence, Style(), box) for i, a in enumerate(out.placed): for b in out.placed[i + 1:]: assert not a.overlaps(b), f"{a.text!r} overlaps {b.text!r}" def test_blank_and_empty_words_are_skipped(self, box): out = layout_sentence( [{"word": "ok"}, {"word": " "}, {"word": ""}, {}], Style(), box ) assert [p.text for p in out.placed] == ["ok"] def test_no_words_yields_empty_layout(self, box): out = layout_sentence([], Style(), box) assert out.placed == [] and out.overflow == [] def test_lines_are_horizontally_centered(self, sentence, box): out = layout_sentence(sentence, Style(), box) for line in {p.line_index for p in out.placed}: words = [p for p in out.placed if p.line_index == line] span = min(w.left for w in words) + max(w.right for w in words) assert span == pytest.approx(0.0, abs=1e-6) def test_words_stay_inside_the_box(self, sentence, box): out = layout_sentence(sentence, Style(), box) for p in out.placed: assert p.left >= -box.width / 2 - 1e-6 assert p.right <= box.width / 2 + 1e-6 def test_later_lines_sit_lower(self, sentence, box): out = layout_sentence(sentence, Style(), box) ys = {p.line_index: p.y for p in out.placed} assert ys[1] < ys[0], "y grows upward, so line 2 must be below line 1" class TestCalibrationAgainstRealExport: """Pins the layout to the user's own hand-placed framing.""" def test_line_spacing_matches_the_export(self, sentence, box): out = layout_sentence(sentence, Style(), box) ys = sorted({round(p.y, 4) for p in out.placed}, reverse=True) assert ys[0] - ys[1] == pytest.approx(132.65, abs=6) def test_first_line_height_matches_the_export(self, sentence, box): out = layout_sentence(sentence, Style(), box) first = next(p for p in out.placed if p.line_index == 0) # The user's line 1 averages -101.1 across its hand-dragged words. assert first.y == pytest.approx(-101.1, abs=8) def test_word_x_tracks_the_export(self, sentence, box): """Within the slop of dragging by hand — the user placed these by eye, so this guards against a scale error, not against small offsets.""" out = layout_sentence(sentence, Style(), box) for p in out.placed: if p.line_index != 0: continue # the user misplaced "assim," onto line 1 assert p.x == pytest.approx(REFERENCE[p.text][0], abs=30) class TestOverflowSplitsIntoSubSentences: def test_long_sentence_overflows_instead_of_spilling(self, box): words = [{"word": f"palavra{i}"} for i in range(40)] out = layout_sentence(words, Style(), box) assert out.overflow, "a 40-word sentence must not all fit" assert out.placed assert len(out.placed) + len(out.overflow) == 40 def test_overflow_preserves_the_original_word_dicts(self, box): words = [{"word": f"palavra{i}", "start": float(i)} for i in range(40)] out = layout_sentence(words, Style(), box) assert all("start" in w for w in out.overflow) def test_placed_words_stay_within_the_band(self, box): words = [{"word": f"palavra{i}"} for i in range(40)] out = layout_sentence(words, Style(), box) top = max(p.top for p in out.placed) bottom = min(p.bottom for p in out.placed) assert top - bottom <= box.height + 1e-6 def test_a_word_wider_than_the_box_is_still_placed(self, box): out = layout_sentence([{"word": "m" * 200}], Style(), box) assert len(out.placed) == 1, "never silently drop a spoken word" class TestPlacedWord: def test_position_param_formats_as_fcp_writes_it(self): w = PlacedWord("oi", 128, False, -251.828, -109.0, 400, 96, 0) assert w.position_param() == "-251.828 -109" def test_boxes_that_touch_do_not_count_as_overlapping(self): a = PlacedWord("a", 128, False, 0, 0, 100, 100, 0) b = PlacedWord("b", 128, False, 100, 0, 100, 100, 0) assert not a.overlaps(b) def test_boxes_that_intersect_do_overlap(self): a = PlacedWord("a", 128, False, 0, 0, 100, 100, 0) b = PlacedWord("b", 128, False, 50, 0, 100, 100, 0) assert a.overlaps(b) class TestLayoutBox: def test_for_frame_halves_pixels_into_points(self): box = LayoutBox.for_frame(2160, 3840, side_margin=0.0) assert box.width == pytest.approx(1080.0) def test_side_margin_narrows_the_usable_width(self): wide = LayoutBox.for_frame(2160, 3840, side_margin=0.0) narrow = LayoutBox.for_frame(2160, 3840, side_margin=0.10) assert narrow.width < wide.width def test_center_y_can_be_overridden(self): assert LayoutBox.for_frame(2160, 3840, center_y=-400.0).center_y == -400.0