diff --git a/pycaption/exceptions.py b/pycaption/exceptions.py index bc5c8b4e..cc4725ee 100644 --- a/pycaption/exceptions.py +++ b/pycaption/exceptions.py @@ -48,3 +48,9 @@ class CaptionRendererError(Exception): Error raised when caption content cannot be rendered correctly, e.g. text runs off screen or required glyphs are missing from the font. """ + + +class CaptionRendererErrorGroup(ExceptionGroup): + """ + Exception group which contains multiple :class:`CaptionRenderError` instances. + """ diff --git a/pycaption/subtitler_image_based.py b/pycaption/subtitler_image_based.py index 28c66ba6..9cada486 100644 --- a/pycaption/subtitler_image_based.py +++ b/pycaption/subtitler_image_based.py @@ -10,7 +10,7 @@ from langcodes import Language, tag_distance from pycaption.base import BaseWriter, CaptionSet, Caption, CaptionNode, CaptionList -from pycaption.exceptions import CaptionRendererError +from pycaption.exceptions import CaptionRendererError, CaptionRendererErrorGroup from pycaption.geometry import UnitEnum, Size @@ -211,17 +211,26 @@ def write_images( fnt = ImageFont.truetype(fnt, font_size) index = 1 + # Try to render all images and collect all exceptions + image_exceptions = [] for i, cap_list in enumerate(caps_final): # Create RGBA image with transparent background img = Image.new('RGBA', (self.video_width, self.video_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - self.printLine(draw, cap_list, fnt, position, align) + try: + self.printLine(draw, cap_list, fnt, position, align) + except CaptionRendererError as e: + image_exceptions.append(e) + continue # Pass RGBA image to subclass - each subclass converts as needed self.save_image(tmpDir, index, img) index = index + 1 + if len(image_exceptions) > 0: + raise CaptionRendererErrorGroup("Errors while rendering captions", image_exceptions) + return caps_final, overlapping def printLine(self, draw: ImageDraw, caption_list: Caption, fnt: ImageFont, position: str = 'bottom', @@ -298,7 +307,7 @@ def printLine(self, draw: ImageDraw, caption_list: Caption, fnt: ImageFont, posi if text_left < 0 or text_top < 0 or text_right > self.video_width or ( position != 'bottom' and text_bottom > self.video_height): raise CaptionRendererError( - f'Text runs off screen: text="{text}"' + f'Text at {caption.format_start()} runs off screen: text="{text}"' ) border = (*self.borderColor, 255) # Add alpha for RGBA diff --git a/tests/test_subtitler_image_based.py b/tests/test_subtitler_image_based.py index da81acc9..39d25582 100644 --- a/tests/test_subtitler_image_based.py +++ b/tests/test_subtitler_image_based.py @@ -6,8 +6,8 @@ from PIL import Image, ImageDraw, ImageFont from pycaption import SRTReader -from pycaption.base import Caption, CaptionNode -from pycaption.exceptions import CaptionRendererError +from pycaption.base import Caption, CaptionList, CaptionNode +from pycaption.exceptions import CaptionRendererError, CaptionRendererErrorGroup from pycaption.filtergraph import FiltergraphWriter from pycaption.geometry import Layout, Point, Size, UnitEnum from pycaption.subtitler_image_based import SubtitleImageBasedWriter @@ -19,9 +19,9 @@ ) -def make_caption(text, layout_info=None): +def make_caption(text, layout_info=None, start=0): nodes = [CaptionNode.create_text(text)] - return Caption(0, 1000000, nodes, layout_info=layout_info) + return Caption(start, start+1000000, nodes, layout_info=layout_info) def make_source_layout(x_pct, y_pct): @@ -47,7 +47,7 @@ def test_long_text_runs_off(self): writer, draw = make_writer_and_draw(200, 100) fnt = ImageFont.truetype(FONT_PATH, 20) caption = make_caption("This text is way too long to fit on a tiny screen") - with pytest.raises(CaptionRendererError, match="Text runs off screen"): + with pytest.raises(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"): writer.printLine(draw, [caption], fnt, position='bottom', align='center') @@ -62,7 +62,7 @@ def test_long_text_runs_off(self): writer, draw = make_writer_and_draw(200, 100) fnt = ImageFont.truetype(FONT_PATH, 20) caption = make_caption("This text is way too long to fit on a tiny screen") - with pytest.raises(CaptionRendererError, match="Text runs off screen"): + with pytest.raises(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"): writer.printLine(draw, [caption], fnt, position='bottom', align='left') @@ -77,7 +77,7 @@ def test_long_text_runs_off(self): writer, draw = make_writer_and_draw(200, 100) fnt = ImageFont.truetype(FONT_PATH, 20) caption = make_caption("This text is way too long to fit on a tiny screen") - with pytest.raises(CaptionRendererError, match="Text runs off screen"): + with pytest.raises(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"): writer.printLine(draw, [caption], fnt, position='bottom', align='right') @@ -95,7 +95,7 @@ def test_right_sticks_out(self): fnt = ImageFont.truetype(FONT_PATH, 20) layout = make_source_layout(x_pct=80, y_pct=50) caption = make_caption("This text sticks out on the right", layout_info=layout) - with pytest.raises(CaptionRendererError, match="Text runs off screen"): + with pytest.raises(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"): writer.printLine(draw, [caption], fnt, position='source', align='left') def test_left_sticks_out(self): @@ -105,9 +105,30 @@ def test_left_sticks_out(self): fnt = ImageFont.truetype(FONT_PATH, 20) layout = make_source_layout(x_pct=10, y_pct=50) caption = make_caption("This text sticks out on the left", layout_info=layout) - with pytest.raises(CaptionRendererError, match="Text runs off screen"): + with pytest.raises(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"): writer.printLine(draw, [caption], fnt, position='source', align='left') + def test_exception_group(self, tmp_path): + """ + The main ``write_images`` interface collects all exceptions and reports them in a ``CaptionRendererErrorGroup``. + """ + # 200px wide screen, text at x=80% (x=160), text wider than remaining space + writer, draw = make_writer_and_draw(200, 200) + layout = make_source_layout(x_pct=80, y_pct=50) + cap_list = CaptionList( + [ + make_caption("This text sticks out on the right 1", layout_info=layout), + make_caption("This text sticks out on the right 2", layout_info=layout, start=2000000) + ], + layout + ) + with pytest.RaisesGroup( + pytest.RaisesExc(CaptionRendererError, match="Text at 00:00:00.000 runs off screen"), + pytest.RaisesExc(CaptionRendererError, match="Text at 00:00:02.000 runs off screen"), + ) as rg: + writer.write_images(cap_list, "en", str(tmp_path), position='source', align='left') + assert isinstance(rg.value, CaptionRendererErrorGroup), "Should be wrapped in a CaptionRendererErrorGroup" + class TestBaselineAlignment: """Render subtitle images with/without descenders to visually verify