Scale SVG images bigger
This commit is contained in:
@@ -663,6 +663,162 @@ class TestAnkiBuild(unittest.TestCase):
|
||||
self.assertFalse(all(byte_equal))
|
||||
|
||||
|
||||
class TestIntrinsicSvgSize(unittest.TestCase):
|
||||
"""Parsing of native figure dimensions — SVG only."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
|
||||
def _write(self, name, body):
|
||||
path = self.root / name
|
||||
path.write_text(body, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def test_svg_unitless_width_and_height(self):
|
||||
p = self._write("a.svg", '<svg width="115.5" height="120"><g/></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (115.5, 120.0))
|
||||
|
||||
def test_svg_px_suffix_is_tolerated(self):
|
||||
p = self._write("a.svg", '<svg width="200px" height="80px"></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (200.0, 80.0))
|
||||
|
||||
def test_svg_percentage_dimension_is_rejected(self):
|
||||
p = self._write("a.svg", '<svg width="100%" height="100%"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_missing_dimension_returns_none(self):
|
||||
p = self._write("a.svg", '<svg width="200"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_without_root_tag_returns_none(self):
|
||||
p = self._write("a.svg", "not an svg at all")
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
def test_svg_leading_decimal_is_accepted(self):
|
||||
p = self._write("a.svg", '<svg width=".5" height="10"></svg>')
|
||||
self.assertEqual(aa.intrinsic_svg_size(p), (0.5, 10.0))
|
||||
|
||||
def test_svg_malformed_number_returns_none_not_raises(self):
|
||||
# Permissive matching used to let "1.2.3" reach float() and abort
|
||||
# the build; now it falls back to None like any other bad value.
|
||||
for bad in ('1.2.3', '.', '1.', '1..2'):
|
||||
p = self._write("a.svg", f'<svg width="{bad}" height="10"></svg>')
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p), bad)
|
||||
|
||||
def test_non_svg_is_not_sized(self):
|
||||
# A PNG with a perfectly valid-looking name is still not sized:
|
||||
# we deliberately only enlarge SVGs.
|
||||
p = self.root / "a.png"
|
||||
p.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 64)
|
||||
self.assertIsNone(aa.intrinsic_svg_size(p))
|
||||
|
||||
|
||||
class TestScaleForSize(unittest.TestCase):
|
||||
"""The clamped enlargement factor."""
|
||||
|
||||
def test_small_figure_gets_full_multiplier(self):
|
||||
self.assertEqual(aa.scale_for_size(115, 120), aa.MEDIA_SCALE)
|
||||
|
||||
def test_wide_figure_is_clamped_by_max_width(self):
|
||||
self.assertEqual(aa.scale_for_size(aa.MEDIA_MAX_WIDTH, 50), 1.0)
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(aa.MEDIA_MAX_WIDTH / 2, 50), 2.0
|
||||
)
|
||||
# Between half-cap and cap: enlarged only until width hits the cap.
|
||||
wide = aa.MEDIA_MAX_WIDTH * 0.75
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(wide, 50), aa.MEDIA_MAX_WIDTH / wide
|
||||
)
|
||||
|
||||
def test_tall_figure_is_clamped_by_max_height(self):
|
||||
self.assertEqual(aa.scale_for_size(50, aa.MEDIA_MAX_HEIGHT), 1.0)
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(50, aa.MEDIA_MAX_HEIGHT / 2), 2.0
|
||||
)
|
||||
tall = aa.MEDIA_MAX_HEIGHT * 0.75
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(50, tall), aa.MEDIA_MAX_HEIGHT / tall
|
||||
)
|
||||
|
||||
def test_large_catalog_schematic_grows_toward_cap(self):
|
||||
# NG205-like (635x460): with the 1024x768 cap it now enlarges,
|
||||
# bounded by width — up to 1024 px wide.
|
||||
self.assertAlmostEqual(
|
||||
aa.scale_for_size(635, 460), aa.MEDIA_MAX_WIDTH / 635
|
||||
)
|
||||
|
||||
def test_never_shrinks_below_native(self):
|
||||
self.assertEqual(aa.scale_for_size(5000, 5000), 1.0)
|
||||
|
||||
def test_degenerate_size_is_native(self):
|
||||
self.assertEqual(aa.scale_for_size(0, 0), 1.0)
|
||||
|
||||
|
||||
class TestImageHtmlScaling(unittest.TestCase):
|
||||
"""End-to-end `<img>` emission and the per-path size cache."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.media_dir = Path(self.tmp.name) / "svgs"
|
||||
self.media_dir.mkdir()
|
||||
|
||||
def _write(self, name, body):
|
||||
(self.media_dir / name).write_text(body, encoding="utf-8")
|
||||
|
||||
def test_small_figure_emits_doubled_width(self):
|
||||
self._write("S_q.svg", '<svg width="115" height="120"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
self.assertIn('width="230"', media.image_html("S_q"))
|
||||
|
||||
def test_figure_at_cap_emits_no_width(self):
|
||||
# Exactly at the cap → scale 1.0 → native size, no width.
|
||||
self._write(
|
||||
"L_q.svg",
|
||||
f'<svg width="{aa.MEDIA_MAX_WIDTH}" '
|
||||
f'height="{aa.MEDIA_MAX_HEIGHT}"></svg>',
|
||||
)
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("L_q")
|
||||
self.assertIn('class="af-media"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_png_is_not_scaled(self):
|
||||
# Only a PNG for this stem (no .svg sibling): emitted at native
|
||||
# size with no width, because we never size raster figures.
|
||||
(self.media_dir / "P_q.png").write_bytes(
|
||||
b"\x89PNG\r\n\x1a\n" + b"\x00" * 64
|
||||
)
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("P_q")
|
||||
self.assertIn('src="P_q.png"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_unreadable_size_falls_back_to_no_width(self):
|
||||
self._write("U_q.svg", '<svg width="100%" height="100%"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
self.assertNotIn("width=", media.image_html("U_q"))
|
||||
|
||||
def test_malformed_number_falls_back_without_raising(self):
|
||||
self._write("M_q.svg", '<svg width="1.2.3" height="10"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
html = media.image_html("M_q") # must not raise
|
||||
self.assertIn('src="M_q.svg"', html)
|
||||
self.assertNotIn("width=", html)
|
||||
|
||||
def test_size_is_read_once_per_path(self):
|
||||
self._write("S_q.svg", '<svg width="115" height="120"></svg>')
|
||||
media = aa.MediaRegistry(self.media_dir)
|
||||
with patch.object(
|
||||
aa, "intrinsic_svg_size", wraps=aa.intrinsic_svg_size
|
||||
) as spy:
|
||||
media.image_html("S_q")
|
||||
media.image_html("S_q")
|
||||
self.assertEqual(spy.call_count, 1)
|
||||
|
||||
|
||||
class TestRealExplanationsFile(unittest.TestCase):
|
||||
"""Validate the repo's actual explanations.json against the live catalog.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user