From 5bcb886b7a318a0845741bb46cf58ea3362de603 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 20 May 2025 11:58:45 +0200 Subject: [PATCH 1/3] Use kCTTypesetterOptionAllowUnboundedLayout option for setting 'longer' strings, ensuring OT features are applied in all cases --- drawBot/context/baseContext.py | 20 ++++++++++++++------ drawBot/context/pdfContext.py | 4 ++-- drawBot/context/svgContext.py | 12 ++++++++++-- drawBot/drawBotDrawingTools.py | 5 +++-- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 3d035b9a..a4e31a66 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..691f1a49 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..5cc92b80 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..527dda23 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) From 9b922f697c341d575c88ced9d3e90962747f33f0 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 20 May 2025 12:09:19 +0200 Subject: [PATCH 2/3] Add test case --- tests/data/expected_textBoxLongText.pdf | Bin 0 -> 5897 bytes tests/data/expected_textBoxLongText.png | Bin 0 -> 6860 bytes tests/data/expected_textBoxLongText.svg | 29 ++++++++++++++++++++++++ tests/drawBotScripts/textBoxLongText.py | 8 +++++++ 4 files changed, 37 insertions(+) create mode 100644 tests/data/expected_textBoxLongText.pdf create mode 100644 tests/data/expected_textBoxLongText.png create mode 100644 tests/data/expected_textBoxLongText.svg create mode 100644 tests/drawBotScripts/textBoxLongText.py diff --git a/tests/data/expected_textBoxLongText.pdf b/tests/data/expected_textBoxLongText.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c3eafad5c91692f524011fa8eac1ad65ed762ad GIT binary patch literal 5897 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 literal 0 HcmV?d00001 diff --git a/tests/data/expected_textBoxLongText.png b/tests/data/expected_textBoxLongText.png new file mode 100644 index 0000000000000000000000000000000000000000..e5adb1920d4d26dd190f6deabb0636cfd6a3dd7c GIT binary patch literal 6860 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 Y!PAt_Kw~`-1pFE5nO-fqa`(~y0{XXXv;Y7A literal 0 HcmV?d00001 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) From d436fe93c6ed659f6d217afe2de0197d247afc86 Mon Sep 17 00:00:00 2001 From: Just van Rossum Date: Tue, 20 May 2025 12:47:50 +0200 Subject: [PATCH 3/3] Fix capitalisation of framesetter and typesetter names --- drawBot/context/baseContext.py | 18 +++++++++--------- drawBot/context/pdfContext.py | 4 ++-- drawBot/context/svgContext.py | 4 ++-- drawBot/drawBotDrawingTools.py | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index a4e31a66..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 = newFrameSetterWithAttributedString(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 = newFrameSetterWithAttributedString(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 = newFrameSetterWithAttributedString(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 = newFrameSetterWithAttributedString(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 = newFrameSetterWithAttributedString(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 = newFrameSetterWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) (w, h), _ = CoreText.CTFramesetterSuggestFrameSizeWithConstraints( setter, (0, 0), None, (width, height), None ) @@ -3044,9 +3044,9 @@ def getFontName(font) -> str | None: return fontName -def newFrameSetterWithAttributedString(attrString): +def newFramesetterWithAttributedString(attrString): allowUnbounded = len(attrString) > 2000 # somewhat arbitrary - typeSetter = CoreText.CTTypesetterCreateWithAttributedStringAndOptions( + typesetter = CoreText.CTTypesetterCreateWithAttributedStringAndOptions( attrString, {CoreText.kCTTypesetterOptionAllowUnboundedLayout: allowUnbounded} ) - return CoreText.CTFramesetterCreateWithTypesetter(typeSetter) + return CoreText.CTFramesetterCreateWithTypesetter(typesetter) diff --git a/drawBot/context/pdfContext.py b/drawBot/context/pdfContext.py index 691f1a49..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, newFrameSetterWithAttributedString +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 = newFrameSetterWithAttributedString(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 5cc92b80..ca06d7a6 100644 --- a/drawBot/context/svgContext.py +++ b/drawBot/context/svgContext.py @@ -15,7 +15,7 @@ Gradient, GraphicsState, Shadow, - newFrameSetterWithAttributedString, + newFramesetterWithAttributedString, ) from .imageContext import _makeBitmapImageRep @@ -406,7 +406,7 @@ def _textBox(self, rawTxt, box, align): if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() - setter = newFrameSetterWithAttributedString(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 527dda23..14d008a9 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -28,7 +28,7 @@ getFontName, getNSFontFromNameOrPath, makeTextBoxes, - newFrameSetterWithAttributedString, + newFramesetterWithAttributedString, ) from .context.dummyContext import DummyContext from .context.tools import drawBotbuiltins, gifTools @@ -2008,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 = newFrameSetterWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) @@ -2031,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 = newFrameSetterWithAttributedString(attrString) + setter = newFramesetterWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None)