diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 3d035b9a..dbc6923c 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -483,7 +483,7 @@ def textBox( path, (x, y) = context._getPathForFrameSetter(box) attributedString = context.attributedString(txt, align) - setter = CoreText.CTFramesetterCreateWithAttributedString(attributedString) + setter = newFramesetterWithAttributedString(attributedString) frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(frame) origins = CoreText.CTFrameGetLineOrigins(frame, (0, len(ctLines)), None) @@ -1086,7 +1086,7 @@ def block(value, rng, stop): AppKit.NSParagraphStyleAttributeName, (0, len(attributedString)), 0, block ) - setter = CoreText.CTFramesetterCreateWithAttributedString(attributedString) + setter = newFramesetterWithAttributedString(attributedString) path = Quartz.CGPathCreateMutable() Quartz.CGPathAddRect(path, None, Quartz.CGRectMake(x, y, w, h * 2)) frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) @@ -1132,7 +1132,7 @@ def block(value, rng, stop): box = (lineX, lineY, width, h * 2) else: lineY = y + originY + firstLineJump - h * 2 - subSetter = CoreText.CTFramesetterCreateWithAttributedString(attributedSubstring) + subSetter = newFramesetterWithAttributedString(attributedSubstring) subPath = Quartz.CGPathCreateMutable() Quartz.CGPathAddRect(subPath, None, Quartz.CGRectMake(lineX, lineY, w, h * 2)) subFrame = CoreText.CTFramesetterCreateFrame(subSetter, (0, 0), subPath, None) @@ -2837,7 +2837,7 @@ def clippedText(self, txt, box, align): if self._state.hyphenation: hyphenIndexes = [i for i, c in enumerate(attrString.string()) if c == "-"] attrString = self.hyphenateAttributedString(attrString, path) - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) visibleRange = CoreText.CTFrameGetVisibleStringRange(box) clip = visibleRange.length @@ -2869,7 +2869,7 @@ def _getTypesetterLinesWithPath(self, attrString, path, offset=None): # get lines for an attribute string with a given path if offset is None: offset = 0, 0 - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) frame = CoreText.CTFramesetterCreateFrame(setter, offset, path, None) return CoreText.CTFrameGetLines(frame) @@ -2902,7 +2902,7 @@ def textSize(self, txt, align, width, height): path = CoreText.CGPathCreateMutable() CoreText.CGPathAddRect(path, None, CoreText.CGRectMake(0, 0, width, height)) attrString = self.hyphenateAttributedString(attrString, path) - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) (w, h), _ = CoreText.CTFramesetterSuggestFrameSizeWithConstraints( setter, (0, 0), None, (width, height), None ) @@ -3042,3 +3042,11 @@ def getFontName(font) -> str | None: if fontName is not None: fontName = str(fontName) return fontName + + +def newFramesetterWithAttributedString(attrString): + allowUnbounded = len(attrString) > 2000 # somewhat arbitrary + typesetter = CoreText.CTTypesetterCreateWithAttributedStringAndOptions( + attrString, {CoreText.kCTTypesetterOptionAllowUnboundedLayout: allowUnbounded} + ) + return CoreText.CTFramesetterCreateWithTypesetter(typesetter) diff --git a/drawBot/context/pdfContext.py b/drawBot/context/pdfContext.py index 1dc8dc96..9b4ae25e 100644 --- a/drawBot/context/pdfContext.py +++ b/drawBot/context/pdfContext.py @@ -5,7 +5,7 @@ from ..macOSVersion import macOSVersion from ..misc import DrawBotError, isGIF, isPDF -from .baseContext import BaseContext, FormattedString +from .baseContext import BaseContext, FormattedString, newFramesetterWithAttributedString from .tools import gifTools @@ -164,7 +164,7 @@ def _textBox(self, txt, box, align): if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) frame = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(frame) diff --git a/drawBot/context/svgContext.py b/drawBot/context/svgContext.py index 2160620b..ca06d7a6 100644 --- a/drawBot/context/svgContext.py +++ b/drawBot/context/svgContext.py @@ -8,7 +8,15 @@ from drawBot.misc import formatNumber, warnings -from .baseContext import BaseContext, Color, FormattedString, Gradient, GraphicsState, Shadow +from .baseContext import ( + BaseContext, + Color, + FormattedString, + Gradient, + GraphicsState, + Shadow, + newFramesetterWithAttributedString, +) from .imageContext import _makeBitmapImageRep @@ -398,7 +406,7 @@ def _textBox(self, rawTxt, box, align): if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) self._svgBeginClipPath() diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 885478cb..14d008a9 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -28,6 +28,7 @@ getFontName, getNSFontFromNameOrPath, makeTextBoxes, + newFramesetterWithAttributedString, ) from .context.dummyContext import DummyContext from .context.tools import drawBotbuiltins, gifTools @@ -2007,7 +2008,7 @@ def textBoxBaselines(self, txt: FormattedString | str, box: BoundingBox, align: raise TypeError("expected 'str' or 'FormattedString', got '%s'" % type(txt).__name__) path, (x, y) = self._dummyContext._getPathForFrameSetter(box) attrString = self._dummyContext.attributedString(txt, align=align) - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) @@ -2030,7 +2031,7 @@ def textBoxCharacterBounds(self, txt: FormattedString | str, box: BoundingBox, a bounds = list() path, (x, y) = self._dummyContext._getPathForFrameSetter(box) attrString = self._dummyContext.attributedString(txt) - setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) diff --git a/tests/data/expected_textBoxLongText.pdf b/tests/data/expected_textBoxLongText.pdf new file mode 100644 index 00000000..6c3eafad Binary files /dev/null and b/tests/data/expected_textBoxLongText.pdf differ diff --git a/tests/data/expected_textBoxLongText.png b/tests/data/expected_textBoxLongText.png new file mode 100644 index 00000000..e5adb192 Binary files /dev/null and b/tests/data/expected_textBoxLongText.png differ diff --git a/tests/data/expected_textBoxLongText.svg b/tests/data/expected_textBoxLongText.svg new file mode 100644 index 00000000..91e6dec2 --- /dev/null +++ b/tests/data/expected_textBoxLongText.svg @@ -0,0 +1,29 @@ + + + + + fiflfiflfiflfiflfiflfiflfiflfiflfi + + + flfiflfiflfiflfiflfiflfiflfiflfifl + + + fiflfiflfiflfiflfiflfiflfiflfiflfi + + + flfiflfiflfiflfiflfiflfiflfiflfifl + + + fiflfiflfiflfiflfiflfiflfiflfiflfi + + + flfiflfiflfiflfiflfiflfiflfiflfifl + + + fiflfiflfiflfiflfiflfiflfiflfiflfi + + + flfiflfiflfiflfiflfiflfiflfiflfifl + + + \ No newline at end of file diff --git a/tests/drawBotScripts/textBoxLongText.py b/tests/drawBotScripts/textBoxLongText.py new file mode 100644 index 00000000..ae64a692 --- /dev/null +++ b/tests/drawBotScripts/textBoxLongText.py @@ -0,0 +1,8 @@ +# See bug: https://github.com/typemytype/drawbot/issues/585 +import drawBot + +drawBot.size(200, 200) +box = (10, 10, 180, 180) +drawBot.fontSize(18) +drawBot.font("Hoefler Text") +drawBot.textBox("fifl" * 3000, box)