diff --git a/pdf/extractor/text.go b/pdf/extractor/text.go index 641a52c3..45b2dc28 100644 --- a/pdf/extractor/text.go +++ b/pdf/extractor/text.go @@ -338,7 +338,7 @@ func (to *textObject) showTextAdjusted(args *core.PdfObjectArray) error { if vertical { dy, dx = dx, dy } - td := translationMatrix(Point{X: dx, Y: dy}) + td := translationMatrix(model.Point{X: dx, Y: dy}) to.Tm = td.Mult(to.Tm) common.Log.Trace("showTextAdjusted: dx,dy=%3f,%.3f Tm=%s", dx, dy, to.Tm) case *core.PdfObjectString: @@ -655,12 +655,12 @@ func (to *textObject) renderText(data []byte) error { } // c is the character size in unscaled text units. - c := Point{X: m.Wx * glyphTextRatio, Y: m.Wy * glyphTextRatio} + c := model.Point{X: m.Wx * glyphTextRatio, Y: m.Wy * glyphTextRatio} // t0 is the end of this character. // t is the displacement of the text cursor when the character is rendered. - t0 := Point{X: (c.X*tfs + w) * th} - t := Point{X: (c.X*tfs + state.Tc + w) * th} + t0 := model.Point{X: (c.X*tfs + w) * th} + t := model.Point{X: (c.X*tfs + state.Tc + w) * th} // td, td0 are t, t0 in matrix form. // td0 is where this character ends. td is where the next character starts. @@ -675,7 +675,6 @@ func (to *textObject) renderText(data []byte) error { string(r), trm, translation(td0.Mult(to.Tm).Mult(to.gs.CTM)), - 1.0*trm.ScalingFactorY(), spaceWidth*trm.ScalingFactorX()) common.Log.Trace("i=%d code=%d xyt=%s trm=%s", i, code, xyt, trm) to.Texts = append(to.Texts, xyt) @@ -692,13 +691,13 @@ func (to *textObject) renderText(data []byte) error { const glyphTextRatio = 1.0 / 1000.0 // translation returns the translation part of `m`. -func translation(m model.Matrix) Point { +func translation(m model.Matrix) model.Point { tx, ty := m.Translation() - return Point{tx, ty} + return model.Point{tx, ty} } // translationMatrix returns a matrix that translates by `p`. -func translationMatrix(p Point) model.Matrix { +func translationMatrix(p model.Point) model.Matrix { return model.TranslationMatrix(p.X, p.Y) } @@ -714,23 +713,24 @@ func (to *textObject) moveTo(tx, ty float64) { // XYText represents text drawn on a page and its position in device coordinates. // All dimensions are in device coordinates. type XYText struct { - Text string // The text. - Orient int // The text orientation. - OrientedStart Point // Left of text in orientation where text is horizontal. - OrientedEnd Point // Right of text in orientation where text is horizontal. - Height float64 // Text height. - SpaceWidth float64 // Best guess at the width of a space in the font the text was rendered with. - count int64 // To help with reading debug logs. + Text string // The text. + Orient int // The text orientation in degrees. This is the current trm rounded to 10°. + OrientedStart model.Point // Left of text in orientation where text is horizontal. + OrientedEnd model.Point // Right of text in orientation where text is horizontal. + Height float64 // Text height. + SpaceWidth float64 // Best guess at the width of a space in the font the text was rendered with. + count int64 // To help with reading debug logs. } // newXYText returns an XYText for text `text` rendered with text rendering matrix `trm` and end // of character device coordinates `end`. `spaceWidth` is our best guess at the width of a space in // the font the text is rendered in device coordinates. -func (to *textObject) newXYText(text string, trm model.Matrix, end Point, - height, spaceWidth float64) XYText { +func (to *textObject) newXYText(text string, trm model.Matrix, end model.Point, spaceWidth float64) XYText { to.e.textCount++ theta := trm.Angle() - if theta%180 == 0 { + orient := nearestMultiple(theta, 10) + var height float64 + if orient%180 != 90 { height = trm.ScalingFactorY() } else { height = trm.ScalingFactorX() @@ -738,7 +738,7 @@ func (to *textObject) newXYText(text string, trm model.Matrix, end Point, return XYText{ Text: text, - Orient: theta, + Orient: orient, OrientedStart: translation(trm).Rotate(theta), OrientedEnd: end.Rotate(theta), Height: height, @@ -747,6 +747,15 @@ func (to *textObject) newXYText(text string, trm model.Matrix, end Point, } } +// nearestMultiple return the multiple of `m` that is closest to `x`. +func nearestMultiple(x float64, m int) int { + if m == 0 { + m = 1 + } + fac := float64(m) + return int(math.Round(x/fac) * fac) +} + // String returns a string describing `t`. func (t XYText) String() string { return fmt.Sprintf("XYText{@%03d [%.3f,%.3f] %.1f %d° %q}", diff --git a/pdf/model/geometry_test.go b/pdf/model/geometry_test.go new file mode 100644 index 00000000..b06efe5d --- /dev/null +++ b/pdf/model/geometry_test.go @@ -0,0 +1,63 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package model + +import ( + "math" + "testing" + + "github.com/unidoc/unidoc/common" +) + +func init() { + common.SetLogger(common.NewConsoleLogger(common.LogLevelDebug)) +} + +// TestAngle tests the Matrix.Angle() function. +func TestAngle(t *testing.T) { + extraTests := []angleCase{} + for theta := 0.01; theta <= 360.0; theta *= 1.1 { + extraTests = append(extraTests, makeAngleCase(2.0, theta)) + } + + const angleTol = 1.0e-10 + + for _, test := range append(angleTests, extraTests...) { + p := test.params + m := NewMatrix(p.a, p.b, p.c, p.d, p.tx, p.ty) + theta := m.Angle() + if math.Abs(theta-test.theta) > angleTol { + t.Fatalf("Bad angle: m=%s expected=%g° actual=%g°", m, test.theta, theta) + } + } +} + +type params struct{ a, b, c, d, tx, ty float64 } +type angleCase struct { + params // Affine transform. + theta float64 // Rotation of affine transform in degrees. +} + +var angleTests = []angleCase{ + {params: params{1, 0, 0, 1, 0, 0}, theta: 0}, + {params: params{0, -1, 1, 0, 0, 0}, theta: 90}, + {params: params{-1, 0, 0, -1, 0, 0}, theta: 180}, + {params: params{0, 1, -1, 0, 0, 0}, theta: 270}, + {params: params{1, -1, 1, 1, 0, 0}, theta: 45}, + {params: params{-1, -1, 1, -1, 0, 0}, theta: 135}, + {params: params{-1, 1, -1, -1, 0, 0}, theta: 225}, + {params: params{1, 1, -1, 1, 0, 0}, theta: 315}, +} + +// makeAngleCase makes an angleCase for a Matrix with scale `r` and angle `theta` degrees. +func makeAngleCase(r, theta float64) angleCase { + radians := theta / 180.0 * math.Pi + a := r * math.Cos(radians) + b := -r * math.Sin(radians) + c := -b + d := a + return angleCase{params{a, b, c, d, 0, 0}, theta} +} diff --git a/pdf/model/matrix.go b/pdf/model/matrix.go index abe80dbc..db02628e 100644 --- a/pdf/model/matrix.go +++ b/pdf/model/matrix.go @@ -8,8 +8,6 @@ package model import ( "fmt" "math" - - "github.com/unidoc/unidoc/common" ) // Matrix is a linear transform matrix in homogenous coordinates. @@ -87,11 +85,6 @@ func (m *Matrix) Translation() (float64, float64) { return m[6], m[7] } -// Translation returns the translation part of `m`. -func (m *Matrix) ScalingX() float64 { - return math.Hypot(m[0], m[1]) -} - // Transform returns coordinates `x`,`y` transformed by `m`. func (m *Matrix) Transform(x, y float64) (float64, float64) { xp := x*m[0] + y*m[1] + m[6] @@ -99,42 +92,25 @@ func (m *Matrix) Transform(x, y float64) (float64, float64) { return xp, yp } -// ScalingFactorX returns X scaling of the affine transform. +// ScalingFactorX returns the X scaling of the affine transform. func (m *Matrix) ScalingFactorX() float64 { - return math.Sqrt(m[0]*m[0] + m[1]*m[1]) + return math.Hypot(m[0], m[1]) } -// ScalingFactorY returns X scaling of the affine transform. +// ScalingFactorY returns the Y scaling of the affine transform. func (m *Matrix) ScalingFactorY() float64 { - return math.Sqrt(m[3]*m[3] + m[4]*m[4]) + return math.Hypot(m[3], m[4]) } -// Angle returns the angle of the affine transform. -// For simplicity, we assume the transform is a multiple of 90 degrees. -func (m *Matrix) Angle() int { - a, b, c, d := m[0], m[1], m[3], m[4] - // We are returning θ for - // a b cos θ -sin θ - // c d = sin θ cos θ - if a > 0 && d > 0 { - // 1 0 - // 0 1 - return 0 - } else if b < 0 && c > 0 { - // 0 1 - // -1 0 - return 90 - } else if a < 0 && d < 0 { - // -1 0 - // 0 -1 - return 180 - } else if b > 0 && c < 0 { - // 0 -1 - // 1 0 - return 270 +// Angle returns the angle of the affine transform in `m` in degrees. +func (m *Matrix) Angle() float64 { + // a, b := m[0], m[1] + theta := math.Atan2(-m[1], m[0]) + if theta < 0.0 { + theta += 2 * math.Pi } - common.Log.Debug("ERROR: Angle not a mulitple of 90°. m=%s", m) - return 0 + return theta / math.Pi * 180.0 + } // fixup forces `m` to have reasonable values. It is a guard against crazy values in corrupt PDF diff --git a/pdf/extractor/point.go b/pdf/model/point.go similarity index 69% rename from pdf/extractor/point.go rename to pdf/model/point.go index 66736232..1f6f95b2 100644 --- a/pdf/extractor/point.go +++ b/pdf/model/point.go @@ -7,13 +7,11 @@ // XXX(peterwilliams97) Change to functional style. i.e. Return new value, don't mutate. -package extractor +package model import ( "fmt" - - "github.com/unidoc/unidoc/common" - "github.com/unidoc/unidoc/pdf/model" + "math" ) // Point defines a point in Cartesian coordinates @@ -34,7 +32,7 @@ func (p *Point) Set(x, y float64) { // Transform transforms `p` by the affine transformation a, b, c, d, tx, ty. func (p *Point) Transform(a, b, c, d, tx, ty float64) { - m := model.NewMatrix(a, b, c, d, tx, ty) + m := NewMatrix(a, b, c, d, tx, ty) p.transformByMatrix(m) } @@ -44,28 +42,19 @@ func (p Point) Displace(delta Point) Point { } // Rotate returns `p` rotated by `theta` degrees. -func (p Point) Rotate(theta int) Point { - switch theta { - case 0: - p.X, p.Y = p.X, p.Y - case 90: - p.X, p.Y = -p.Y, p.X - case 180: - p.X, p.Y = -p.X, -p.Y - case 270: - p.X, p.Y = p.Y, -p.X - default: - common.Log.Debug("ERROR: Unsupported rotation %d", theta) - } - return p +func (p Point) Rotate(theta float64) Point { + radians := theta / 180.0 * math.Pi + r := math.Hypot(p.X, p.Y) + t := math.Atan2(p.Y, p.X) + return Point{r * math.Cos(t+radians), r * math.Sin(t+radians)} } // transformByMatrix transforms `p` by the affine transformation `m`. -func (p *Point) transformByMatrix(m model.Matrix) { +func (p *Point) transformByMatrix(m Matrix) { p.X, p.Y = m.Transform(p.X, p.Y) } // String returns a string describing `p`. -func (p *Point) String() string { +func (p Point) String() string { return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y) }