Skip to content

Commit

Permalink
improved elliptic arc handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cdillond committed May 20, 2024
1 parent 2516d7e commit 73f5955
Show file tree
Hide file tree
Showing 18 changed files with 1,206 additions and 222 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ old/*
*.json
.vscode/*
*/*/test/*
*/test/*
*/test/*
test/*
126 changes: 126 additions & 0 deletions arc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package gdf

import (
"math"
)

func unitCircle(theta float64) (p Point) {
p.Y, p.X = math.Sincos(theta)
return p
}

func circleDxDy(theta float64) (dx, dy float64) {
dx, dy = math.Sincos(theta)
return -dx, dy
}

// Affine transformations of Points

func (p *Point) scale(x, y float64) *Point {
p.X *= x
p.Y *= y
return p
}

func (p *Point) translate(dx, dy float64) *Point {
p.X += dx
p.Y += dy
return p
}

func (p *Point) rotate(theta float64) *Point {
sin, cos := math.Sincos(theta)
x, y := p.X, p.Y
p.X = x*cos - y*sin
p.Y = x*sin + y*cos
return p
}

// Returns the distance between a unit circle segment end point and its Bézier control point for a given theta.
func kappa(theta float64) float64 { return 4. / 3. * math.Tan(theta/4.) }

// basic arc returns the arc from theta to theta+delta iff delta <= pi.
func basicArc(theta, delta float64) (p1, q1, q2, p2 Point) {
p1 = unitCircle(0)
p2 = unitCircle(delta)

k := kappa(delta)

dx, dy := circleDxDy(0)
q1.X = p1.X - dx*k
q1.Y = p1.Y + dy*k

dx, dy = circleDxDy(delta)
q2.X = p2.X - dx*k
q2.Y = p2.Y - dy*k

p1.rotate(theta)
q1.rotate(theta)
q2.rotate(theta)
p2.rotate(theta)
return p1, q1, q2, p2
}

// Arc draws an elliptic arc. If e is an ellipse defined by the parametric
// function y(theta) = cy + ry*sin(theta) and x(theta) = cx + rx*cos(theta),
// then the arc is the segment of the ellipse that begins at e(theta) and
// ends at e(theta+delta). If delta is negative, the arc is drawn clockwise.
func (c *ContentStream) Arc(cx, cy, rx, ry, theta, delta float64) {
c.Arc2(cx, cy, rx, ry, theta, delta, 0, math.Pi/2.)
}

// Circle begins a new path and appends a circle of radius r with a center point of (cx, cy) to the path.
// The new current point becomes (cx + r, cy).
func (c *ContentStream) Circle(cx, cy, r float64) {
c.Arc2(cx, cy, r, r, 0, 2*math.Pi, 0, math.Pi/4.)
}

// Ellipse begins a new path and appends an ellipse with a center point of (cx, cy), an x-radius of rx, and
// a y-radius of ry to the path.
func (c *ContentStream) Ellipse(cx, cy, rx, ry float64) {
c.Arc2(cx, cy, rx, ry, 0, 2*math.Pi, 0, math.Pi/4.)
}

// Arc2 draws an elliptic arc. It is similar to Arc, but includes additional parameters. phi indicates
// an angle relative to the x-axis that the arc is rotated about its center point. step is the maximum
// size in radians of each segment of the arc. This value must be greater than 0 and less than or equal
// to pi. Lower values increase the accuracy of the arc, but can cause significant performance degradations.
// In practice, values of pi/2 and pi/4 are reasonable upper- and lower-bounds. The Arc method uses
// pi/2 by default. Note: the step argument has no effect on arcs with deltas that have an absolute value
// less than the value of step.
func (c *ContentStream) Arc2(cx, cy, rx, ry, theta, delta, phi, step float64) {
var tau = min(math.Abs(step), math.Pi)
if tau <= 0 {
tau = math.Pi / 8.
}
var p1, q1, q2, p2 Point
if delta > 0 {
for beta := 0.0; beta < delta; beta += tau {
p1, q1, q2, p2 = basicArc(theta, min(tau, delta-beta))
p1.scale(rx, ry).rotate(phi).translate(cx, cy)
q1.scale(rx, ry).rotate(phi).translate(cx, cy)
q2.scale(rx, ry).rotate(phi).translate(cx, cy)
p2.scale(rx, ry).rotate(phi).translate(cx, cy)
if beta == 0 {
c.MoveTo(p1.X, p1.Y)
}
c.CubicBezier1(q1.X, q1.Y, q2.X, q2.Y, p2.X, p2.Y)
theta += tau
}

} else if delta < 0 {
for beta := 0.0; beta > delta; beta -= tau {
p1, q1, q2, p2 := basicArc(theta, max(-tau, delta-beta))
p1.scale(rx, ry).rotate(phi).translate(cx, cy)
q1.scale(rx, ry).rotate(phi).translate(cx, cy)
q2.scale(rx, ry).rotate(phi).translate(cx, cy)
p2.scale(rx, ry).rotate(phi).translate(cx, cy)
if beta == 0 {
c.MoveTo(p1.X, p1.Y)
}
c.CubicBezier1(q1.X, q1.Y, q2.X, q2.Y, p2.X, p2.Y)
theta -= tau
}
}

}
2 changes: 1 addition & 1 deletion color.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func (c *ContentStream) SetColorStroke(cl Color) {
c.NColor = cl
}

// Sets c's non-stroking color (NColor) to cl and sets its NColorSpace to cl's ColorSpace.
// Sets c's nonstroking color (NColor) to cl and sets its NColorSpace to cl's ColorSpace.
func (c *ContentStream) SetColor(cl Color) {
switch v := cl.(type) {
case GColor:
Expand Down
6 changes: 3 additions & 3 deletions gs.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ type GS struct {
DashPattern
PathState
CurPt Point // Current path point
NColorSpace ColorSpace // non-stroking
NColorSpace ColorSpace // nonstroking
SColorSpace ColorSpace // stroking
NColor Color // non-stroking
NColor Color // nonstroking
SColor Color // stroking
LineWidth float64
MiterLimit float64
Expand Down Expand Up @@ -169,7 +169,7 @@ func (c *ContentStream) SetFlatness(f float64) {
c.buf = cmdf(c.buf, op_i, f)
}

// SetAlphaConst sets c's non-stroking or stroking alpha constant to a, which must be a value in [0.0, 1.0], where
// SetAlphaConst sets c's nonstroking or stroking alpha constant to a, which must be a value in [0.0, 1.0], where
// 0 corresponds to full transparency and 1 corresponds to full opacity.
func (c *ContentStream) SetAlphaConst(a float64, stroke bool) {
key := "/ca"
Expand Down
2 changes: 1 addition & 1 deletion matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func ScaleBy(scaleX, scaleY float64) Matrix {
func Rotate(theta float64) Matrix {
// [rc rs -rs rc 0 0]
/*
Rotations shall be produced by [rc rs -rs rc 0 0], where rc = cos(q ) and rs = sin(q) which has the
Rotations shall be produced by [rc rs -rs rc 0 0], where rc = cos(q) and rs = sin(q) which has the
effect of rotating the coordinate system axes by an angle q counter clockwise.
*/
return Matrix{math.Cos(theta), math.Sin(theta), -math.Sin(theta), math.Cos(theta), 0, 0}
Expand Down
168 changes: 94 additions & 74 deletions ops.go
Original file line number Diff line number Diff line change
@@ -1,88 +1,108 @@
package gdf

// graphics state ops (Table 56)
const (
// graphics state ops (Table 56
op_q = "q\n"
op_Q = "Q\n"
op_cm = "cm\n"
op_w = "w\n"
op_J = "J\n"
op_j = "j\n"
op_M = "M\n"
op_d = "d\n"
op_ri = "ri\n"
op_i = "i\n"
op_gs = "gs\n"
op_q = "q\n" // push graphics state
op_Q = "Q\n" // pop graphics state
op_cm = "cm\n" // concat matrix
op_w = "w\n" // line width
op_J = "J\n" // line cap
op_j = "j\n" // line join
op_M = "M\n" // miter limit
op_d = "d\n" // dash pattern
op_ri = "ri\n" // rendering intent
op_i = "i\n" // flatness
op_gs = "gs\n" // extended graphics state
)

// path construction ops (Table 58
op_m = "m\n"
op_l = "l\n"
op_c = "c\n"
op_v = "v\n"
op_y = "y\n"
op_h = "h\n"
op_re = "re\n"
// path construction ops (Table 58)
const (
op_m = "m\n" // begin subpath
op_l = "l\n" // append line
op_c = "c\n" // append cubic Bézier curve 1
op_v = "v\n" // append cubic Bézier curve 2
op_y = "y\n" // append cubic Bézier curve 3
op_h = "h\n" // close subpath
op_re = "re\n" // append rectangle
)

// path painting ops (Table 59
op_S = "S\n"
op_s = "s\n"
op_f = "f\n"
op_f_X = "f*\n"
op_B = "B\n"
op_B_X = "B*\n"
op_b = "b\n"
op_b_X = "b*\n"
op_n = "n\n"
// path painting ops (Table 59)
const (
op_S = "S\n" // stroke path
op_s = "s\n" // close and stroke path
op_f = "f\n" // fill path non-zero winding
op_f_X = "f*\n" // fill path even-odd
op_B = "B\n" // fill path non-zero winding and then stroke path
op_B_X = "B*\n" // fill path even-odd and then stroke path
op_b = "b\n" // close path, fill path non-zero winding, and then stroke path
op_b_X = "b*\n" // close path, fill path even-odd, and then stroke path
op_n = "n\n" // end path without filling or stroking
)

// clipping path ops (Table 60
op_W = "W\n"
op_W_X = "W*\n"
// clipping path ops (Table 60)
const (
op_W = "W\n" // intersect the clipping path with the current path using the non-zero winding rule
op_W_X = "W*\n" // intersect the clipping path with the current path using the even-odd rule
)

// color ops (Table 73; upper
op_CS = "CS\n"
op_cs = "cs\n"
op_SC = "SC\n"
op_SCN = "SCN\n"
op_sc = "sc\n"
op_scn = "scn\n"
op_G = "G\n"
op_g = "g\n"
op_RG = "RG\n"
op_rg = "rg\n"
op_K = "K\n"
op_k = "k\n"
// color ops (Table 73)
const (
op_CS = "CS\n" // stroking color space
op_cs = "cs\n" // nonstroking color space
op_SC = "SC\n" // stroking color
op_SCN = "SCN\n" // stroking color with support for additional color spaces
op_sc = "sc\n" // nonstroking color
op_scn = "scn\n" // nonstroking color with support for additional color spaces
op_G = "G\n" // set stroking color to a DeviceGray color
op_g = "g\n" // set nonstroking color to a DeviceGray color
op_RG = "RG\n" // set stroking color to a DeviceRGB color
op_rg = "rg\n" // set nonstroking color to a DeviceRGB color
op_K = "K\n" // set stroking color to a DeviceCMYK color
op_k = "k\n" // set nonstroking color to a DeviceCMYK color
)

// XObject op (Table 86
op_Do = "Do\n"
// XObject op (Table 86)
const (
op_Do = "Do\n" // print XObject
)

// text state ops (Table 103
op_Tc = "Tc\n"
op_Tw = "Tw\n"
op_Tz = "Tz\n"
op_TL = "TL\n"
op_Tf = "Tf\n"
op_Tr = "Tr\n"
op_Ts = "Ts\n"
// text state ops (Table 103)
const (
op_Tc = "Tc\n" // character spacing
op_Tw = "Tw\n" // word spacing
op_Tz = "Tz\n" // horizontal scaling
op_TL = "TL\n" // text leading
op_Tf = "Tf\n" // font size
op_Tr = "Tr\n" // text rendering mode
op_Ts = "Ts\n" // text rise
)

// text object ops (Table 105
op_BT = "BT\n"
op_ET = "ET\n"
// text object ops (Table 105)
const (
op_BT = "BT\n" // begin text object
op_ET = "ET\n" // end text object
)

// text positioning ops (Table 106
op_Td = "Td\n"
op_TD = "TD\n"
op_Tm = "Tm\n"
op_T_X = "T*\n"
// text positioning ops (Table 106)
const (
op_Td = "Td\n" // new line with offset
op_TD = "TD\n" // new line with offset and set leading
op_Tm = "Tm\n" // text matrix
op_T_X = "T*\n" // new line and set leading
)

// text showing ops (Table 107
op_Tj = "Tj\n"
op_APOSTROPHE = "'\n"
op_QUOTE = "\"\n"
op_TJ = "TJ\n"
// text showing ops (Table 107)
const (
op_Tj = "Tj\n" // show text
op_APOSTROPHE = "'\n" // move to new line and show text
op_QUOTE = "\"\n" // move to new line and show text with character and word spacing
op_TJ = "TJ\n" // show text arrays with glyph positioning
)

// marked content operators (Table 352
op_MP = "MP\n"
op_DP = "DP\n"
op_BMC = "BMC\n"
op_EMC = "EMC\n"
// marked content operators (Table 352)
const (
op_MP = "MP\n" // tag marked-content point
op_DP = "DP\n" // tag marked-content point with properties list
op_BMC = "BMC\n" // begin marked-content sequence
op_EMC = "EMC\n" // end marked-content sequence
)
Loading

0 comments on commit 73f5955

Please sign in to comment.