In my last blog article on converting SVG to PDF, I crassly suggested that my blithe suggestion that interns might undertake doing a conversion SVG to PDF as a side-project was not, quite, hubris. I was wrong, I take it back; that was pure hubris; converting arcs to Bézier curves kicked my a-hem.
And it was totally worth it, now that I made the key insight that made it almost trivial to implement; a conceptual leap over a chasm that was… not so wide as I initially thought. But before I get to that, some background.
Before the Dot Net and Java interface, there was the DLI interface which was a C/C++-based layer on top of APDFL. Its purpose was to facilitate document creation. DLI is still available, even if it is no longer bundled with APDFL by default. Much of its functionality migrated to the Dot Net and Java interface. But for whatever reason, one aspect that was not carried over was its functions for drawing arcs, including elliptical arcs.
PDF as a format does not support drawing arcs; arcs have to be stated in the form of one or more Bézier curves. I’ve looked for a straightforward explanation of how to convert the parameters of an arc to those of a Bézier curve, but I’ve never really found one. Maybe I’ve been looking in all of the wrong places. Long ago, I’d found a paper for how to approximate a circle with four Bézier curves using a constant kappa added and subtracted to the basic circle formula. I eventually restated that in trigonometric functions, assuming right angles, and I later worked out how to make an ellipse. But it wasn’t until I came across Gabi’s article on how to split a Bézier curve into two that I saw a way to draw an arc to an arbitrary angle.
SVG as a format requires supporting arcs. I’m actually rather impressed with how SVG parametrized the arc command. It follows the path command pattern of starting from an implicit point and ending on an explicit point. But it does so without requiring common parameters like an explicit center or a starting angle, or a sweep angle. The specification describes the math for calculating those values in its appendix F.6. That part was just a matter of accurately transcribing the math into code. And checking it twice, about twenty times.
To be fair, as part of tracking down my transcription errors, I incorporated the nomenclature and some tweaks from Mozilla’s SVG parser code.
After that, I was having trouble working the angle of rotation arc command parameter into my control point calculations. And also, adjusting the formula to also work going counter-counter-clockwise. So I took another look at the formulas I was using for calculating the control points of an unrotated ellipse.
If several transformations are combined, the order in which they are applied is significant. For example, first scaling and then translating the x axis is not the same as first translating and then scaling it. In general, to obtain the expected results, transformations should be done in the following order:
3. Scale or skew
And while that is a true statement, an important codicil is that order does not matter if you are multiplying two translation matrices. Or two scaling matrices. Or two rotation matrices (if you hand calculate the matrix multiplication, you will find the formula for the sin and cosine of a sum). So, to my rotation of control points by theta, I can tack on another phi rotation matrix. Done with handling counter-clockwise-drawn rotated elliptical arcs.
The other difficulty was clock-wise-drawn rotated elliptical arcs. Now note that when we start from the unit circle at angle 0, we move clockwise to -90 degrees and our control points are mirrored vertically from when we rotate counter-clockwise to 90 degrees. There is an affine transformation for vertical mirroring, but it’s simpler to pre-calculate the clockwise matrix: [ 1 -k k -1 0 0]
The algorithm then becomes draw n 90 degree segments:
If there is still another segment to draw, then we calculate another 90 degree segment, and split it at the appropriate point. But we can throw away the second half of this last segment as we don’t need it.
I did land my whale:
and was able to render more besides:
The code for this is starting to get a bit less tiny, but I don’t think I’m quite done yet.
If you have any questions, comment below or contact us.