Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pycaption/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
15 changes: 12 additions & 3 deletions pycaption/subtitler_image_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
39 changes: 30 additions & 9 deletions tests/test_subtitler_image_based.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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')


Expand All @@ -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')


Expand All @@ -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')


Expand All @@ -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):
Expand All @@ -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
Expand Down
Loading