Skip to content

Commit 53893d6

Browse files
authored
improving test for imageObjects and all their filters (#579)
* improving test for imageObjects and all their filters * wrong path to test files * ignore macos15+ filters * ignore macos15+ filters * fixing docs * update min python version
1 parent 15781e6 commit 53893d6

7 files changed

Lines changed: 1638 additions & 1824 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66

77
DrawBot is a powerful, free application for macOS that invites you to write Python scripts to generate two-dimensional graphics. The built-in graphics primitives support rectangles, ovals, (bezier) paths, polygons, text objects, colors, transparency and much more. You can program multi-page documents and stop-motion animations. Export formats include PDF, SVG, PNG, JPEG, TIFF, animated GIF and MP4 video.
88

9-
To download the latest version of the app, go to
9+
To download the latest version of the app, go to
1010
http://www.drawbot.com/content/download.html
1111

1212
---
1313

1414
## Using DrawBot as a Python module
1515

16-
DrawBot can also be installed as a Python module, the app is not required. It works on Python3.10+.
16+
DrawBot can also be installed as a Python module, the app is not required. It works on Python3.11+.
1717

18-
#### Install
18+
#### Install
1919

2020
The easiest way is to use pip:
2121

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,5 @@ def format_args(self):
475475
def setup(app):
476476
app.add_directive('showcode', ShowCode)
477477
app.add_directive('downloadcode', DownloadCode)
478-
app.add_autodocumenter(DrawBotDocumenter)
478+
# 'self' seems to be removed upstream while formatting the arguments
479+
# app.add_autodocumenter(DrawBotDocumenter)

docs/content/shapes/bezierPath.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Bezier Paths
66
:undoc-members:
77
:inherited-members:
88
:show-inheritance:
9-
:exclude-members: copyContextProperties
9+
:exclude-members: copyContextProperties, add_note, args, with_traceback, log, MissingComponentError, svgClass, svgID, svgLink

docs/content/text/formattedString.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ Formatted Strings
66
:undoc-members:
77
:inherited-members:
88
:show-inheritance:
9-
:exclude-members: copyContextProperties
9+
:exclude-members: copyContextProperties, svgClass, svgID, svgLink

drawBot/context/tools/imageObject.py

Lines changed: 1099 additions & 1493 deletions
Large diffs are not rendered by default.

scripting/imageObjectCodeExtractor.py

Lines changed: 105 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def addDict(self, attribute, data, space=" ", trailing=""):
3434
if value:
3535
trailing = "" if index == len(data) - 1 else ","
3636
self.addDict(key, value, space="", trailing=trailing)
37+
trailing = ""
3738
else:
3839
comma = ","
3940
if index == len(data) - 1:
@@ -49,6 +50,7 @@ def appendCode(self, otherCode):
4950
def get(self, indentLevel=0):
5051
return self.INDENT*indentLevel + f"\n{self.INDENT*indentLevel}".join(self.code)
5152

53+
5254
class UnitTestWriter(CodeWriter):
5355

5456
def header(self):
@@ -57,8 +59,10 @@ def header(self):
5759
self.add("import drawBot")
5860
self.add("from testSupport import DrawBotBaseTest")
5961
self.newline()
62+
self.add('sourceImagePath = "tests/data/drawBot144.png"')
6063
self.add('sampleImage = drawBot.ImageObject("tests/data/drawBot.png")')
61-
self.add('fs = drawBot.FormattedString("Hello World")')
64+
self.add('sampleFormattedString = drawBot.FormattedString("Hello World")')
65+
self.add('sampleText = drawBot.FormattedString("Hello World")')
6266
self.newline()
6367
self.newline()
6468
self.add("class ImageObjectTest(DrawBotBaseTest):")
@@ -96,13 +100,17 @@ def camelCase(txt):
96100
"rectangle": "AppKit.CIVector.vectorWithValues_count_({inputKey}, 4)",
97101
"lightPosition": "AppKit.CIVector.vectorWithValues_count_({inputKey}, 3)",
98102
"angle": "radians({inputKey})",
103+
"rotation": "radians({inputKey})",
99104
"message": "AppKit.NSData.dataWithBytes_length_({inputKey}, len({inputKey}))",
100105
"text": "text.getNSObject()",
106+
"image": "{inputKey}._ciImage()",
107+
("size", "CIStretchCrop"): "AppKit.CIVector.vectorWithValues_count_({inputKey}, 2)",
101108
}
102109

103110
variableValues = {
104111
"image": "an Image object",
105112
"size": "a tuple (w, h)",
113+
("size", "CIStretchCrop"): "a float",
106114
"center": "a tuple (x, y)",
107115
"angle": "a float in degrees",
108116
"minComponents": "RGBA tuple values for the lower end of the range.",
@@ -222,6 +230,14 @@ def getVariableValue(key, fallback=None):
222230
key = key[0]
223231
return variableValues.get(key, fallback)
224232

233+
234+
def getConverterValue(key, fallback=None):
235+
if key in converters:
236+
return converters[key]
237+
key = key[0]
238+
return converters.get(key, fallback)
239+
240+
225241
argumentToHint = {"text": ": FormattedString", "message": ": str"}
226242

227243
toCopy = {
@@ -240,6 +256,9 @@ def getVariableValue(key, fallback=None):
240256
"glassesImage",
241257
"hairImage",
242258
"matteImage",
259+
"paletteImage",
260+
"guideImage",
261+
"smallImage",
243262
),
244263
"message": ("cube0Data", "cube1Data"),
245264
"rectangle": (
@@ -257,6 +276,7 @@ def getVariableValue(key, fallback=None):
257276
"blueCoefficients",
258277
"alphaCoefficients",
259278
"biasVector",
279+
"focusRect"
260280
),
261281
"color": (
262282
"replacementColor3",
@@ -283,6 +303,13 @@ def getVariableValue(key, fallback=None):
283303
"topRight",
284304
"bottomLeft",
285305
"bottomRight",
306+
"breakpoint0",
307+
"breakpoint1",
308+
"growAmount",
309+
"nosePositions",
310+
"leftEyePositions",
311+
"rightEyePositions",
312+
"chinPositions",
286313
),
287314
"lightPosition": ("lightPointsAt"),
288315
"angle": ("acuteAngle", "crossAngle"),
@@ -298,7 +325,7 @@ def getVariableValue(key, fallback=None):
298325

299326
ignoreInputKeys = ["inputImage"]
300327

301-
generators = list(AppKit.CIFilter.filterNamesInCategory_("CICategoryGenerator"))
328+
generators = list(AppKit.CIFilter.filterNamesInCategory_("CICategoryGenerator"))
302329
generators.extend(
303330
[
304331
"CIPDF417BarcodeGenerator",
@@ -309,7 +336,7 @@ def getVariableValue(key, fallback=None):
309336
]
310337
)
311338

312-
allFilterNames = AppKit.CIFilter.filterNamesInCategory_(None)
339+
allFilterNames = AppKit.CIFilter.filterNamesInCategory_(None)
313340

314341
excludeFilterNames = [
315342
"CIBarcodeGenerator",
@@ -318,11 +345,44 @@ def getVariableValue(key, fallback=None):
318345
"CIMedianFilter",
319346
"CIColorCube",
320347
"CIColorCubeWithColorSpace",
348+
"CIHueSaturationValueGradient",
321349
# this one requires a colorspace which is difficult to express for regular drawBot users
322350
# little use for a filter like this, it does not make sense to abstract this for now
323351
# no default value for the colorspace makes it difficult to use it
324352
"CIColorCubesMixedWithMask",
353+
"CIColorCurves",
325354
"CIAffineTransform",
355+
356+
# use drawBot to draw text/formattedStrings into an image
357+
"CITextImageGenerator",
358+
"CIAttributedTextImageGenerator",
359+
360+
# no idea what inputCalibrationData or inputAuxDataMetadata is
361+
"CIDepthBlurEffect",
362+
363+
# no idea what inputModel should be
364+
"CICoreMLModelFilter",
365+
366+
# make an issue for a very good reason why DrawBot needs these filters!
367+
"CIConvolution3X3",
368+
"CIConvolution5X5",
369+
"CIConvolution7X7",
370+
"CIConvolution9Horizontal",
371+
"CIConvolution9Vertical",
372+
"CIConvolutionRGB7X7",
373+
"CIConvolutionRGB9Vertical",
374+
"CIConvolutionRGB9Horizontal",
375+
"CIConvolutionRGB5X5",
376+
"CIConvolutionRGB3X3",
377+
"CIAreaAlphaWeightedHistogram",
378+
"CIAreaBoundsRed",
379+
380+
"CIDistanceGradientFromRedMask", # macos15+
381+
"CIMaximumScaleTransform", # macos15+
382+
"CIToneCurve", # macos15+
383+
"CIToneMapHeadroom" # macos15+
384+
385+
326386
]
327387

328388
degreesAngleFilterNames = ["CIVortexDistortion"]
@@ -346,19 +406,19 @@ def generateImageObjectCode() -> tuple[str, str]:
346406
code = CodeWriter()
347407
unitTests = UnitTestWriter()
348408
unitTests.header()
349-
409+
350410
for filterName in allFilterNames:
351411
if filterName in excludeFilterNames:
352412
continue
353-
ciFilter = AppKit.CIFilter.filterWithName_(filterName)
413+
ciFilter = AppKit.CIFilter.filterWithName_(filterName)
354414
ciFilterAttributes = ciFilter.attributes()
355415
doc = CodeWriter()
356-
doc.add(AppKit.CIFilter.localizedDescriptionForFilterName_(filterName))
357-
416+
doc.add(AppKit.CIFilter.localizedDescriptionForFilterName_(filterName))
417+
358418
args = []
359419
unitTestsArgs = []
360420
inputCode = CodeWriter()
361-
421+
362422
inputKeys = [
363423
inputKey
364424
for inputKey in ciFilter.inputKeys()
@@ -371,38 +431,39 @@ def generateImageObjectCode() -> tuple[str, str]:
371431
ciFilterAttributes.get(x, dict()).get("CIAttributeDefault") is not None
372432
)
373433
)
374-
434+
375435
attributes = dict()
376-
436+
377437
if inputKeys or filterName == "CIRandomGenerator":
378438
doc.newline()
379439
doc.add("**Arguments:**")
380440
doc.newline()
381441
if filterName in generators:
382442
args.append("size: Size")
383443
unitTestsArgs.append("size=(100, 100)")
384-
doc.add(f"`size` {variableValues['size']}")
444+
doc.add(f"* `size` {variableValues['size']}")
385445
for inputKey in inputKeys:
386446
info = ciFilterAttributes.get(inputKey)
387447
default = info.get("CIAttributeDefault")
388448
defaultClass = info.get("CIAttributeClass")
389-
449+
390450
description = info.get("CIAttributeDescription", "")
451+
filterInputKey = inputKey
391452
inputKey = camelCase(inputKey[5:])
392453
arg = inputKey
393-
454+
394455
if inputKey in toCopy["image"]:
395456
arg += ": Self"
396-
457+
397458
if inputKey in argumentToHint:
398459
arg += argumentToHint[inputKey]
399-
460+
400461
# if filterName == "CIAztecCodeGenerator":
401462
# print(inputKeys)
402463
# print(ciFilterAttributes)
403-
464+
404465
if default is not None:
405-
if isinstance(default, AppKit.CIVector):
466+
if isinstance(default, AppKit.CIVector):
406467
if default.count() == 2:
407468
default = default.X(), default.Y()
408469
arg += ": Point"
@@ -414,22 +475,22 @@ def generateImageObjectCode() -> tuple[str, str]:
414475
default.valueAtIndex_(i) for i in range(default.count())
415476
)
416477
arg += ": tuple"
417-
478+
418479
elif isinstance(default, bool):
419480
arg += ": bool"
420-
481+
421482
elif isinstance(default, (AppKit.NSString, str)):
422483
default = f"'{default}'"
423484
arg += ": str"
424-
485+
425486
elif isinstance(default, AppKit.NSNumber):
426487
default = float(default)
427488
arg += ": float"
428-
429-
elif isinstance(default, AppKit.NSAffineTransform):
489+
490+
elif isinstance(default, AppKit.NSAffineTransform):
430491
default = tuple(default.transformStruct())
431492
arg += ": TransformTuple"
432-
493+
433494
elif isinstance(default, AppKit.CIColor):
434495
default = (
435496
default.red(),
@@ -438,46 +499,45 @@ def generateImageObjectCode() -> tuple[str, str]:
438499
default.alpha(),
439500
)
440501
arg += ": RGBAColorTuple"
441-
502+
442503
elif isinstance(default, AppKit.NSData):
443504
default = None
444505
arg += ": bytes | None"
445-
506+
446507
elif isinstance(default, type(Quartz.CGColorSpaceCreateDeviceCMYK())): # type: ignore
447508
default = None
448-
509+
449510
else:
450511
print(filterName, ciFilterAttributes)
451512
raise ValueError(f"We can't parse this default class of `{inputKey}`: {defaultClass}, {default}, {type(default)}")
452-
513+
453514
arg += f" = {default}"
454-
455-
if filterName in degreesAngleFilterNames:
515+
516+
if filterName in degreesAngleFilterNames and inputKey == "angle":
456517
value = inputKey
457518
else:
458-
value = converters.get(inputKey, inputKey).format(inputKey=inputKey)
519+
value = getConverterValue((inputKey, filterName), inputKey).format(inputKey=inputKey)
459520
docValue = getVariableValue((inputKey, filterName), "a float")
460-
attributes[inputKey] = value
461-
462-
doc.add(f"`{inputKey}` {docValue}. {pythonifyDescription(description)}")
521+
attributes[filterInputKey] = value
522+
523+
doc.add(f"* `{inputKey}` {docValue}. {pythonifyDescription(description)}")
463524
args.append(arg)
464-
465-
525+
466526
match inputKey:
467527
case inputKey if inputKey.endswith("Image"):
468528
value = "sampleImage"
469529
case "gainMap" | "texture" | "mask":
470530
value = "sampleImage"
471531
case "text":
472-
value = "fs"
532+
value = "sampleFormattedString"
473533
case "message":
474534
value = "b'Hello World'"
475535
case "topLeft" | "topRight" | "bottomRight" | "bottomLeft":
476536
value = "(2, 2)"
477537
case _:
478538
value = default
479539
unitTestsArgs.append(f"{inputKey}={value}")
480-
540+
481541
drawBotFilterName = camelCase(filterName[2:])
482542
code.add(
483543
f"def {drawBotFilterName}"
@@ -496,28 +556,29 @@ def generateImageObjectCode() -> tuple[str, str]:
496556
filterDict["isGenerator"] = "True"
497557
if filterName.endswith("CodeGenerator"):
498558
filterDict["fitImage"] = "True"
499-
559+
500560
code.addDict("filterDict", filterDict)
501-
561+
502562
code.add("self._addFilter(filterDict)")
503563
code.dedent()
504564
code.newline()
505-
565+
506566
unitTests.add(f"def test_{drawBotFilterName}(self):")
507567
unitTests.indent()
508-
unitTests.add("img = drawBot.ImageObject()")
568+
unitTests.add("img = drawBot.ImageObject(sourceImagePath)")
509569
unitTests.add(f"img.{drawBotFilterName}({', '.join(unitTestsArgs)})")
570+
unitTests.add("img._applyFilters()")
510571
unitTests.newline()
511572
unitTests.dedent()
512-
573+
513574
imageObjectText = IMAGE_OBJECT_PATH.read_text()
514-
575+
515576
beforeFilters = []
516577
for eachLine in imageObjectText.splitlines():
517578
beforeFilters.append(eachLine)
518579
if eachLine == " # --- filters ---":
519580
break
520-
581+
521582
imageObjectCode = "\n".join(beforeFilters) + "\n" + code.get(indentLevel=1).replace("“", '"').replace("”", '"')
522583
unitTests.footer()
523584
unitTestsCode = unitTests.get()

0 commit comments

Comments
 (0)