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