Skip to content

Commit 2f61130

Browse files
Merge pull request #23 from BlackFoundryCom/surface-canvas-context
Surface API change: get canvas with context manager
2 parents f819240 + e553a85 commit 2f61130

15 files changed

+356
-170
lines changed

Lib/blackrenderer/backends/base.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ class Surface(ABC):
9999
fileExtension = ".png"
100100

101101
@abstractmethod
102-
def __init__(self, boundingBox):
103-
# boundingBox = (xMin, yMin, xMax, yMax)
102+
def __init__(self):
104103
...
105104

106-
@property
107105
@abstractmethod
108-
def canvas(self):
106+
@contextmanager
107+
def canvas(self, boundingBox):
108+
# boundingBox = (xMin, yMin, xMax, yMax)
109109
...
110110

111111
@abstractmethod

Lib/blackrenderer/backends/cairo.py

+36-21
Original file line numberDiff line numberDiff line change
@@ -193,46 +193,61 @@ def _drawGradient(self, path, gradient, gradientTransform):
193193
class CairoPixelSurface(Surface):
194194
fileExtension = ".png"
195195

196-
def __init__(self, boundingBox):
196+
def __init__(self):
197+
self._surfaces = []
198+
199+
@contextmanager
200+
def canvas(self, boundingBox):
197201
x, y, xMax, yMax = boundingBox
198202
width = xMax - x
199203
height = yMax - y
200-
self.surface = self._setupCairoSurface(width, height)
201-
self.context = cairo.Context(self.surface)
202-
self.context.translate(-x, height + y)
203-
self.context.scale(1, -1)
204-
self._canvas = CairoCanvas(self.context)
204+
surface = self._setupCairoSurface(width, height)
205+
self._surfaces.append((surface, (width, height)))
206+
context = cairo.Context(surface)
207+
context.translate(-x, height + y)
208+
context.scale(1, -1)
209+
yield CairoCanvas(context)
205210

206211
def _setupCairoSurface(self, width, height):
207212
return cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
208213

209-
@property
210-
def canvas(self):
211-
return self._canvas
212-
213214
def saveImage(self, path):
214-
self.surface.flush()
215-
self.surface.write_to_png(os.fspath(path))
216-
self.surface.finish()
215+
surface, _ = self._surfaces[-1]
216+
surface.flush()
217+
surface.write_to_png(os.fspath(path))
218+
surface.finish()
217219

218220

219221
class CairoPDFSurface(CairoPixelSurface):
220222
fileExtension = ".pdf"
221-
_cairoVectorSurfaceClass = cairo.PDFSurface
222223

223224
def _setupCairoSurface(self, width, height):
224-
self.width = width
225-
self.height = height
226225
return cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, (0, 0, width, height))
227226

228227
def saveImage(self, path):
229-
pdfSurface = self._cairoVectorSurfaceClass(path, self.width, self.height)
230-
pdfContext = cairo.Context(pdfSurface)
231-
pdfContext.set_source_surface(self.surface, 0.0, 0.0)
232-
pdfContext.paint()
228+
_, (width, height) = self._surfaces[0]
229+
pdfSurface = cairo.PDFSurface(path, width, height)
230+
pdfContext = None
231+
for surface, (width, height) in self._surfaces:
232+
pdfSurface.set_size(width, height)
233+
if pdfContext is None:
234+
# It's important to call the first set_size() *before*
235+
# the context is created, or we'll get an additional
236+
# empty page
237+
pdfContext = cairo.Context(pdfSurface)
238+
pdfContext.set_source_surface(surface, 0.0, 0.0)
239+
pdfContext.paint()
240+
pdfContext.show_page()
233241
pdfSurface.flush()
234242

235243

236244
class CairoSVGSurface(CairoPDFSurface):
237245
fileExtension = ".svg"
238-
_cairoVectorSurfaceClass = cairo.SVGSurface
246+
247+
def saveImage(self, path):
248+
surface, (width, height) = self._surfaces[-1]
249+
svgSurface = cairo.SVGSurface(path, width, height)
250+
pdfContext = cairo.Context(svgSurface)
251+
pdfContext.set_source_surface(surface, 0.0, 0.0)
252+
pdfContext.paint()
253+
svgSurface.flush()

Lib/blackrenderer/backends/coregraphics.py

+23-18
Original file line numberDiff line numberDiff line change
@@ -218,24 +218,23 @@ def _unpackColorLine(colorLine):
218218
class CoreGraphicsPixelSurface(Surface):
219219
fileExtension = ".png"
220220

221-
def __init__(self, boundingBox):
221+
def __init__(self):
222+
self.context = None
223+
224+
@contextmanager
225+
def canvas(self, boundingBox):
222226
x, y, xMax, yMax = boundingBox
223227
width = xMax - x
224228
height = yMax - y
225-
self.context = self._setupCGContext(x, y, width, height)
226-
self._canvas = CoreGraphicsCanvas(self.context)
229+
self._setupCGContext(x, y, width, height)
230+
yield CoreGraphicsCanvas(self.context)
227231

228232
def _setupCGContext(self, x, y, width, height):
229233
rgbColorSpace = CG.CGColorSpaceCreateDeviceRGB()
230-
context = CG.CGBitmapContextCreate(
234+
self.context = CG.CGBitmapContextCreate(
231235
None, width, height, 8, 0, rgbColorSpace, CG.kCGImageAlphaPremultipliedFirst
232236
)
233-
CG.CGContextTranslateCTM(context, -x, -y)
234-
return context
235-
236-
@property
237-
def canvas(self):
238-
return self._canvas
237+
CG.CGContextTranslateCTM(self.context, -x, -y)
239238

240239
def saveImage(self, path):
241240
image = CG.CGBitmapContextCreateImage(self.context)
@@ -245,19 +244,25 @@ def saveImage(self, path):
245244
class CoreGraphicsPDFSurface(CoreGraphicsPixelSurface):
246245
fileExtension = ".pdf"
247246

247+
@contextmanager
248+
def canvas(self, boundingBox):
249+
with super().canvas(boundingBox) as canvas:
250+
CG.CGContextBeginPage(self.context, self._mediaBox)
251+
yield canvas
252+
CG.CGContextEndPage(self.context)
253+
248254
def _setupCGContext(self, x, y, width, height):
249-
mediaBox = ((x, y), (width, height))
250-
self.data = CFDataCreateMutable(None, 0)
251-
consumer = CG.CGDataConsumerCreateWithCFData(self.data)
252-
context = CG.CGPDFContextCreate(consumer, mediaBox, None)
253-
CG.CGContextBeginPage(context, mediaBox)
254-
return context
255+
if self.context is None:
256+
self._mediaBox = ((x, y), (width, height))
257+
self._data = CFDataCreateMutable(None, 0)
258+
consumer = CG.CGDataConsumerCreateWithCFData(self._data)
259+
self.context = CG.CGPDFContextCreate(consumer, self._mediaBox, None)
260+
return self.context
255261

256262
def saveImage(self, path):
257-
CG.CGContextEndPage(self.context)
258263
CG.CGPDFContextClose(self.context)
259264
with open(path, "wb") as f:
260-
f.write(self.data)
265+
f.write(self._data)
261266

262267

263268
def saveImageAsPNG(image, path):

Lib/blackrenderer/backends/skia.py

+35-25
Original file line numberDiff line numberDiff line change
@@ -180,56 +180,66 @@ def _unpackColorLine(colorLine):
180180
return colors, stops
181181

182182

183-
class SkiaPixelSurface(Surface):
184-
fileExtension = ".png"
185-
186-
def __init__(self, boundingBox):
183+
class _SkiaBaseSurface(Surface):
184+
@contextmanager
185+
def canvas(self, boundingBox):
187186
x, y, xMax, yMax = boundingBox
188187
width = xMax - x
189188
height = yMax - y
190-
skCanvas = self._setupSkCanvas(x, y, width, height)
189+
skCanvas, surfaceData = self._setupSkCanvas(x, y, width, height)
191190
skCanvas.translate(-x, height + y)
192191
skCanvas.scale(1, -1)
193-
self._canvas = SkiaCanvas(skCanvas)
192+
yield SkiaCanvas(skCanvas)
193+
self._finalizeCanvas(surfaceData)
194+
195+
196+
class SkiaPixelSurface(_SkiaBaseSurface):
197+
fileExtension = ".png"
198+
199+
def __init__(self):
200+
self._image = None
194201

195202
def _setupSkCanvas(self, x, y, width, height):
196-
self.surface = skia.Surface(width, height)
197-
return self.surface.getCanvas()
203+
surface = skia.Surface(width, height)
204+
return surface.getCanvas(), surface
198205

199-
@property
200-
def canvas(self):
201-
return self._canvas
206+
def _finalizeCanvas(self, surface):
207+
self._image = surface.makeImageSnapshot()
202208

203209
def saveImage(self, path, format=skia.kPNG):
204-
image = self.surface.makeImageSnapshot()
205-
image.save(os.fspath(path), format)
210+
self._image.save(os.fspath(path), format)
206211

207212

208-
class SkiaPDFSurface(SkiaPixelSurface):
213+
class SkiaPDFSurface(_SkiaBaseSurface):
209214
fileExtension = ".pdf"
210215

216+
def __init__(self):
217+
self._pictures = []
218+
211219
def _setupSkCanvas(self, x, y, width, height):
212-
self.recorder = skia.PictureRecorder()
213-
return self.recorder.beginRecording(width, height)
220+
recorder = skia.PictureRecorder()
221+
return recorder.beginRecording(width, height), recorder
222+
223+
def _finalizeCanvas(self, recorder):
224+
self._pictures.append(recorder.finishRecordingAsPicture())
214225

215226
def saveImage(self, path):
216227
stream = skia.FILEWStream(os.fspath(path))
217-
picture = self.recorder.finishRecordingAsPicture()
218-
self._drawPictureToStream(picture, stream)
219-
220-
def _drawPictureToStream(self, picture, stream):
221228
with skia.PDF.MakeDocument(stream) as document:
222-
x, y, width, height = picture.cullRect()
223-
assert x == 0 and y == 0
224-
with document.page(width, height) as canvas:
225-
canvas.drawPicture(picture)
229+
for picture in self._pictures:
230+
x, y, width, height = picture.cullRect()
231+
assert x == 0 and y == 0
232+
with document.page(width, height) as canvas:
233+
canvas.drawPicture(picture)
226234
stream.flush()
227235

228236

229237
class SkiaSVGSurface(SkiaPDFSurface):
230238
fileExtension = ".svg"
231239

232-
def _drawPictureToStream(self, picture, stream):
240+
def saveImage(self, path):
241+
stream = skia.FILEWStream(os.fspath(path))
242+
picture = self._pictures[-1]
233243
canvas = skia.SVGCanvas.Make(picture.cullRect(), stream)
234244
canvas.drawPicture(picture)
235245
del canvas # hand holding skia-python with GC: it needs to go before stream

Lib/blackrenderer/backends/svg.py

+59-60
Original file line numberDiff line numberDiff line change
@@ -219,74 +219,73 @@ def _gradientToSVG(
219219
class SVGSurface(Surface):
220220
fileExtension = ".svg"
221221

222-
def __init__(self, boundingBox):
222+
def __init__(self):
223+
self._svgElements = None
224+
225+
@contextmanager
226+
def canvas(self, boundingBox):
223227
x, y, xMax, yMax = boundingBox
224228
width = xMax - x
225229
height = yMax - y
226-
self.viewBox = x, y, width, height
230+
self._viewBox = x, y, width, height
227231
transform = Transform(1, 0, 0, -1, 0, height + 2 * y)
228-
self._canvas = SVGCanvas(transform)
232+
canvas = SVGCanvas(transform)
233+
yield canvas
234+
self._svgElements = canvas.elements
235+
236+
def saveImage(self, path):
237+
with open(path, "wb") as f:
238+
writeSVGElements(self._svgElements, self._viewBox, f)
239+
240+
241+
def writeSVGElements(elements, viewBox, stream):
242+
clipPaths = {}
243+
gradients = {}
244+
for fillPath, fillT, clipPath, clipT, paint, paintT in elements:
245+
clipKey = clipPath, clipT
246+
if clipPath is not None and clipKey not in clipPaths:
247+
clipPaths[clipKey] = f"clip_{len(clipPaths)}"
248+
gradientKey = paint, paintT
249+
if not isinstance(paint, RGBAPaint) and gradientKey not in gradients:
250+
gradients[gradientKey] = f"gradient_{len(gradients)}"
251+
252+
root = ET.Element(
253+
"svg",
254+
width=formatNumber(viewBox[2]),
255+
height=formatNumber(viewBox[3]),
256+
preserveAspectRatio="xMinYMin slice",
257+
viewBox=" ".join(formatNumber(n) for n in viewBox),
258+
version="1.1",
259+
xmlns="http://www.w3.org/2000/svg",
260+
)
229261

230-
@property
231-
def canvas(self):
232-
return self._canvas
262+
# root.attrib["xmlns:link"] = "http://www.w3.org/1999/xlink"
233263

234-
def saveImage(self, pathOrFile):
235-
if hasattr(pathOrFile, "write"):
236-
self.writeSVG(pathOrFile)
237-
else:
238-
with open(pathOrFile, "wb") as f:
239-
self.writeSVG(f)
240-
241-
def writeSVG(self, stream):
242-
elements = self.canvas.elements
243-
clipPaths = {}
244-
gradients = {}
245-
for fillPath, fillT, clipPath, clipT, paint, paintT in elements:
246-
clipKey = clipPath, clipT
247-
if clipPath is not None and clipKey not in clipPaths:
248-
clipPaths[clipKey] = f"clip_{len(clipPaths)}"
249-
gradientKey = paint, paintT
250-
if not isinstance(paint, RGBAPaint) and gradientKey not in gradients:
251-
gradients[gradientKey] = f"gradient_{len(gradients)}"
252-
253-
root = ET.Element(
254-
"svg",
255-
width=formatNumber(self.viewBox[2]),
256-
height=formatNumber(self.viewBox[3]),
257-
preserveAspectRatio="xMinYMin slice",
258-
viewBox=" ".join(formatNumber(n) for n in self.viewBox),
259-
version="1.1",
260-
xmlns="http://www.w3.org/2000/svg",
264+
if gradients:
265+
defs = ET.SubElement(root, "defs")
266+
for (gradient, gradientTransform), gradientID in gradients.items():
267+
defs.append(gradient.toSVG(gradientID, gradientTransform))
268+
269+
for (clipPath, clipTransform), clipID in clipPaths.items():
270+
clipElement = ET.SubElement(root, "clipPath", id=clipID)
271+
ET.SubElement(
272+
clipElement, "path", d=clipPath, transform=formatMatrix(clipTransform)
261273
)
262274

263-
# root.attrib["xmlns:link"] = "http://www.w3.org/1999/xlink"
264-
265-
if gradients:
266-
defs = ET.SubElement(root, "defs")
267-
for (gradient, gradientTransform), gradientID in gradients.items():
268-
defs.append(gradient.toSVG(gradientID, gradientTransform))
269-
270-
for (clipPath, clipTransform), clipID in clipPaths.items():
271-
clipElement = ET.SubElement(root, "clipPath", id=clipID)
272-
ET.SubElement(
273-
clipElement, "path", d=clipPath, transform=formatMatrix(clipTransform)
274-
)
275-
276-
for fillPath, fillT, clipPath, clipT, paint, paintT in elements:
277-
attrs = [("d", fillPath)]
278-
if isinstance(paint, RGBAPaint):
279-
attrs += colorToSVGAttrs(paint)
280-
else:
281-
attrs.append(("fill", f"url(#{gradients[paint, paintT]})"))
282-
attrs.append(("transform", formatMatrix(fillT)))
283-
if clipPath is not None:
284-
clipKey = clipPath, clipTransform
285-
attrs.append(("clip-path", f"url(#{clipPaths[clipKey]})"))
286-
ET.SubElement(root, "path", dict(attrs))
287-
288-
tree = ET.ElementTree(root)
289-
tree.write(stream, pretty_print=True, xml_declaration=True)
275+
for fillPath, fillT, clipPath, clipT, paint, paintT in elements:
276+
attrs = [("d", fillPath)]
277+
if isinstance(paint, RGBAPaint):
278+
attrs += colorToSVGAttrs(paint)
279+
else:
280+
attrs.append(("fill", f"url(#{gradients[paint, paintT]})"))
281+
attrs.append(("transform", formatMatrix(fillT)))
282+
if clipPath is not None:
283+
clipKey = clipPath, clipTransform
284+
attrs.append(("clip-path", f"url(#{clipPaths[clipKey]})"))
285+
ET.SubElement(root, "path", dict(attrs))
286+
287+
tree = ET.ElementTree(root)
288+
tree.write(stream, pretty_print=True, xml_declaration=True)
290289

291290

292291
def formatCoord(pt):

0 commit comments

Comments
 (0)