@@ -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+
5254class 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
103110variableValues = {
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+
225241argumentToHint = {"text" : ": FormattedString" , "message" : ": str" }
226242
227243toCopy = {
@@ -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
299326ignoreInputKeys = ["inputImage" ]
300327
301- generators = list (AppKit .CIFilter .filterNamesInCategory_ ("CICategoryGenerator" ))
328+ generators = list (AppKit .CIFilter .filterNamesInCategory_ ("CICategoryGenerator" ))
302329generators .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
314341excludeFilterNames = [
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
328388degreesAngleFilterNames = ["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