David PHAM-VAN

Add arcs to SVG drawShape

@@ -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 }