diff --git a/README.md b/README.md index 1d25b593..3840a6b5 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ __Required packages:__ * [ufo2svg](https://github.com/typesupply/ufo2svg) * [fontTools](https://github.com/fonttools/fonttools) * [PyObjC](https://pythonhosted.org/pyobjc/) (Only if you're not building with the system Python 2.7) +* [PIL](https://github.com/python-pillow/Pillow) (only for running tests) __Compile:__ diff --git a/docs/_themes/drawBotTheme/static/drawBot.css_t b/docs/_themes/drawBotTheme/static/drawBot.css_t index 78a719db..e48d4705 100644 --- a/docs/_themes/drawBotTheme/static/drawBot.css_t +++ b/docs/_themes/drawBotTheme/static/drawBot.css_t @@ -44,6 +44,12 @@ div.downloadlink a.drawbotlink { font-size: 1.2em } +div.example-image { + margin-bottom: 20px; + border: 2px solid {{ theme_headercolor1 }}; + display: inline-block; +} + h1, h2, h3, h4, h5, h6 { font-family: {{ theme_headerfont }}; font-weight: bold; diff --git a/docs/conf.py b/docs/conf.py index 11d778eb..16ede68e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -315,16 +315,28 @@ def __getattr__(cls, name): from sphinx.ext import autodoc from sphinx.writers.html import HTMLTranslator +downloadCodeRoot = os.path.join(os.path.dirname(__file__), "downloads") +if os.path.exists(downloadCodeRoot): + shutil.rmtree(downloadCodeRoot) + +os.mkdir(downloadCodeRoot) + +imageSourceRoot = os.path.join(os.path.dirname(__file__), "..", "tests", "data") + def visit_download_reference(self, node): if node.hasattr('filename'): - data = dict( - urlPath=posixpath.join(self.builder.dlpath, node['filename']), - fileName=node['filename'] + if not node.get("dontShowThisNode"): + data = dict( + urlPath=posixpath.join(self.builder.dlpath, node['filename']), + fileName=node['filename'] ) - self.body.append('' % data) + self.body.append('' % data) + if node.get('imageFileName'): + imageUrl = posixpath.join(self.builder.dlpath, node['imageFileName']) + self.body.append('
' % imageUrl) node.clear() @@ -350,13 +362,6 @@ def run(self): return nodes -downloadCodeRoot = os.path.join(os.path.dirname(__file__), "downloads") -if os.path.exists(downloadCodeRoot): - shutil.rmtree(downloadCodeRoot) - -os.mkdir(downloadCodeRoot) - - class DownloadCode(CodeBlock): def run(self): @@ -383,10 +388,25 @@ def run(self): f = open(path, "w") f.write(code.encode("utf-8")) f.close() + # add example image if present + imageBaseName, _ = os.path.splitext(fileName) + imageFileName = "example_%s.png" % imageBaseName + imagePath = os.path.join(imageSourceRoot, imageFileName) + if os.path.exists(imagePath): + imageDestPath = os.path.join(downloadCodeRoot, imageFileName) + shutil.copy(imagePath, imageDestPath) + else: + imageFileName = "" # add download links node = addnodes.download_reference() node['reftarget'] = "/downloads/" + fileName + node['imageFileName'] = imageFileName nodes.append(node) + if imageFileName: + node = addnodes.download_reference() + node['reftarget'] = "/downloads/" + imageFileName + node['dontShowThisNode'] = True + nodes.append(node) return nodes def checkPath(self, path, sourcePath=None, add=1): diff --git a/docs/content/quickReference.rst b/docs/content/quickReference.rst index 17082b44..968ad5ce 100644 --- a/docs/content/quickReference.rst +++ b/docs/content/quickReference.rst @@ -208,6 +208,6 @@ Quick Reference saveImage(u"~/Desktop/drawBotTest.pdf") saveImage(u"~/Desktop/drawBotTest.png") saveImage(u"~/Desktop/drawBotTest.svg") - saveImage(u"~/Desktop/drawBotTest.mov") + saveImage(u"~/Desktop/drawBotTest.mp4") print "Done" \ No newline at end of file diff --git a/docs/content/shapes/drawingPath.rst b/docs/content/shapes/drawingPath.rst index bba77a9e..4fb14cda 100644 --- a/docs/content/shapes/drawingPath.rst +++ b/docs/content/shapes/drawingPath.rst @@ -11,40 +11,4 @@ Using bezier paths. .. autofunction:: drawBot.arcTo .. autofunction:: drawBot.closePath .. autofunction:: drawBot.drawPath - -.. downloadcode:: path.py - - # create a new empty path - newPath() - # set the first oncurve point - moveTo((100, 100)) - # line to from the previous point to a new point - lineTo((100, 200)) - lineTo((200, 200)) - - # curve to a point with two given handles - curveTo((200, 100), (150, 100), (100, 100)) - - # close the path - closePath() - # draw the path - drawPath() - -.. autofunction:: drawBot.clipPath - -.. downloadcode:: clipPath.py - - # create a bezier path - path = BezierPath() - - # move to a point - path.moveTo((100, 100)) - # line to a point - path.lineTo((100, 200)) - path.lineTo((200, 200)) - # close the path - path.closePath() - # set the path as a clipping path - clipPath(path) - # the oval will be clipped inside the path - oval(100, 100, 100, 100) +.. autofunction:: drawBot.clipPath \ No newline at end of file diff --git a/drawBot/context/baseContext.py b/drawBot/context/baseContext.py index 549e255b..ba83e861 100644 --- a/drawBot/context/baseContext.py +++ b/drawBot/context/baseContext.py @@ -1232,6 +1232,7 @@ def openTypeFeatures(self, *args, **features): .. downloadcode:: openTypeFeaturesFormattedString.py + size(1000, 200) # create an empty formatted string object t = FormattedString() # set a font @@ -1245,7 +1246,7 @@ def openTypeFeatures(self, *args, **features): # add some text t += " 0123456789 Hello" # draw the formatted string - text(t, (10, 100)) + text(t, (10, 80)) """ if args and args[0] is None: self._openTypeFeatures.clear() @@ -1314,11 +1315,12 @@ def indent(self, indent): .. downloadcode:: indent.py # setting up some variables - x, y, w, h = 10, 10, 200, 300 + x, y, w, h = 10, 10, 500, 600 - txtIndent = 50 - txtFirstLineIndent = 70 - txtTailIndent = -50 + txtIndent = 100 + txtFirstLineIndent = 200 + txtTailIndent = -100 + txtFontSize = 22 paragraphTop = 3 paragraphBottom = 10 @@ -1347,7 +1349,7 @@ def indent(self, indent): rect(x, y, w, h) # create a formatted string - t = FormattedString() + t = FormattedString(fontSize=txtFontSize) # set alignment t.align("justified") # add text @@ -1355,7 +1357,7 @@ def indent(self, indent): # add hard return t += "\\n" # set style for indented text - t.fontSize(6) + t.fontSize(txtFontSize*.6) t.paragraphTopSpacing(paragraphTop) t.paragraphBottomSpacing(paragraphBottom) t.firstLineIndent(txtFirstLineIndent) @@ -1366,7 +1368,7 @@ def indent(self, indent): # add hard return t += "\\n" # reset style - t.fontSize(10) + t.fontSize(txtFontSize) t.indent(None) t.tailIndent(None) t.firstLineIndent(None) @@ -1555,16 +1557,17 @@ def appendGlyph(self, *glyphNames): .. downloadcode:: appendGlyphFormattedString.py + size(1000, 400) # create an empty formatted string object t = FormattedString() # set a font t.font("Menlo-Regular") # set a font size - t.fontSize(60) - # add some glyphs - t.appendGlyph("Eng", "Eng.alt") + t.fontSize(300) + # add some glyphs by glyph name + t.appendGlyph("A", "ampersand", "Eng", "Eng.alt") # draw the formatted string - text(t, (10, 100)) + text(t, (100, 100)) """ # use a non breaking space as replacement character baseString = unichr(0x00A0) diff --git a/drawBot/context/tools/imageObject.py b/drawBot/context/tools/imageObject.py index fdfe0543..d25fbe21 100644 --- a/drawBot/context/tools/imageObject.py +++ b/drawBot/context/tools/imageObject.py @@ -6,6 +6,7 @@ from drawBot.misc import DrawBotError, optimizePath from fontTools.misc.py23 import basestring +from drawBot.context.imageContext import _makeBitmapImageRep class ImageObject(object): @@ -69,7 +70,8 @@ def open(self, path): im = AppKit.NSImage.alloc().initByReferencingURL_(url) else: raise DrawBotError("Cannot read image path '%s'." % path) - ciImage = AppKit.CIImage.imageWithData_(im.TIFFRepresentation()) + rep = _makeBitmapImageRep(im) + ciImage = AppKit.CIImage.imageWithData_(rep.TIFFRepresentation()) self._merge(ciImage, doCrop=True) def copy(self): @@ -121,7 +123,8 @@ def unlockFocus(self): # create an image im = AppKit.NSImage.alloc().initWithData_(page.dataRepresentation()) # create an CIImage object - ciImage = AppKit.CIImage.imageWithData_(im.TIFFRepresentation()) + rep = _makeBitmapImageRep(im) + ciImage = AppKit.CIImage.imageWithData_(rep.TIFFRepresentation()) # merge it with the already set data, if there already an image self._merge(ciImage) @@ -198,7 +201,8 @@ def _applyFilters(self): ctx.setImageInterpolation_(AppKit.NSImageInterpolationNone) generator.drawAtPoint_fromRect_operation_fraction_((0, 0), ((0, 0), (w, h)), AppKit.NSCompositeCopy, 1) dummy.unlockFocus() - self._cachedImage = AppKit.CIImage.imageWithData_(dummy.TIFFRepresentation()) + rep = _makeBitmapImageRep(dummy) + self._cachedImage = AppKit.CIImage.imageWithData_(rep.TIFFRepresentation()) del dummy elif hasattr(self, "_cachedImage"): ciFilter.setValue_forKey_(self._cachedImage, "inputImage") diff --git a/drawBot/drawBotDrawingTools.py b/drawBot/drawBotDrawingTools.py index 6f7a970c..7c467c1a 100644 --- a/drawBot/drawBotDrawingTools.py +++ b/drawBot/drawBotDrawingTools.py @@ -59,7 +59,6 @@ def _getmodulecontents(module, names=None): class SavedStateContextManager(object): - """ Internal helper class for DrawBotDrawingTool.savedState() allowing 'with' notation: @@ -515,7 +514,6 @@ def savedState(self): # already returned to the previously saved state # so this will be a black rectangle rect(0, 0, 50, 50) - """ return SavedStateContextManager(self) @@ -529,7 +527,7 @@ def rect(self, x, y, w, h): # draw a rectangle # x y w h - rect(100, 100, 200, 200) + rect(100, 100, 800, 800) """ self._requiresNewFirstPage = True self._addInstruction("rect", x, y, w, h) @@ -542,7 +540,7 @@ def oval(self, x, y, w, h): # draw an oval # x y w h - oval(100, 100, 200, 200) + oval(100, 100, 800, 800) """ self._requiresNewFirstPage = True self._addInstruction("oval", x, y, w, h) @@ -609,6 +607,24 @@ def closePath(self): def drawPath(self, path=None): """ Draw the current path, or draw the provided path. + + .. downloadcode:: drawPath.py + + # create a new empty path + newPath() + # set the first oncurve point + moveTo((100, 100)) + # line to from the previous point to a new point + lineTo((100, 900)) + lineTo((900, 900)) + + # curve to a point with two given handles + curveTo((900, 500), (500, 100), (100, 100)) + + # close the path + closePath() + # draw the path + drawPath() """ if isinstance(path, AppKit.NSBezierPath): path = self._bezierPathClass(path) @@ -621,6 +637,27 @@ def clipPath(self, path=None): """ Use the current path as a clipping path. The clipping path will be used until the canvas gets a `restore()`. + + .. downloadcode:: clipPath.py + + # create a bezier path + path = BezierPath() + # draw a triangle + # move to a point + path.moveTo((100, 100)) + # line to a point + path.lineTo((100, 900)) + path.lineTo((900, 900)) + # close the path + path.closePath() + # save the current state + save() + # set the path as a clipping path + clipPath(path) + # the oval will be clipped inside the path + oval(100, 100, 800, 800) + # restore: this will remove the clip path + restore() """ self._requiresNewFirstPage = True self._addInstruction("clipPath", path) @@ -634,7 +671,7 @@ def line(self, point1, point2): # set a stroke color stroke(0) # draw a line between two given points - line((100, 100), (200, 200)) + line((100, 100), (900, 900)) """ path = self._bezierPathClass() path.moveTo(point1) @@ -650,7 +687,7 @@ def polygon(self, *points, **kwargs): .. downloadcode:: polygon.py # draw a polygon with x-amount of points - polygon((100, 100), (100, 200), (200, 200), (120, 180), close=True) + polygon((100, 100), (100, 900), (900, 900), (200, 800), close=True) """ if len(points) <= 1: raise TypeError("polygon() expects more than a single point") @@ -721,15 +758,15 @@ def blendMode(self, operation): # set a blend mode blendMode("multiply") - # set a color cmykFill(1, 0, 0, 0) # draw a rectangle - rect(10, 10, 100, 100) + rect(10, 10, 600, 600) # set an other color cmykFill(0, 1, 0, 0) # overlap a second rectangle - rect(60, 60, 100, 100) + rect(390, 390, 600, 600) + """ if operation not in self._dummyContext._blendModeMap.keys(): raise DrawBotError("blend mode must be %s" % (", ".join(self._dummyContext._blendModeMap.keys()))) @@ -745,27 +782,27 @@ def fill(self, r=None, g=None, b=None, alpha=1): fill(1, 0, 0, .5) # draw a rect - rect(0, 10, 10, 100) + rect(10, 10, 200, 980) # only set a gray value fill(0) # draw a rect - rect(10, 10, 10, 100) + rect(200, 10, 200, 980) # only set a gray value with an alpha fill(0, .5) # draw a rect - rect(20, 10, 10, 100) + rect(400, 10, 200, 980) # set rgb with no alpha fill(1, 0, 0) # draw a rect - rect(30, 10, 10, 100) + rect(600, 10, 200, 980) # set rgb with an alpha value fill(1, 0, 0, .5) # draw a rect - rect(40, 10, 10, 100) + rect(800, 10, 190, 980) """ self._requiresNewFirstPage = True self._addInstruction("fill", r, g, b, alpha) @@ -779,29 +816,31 @@ def stroke(self, r=None, g=None, b=None, alpha=1): # set the fill to none fill(None) + # set a stroke width stroke(1, 0, 0, .3) + strokeWidth(10) # draw a rect - rect(10, 10, 10, 100) + rect(10, 10, 180, 980) # only set a gray value stroke(0) # draw a rect - rect(30, 10, 10, 100) + rect(210, 10, 180, 980) # only set a gray value with an alpha stroke(0, .5) # draw a rect - rect(50, 10, 10, 100) + rect(410, 10, 180, 980) # set rgb with no alpha stroke(1, 0, 0) # draw a rect - rect(70, 10, 10, 100) + rect(610, 10, 180, 980) # set rgb with an alpha value stroke(1, 0, 0, .5) # draw a rect - rect(90, 10, 10, 100) + rect(810, 10, 180, 980) """ self._requiresNewFirstPage = True self._addInstruction("stroke", r, g, b, alpha) @@ -814,27 +853,18 @@ def cmykFill(self, c, m=None, y=None, k=None, alpha=1): .. downloadcode:: cmykFill.py - x, y = 0, 0 - s = 100 - # cyan cmykFill(1, 0, 0, 0) - rect(x, y, s, s) - x += s - + rect(0, 0, 250, 1000) # magenta cmykFill(0, 1, 0, 0) - rect(x, y, s, s) - x += s - + rect(250, 0, 250, 1000) # yellow cmykFill(0, 0, 1, 0) - rect(x, y, s, s) - x += s - + rect(500, 0, 250, 1000) # black cmykFill(0, 0, 0, 1) - rect(x, y, s, s) + rect(750, 0, 250, 1000) """ self._requiresNewFirstPage = True self._addInstruction("cmykFill", c, m, y, k, alpha) @@ -847,17 +877,22 @@ def cmykStroke(self, c, m=None, y=None, k=None, alpha=1): .. downloadcode:: cmykStroke.py + # define x, y and the amount of lines needed x, y = 20, 20 - lines = 20 - + lines = 49 + # calculate the smallest step colorStep = 1.00 / lines - + # set stroke width strokeWidth(10) - + # start a loop for i in range(lines): + # set a cmyk color + # the magenta value is calculated cmykStroke(0, i * colorStep, 1, 0) - line((x, y), (x, y + 200)) - translate(12, 0) + # draw a line + line((x, y), (x, y + 960)) + # translate the canvas + translate(20, 0) """ self._requiresNewFirstPage = True self._addInstruction("cmykStroke", c, m, y, k, alpha) @@ -871,10 +906,9 @@ def shadow(self, offset, blur=None, color=None): .. downloadcode:: shadow.py # a red shadow with some blur and a offset - shadow((3, 3), 10, (1, 0, 0)) - + shadow((100, 100), 100, (1, 0, 0)) # draw a rect - rect(100, 100, 30, 30) + rect(100, 100, 600, 600) """ if color is None: color = (0, 0, 0) @@ -890,11 +924,10 @@ def cmykShadow(self, offset, blur=None, color=None): .. downloadcode:: cmykShadow.py - # a red shadow with some blur and a offset - cmykShadow((3, 3), 10, (1, 0, 0, 0)) - + # a cyan with some blur and a offset + cmykShadow((100, 100), 100, (1, 0, 0, 0)) # draw a rect - rect(100, 100, 30, 30) + rect(100, 100, 600, 600) """ if color is None: color = (0, 0, 0, 1, 1) @@ -919,12 +952,12 @@ def linearGradient(self, startPoint=None, endPoint=None, colors=None, locations= # set a gradient as the fill color linearGradient( (100, 100), # startPoint - (200, 200), # endPoint + (800, 800), # endPoint [(1, 0, 0), (0, 0, 1), (0, 1, 0)], # colors [0, .2, 1] # locations ) # draw a rectangle - rect(100, 100, 100, 100) + rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True self._addInstruction("linearGradient", startPoint, endPoint, colors, locations) @@ -943,14 +976,14 @@ def cmykLinearGradient(self, startPoint=None, endPoint=None, colors=None, locati .. downloadcode:: cmykLinearGradient.py # set a gradient as the fill color - linearGradient( - (100, 100), # startPoint - (200, 200), # endPoint - [(1, 0, 0, 1), (0, 0, 1, 0), (0, 1, 0, .2)], # colors - [0, .2, 1] # locations + cmykLinearGradient( + (100, 100), # startPoint + (800, 800), # endPoint + [(1, 0, 0, 0), (0, 0, 1, 0), (0, 1, 0, 0)], # colors + [0, .2, 1] # locations ) # draw a rectangle - rect(100, 100, 100, 100) + rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True self._addInstruction("cmykLinearGradient", startPoint, endPoint, colors, locations) @@ -972,15 +1005,15 @@ def radialGradient(self, startPoint=None, endPoint=None, colors=None, locations= # set a gradient as the fill color radialGradient( - (100, 100), # startPoint - (200, 200), # endPoint + (300, 300), # startPoint + (600, 600), # endPoint [(1, 0, 0), (0, 0, 1), (0, 1, 0)], # colors [0, .2, 1], # locations 0, # startRadius - 100 # endRadius + 500 # endRadius ) # draw a rectangle - rect(100, 100, 100, 100) + rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True self._addInstruction("radialGradient", startPoint, endPoint, colors, locations, startRadius, endRadius) @@ -1002,15 +1035,15 @@ def cmykRadialGradient(self, startPoint=None, endPoint=None, colors=None, locati # set a gradient as the fill color cmykRadialGradient( - (100, 100), # startPoint - (200, 200), # endPoint + (300, 300), # startPoint + (600, 600), # endPoint [(1, 0, 0, 1), (0, 0, 1, 0), (0, 1, 0, .2)], # colors [0, .2, 1], # locations 0, # startRadius - 100 # endRadius + 500 # endRadius ) # draw a rectangle - rect(100, 100, 100, 100) + rect(10, 10, 980, 980) """ self._requiresNewFirstPage = True self._addInstruction("cmykRadialGradient", startPoint, endPoint, colors, locations, startRadius, endRadius) @@ -1028,13 +1061,13 @@ def strokeWidth(self, value): # set black as the stroke color stroke(0) # loop over a range of 10 - for i in range(10): + for i in range(20): # in each loop set the stroke width strokeWidth(i) # draw a line - line((100, 100), (200, 200)) + line((100, 100), (200, 900)) # and translate the canvas - translate(15, 0) + translate(30, 0) """ self._requiresNewFirstPage = True self._addInstruction("strokeWidth", value) @@ -1047,29 +1080,23 @@ def miterLimit(self, value): # create a path path = BezierPath() - # move to a point path.moveTo((100, 100)) # line to a point - path.lineTo((100, 200)) - path.lineTo((120, 100)) - + path.lineTo((150, 700)) + path.lineTo((300, 100)) # set stroke color to black stroke(0) # set no fill fill(None) # set the width of the stroke - strokeWidth(10) - + strokeWidth(50) # draw the path drawPath(path) - # move the canvas - translate(100, 0) - + translate(500, 0) # set a miter limit - miterLimit(50) - + miterLimit(5) # draw the same path again drawPath(path) """ @@ -1084,43 +1111,37 @@ def lineJoin(self, value): .. downloadcode:: lineJoin.py - # set no fill - fill(None) # set the stroke color to black stroke(0) + # set no fill + fill(None) # set a stroke width - strokeWidth(10) + strokeWidth(30) # set a miter limit miterLimit(30) - # create a bezier path path = BezierPath() # move to a point path.moveTo((100, 100)) # line to a point - path.lineTo((100, 200)) - path.lineTo((110, 100)) - + path.lineTo((100, 600)) + path.lineTo((160, 100)) # set a line join style lineJoin("miter") # draw the path drawPath(path) # translate the canvas - translate(100, 0) - + translate(300, 0) # set a line join style lineJoin("round") # draw the path drawPath(path) # translate the canvas - translate(100, 0) - + translate(300, 0) # set a line join style lineJoin("bevel") # draw the path drawPath(path) - # translate the canvas - translate(100, 0) """ self._requiresNewFirstPage = True self._addInstruction("lineJoin", value) @@ -1136,29 +1157,25 @@ def lineCap(self, value): # set stroke color to black stroke(0) # set a strok width - strokeWidth(10) - + strokeWidth(50) # translate the canvas - translate(100, 100) - + translate(150, 50) # set a line cap style lineCap("butt") # draw a line - line((0, 30), (0, 200)) - - # rotate the canvas - rotate(-30) + line((0, 200), (0, 800)) + # translate the canvas + translate(300, 0) # set a line cap style lineCap("square") # draw a line - line((0, 30), (0, 200)) - - # rotate the canvase - rotate(-30) + line((0, 200), (0, 800)) + # translate the canvase + translate(300, 0) # set a line cap style lineCap("round") # draw a line - line((0, 30), (0, 200)) + line((0, 200), (0, 800)) """ self._requiresNewFirstPage = True self._addInstruction("lineCap", value) @@ -1173,29 +1190,25 @@ def lineDash(self, *value): # set stroke color to black stroke(0) # set a strok width - strokeWidth(10) - + strokeWidth(50) # translate the canvas - translate(100, 100) - + translate(150, 50) # set a line dash lineDash(2, 2) # draw a line - line((0, 30), (0, 200)) - - # rotate the canvas - rotate(-30) + line((0, 200), (0, 800)) + # translate the canvas + translate(300, 0) # set a line dash lineDash(2, 10, 5, 5) # draw a line - line((0, 30), (0, 200)) - - # rotate the canvase - rotate(-30) + line((0, 200), (0, 800)) + # translate the canvase + translate(300, 0) # reset the line dash lineDash(None) # draw a line - line((0, 30), (0, 200)) + line((0, 200), (0, 800)) """ if not value: raise DrawBotError("lineDash must be a list of dashes or None") @@ -1308,8 +1321,12 @@ def lineHeight(self, value): .. downloadcode:: lineHeight.py - lineHeight(20) - textBox("Hello\\nWorld", (10, 10, 100, 100)) + # set line height + lineHeight(150) + # set font size + fontSize(60) + # draw text in a box + textBox("Hello World " * 10, (100, 100, 800, 800)) """ self._dummyContext.lineHeight(value) self._addInstruction("lineHeight", value) @@ -1320,6 +1337,7 @@ def tracking(self, value): .. downloadcode:: tracking.py + size(1000, 350) # set tracking tracking(100) # set font size @@ -1364,9 +1382,12 @@ def hyphenation(self, value): .. downloadcode:: hyphenation.py txt = '''Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum.''' - + # enable hyphenation hyphenation(True) - textBox(txt, (10, 10, 200, 200)) + # set font size + fontSize(50) + # draw text in a box + textBox(txt, (100, 100, 800, 800)) """ self._dummyContext.hyphenation(value) self._checkLanguageHyphenation() @@ -1407,13 +1428,13 @@ def language(self, language): .. downloadcode:: language.py + size(1000, 600) # a long dutch word word = "paardenkop" # a box where we draw in - box = (10, 10, 100, 100) - + box = (100, 50, 400, 500) # set font size - fontSize(28) + fontSize(118) # enable hyphenation hyphenation(True) # draw the text with no language set @@ -1421,7 +1442,7 @@ def language(self, language): # set language to dutch (nl) language("nl") # shift up a bit - translate(0, 150) + translate(500, 0) # darw the text again with a language set textBox(word, box) """ @@ -1455,16 +1476,17 @@ def openTypeFeatures(self, *args, **features): .. downloadcode:: openTypeFeatures.py + size(1000, 300) # set a font font("ACaslonPro-Regular") # set the font size fontSize(50) # draw a string - text("aa1465", (100, 200)) + text("aabcde1234567890", (100, 200)) # enable some OpenType features - openTypeFeatures(lnum=True, smcp=True) + openTypeFeatures(onum=True, smcp=True) # draw the same string - text("aa1465", (100, 100)) + text("aabcde1234567890", (100, 100)) """ self._dummyContext.openTypeFeatures(*args, **features) self._addInstruction("openTypeFeatures", *args, **features) @@ -1483,6 +1505,7 @@ def fontVariations(self, *args, **axes): .. downloadcode:: fontVariations.py + size(1000, 500) # pick a font font("Skia") # pick a font size @@ -1525,8 +1548,11 @@ def text(self, txt, x, y=None, align=None): .. downloadcode:: text.py - font("Times-Italic") - text("hallo, I'm Times", (100, 100)) + # set a font and font size + font("Times-Italic", 200) + # draw text + text("hallo", (200, 600)) + text("I'm Times", (100, 300)) """ if PY2 and isinstance(txt, basestring): try: @@ -1599,14 +1625,22 @@ def textBox(self, txt, box, align=None): .. downloadcode:: textBox.py - x, y, w, h = 100, 100, 256, 174 - + # a box has an x, y, width and height + x, y, w, h = 100, 100, 800, 800 + # set a fill fill(1, 0, 0) + # draw a rectangle with variables from above rect(x, y, w, h) + # set a diferent fill fill(1) - fontSize(50) + # set a font size + fontSize(200) + # draw text in a text box + # with varibales from above overflow = textBox("hallo, this text is a bit to long", (x, y, w, h), align="center") + # a text box returns text overflow + # text that did not make it into the box print(overflow) The returned overflow can be used to add new pages until all text is set: @@ -1663,7 +1697,7 @@ def textBox(self, txt, box, align=None): # draw the text textBox("%s" % pageCount(), (10, 10, 480, th), align="center") - saveImage("~/Desktop/drawbot.mov") + saveImage("~/Desktop/drawbot.mp4") Another example, this time using a bezierPath as a text envelope: @@ -1749,6 +1783,7 @@ def FormattedString(self, *args, **kwargs): .. downloadcode:: formattedString.py + size(1000, 200) # create a formatted string txt = FormattedString() @@ -1763,7 +1798,7 @@ def FormattedString(self, *args, **kwargs): txt += "hello again" # drawing the formatted string - text(txt, (10, 10)) + text(txt, (10, 30)) # create a formatted string @@ -1774,7 +1809,7 @@ def FormattedString(self, *args, **kwargs): # adding more text with an txt.append("world", font="ACaslonPro-Regular", fontSize=50, openTypeFeatures=dict(smcp=True)) - text(txt, (10, 110)) + text(txt, (10, 150)) .. autoclass:: drawBot.context.baseContext.FormattedString :members: @@ -1977,7 +2012,7 @@ def frameDuration(self, seconds): rect(-50, -50, 50, 50) # save the image as a mov on the desktop - saveImage('~/Desktop/frameDuration.mov') + saveImage('~/Desktop/frameDuration.gif') """ self._requiresNewFirstPage = True self._addInstruction("frameDuration", seconds) @@ -2197,7 +2232,7 @@ def BezierPath(self, path=None, glyphSet=None): # in each loop draw the path drawPath(path) # translate the canvas - translate(5, 5) + translate(50, 50) path.text("Hello world", font="Helvetica", fontSize=30, offset=(210, 210)) @@ -2218,7 +2253,12 @@ def BezierPath(self, path=None, glyphSet=None): print(["contour is closed", "contour is open"][contour.open]) # translate the path - path.translate(0, 300) + path.translate(0, -100) + # draw the path again + drawPath(path) + # translate the path + path.translate(-300, 0) + path.scale(2) # draw the path again drawPath(path) @@ -2236,6 +2276,7 @@ def ImageObject(self, path=None): .. downloadcode:: imageObject.py + size(550, 300) # initiate a new image object im = ImageObject() @@ -2278,7 +2319,7 @@ def Variable(self, variables, workSpace): .. image:: assets/variables.png - .. downloadcode:: variables.py + .. downloadcode:: variablesUI.py # create small ui element for variables in the script @@ -2315,7 +2356,7 @@ def Variable(self, variables, workSpace): # draw some text text("Hello Variable", (w, h)) - .. downloadcode:: vanillaVariables.py + .. downloadcode:: vanillaVariablesUI.py # Variable == vanilla power in DrawBot from AppKit import NSColor diff --git a/tests/data/example_appendGlyphFormattedString.png b/tests/data/example_appendGlyphFormattedString.png new file mode 100644 index 00000000..07fa012a Binary files /dev/null and b/tests/data/example_appendGlyphFormattedString.png differ diff --git a/tests/data/example_bezierPath.png b/tests/data/example_bezierPath.png index 0fb59925..cdfa2bbe 100644 Binary files a/tests/data/example_bezierPath.png and b/tests/data/example_bezierPath.png differ diff --git a/tests/data/example_blendMode.png b/tests/data/example_blendMode.png index 3c61365f..7bb9eaf4 100644 Binary files a/tests/data/example_blendMode.png and b/tests/data/example_blendMode.png differ diff --git a/tests/data/example_clipPath.png b/tests/data/example_clipPath.png new file mode 100644 index 00000000..b9ce4045 Binary files /dev/null and b/tests/data/example_clipPath.png differ diff --git a/tests/data/example_cmykFill.png b/tests/data/example_cmykFill.png index a3b8965e..a2b67e81 100644 Binary files a/tests/data/example_cmykFill.png and b/tests/data/example_cmykFill.png differ diff --git a/tests/data/example_cmykLinearGradient.png b/tests/data/example_cmykLinearGradient.png index 063af577..8d184107 100644 Binary files a/tests/data/example_cmykLinearGradient.png and b/tests/data/example_cmykLinearGradient.png differ diff --git a/tests/data/example_cmykRadialGradient.png b/tests/data/example_cmykRadialGradient.png index b3fdd4da..fee5b4a0 100644 Binary files a/tests/data/example_cmykRadialGradient.png and b/tests/data/example_cmykRadialGradient.png differ diff --git a/tests/data/example_cmykShadow.png b/tests/data/example_cmykShadow.png index 036101a4..53461c1c 100644 Binary files a/tests/data/example_cmykShadow.png and b/tests/data/example_cmykShadow.png differ diff --git a/tests/data/example_cmykStroke.png b/tests/data/example_cmykStroke.png index 230987ad..b6b42a37 100644 Binary files a/tests/data/example_cmykStroke.png and b/tests/data/example_cmykStroke.png differ diff --git a/tests/data/example_drawPath.png b/tests/data/example_drawPath.png new file mode 100644 index 00000000..4610e4cf Binary files /dev/null and b/tests/data/example_drawPath.png differ diff --git a/tests/data/example_fill.png b/tests/data/example_fill.png index 5d27b7d4..136295bd 100644 Binary files a/tests/data/example_fill.png and b/tests/data/example_fill.png differ diff --git a/tests/data/example_fontVariations.png b/tests/data/example_fontVariations.png index c2344828..e78e4c3e 100644 Binary files a/tests/data/example_fontVariations.png and b/tests/data/example_fontVariations.png differ diff --git a/tests/data/example_formattedString.png b/tests/data/example_formattedString.png index df4a985f..d8677efe 100644 Binary files a/tests/data/example_formattedString.png and b/tests/data/example_formattedString.png differ diff --git a/tests/data/example_hyphenation.png b/tests/data/example_hyphenation.png index 63e67816..89a75702 100644 Binary files a/tests/data/example_hyphenation.png and b/tests/data/example_hyphenation.png differ diff --git a/tests/data/example_imageObject.png b/tests/data/example_imageObject.png index e284afbf..4d3bc98a 100644 Binary files a/tests/data/example_imageObject.png and b/tests/data/example_imageObject.png differ diff --git a/tests/data/example_indent.png b/tests/data/example_indent.png new file mode 100644 index 00000000..5486a0d3 Binary files /dev/null and b/tests/data/example_indent.png differ diff --git a/tests/data/example_language.png b/tests/data/example_language.png index acda418e..a0b5a161 100644 Binary files a/tests/data/example_language.png and b/tests/data/example_language.png differ diff --git a/tests/data/example_line.png b/tests/data/example_line.png index f14f98b9..55ced1fb 100644 Binary files a/tests/data/example_line.png and b/tests/data/example_line.png differ diff --git a/tests/data/example_lineCap.png b/tests/data/example_lineCap.png index a15a867d..e27caa13 100644 Binary files a/tests/data/example_lineCap.png and b/tests/data/example_lineCap.png differ diff --git a/tests/data/example_lineDash.png b/tests/data/example_lineDash.png index 31d81d6e..66d97a5d 100644 Binary files a/tests/data/example_lineDash.png and b/tests/data/example_lineDash.png differ diff --git a/tests/data/example_lineHeight.png b/tests/data/example_lineHeight.png index 300ee126..aa12f5c6 100644 Binary files a/tests/data/example_lineHeight.png and b/tests/data/example_lineHeight.png differ diff --git a/tests/data/example_lineJoin.png b/tests/data/example_lineJoin.png index a9aeee2c..26739695 100644 Binary files a/tests/data/example_lineJoin.png and b/tests/data/example_lineJoin.png differ diff --git a/tests/data/example_linearGradient.png b/tests/data/example_linearGradient.png index 063af577..203a4b58 100644 Binary files a/tests/data/example_linearGradient.png and b/tests/data/example_linearGradient.png differ diff --git a/tests/data/example_miterLimit.png b/tests/data/example_miterLimit.png index fc22acba..a79fa9f4 100644 Binary files a/tests/data/example_miterLimit.png and b/tests/data/example_miterLimit.png differ diff --git a/tests/data/example_openTypeFeatures.png b/tests/data/example_openTypeFeatures.png index 8c0ba2f9..18c4dd3b 100644 Binary files a/tests/data/example_openTypeFeatures.png and b/tests/data/example_openTypeFeatures.png differ diff --git a/tests/data/example_openTypeFeaturesFormattedString.png b/tests/data/example_openTypeFeaturesFormattedString.png new file mode 100644 index 00000000..d0cae668 Binary files /dev/null and b/tests/data/example_openTypeFeaturesFormattedString.png differ diff --git a/tests/data/example_oval.png b/tests/data/example_oval.png index 5a156175..6154401a 100644 Binary files a/tests/data/example_oval.png and b/tests/data/example_oval.png differ diff --git a/tests/data/example_polygon.png b/tests/data/example_polygon.png index c9aba128..c6a33f06 100644 Binary files a/tests/data/example_polygon.png and b/tests/data/example_polygon.png differ diff --git a/tests/data/example_radialGradient.png b/tests/data/example_radialGradient.png index 8941a2b6..96c7a6f4 100644 Binary files a/tests/data/example_radialGradient.png and b/tests/data/example_radialGradient.png differ diff --git a/tests/data/example_rect.png b/tests/data/example_rect.png index 853035db..abc5d9e5 100644 Binary files a/tests/data/example_rect.png and b/tests/data/example_rect.png differ diff --git a/tests/data/example_shadow.png b/tests/data/example_shadow.png index 9b3a03db..c7f69dd4 100644 Binary files a/tests/data/example_shadow.png and b/tests/data/example_shadow.png differ diff --git a/tests/data/example_stroke.png b/tests/data/example_stroke.png index 02743894..3dc0bf57 100644 Binary files a/tests/data/example_stroke.png and b/tests/data/example_stroke.png differ diff --git a/tests/data/example_strokeWidth.png b/tests/data/example_strokeWidth.png index 3bc47712..3ff0d09a 100644 Binary files a/tests/data/example_strokeWidth.png and b/tests/data/example_strokeWidth.png differ diff --git a/tests/data/example_variables.png b/tests/data/example_tabsFormattedString.png similarity index 51% rename from tests/data/example_variables.png rename to tests/data/example_tabsFormattedString.png index 1a25e4a7..3a1a6087 100644 Binary files a/tests/data/example_variables.png and b/tests/data/example_tabsFormattedString.png differ diff --git a/tests/data/example_text.png b/tests/data/example_text.png index 315b0744..d5b3135c 100644 Binary files a/tests/data/example_text.png and b/tests/data/example_text.png differ diff --git a/tests/data/example_textBox.png b/tests/data/example_textBox.png index 934127d6..0913b869 100644 Binary files a/tests/data/example_textBox.png and b/tests/data/example_textBox.png differ diff --git a/tests/data/example_tracking.png b/tests/data/example_tracking.png index b08553a0..be691f0a 100644 Binary files a/tests/data/example_tracking.png and b/tests/data/example_tracking.png differ diff --git a/tests/data/example_variablesUI.png b/tests/data/example_variablesUI.png new file mode 100644 index 00000000..484cde19 Binary files /dev/null and b/tests/data/example_variablesUI.png differ diff --git a/tests/data/expected_save1.pdf b/tests/data/expected_save1.pdf new file mode 100644 index 00000000..f35aa1ac Binary files /dev/null and b/tests/data/expected_save1.pdf differ diff --git a/tests/data/expected_save1.png b/tests/data/expected_save1.png new file mode 100644 index 00000000..62097d30 Binary files /dev/null and b/tests/data/expected_save1.png differ diff --git a/tests/data/expected_save1.svg b/tests/data/expected_save1.svg new file mode 100644 index 00000000..64738128 --- /dev/null +++ b/tests/data/expected_save1.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/data/expected_saveCTM.pdf b/tests/data/expected_savedState.pdf similarity index 100% rename from tests/data/expected_saveCTM.pdf rename to tests/data/expected_savedState.pdf diff --git a/tests/data/expected_saveCTM.png b/tests/data/expected_savedState.png similarity index 100% rename from tests/data/expected_saveCTM.png rename to tests/data/expected_savedState.png diff --git a/tests/data/expected_saveCTM.svg b/tests/data/expected_savedState.svg similarity index 100% rename from tests/data/expected_saveCTM.svg rename to tests/data/expected_savedState.svg diff --git a/tests/data/expected_savedState1.pdf b/tests/data/expected_savedState1.pdf new file mode 100644 index 00000000..f35aa1ac Binary files /dev/null and b/tests/data/expected_savedState1.pdf differ diff --git a/tests/data/expected_savedState1.png b/tests/data/expected_savedState1.png new file mode 100644 index 00000000..62097d30 Binary files /dev/null and b/tests/data/expected_savedState1.png differ diff --git a/tests/data/expected_savedState1.svg b/tests/data/expected_savedState1.svg new file mode 100644 index 00000000..64738128 --- /dev/null +++ b/tests/data/expected_savedState1.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/tests/drawBotScripts/save1.py b/tests/drawBotScripts/save1.py new file mode 100644 index 00000000..cfc08f1e --- /dev/null +++ b/tests/drawBotScripts/save1.py @@ -0,0 +1,13 @@ +import drawBot +drawBot.size(300, 300) +drawBot.save() +drawBot.fill(1, 0, 0) +drawBot.translate(150, 150) +drawBot.rect(0, 0, 100, 100) +drawBot.save() +drawBot.rotate(45) +drawBot.fill(0, 1, 0) +drawBot.rect(0, 0, 100, 100) +drawBot.restore() +drawBot.restore() +drawBot.rect(0, 0, 100, 100) diff --git a/tests/drawBotScripts/saveCTM.py b/tests/drawBotScripts/savedState.py similarity index 100% rename from tests/drawBotScripts/saveCTM.py rename to tests/drawBotScripts/savedState.py diff --git a/tests/drawBotScripts/savedState1.py b/tests/drawBotScripts/savedState1.py new file mode 100644 index 00000000..24a1ffd8 --- /dev/null +++ b/tests/drawBotScripts/savedState1.py @@ -0,0 +1,11 @@ +import drawBot +drawBot.size(300, 300) +with drawBot.savedState(): + drawBot.fill(1, 0, 0) + drawBot.translate(150, 150) + drawBot.rect(0, 0, 100, 100) + with drawBot.savedState(): + drawBot.rotate(45) + drawBot.fill(0, 1, 0) + drawBot.rect(0, 0, 100, 100) +drawBot.rect(0, 0, 100, 100) diff --git a/tests/testExamples.py b/tests/testExamples.py index 43fa9ecb..554fc498 100644 --- a/tests/testExamples.py +++ b/tests/testExamples.py @@ -6,9 +6,10 @@ import unittest import re import random +import AppKit import drawBot from drawBot.drawBotDrawingTools import DrawBotDrawingTool -from testSupport import StdOutCollector, randomSeed, testRootDir, tempTestDataDir, testDataDir, readData +from testSupport import StdOutCollector, randomSeed, testRootDir, tempTestDataDir, testDataDir, readData, compareImages _namePattern = re.compile(r"( +).. downloadcode:: ([A-Za-z0-9_]+).py\s*$") @@ -26,45 +27,48 @@ def _dedent(lines): return [line[minIndentation:] for line in lines] -def _collectExamples(module): +def _collectExamples(modules): allExamples = {} names = [] - for n in module.__dict__: - code = None - name = None - indentation = None - if n[0] != "_": - method = getattr(module, n) - if method.__doc__ and "downloadcode" in method.__doc__: - for line in method.__doc__.splitlines() + ["."]: - assert "\t" not in line, (name, repr(line)) - if code is None: - m = _namePattern.match(line) - if m is not None: - name = m.group(2) - # print(name) - names.append(name) - indentation = len(m.group(1)) - code = [] - else: - if not line.strip() and not code: - continue - m = _indentPattern.match(line) - if line.strip() and (m is None or len(m.group(1)) <= indentation): - assert name not in allExamples - allExamples[name] = "\n".join(_dedent(code)) - name = None - code = None - indentation = None + for module in modules: + for n in module.__dict__: + code = None + name = None + indentation = None + if n[0] != "_": + method = getattr(module, n) + if method.__doc__ and "downloadcode" in method.__doc__: + for line in method.__doc__.splitlines() + ["."]: + assert "\t" not in line, (name, repr(line)) + if code is None: + m = _namePattern.match(line) + if m is not None: + name = m.group(2) + # print(name) + names.append(name) + indentation = len(m.group(1)) + code = [] else: - code.append(line) + if not line.strip() and not code: + continue + m = _indentPattern.match(line) + if line.strip() and (m is None or len(m.group(1)) <= indentation): + assert name not in allExamples + allExamples[name] = "\n".join(_dedent(code)) + name = None + code = None + indentation = None + else: + code.append(line) return allExamples - - class ExampleTester(unittest.TestCase): + def assertImagesSimilar(self, path1, path2): + similarity = compareImages(path1, path2) + self.assertLessEqual(similarity, 0.002, "Images %r and %s are not similar enough" % (path1, path2)) + def assertFilesEqual(self, path1, path2): self.assertEqual(readData(path1), readData(path2), "Files %r and %s are not the same" % (path1, path2)) @@ -74,7 +78,10 @@ def assertFilesEqual(self, path1, path2): assert os.path.exists(mockedImagePath) def mockImage(path, position, alpha=1): - drawBot.image(mockedImagePath, position, alpha) + if isinstance(path, DrawBotDrawingTool._imageClass): + drawBot.image(path, position, alpha) + else: + drawBot.image(mockedImagePath, position, alpha) def mockImageSize(path): return drawBot.imageSize(mockedImagePath) @@ -86,7 +93,20 @@ def mockVariable(definitions, namespace): for item in definitions: name = item["name"] args = item.get("args", {}) - value = args.get("value", 50) + value = args.get("value", None) + if value is None: + # no value is set + uiElement = item["ui"] + if uiElement == "ColorWell": + # in case of a color well + # the default color is black nscolor object + value = AppKit.NSColor.blackColor() + elif uiElement == "Checkbox": + # the default is off + value = False + else: + # fallback to slider value + value = 50 namespace[name] = value def mockPrintImage(pdf=None): @@ -106,7 +126,7 @@ def mockRandInt(lo, hi): return int(lo + extent * random.random()) -def _makeTestCase(exampleName, source, doSaveImage): +def _makeTestCase(exampleName, source, doSaveImage, allowFuzzyImageComparison): def test(self): import __future__ @@ -140,20 +160,31 @@ def mockSaveImage(path, **options): expectedImagePath = os.path.join(testDataDir, fileName) if doSaveImage: drawBot.saveImage(imagePath) - self.assertFilesEqual(imagePath, expectedImagePath) + if allowFuzzyImageComparison: + self.assertImagesSimilar(imagePath, expectedImagePath) + else: + self.assertFilesEqual(imagePath, expectedImagePath) return test skip = {} expectedFailures = {} -dontSaveImage = ["test_imageSize"] +dontSaveImage = {"test_imageSize"} +allowFuzzyImageComparison = {"test_imageObject"} def _addExampleTests(): - allExamples = _collectExamples(DrawBotDrawingTool) + allExamples = _collectExamples([ + DrawBotDrawingTool, + DrawBotDrawingTool._formattedStringClass, + DrawBotDrawingTool._bezierPathClass, + DrawBotDrawingTool._imageClass + ]) + for exampleName, source in allExamples.items(): testMethodName = "test_%s" % exampleName - testMethod = _makeTestCase(exampleName, source, doSaveImage=testMethodName not in dontSaveImage) + testMethod = _makeTestCase(exampleName, source, doSaveImage=testMethodName not in dontSaveImage, + allowFuzzyImageComparison=testMethodName in allowFuzzyImageComparison) testMethod.__name__ = testMethodName if testMethodName in skip: continue diff --git a/tests/testSupport.py b/tests/testSupport.py index 0dddd24a..808746ea 100644 --- a/tests/testSupport.py +++ b/tests/testSupport.py @@ -6,6 +6,7 @@ import tempfile import shutil import random +from PIL import Image, ImageChops from drawBot.misc import warnings @@ -118,3 +119,41 @@ def readData(path): """Return the raw data from a path.""" with open(path, "rb") as f: return f.read() + + +def compareImages(path1, path2): + """Compare two image files and return a number representing how similar they are. + A value of 0 means that the images are identical, a value of 1 means they are maximally + different or not comparable (for example, when their dimensions differ). + """ + im1 = Image.open(path1) + im2 = Image.open(path2) + + if im1.size != im2.size: + # Dimensions differ, can't compare further + return 1 + + if im1 == im2: + # Image data is identical (I checked PIL's Image.__eq__ method: it's solid) + return 0 + + # Get the difference between the images + diff = ImageChops.difference(im1, im2) + + # We'll calculate the average difference based on the histogram provided by PIL + hist = diff.histogram() + assert len(hist) == 4 * 256 # Assuming 4x8-bit RGBA for now. TODO: make this work for L and RGB modes + # Sum the histograms of each channel + summedHist = [sum(hist[pixelValue + ch * 256] for ch in range(4)) for pixelValue in range(256)] + + assert len(summedHist) == 256 + assert sum(hist) == sum(summedHist) + # Calculate the average of the difference + # First add all pixel values together + totalSum = sum(summedHist[pixelValue] * pixelValue for pixelValue in range(256)) + # Then divide by the total number of channel values + average = totalSum / sum(summedHist) + # Scale pixel value range from 0-255 to 0-1 + average = average / 255 + assert 0.0 <= average <= 1.0 + return average