Skip to content

Commit 8ffd1fa

Browse files
committed
Merge branch 'draw_word_wrap'
2 parents 19e1e01 + e2f50df commit 8ffd1fa

2 files changed

Lines changed: 90 additions & 30 deletions

File tree

docs/_images/draw-word-wrap.png

-474 Bytes
Loading

docs/guide/draw.rst

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -602,72 +602,132 @@ form of word-wrapping, and users of the ``wand`` library would be responsible
602602
for implementing this behavior unique to their business requirements.
603603

604604
ImageMagick's ``caption:`` coder does offer a word-wrapping solution with
605-
:meth:`Image.caption() <wand.image.BaseImage.caption>` method, but Python's :mod:`textwrap` is
605+
:meth:`Image.caption() <wand.image.BaseImage.caption>` method, but this version is
606606
a little more sophisticated.
607607

608608
.. code::
609609
610-
from textwrap import wrap
611-
from wand.color import Color
612610
from wand.drawing import Drawing
613611
from wand.image import Image
612+
import re
614613
615614
616-
def draw_roi(contxt, roi_width, roi_height):
615+
def draw_roi(ctx, roi_width, roi_height):
617616
"""Let's draw a blue box so we can identify what
618617
our region of interest is."""
619618
ctx.push()
620-
ctx.stroke_color = Color('BLUE')
621-
ctx.fill_color = Color('TRANSPARENT')
619+
ctx.stroke_color = 'BLUE'
620+
ctx.fill_color = 'TRANSPARENT'
622621
ctx.rectangle(left=75, top=255, width=roi_width, height=roi_height)
623622
ctx.pop()
624623
625624
626625
def word_wrap(image, ctx, text, roi_width, roi_height):
627-
"""Break long text to multiple lines, and reduce point size
626+
"""Break long text to multiple lines, and/or reduce point size
628627
until all text fits within a bounding box."""
629-
mutable_message = text
630-
iteration_attempts = 100
628+
629+
def substring_font_metrics_width(text, length):
630+
""" Get width of rendered substring. """
631+
metrics = ctx.get_font_metrics(image, text[0:length], True)
632+
return metrics.text_width
631633
632634
def eval_metrics(txt):
633635
"""Quick helper function to calculate width/height of text."""
634636
metrics = ctx.get_font_metrics(image, txt, True)
635637
return (metrics.text_width, metrics.text_height)
636638
637-
while ctx.font_size > 0 and iteration_attempts:
639+
# Start with the original text.
640+
wrapped_text = text
641+
642+
# If this message can't be wrapped, just scale it.
643+
loop_continues = True
644+
message_width, message_height = eval_metrics(wrapped_text)
645+
if message_width > roi_width and ' ' not in text:
646+
ctx.font_size *= roi_width / message_width # Scale pointsize
647+
loop_continues = False
648+
649+
# Loop until a successful word-wrap is calculated.
650+
iteration_attempts = 100
651+
while loop_continues and ctx.font_size > 0 and iteration_attempts:
638652
iteration_attempts -= 1
639-
width, height = eval_metrics(mutable_message)
640-
if height > roi_height:
641-
ctx.font_size -= 0.75 # Reduce pointsize
642-
mutable_message = text # Restore original text
643-
elif width > roi_width:
644-
columns = len(mutable_message)
645-
while columns > 0:
646-
columns -= 1
647-
mutable_message = '\n'.join(wrap(mutable_message, columns))
648-
wrapped_width, _ = eval_metrics(mutable_message)
649-
if wrapped_width <= roi_width:
653+
654+
# Prepare to break this string into lines.
655+
text_lines = []
656+
while len(wrapped_text) > 0:
657+
# If the rest fits, we're done.
658+
value_mid = substring_font_metrics_width(wrapped_text,
659+
len(wrapped_text))
660+
if value_mid <= roi_width:
661+
text_lines.append(wrapped_text)
662+
wrapped_text = '\n'.join(text_lines)
663+
message_width, message_height = eval_metrics(wrapped_text)
664+
if message_height > roi_height:
665+
ctx.font_size *= 0.9 # Reduce pointsize
666+
wrapped_text = text # Restore original text
667+
else:
668+
loop_continues = False
669+
break
670+
671+
# Find where to break this string so that it fits inside the width.
672+
index_low = 0
673+
index_high = len(wrapped_text) - 1
674+
while index_low <= index_high:
675+
index_mid = (index_low + index_high) // 2
676+
value_mid = substring_font_metrics_width(wrapped_text,
677+
index_mid)
678+
if value_mid == roi_width:
650679
break
651-
if columns < 1:
652-
ctx.font_size -= 0.75 # Reduce pointsize
653-
mutable_message = text # Restore original text
654-
else:
655-
break
680+
elif value_mid < roi_width:
681+
index_low = index_mid + 1
682+
index_mid = index_low
683+
else:
684+
index_high = index_mid - 1
685+
index_mid = index_low
686+
687+
# Find the last occurrence of whitespace.
688+
whitespace_matches = list(
689+
re.finditer(r'\s+', wrapped_text[0:index_mid])
690+
)
691+
if whitespace_matches:
692+
index_mid = whitespace_matches[-1].start()
693+
else:
694+
index_mid = -1
695+
if index_mid <= 0:
696+
ctx.font_size *= 0.9 # Reduce pointsize
697+
wrapped_text = text # Restore original text
698+
break
699+
else:
700+
701+
# Break the line here.
702+
text_lines.append(wrapped_text[0:index_mid])
703+
704+
# Prepare to break the rest.
705+
wrapped_text = wrapped_text[index_mid:].lstrip()
706+
707+
# If the incompletely wrapped text is already too high,
708+
# reduce the font size and try again.
709+
message_so_far = '\n'.join(text_lines + [wrapped_text])
710+
message_width, message_height = eval_metrics(message_so_far)
711+
if message_height > roi_height:
712+
ctx.font_size *= 0.9 # Reduce pointsize
713+
wrapped_text = text # Restore original text
714+
break
715+
656716
if iteration_attempts < 1:
657717
raise RuntimeError("Unable to calculate word_wrap for " + text)
658-
return mutable_message
718+
return wrapped_text
659719
660720
661-
message = """This is some really long sentence with the
662-
word "Mississippi" in it."""
721+
message = ("""This is some really long sentence with"""
722+
""" the word "Mississippi" in it.""")
663723
664724
ROI_SIDE = 175
665725
666726
with Image(filename='logo:') as img:
667727
with Drawing() as ctx:
668728
draw_roi(ctx, ROI_SIDE, ROI_SIDE)
669729
# Set the font style
670-
ctx.fill_color = Color('RED')
730+
ctx.fill_color = 'RED'
671731
ctx.font_family = 'Times New Roman'
672732
ctx.font_size = 32
673733
mutable_message = word_wrap(img,

0 commit comments

Comments
 (0)