• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sandialabs / toyplot / 9912662049

12 Jul 2024 06:45PM UTC coverage: 94.497% (-0.1%) from 94.629%
9912662049

Pull #214

github

web-flow
Merge cce091bb5 into 93ab5d60c
Pull Request #214: add as_float, change repr(x) -> str(x)

70 of 87 new or added lines in 10 files covered. (80.46%)

31 existing lines in 2 files now uncovered.

5409 of 5724 relevant lines covered (94.5%)

8.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.97
/toyplot/reportlab/__init__.py
1
# Copyright 2014, Sandia Corporation. Under the terms of Contract
2
# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains certain
3
# rights in this software.
4

5
"""Support functions for rendering using ReportLab."""
6

7

8
import base64
9✔
9
import io
9✔
10
import re
9✔
11

12
import numpy
9✔
13
import reportlab.lib.colors
9✔
14
import reportlab.lib.utils
9✔
15

16
import toyplot.color
9✔
17
import toyplot.units
9✔
18
from toyplot.require import as_float
9✔
19

20

21
def render(svg, canvas):
9✔
22
    """Render the SVG representation of a toyplot canvas to a ReportLab canvas.
23

24
    Parameters
25
    ----------
26
    svg: xml.etree.ElementTree.Element
27
      SVG representation of a :class:`toyplot.canvas.Canvas` returned by
28
      :func:`toyplot.svg.render()`.
29

30
    canvas: reportlab.pdfgen.canvas.Canvas
31
      ReportLab canvas that will be used to render the plot.
32
    """
33
    def get_fill(root, style):
9✔
34
        if "fill" not in style:
9✔
35
            return None, None # pragma: no cover
36

37
        gradient_id = re.match("^url[(]#(.*)[)]$", style["fill"])
9✔
38
        if gradient_id:
9✔
39
            gradient_id = gradient_id.group(1)
9✔
40
            gradient_xml = root.find(".//*[@id='%s']" % gradient_id)
9✔
41
            if gradient_xml.tag != "linearGradient":
9✔
42
                raise NotImplementedError("Only linear gradients are implemented.") # pragma: no cover
43
            if gradient_xml.get("gradientUnits") != "userSpaceOnUse":
9✔
44
                raise NotImplementedError("Only userSpaceOnUse gradients are implemented.") # pragma: no cover
45
            return None, gradient_xml
9✔
46

47
        color = toyplot.color.css(style["fill"])
9✔
48
        if color is None:
9✔
49
            return None, None
9✔
50

51
        fill_opacity = as_float(style.get("fill-opacity", 1.0))
9✔
52
        opacity = as_float(style.get("opacity", 1.0))
9✔
53
        fill = toyplot.color.rgba(
9✔
54
            color["r"],
55
            color["g"],
56
            color["b"],
57
            color["a"] * fill_opacity * opacity,
58
            )
59
        return fill, None
9✔
60

61
    def get_stroke(style):
9✔
62
        if "stroke" not in style:
9✔
63
            return None # pragma: no cover
64

65
        color = toyplot.color.css(style["stroke"])
9✔
66
        if color is None:
9✔
67
            return None
9✔
68

69
        stroke_opacity = as_float(style.get("stroke-opacity", 1.0))
9✔
70
        opacity = as_float(style.get("opacity", 1.0))
9✔
71
        return toyplot.color.rgba(
9✔
72
            color["r"],
73
            color["g"],
74
            color["b"],
75
            color["a"] * stroke_opacity * opacity,
76
            )
77

78
    def get_line_cap(style):
9✔
79
        if "stroke-linecap" not in style:
9✔
80
            return 0
9✔
81
        elif style["stroke-linecap"] == "butt":
×
82
            return 0
×
83
        elif style["stroke-linecap"] == "round":
×
84
            return 1
×
85
        elif style["stroke-linecap"] == "square":
×
86
            return 2
×
87

88
    def get_font_family(style):
9✔
89
        if "font-family" not in style:
9✔
90
            return None # pragma: no cover
91

92
        bold = True if style.get("font-weight", "") == "bold" else False
9✔
93
        italic = True if style.get("font-style", "") == "italic" else False
9✔
94
        for font_family in style["font-family"].split(","):
9✔
95
            font_family = font_family.lower()
9✔
96
            if font_family in get_font_family.substitutions:
9✔
97
                font_family = get_font_family.substitutions[font_family]
9✔
98
                return get_font_family.font_table[(font_family, bold, italic)]
9✔
99

100
        raise ValueError("Unknown font family: %s" % style["font-family"]) # pragma: no cover
101

102
    get_font_family.font_table = {
9✔
103
        ("courier", False, False): "Courier",
104
        ("courier", True, False): "Courier-Bold",
105
        ("courier", False, True): "Courier-Oblique",
106
        ("courier", True, True): "Courier-BoldOblique",
107
        ("helvetica", False, False): "Helvetica",
108
        ("helvetica", True, False): "Helvetica-Bold",
109
        ("helvetica", False, True): "Helvetica-Oblique",
110
        ("helvetica", True, True): "Helvetica-BoldOblique",
111
        ("times", False, False): "Times-Roman",
112
        ("times", True, False): "Times-Bold",
113
        ("times", False, True): "Times-Italic",
114
        ("times", True, True): "Times-BoldItalic",
115
        }
116

117
    get_font_family.substitutions = {
9✔
118
        "courier": "courier",
119
        "helvetica": "helvetica",
120
        "monospace": "courier",
121
        "sans-serif": "helvetica",
122
        "serif": "times",
123
        "times": "times",
124
        }
125

126
    def set_fill_color(canvas, color):
9✔
127
        canvas.setFillColorRGB(color["r"], color["g"], color["b"])
9✔
128
        canvas.setFillAlpha(color["a"].item())
9✔
129

130
    def set_stroke_color(canvas, color):
9✔
131
        canvas.setStrokeColorRGB(color["r"], color["g"], color["b"])
9✔
132
        canvas.setStrokeAlpha(color["a"].item())
9✔
133

134
    def render_element(root, element, canvas, styles):
9✔
135
        canvas.saveState()
9✔
136

137
        current_style = {}
9✔
138
        if styles:
9✔
139
            current_style.update(styles[-1])
9✔
140
        for declaration in element.get("style", "").split(";"):
9✔
141
            if declaration == "":
9✔
142
                continue
9✔
143
            key, value = declaration.split(":")
9✔
144
            current_style[key] = value
9✔
145
        styles.append(current_style)
9✔
146

147
        if "stroke-width" in current_style:
9✔
148
            canvas.setLineWidth(as_float(current_style["stroke-width"]))
9✔
149

150
        if "stroke-dasharray" in current_style:
9✔
151
            canvas.setDash([as_float(length) for length in current_style["stroke-dasharray"].split(",")])
9✔
152

153
        if current_style.get("visibility") != "hidden":
9✔
154

155
            if "transform" in element.attrib:
9✔
156
                for transformation in element.get("transform").split(")")[::1]:
9✔
157
                    if transformation:
9✔
158
                        transform, arguments = transformation.split("(")
9✔
159
                        arguments = arguments.split(",")
9✔
160
                        if transform.strip() == "translate":
9✔
161
                            if len(arguments) == 2:
9✔
162
                                canvas.translate(as_float(arguments[0]), as_float(arguments[1]))
9✔
163
                        elif transform.strip() == "rotate":
9✔
164
                            if len(arguments) == 1:
9✔
165
                                canvas.rotate(as_float(arguments[0]))
9✔
166
                            if len(arguments) == 3:
9✔
NEW
167
                                canvas.translate(as_float(arguments[1]), as_float(arguments[2]))
×
NEW
168
                                canvas.rotate(as_float(arguments[0]))
×
NEW
169
                                canvas.translate(-as_float(arguments[1]), -as_float(arguments[2]))
×
170

171
            if element.tag == "svg":
9✔
172
                if "background-color" in current_style:
9✔
173
                    set_fill_color(canvas, toyplot.color.css(current_style["background-color"]))
9✔
174
                    canvas.rect(
9✔
175
                        0,
176
                        0,
177
                        as_float(element.get("width")[:-2]),
178
                        as_float(element.get("height")[:-2]),
179
                        stroke=0,
180
                        fill=1,
181
                        )
182

183
                if current_style["border-style"] != "none":
9✔
184
                    set_stroke_color(canvas, toyplot.color.css(current_style["border-color"]))
9✔
185
                    canvas.setLineWidth(as_float(current_style["border-width"]))
9✔
186
                    canvas.rect(
9✔
187
                        0,
188
                        0,
189
                        as_float(element.get("width")[:-2]),
190
                        as_float(element.get("height")[:-2]),
191
                        stroke=1,
192
                        fill=0,
193
                        )
194

195
                for child in element:
9✔
196
                    render_element(root, child, canvas, styles)
9✔
197

198
            elif element.tag == "a":
9✔
199
                # At the moment, it doesn't look like reportlab supports external hyperlinks.
200
                for child in element:
9✔
201
                    render_element(root, child, canvas, styles)
9✔
202

203
            elif element.tag == "g":
9✔
204
                if element.get("clip-path", None) is not None:
9✔
205
                    clip_id = element.get("clip-path")[5:-1]
9✔
206
                    clip_path = root.find(".//*[@id='%s']" % clip_id)
9✔
207
                    for child in clip_path:
9✔
208
                        if child.tag == "rect":
9✔
209
                            x = as_float(child.get("x"))
9✔
210
                            y = as_float(child.get("y"))
9✔
211
                            width = as_float(child.get("width"))
9✔
212
                            height = as_float(child.get("height"))
9✔
213
                            path = canvas.beginPath()
9✔
214
                            path.moveTo(x, y)
9✔
215
                            path.lineTo(x + width, y)
9✔
216
                            path.lineTo(x + width, y + height)
9✔
217
                            path.lineTo(x, y + height)
9✔
218
                            path.close()
9✔
219
                            canvas.clipPath(path, stroke=0, fill=1)
9✔
220
                        else:
221
                            toyplot.log.error("Unhandled clip tag: %s", child.tag) # pragma: no cover
222

223
                for child in element:
9✔
224
                    render_element(root, child, canvas, styles)
9✔
225

226
            elif element.tag == "clipPath":
9✔
227
                pass
9✔
228

229
            elif element.tag == "line":
9✔
230
                stroke = get_stroke(current_style)
9✔
231
                if stroke is not None:
9✔
232
                    set_stroke_color(canvas, stroke)
9✔
233
                    canvas.setLineCap(get_line_cap(current_style))
9✔
234
                    canvas.line(
9✔
235
                        as_float(element.get("x1", 0)),
236
                        as_float(element.get("y1", 0)),
237
                        as_float(element.get("x2", 0)),
238
                        as_float(element.get("y2", 0)),
239
                        )
240
            elif element.tag == "path":
9✔
241
                stroke = get_stroke(current_style)
9✔
242
                if stroke is not None:
9✔
243
                    set_stroke_color(canvas, stroke)
9✔
244
                    canvas.setLineCap(get_line_cap(current_style))
9✔
245
                    path = canvas.beginPath()
9✔
246
                    commands = element.get("d").split()
9✔
247
                    while commands:
9✔
248
                        command = commands.pop(0)
9✔
249
                        if command == "L":
9✔
250
                            path.lineTo(
9✔
251
                                as_float(commands.pop(0)), as_float(commands.pop(0)))
252
                        elif command == "M":
9✔
253
                            path.moveTo(
9✔
254
                                as_float(commands.pop(0)), as_float(commands.pop(0)))
255
                    canvas.drawPath(path)
9✔
256
            elif element.tag == "polygon":
9✔
257
                fill, fill_gradient = get_fill(root, current_style)
9✔
258
                if fill_gradient is not None:
9✔
259
                    raise NotImplementedError("Gradient <polygon> not implemented.") # pragma: no cover
260
                if fill is not None:
9✔
261
                    set_fill_color(canvas, fill)
9✔
262
                stroke = get_stroke(current_style)
9✔
263
                if stroke is not None:
9✔
264
                    set_stroke_color(canvas, stroke)
9✔
265

266
                points = [point.split(",") for point in element.get("points").split()]
9✔
267
                path = canvas.beginPath()
9✔
268
                for point in points[:1]:
9✔
269
                    path.moveTo(as_float(point[0]), as_float(point[1]))
9✔
270
                for point in points[1:]:
9✔
271
                    path.lineTo(as_float(point[0]), as_float(point[1]))
9✔
272
                path.close()
9✔
273
                canvas.drawPath(path, stroke=stroke is not None, fill=fill is not None)
9✔
274
            elif element.tag == "rect":
9✔
275
                fill, fill_gradient = get_fill(root, current_style)
9✔
276
                if fill is not None:
9✔
277
                    set_fill_color(canvas, fill)
9✔
278
                stroke = get_stroke(current_style)
9✔
279
                if stroke is not None:
9✔
280
                    set_stroke_color(canvas, stroke)
9✔
281

282
                x = as_float(element.get("x", 0))
9✔
283
                y = as_float(element.get("y", 0))
9✔
284
                width = as_float(element.get("width"))
9✔
285
                height = as_float(element.get("height"))
9✔
286

287
                path = canvas.beginPath()
9✔
288
                path.moveTo(x, y)
9✔
289
                path.lineTo(x + width, y)
9✔
290
                path.lineTo(x + width, y + height)
9✔
291
                path.lineTo(x, y + height)
9✔
292
                path.close()
9✔
293

294
                if fill_gradient is not None:
9✔
295
                    pdf_colors = []
9✔
296
                    pdf_offsets = []
9✔
297
                    for stop in fill_gradient:
9✔
298
                        offset = as_float(stop.get("offset"))
9✔
299
                        color = toyplot.color.css(stop.get("stop-color"))
9✔
300
                        opacity = as_float(stop.get("stop-opacity"))
9✔
301
                        pdf_colors.append(reportlab.lib.colors.Color(color["r"], color["g"], color["b"], color["a"] * opacity))
9✔
302
                        pdf_offsets.append(offset)
9✔
303
                    canvas.saveState()
9✔
304
                    canvas.clipPath(path, stroke=0, fill=1)
9✔
305
                    canvas.setFillAlpha(1)
9✔
306
                    canvas.linearGradient(
9✔
307
                        as_float(fill_gradient.get("x1")),
308
                        as_float(fill_gradient.get("y1")),
309
                        as_float(fill_gradient.get("x2")),
310
                        as_float(fill_gradient.get("y2")),
311
                        pdf_colors,
312
                        pdf_offsets,
313
                        )
314
                    canvas.restoreState()
9✔
315

316
                canvas.drawPath(path, stroke=stroke is not None, fill=fill is not None)
9✔
317
            elif element.tag == "circle":
9✔
318
                fill, fill_gradient = get_fill(root, current_style)
9✔
319
                if fill_gradient is not None:
9✔
320
                    raise NotImplementedError("Gradient <circle> not implemented.") # pragma: no cover
321
                if fill is not None:
9✔
322
                    set_fill_color(canvas, fill)
9✔
323
                stroke = get_stroke(current_style)
9✔
324
                if stroke is not None:
9✔
325
                    set_stroke_color(canvas, stroke)
9✔
326

327
                cx = as_float(element.get("cx", 0))
9✔
328
                cy = as_float(element.get("cy", 0))
9✔
329
                r = as_float(element.get("r"))
9✔
330
                canvas.circle(cx, cy, r, stroke=stroke is not None, fill=fill is not None)
9✔
331
            elif element.tag == "text":
9✔
332
                x = as_float(element.get("x", 0))
9✔
333
                y = as_float(element.get("y", 0))
9✔
334
                fill, fill_gradient = get_fill(element, current_style)
9✔
335
                stroke = get_stroke(current_style)
9✔
336
                font_family = get_font_family(current_style)
9✔
337
                font_size = toyplot.units.convert(current_style["font-size"], target="px")
9✔
338
                text = element.text
9✔
339

340
                canvas.saveState()
9✔
341
                canvas.setFont(font_family, font_size)
9✔
342
                if fill is not None:
9✔
343
                    set_fill_color(canvas, fill)
9✔
344
                if stroke is not None:
9✔
345
                    set_stroke_color(canvas, stroke)
×
346
                canvas.translate(x, y)
9✔
347
                canvas.scale(1, -1)
9✔
348
                canvas.drawString(0, 0, text)
9✔
349
                canvas.restoreState()
9✔
350

351
            elif element.tag == "image":
9✔
352
                import PIL.Image
9✔
353
                image = element.get("xlink:href")
9✔
354
                if not image.startswith("data:image/png;base64,"):
9✔
355
                    raise ValueError("Unsupported image type.") # pragma: no cover
356
                image = base64.standard_b64decode(image[22:])
9✔
357
                image = io.BytesIO(image)
9✔
358
                image = PIL.Image.open(image)
9✔
359
                image = reportlab.lib.utils.ImageReader(image)
9✔
360

361
                x = as_float(element.get("x", 0))
9✔
362
                y = as_float(element.get("y", 0))
9✔
363
                width = as_float(element.get("width"))
9✔
364
                height = as_float(element.get("height"))
9✔
365

366
                canvas.saveState()
9✔
367
                path = canvas.beginPath()
9✔
368
                set_fill_color(canvas, toyplot.color.rgb(1, 1, 1))
9✔
369
                canvas.rect(x, y, width, height, stroke=0, fill=1)
9✔
370
                canvas.translate(x, y + height)
9✔
371
                canvas.scale(1, -1)
9✔
372
                canvas.drawImage(image=image, x=0, y=0, width=width, height=height, mask=None)
9✔
373
                canvas.restoreState()
9✔
374

375
            elif element.tag in ["defs", "title"]:
9✔
376
                pass
9✔
377

378
            else:
379
                raise Exception("unhandled tag: %s" % element.tag) # pragma: no cover
380

381
        styles.pop()
9✔
382
        canvas.restoreState()
9✔
383

384
    render_element(svg, svg, canvas, [])
9✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc