mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-30 13:48:51 +08:00
Made Matrix and Point structs more general and moved them to their own files in pdf/model.
This commit is contained in:
parent
da8544e68b
commit
7bbcec65fa
@ -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}",
|
||||
|
63
pdf/model/geometry_test.go
Normal file
63
pdf/model/geometry_test.go
Normal file
@ -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}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user