Showing
4 changed files
with
179 additions
and
8 deletions
@@ -2,6 +2,7 @@ | @@ -2,6 +2,7 @@ | ||
2 | * Improve PdfPoint and PdfRect | 2 | * Improve PdfPoint and PdfRect |
3 | * Change PdfColor.fromInt to const constructor | 3 | * Change PdfColor.fromInt to const constructor |
4 | * Fix drawShape Bézier curves | 4 | * Fix drawShape Bézier curves |
5 | +* Add arcs to SVG drawShape | ||
5 | 6 | ||
6 | # 1.1.0 | 7 | # 1.1.0 |
7 | * Rename classes to satisfy Dart conventions | 8 | * Rename classes to satisfy Dart conventions |
@@ -20,6 +20,7 @@ library pdf; | @@ -20,6 +20,7 @@ library pdf; | ||
20 | 20 | ||
21 | import 'dart:convert'; | 21 | import 'dart:convert'; |
22 | import 'dart:typed_data'; | 22 | import 'dart:typed_data'; |
23 | +import 'dart:math' as math; | ||
23 | 24 | ||
24 | import 'package:meta/meta.dart'; | 25 | import 'package:meta/meta.dart'; |
25 | import 'package:utf/utf.dart'; | 26 | import 'package:utf/utf.dart'; |
@@ -215,13 +215,150 @@ class PdfGraphics { | @@ -215,13 +215,150 @@ class PdfGraphics { | ||
215 | buf.putString(" m\n"); | 215 | buf.putString(" m\n"); |
216 | } | 216 | } |
217 | 217 | ||
218 | - /// Draw a bézier curve | 218 | + /// Draw a cubic bézier curve from the current point to (x3,y3) |
219 | + /// using (x1,y1) as the control point at the beginning of the curve | ||
220 | + /// and (x2,y2) as the control point at the end of the curve. | ||
221 | + /// | ||
222 | + /// @param x1 first control point | ||
223 | + /// @param y1 first control point | ||
224 | + /// @param x2 second control point | ||
225 | + /// @param y2 second control point | ||
226 | + /// @param x3 end point | ||
227 | + /// @param y3 end point | ||
219 | void curveTo( | 228 | void curveTo( |
220 | double x1, double y1, double x2, double y2, double x3, double y3) { | 229 | double x1, double y1, double x2, double y2, double x3, double y3) { |
221 | buf.putNumList(<double>[x1, y1, x2, y2, x3, y3]); | 230 | buf.putNumList(<double>[x1, y1, x2, y2, x3, y3]); |
222 | buf.putString(" c\n"); | 231 | buf.putString(" c\n"); |
223 | } | 232 | } |
224 | 233 | ||
234 | + double _vectorAngle(double ux, double uy, double vx, double vy) { | ||
235 | + final d = math.sqrt(ux * ux + uy * uy) * math.sqrt(vx * vx + vy * vy); | ||
236 | + if (d == 0.0) return 0.0; | ||
237 | + var c = (ux * vx + uy * vy) / d; | ||
238 | + if (c < -1.0) { | ||
239 | + c = -1.0; | ||
240 | + } else if (c > 1.0) c = 1.0; | ||
241 | + final s = ux * vy - uy * vx; | ||
242 | + c = math.acos(c); | ||
243 | + return c.sign == s.sign ? c : -c; | ||
244 | + } | ||
245 | + | ||
246 | + void _endToCenterParameters(double x1, double y1, double x2, double y2, | ||
247 | + bool large, bool sweep, double rx, double ry) { | ||
248 | + // See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes F.6.5 | ||
249 | + | ||
250 | + rx = rx.abs(); | ||
251 | + ry = ry.abs(); | ||
252 | + | ||
253 | + final x1d = 0.5 * (x1 - x2); | ||
254 | + final y1d = 0.5 * (y1 - y2); | ||
255 | + | ||
256 | + var r = x1d * x1d / (rx * rx) + y1d * y1d / (ry * ry); | ||
257 | + if (r > 1.0) { | ||
258 | + var rr = math.sqrt(r); | ||
259 | + rx *= rr; | ||
260 | + ry *= rr; | ||
261 | + r = x1d * x1d / (rx * rx) + y1d * y1d / (ry * ry); | ||
262 | + } else if (r != 0.0) r = 1.0 / r - 1.0; | ||
263 | + | ||
264 | + if (-1e-10 < r && r < 0.0) r = 0.0; | ||
265 | + | ||
266 | + r = math.sqrt(r); | ||
267 | + if (large == sweep) r = -r; | ||
268 | + | ||
269 | + final cxd = (r * rx * y1d) / ry; | ||
270 | + final cyd = -(r * ry * x1d) / rx; | ||
271 | + | ||
272 | + final cx = cxd + 0.5 * (x1 + x2); | ||
273 | + final cy = cyd + 0.5 * (y1 + y2); | ||
274 | + | ||
275 | + final theta = _vectorAngle(1.0, 0.0, (x1d - cxd) / rx, (y1d - cyd) / ry); | ||
276 | + var dTheta = _vectorAngle((x1d - cxd) / rx, (y1d - cyd) / ry, | ||
277 | + (-x1d - cxd) / rx, (-y1d - cyd) / ry) % | ||
278 | + (math.pi * 2.0); | ||
279 | + if (sweep == false && dTheta > 0.0) | ||
280 | + dTheta -= math.pi * 2.0; | ||
281 | + else if (sweep == true && dTheta < 0.0) dTheta += math.pi * 2.0; | ||
282 | + _bezierArcFromCentre(cx, cy, rx, ry, -theta, -dTheta); | ||
283 | + } | ||
284 | + | ||
285 | + void _bezierArcFromCentre(double cx, double cy, double rx, double ry, | ||
286 | + double startAngle, double extent) { | ||
287 | + int fragmentsCount; | ||
288 | + double fragmentsAngle; | ||
289 | + | ||
290 | + if (extent.abs() <= math.pi / 2.0) { | ||
291 | + fragmentsCount = 1; | ||
292 | + fragmentsAngle = extent; | ||
293 | + } else { | ||
294 | + fragmentsCount = (extent.abs() / (math.pi / 2.0)).ceil().toInt(); | ||
295 | + fragmentsAngle = extent / fragmentsCount.toDouble(); | ||
296 | + } | ||
297 | + if (fragmentsAngle == 0.0) { | ||
298 | + return; | ||
299 | + } | ||
300 | + | ||
301 | + final halfFragment = fragmentsAngle * 0.5; | ||
302 | + var kappa = | ||
303 | + (4.0 / 3.0 * (1.0 - math.cos(halfFragment)) / math.sin(halfFragment)) | ||
304 | + .abs(); | ||
305 | + | ||
306 | + if (fragmentsAngle < 0.0) kappa = -kappa; | ||
307 | + | ||
308 | + var theta = startAngle; | ||
309 | + final startFragment = theta + fragmentsAngle; | ||
310 | + | ||
311 | + var c1 = math.cos(theta); | ||
312 | + var s1 = math.sin(theta); | ||
313 | + for (var i = 0; i < fragmentsCount; i++) { | ||
314 | + final c0 = c1; | ||
315 | + final s0 = s1; | ||
316 | + theta = startFragment + i * fragmentsAngle; | ||
317 | + c1 = math.cos(theta); | ||
318 | + s1 = math.sin(theta); | ||
319 | + curveTo( | ||
320 | + cx + rx * (c0 - kappa * s0), | ||
321 | + cy - ry * (s0 + kappa * c0), | ||
322 | + cx + rx * (c1 + kappa * s1), | ||
323 | + cy - ry * (s1 - kappa * c1), | ||
324 | + cx + rx * c1, | ||
325 | + cy - ry * s1); | ||
326 | + } | ||
327 | + } | ||
328 | + | ||
329 | + /// Draws an elliptical arc from (x1, y1) to (x2, y2). | ||
330 | + /// The size and orientation of the ellipse are defined by two radii (rx, ry) | ||
331 | + /// The center (cx, cy) of the ellipse is calculated automatically to satisfy | ||
332 | + /// the constraints imposed by the other parameters. large and sweep flags | ||
333 | + /// contribute to the automatic calculations and help determine how the arc is drawn. | ||
334 | + void _bezierArc( | ||
335 | + double x1, double y1, double rx, double ry, double x2, double y2, | ||
336 | + {large = false, sweep = false, phi = 0.0}) { | ||
337 | + if (x1 == x2 && y1 == y2) { | ||
338 | + // From https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes: | ||
339 | + // If the endpoints (x1, y1) and (x2, y2) are identical, then this is | ||
340 | + // equivalent to omitting the elliptical arc segment entirely. | ||
341 | + return; | ||
342 | + } | ||
343 | + | ||
344 | + if (rx.abs() <= 1e-10 || ry.abs() <= 1e-10) { | ||
345 | + lineTo(x2, y2); | ||
346 | + return; | ||
347 | + } | ||
348 | + | ||
349 | + if (phi != 0.0) { | ||
350 | + // Our box bézier arcs can't handle rotations directly | ||
351 | + // move to a well known point, eliminate phi and transform the other point | ||
352 | + final mat = Matrix4.identity(); | ||
353 | + mat.translate(-x1, -y1); | ||
354 | + mat.rotateZ(-phi); | ||
355 | + final tr = mat.transform3(Vector3(x2, y2, 0.0)); | ||
356 | + _endToCenterParameters(0.0, 0.0, tr[0], tr[1], large, sweep, rx, ry); | ||
357 | + } else { | ||
358 | + _endToCenterParameters(x1, y1, x2, y2, large, sweep, rx, ry); | ||
359 | + } | ||
360 | + } | ||
361 | + | ||
225 | /// https://github.com/deeplook/svglib/blob/master/svglib/svglib.py#L911 | 362 | /// https://github.com/deeplook/svglib/blob/master/svglib/svglib.py#L911 |
226 | void drawShape(String d, {stroke = true}) { | 363 | void drawShape(String d, {stroke = true}) { |
227 | final exp = RegExp(r"([MmZzLlHhVvCcSsQqTtAaE])|(-[\.0-9]+)|([\.0-9]+)"); | 364 | final exp = RegExp(r"([MmZzLlHhVvCcSsQqTtAaE])|(-[\.0-9]+)|([\.0-9]+)"); |
@@ -348,10 +485,32 @@ class PdfGraphics { | @@ -348,10 +485,32 @@ class PdfGraphics { | ||
348 | // break; | 485 | // break; |
349 | // case 't': // quadratic bezier, relative | 486 | // case 't': // quadratic bezier, relative |
350 | // break; | 487 | // break; |
351 | - // case 'A': // elliptical arc, absolute | ||
352 | - // break; | ||
353 | - // case 'a': // elliptical arc, relative | ||
354 | - // break; | 488 | + case 'A': // elliptical arc, absolute |
489 | + var len = 0; | ||
490 | + while (len < points.length) { | ||
491 | + _bezierArc(lastPoint.x, lastPoint.y, points[len + 0], | ||
492 | + points[len + 1], points[len + 5], points[len + 6], | ||
493 | + phi: points[len + 2] * math.pi / 180.0, | ||
494 | + large: points[len + 3] != 0.0, | ||
495 | + sweep: points[len + 4] != 0.0); | ||
496 | + lastPoint = PdfPoint(points[len + 5], points[len + 6]); | ||
497 | + len += 7; | ||
498 | + } | ||
499 | + break; | ||
500 | + case 'a': // elliptical arc, relative | ||
501 | + var len = 0; | ||
502 | + while (len < points.length) { | ||
503 | + points[len + 5] += lastPoint.x; | ||
504 | + points[len + 6] += lastPoint.y; | ||
505 | + _bezierArc(lastPoint.x, lastPoint.y, points[len + 0], | ||
506 | + points[len + 1], points[len + 5], points[len + 6], | ||
507 | + phi: points[len + 2] * math.pi / 180.0, | ||
508 | + large: points[len + 3] != 0.0, | ||
509 | + sweep: points[len + 4] != 0.0); | ||
510 | + lastPoint = PdfPoint(points[len + 5], points[len + 6]); | ||
511 | + len += 7; | ||
512 | + } | ||
513 | + break; | ||
355 | case 'Z': // close path | 514 | case 'Z': // close path |
356 | case 'z': // close path | 515 | case 'z': // close path |
357 | if (stroke) closePath(); | 516 | if (stroke) closePath(); |
@@ -23,8 +23,8 @@ void main() { | @@ -23,8 +23,8 @@ void main() { | ||
23 | 23 | ||
24 | g.saveContext(); | 24 | g.saveContext(); |
25 | var tm = Matrix4.identity(); | 25 | var tm = Matrix4.identity(); |
26 | - tm.translate(50.0, 290.0); | ||
27 | - tm.rotateZ(pi); | 26 | + tm.translate(10.0, 290.0); |
27 | + tm.scale(1.0, -1.0); | ||
28 | g.setTransform(tm); | 28 | g.setTransform(tm); |
29 | g.setColor(PdfColor(0.0, 0.0, 0.0)); | 29 | g.setColor(PdfColor(0.0, 0.0, 0.0)); |
30 | g.drawShape( | 30 | g.drawShape( |
@@ -33,6 +33,16 @@ void main() { | @@ -33,6 +33,16 @@ void main() { | ||
33 | g.fillPath(); | 33 | g.fillPath(); |
34 | g.restoreContext(); | 34 | g.restoreContext(); |
35 | 35 | ||
36 | + g.saveContext(); | ||
37 | + tm = Matrix4.identity(); | ||
38 | + tm.translate(200.0, 290.0); | ||
39 | + tm.scale(.1, -.1); | ||
40 | + g.setTransform(tm); | ||
41 | + g.setColor(PdfColor(0.0, 0.0, 0.0)); | ||
42 | + g.drawShape( | ||
43 | + "M300,200 h-150 a150,150 0 1,0 150,-150 z M275,175 v-150 a150,150 0 0,0 -150,150 z"); | ||
44 | + g.restoreContext(); | ||
45 | + | ||
36 | var font1 = g.defaultFont; | 46 | var font1 = g.defaultFont; |
37 | 47 | ||
38 | var font2 = PdfTtfFont( | 48 | var font2 = PdfTtfFont( |
@@ -70,7 +80,7 @@ void main() { | @@ -70,7 +80,7 @@ void main() { | ||
70 | g.restoreContext(); | 80 | g.restoreContext(); |
71 | } | 81 | } |
72 | 82 | ||
73 | - var file = File('file.pdf'); | 83 | + var file = File('complex.pdf'); |
74 | file.writeAsBytesSync(pdf.save()); | 84 | file.writeAsBytesSync(pdf.save()); |
75 | }); | 85 | }); |
76 | } | 86 | } |
-
Please register or login to post a comment