diff --git a/pdf/contentstream/matrix.go b/pdf/contentstream/matrix.go index 6997b510..c2fd285d 100644 --- a/pdf/contentstream/matrix.go +++ b/pdf/contentstream/matrix.go @@ -87,11 +87,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 +94,24 @@ 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 { + theta := math.Atan2(-m[1], m[0]) + if theta < 0.0 { + theta += 2 * math.Pi } - common.Log.Debug("ERROR: Angle not a multiple 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/model/geometry_test.go b/pdf/contentstream/matrix_test.go similarity index 98% rename from pdf/model/geometry_test.go rename to pdf/contentstream/matrix_test.go index b06efe5d..bd960d25 100644 --- a/pdf/model/geometry_test.go +++ b/pdf/contentstream/matrix_test.go @@ -3,7 +3,7 @@ * file 'LICENSE.md', which is part of this source code package. */ -package model +package contentstream import ( "math" diff --git a/pdf/contentstream/processor.go b/pdf/contentstream/processor.go index 1c104501..f92dee7c 100644 --- a/pdf/contentstream/processor.go +++ b/pdf/contentstream/processor.go @@ -20,7 +20,7 @@ type GraphicsState struct { ColorspaceNonStroking model.PdfColorspace ColorStroking model.PdfColor ColorNonStroking model.PdfColor - CTM model.Matrix + CTM Matrix } type GraphicStateStack []GraphicsState @@ -211,7 +211,7 @@ func (proc *ContentStreamProcessor) Process(resources *model.PdfPageResources) e proc.graphicsState.ColorspaceNonStroking = model.NewPdfColorspaceDeviceGray() proc.graphicsState.ColorStroking = model.NewPdfColorDeviceGray(0) proc.graphicsState.ColorNonStroking = model.NewPdfColorDeviceGray(0) - proc.graphicsState.CTM = model.IdentityMatrix() + proc.graphicsState.CTM = IdentityMatrix() for _, op := range proc.operations { var err error @@ -563,15 +563,15 @@ func (proc *ContentStreamProcessor) handleCommand_k(op *ContentStreamOperation, func (proc *ContentStreamProcessor) handleCommand_cm(op *ContentStreamOperation, resources *model.PdfPageResources) error { if len(op.Params) != 6 { - common.Log.Debug("Invalid number of parameters for cm: %d", len(op.Params)) - return errors.New("Invalid number of parameters") + common.Log.Debug("ERROR: Invalid number of parameters for cm: %d", len(op.Params)) + return errors.New("invalid number of parameters") } f, err := core.GetNumbersAsFloat(op.Params) if err != nil { return err } - m := model.NewMatrix(f[0], f[1], f[2], f[3], f[4], f[5]) + m := NewMatrix(f[0], f[1], f[2], f[3], f[4], f[5]) proc.graphicsState.CTM.Concat(m) return nil diff --git a/pdf/model/point.go b/pdf/extractor/point.go similarity index 89% rename from pdf/model/point.go rename to pdf/extractor/point.go index 914e4a64..d5caf4c5 100644 --- a/pdf/model/point.go +++ b/pdf/extractor/point.go @@ -7,11 +7,13 @@ // FIXME(peterwilliams97) Change to functional style. i.e. Return new value, don't mutate. -package model +package extractor import ( "fmt" "math" + + "github.com/unidoc/unidoc/pdf/contentstream" ) // Point defines a point (X,Y) in Cartesian coordinates. @@ -32,7 +34,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 := NewMatrix(a, b, c, d, tx, ty) + m := contentstream.NewMatrix(a, b, c, d, tx, ty) p.transformByMatrix(m) } @@ -50,7 +52,7 @@ func (p Point) Rotate(theta float64) Point { } // transformByMatrix transforms `p` by the affine transformation `m`. -func (p *Point) transformByMatrix(m Matrix) { +func (p *Point) transformByMatrix(m contentstream.Matrix) { p.X, p.Y = m.Transform(p.X, p.Y) } diff --git a/pdf/extractor/text.go b/pdf/extractor/text.go index 771e2930..b0f714a3 100644 --- a/pdf/extractor/text.go +++ b/pdf/extractor/text.go @@ -318,7 +318,7 @@ func (to *textObject) setTextMatrix(f []float64) { return } a, b, c, d, tx, ty := f[0], f[1], f[2], f[3], f[4], f[5] - to.Tm = model.NewMatrix(a, b, c, d, tx, ty) + to.Tm = contentstream.NewMatrix(a, b, c, d, tx, ty) to.Tlm = to.Tm } @@ -342,7 +342,7 @@ func (to *textObject) showTextAdjusted(args *core.PdfObjectArray) error { if vertical { dy, dx = dx, dy } - td := translationMatrix(model.Point{X: dx, Y: dy}) + td := translationMatrix(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: @@ -574,9 +574,9 @@ type textObject struct { gs contentstream.GraphicsState fontStack *fontStacker State *textState - Tm model.Matrix // Text matrix. For the character pointer. - Tlm model.Matrix // Text line matrix. For the start of line pointer. - Texts []XYText // Text gets written here. + Tm contentstream.Matrix // Text matrix. For the character pointer. + Tlm contentstream.Matrix // Text line matrix. For the start of line pointer. + Texts []XYText // Text gets written here. } // newTextState returns a default textState. @@ -595,8 +595,8 @@ func newTextObject(e *Extractor, gs contentstream.GraphicsState, state *textStat gs: gs, fontStack: fontStack, State: state, - Tm: model.IdentityMatrix(), - Tlm: model.IdentityMatrix(), + Tm: contentstream.IdentityMatrix(), + Tlm: contentstream.IdentityMatrix(), } } @@ -624,7 +624,7 @@ func (to *textObject) renderText(data []byte) error { spaceWidth := spaceMetrics.Wx * glyphTextRatio common.Log.Trace("spaceWidth=%.2f text=%q font=%s fontSize=%.1f", spaceWidth, runes, font, tfs) - stateMatrix := model.NewMatrix( + stateMatrix := contentstream.NewMatrix( tfs*th, 0, 0, tfs, 0, state.Trise) @@ -658,12 +658,12 @@ func (to *textObject) renderText(data []byte) error { } // c is the character size in unscaled text units. - c := model.Point{X: m.Wx * glyphTextRatio, Y: m.Wy * glyphTextRatio} + c := 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 := model.Point{X: (c.X*tfs + w) * th} - t := model.Point{X: (c.X*tfs + state.Tc + w) * th} + t0 := Point{X: (c.X*tfs + w) * th} + t := 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. @@ -694,14 +694,14 @@ 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) model.Point { +func translation(m contentstream.Matrix) Point { tx, ty := m.Translation() - return model.Point{tx, ty} + return Point{tx, ty} } // translationMatrix returns a matrix that translates by `p`. -func translationMatrix(p model.Point) model.Matrix { - return model.TranslationMatrix(p.X, p.Y) +func translationMatrix(p Point) contentstream.Matrix { + return contentstream.TranslationMatrix(p.X, p.Y) } // moveTo moves the start of line pointer by `tx`,`ty` and sets the text pointer to the @@ -709,26 +709,26 @@ func translationMatrix(p model.Point) model.Matrix { // Move to the start of the next line, offset from the start of the current line by (tx, ty). // `tx` and `ty` are in unscaled text space units. func (to *textObject) moveTo(tx, ty float64) { - to.Tlm = model.NewMatrix(1, 0, 0, 1, tx, ty).Mult(to.Tlm) + to.Tlm = contentstream.NewMatrix(1, 0, 0, 1, tx, ty).Mult(to.Tlm) to.Tm = to.Tlm } // 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 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. + Text string // The text. + Orient int // The text orientation in degrees. This is the current trm rounded to 10°. + 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. } // 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 model.Point, spaceWidth float64) XYText { +func (to *textObject) newXYText(text string, trm contentstream.Matrix, end Point, spaceWidth float64) XYText { to.e.textCount++ theta := trm.Angle() orient := nearestMultiple(theta, 10) diff --git a/pdf/extractor/utils.go b/pdf/extractor/utils.go index 2387de4b..1122cabc 100644 --- a/pdf/extractor/utils.go +++ b/pdf/extractor/utils.go @@ -13,7 +13,6 @@ import ( "github.com/unidoc/unidoc/common/license" "github.com/unidoc/unidoc/pdf/contentstream" "github.com/unidoc/unidoc/pdf/core" - "github.com/unidoc/unidoc/pdf/model" ) // The text rendering mode, Tmode, determines whether showing text shall cause glyph outlines to be @@ -28,15 +27,15 @@ const ( RenderModeClip ) -func toPageCoords(gs contentstream.GraphicsState, objs []core.PdfObject) (model.Point, error) { +func toPageCoords(gs contentstream.GraphicsState, objs []core.PdfObject) (Point, error) { x, y, err := toFloatXY(objs) if err != nil { - return model.Point{}, err + return Point{}, err } return toPagePoint(gs, x, y), nil } -func toPagePointList(gs contentstream.GraphicsState, objs []core.PdfObject) (points []model.Point, err error) { +func toPagePointList(gs contentstream.GraphicsState, objs []core.PdfObject) (points []Point, err error) { if len(objs)%2 != 0 { err = fmt.Errorf("Invalid number of params: %d", len(objs)) common.Log.Debug("toPagePointList: err=%v", err) @@ -53,9 +52,9 @@ func toPagePointList(gs contentstream.GraphicsState, objs []core.PdfObject) (poi return } -func toPagePoint(gs contentstream.GraphicsState, x, y float64) model.Point { +func toPagePoint(gs contentstream.GraphicsState, x, y float64) Point { xp, yp := gs.Transform(x, y) - return model.Point{xp, yp} + return Point{xp, yp} } // toFloatXY returns `objs` as 2 floats, if that's what `objs` is, or an error if it isn't. diff --git a/pdf/model/matrix.go b/pdf/model/matrix.go deleted file mode 100644 index ca552353..00000000 --- a/pdf/model/matrix.go +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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 ( - "fmt" - "math" -) - -// Matrix is a linear transform matrix in homogenous coordinates. -// PDF coordinate transforms are always affine so we only need 6 of these. See newMatrix. -type Matrix [9]float64 - -// IdentityMatrix returns the identity transform. -func IdentityMatrix() Matrix { - return NewMatrix(1, 0, 0, 1, 0, 0) -} - -// TranslationMatrix returns a matrix that translates by `tx`, `ty`. -func TranslationMatrix(tx, ty float64) Matrix { - return NewMatrix(1, 0, 0, 1, tx, ty) -} - -// NewMatrix returns an affine transform matrix laid out in homogenous coordinates as -// a b 0 -// c d 0 -// tx ty 1 -func NewMatrix(a, b, c, d, tx, ty float64) Matrix { - m := Matrix{ - a, b, 0, - c, d, 0, - tx, ty, 1, - } - m.fixup() - return m -} - -// String returns a string describing `m`. -func (m Matrix) String() string { - a, b, c, d, tx, ty := m[0], m[1], m[3], m[4], m[6], m[7] - return fmt.Sprintf("[%.4f,%.4f,%.4f,%.4f:%.4f,%.4f]", a, b, c, d, tx, ty) -} - -// Set sets `m` to affine transform a,b,c,d,tx,ty. -func (m *Matrix) Set(a, b, c, d, tx, ty float64) { - m[0], m[1] = a, b - m[3], m[4] = c, d - m[6], m[7] = tx, ty - m.fixup() -} - -// Concat sets `m` to `m` × `b`. -// `b` needs to be created by newMatrix. i.e. It must be an affine transform. -// m00 m01 0 b00 b01 0 m00*b00 + m01*b01 m00*b10 + m01*b11 0 -// m10 m11 0 × b10 b11 0 = m10*b00 + m11*b01 m10*b10 + m11*b11 0 -// m20 m21 1 b20 b21 1 m20*b00 + m21*b10 + b20 m20*b01 + m21*b11 + b21 1 -func (m *Matrix) Concat(b Matrix) { - *m = Matrix{ - m[0]*b[0] + m[1]*b[3], m[0]*b[1] + m[1]*b[4], 0, - m[3]*b[0] + m[4]*b[3], m[3]*b[1] + m[4]*b[4], 0, - m[6]*b[0] + m[7]*b[3] + b[6], m[6]*b[1] + m[7]*b[4] + b[7], 1, - } - m.fixup() -} - -// Mult returns `m` × `b`. -func (m Matrix) Mult(b Matrix) Matrix { - m.Concat(b) - return m -} - -// Translate appends a translation of `dx`,`dy` to `m`. -// m.Translate(dx, dy) is equivalent to m.Concat(NewMatrix(1, 0, 0, 1, dx, dy)) -func (m *Matrix) Translate(dx, dy float64) { - m[6] += dx - m[7] += dy - m.fixup() -} - -// Translation returns the translation part of `m`. -func (m *Matrix) Translation() (float64, float64) { - return m[6], m[7] -} - -// 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] - yp := x*m[3] + y*m[4] + m[7] - return xp, yp -} - -// ScalingFactorX returns the X scaling of the affine transform. -func (m *Matrix) ScalingFactorX() float64 { - return math.Hypot(m[0], m[1]) -} - -// ScalingFactorY returns the Y scaling of the affine transform. -func (m *Matrix) ScalingFactorY() float64 { - return math.Hypot(m[3], m[4]) -} - -// Angle returns the angle of the affine transform in `m` in degrees. -func (m *Matrix) Angle() float64 { - theta := math.Atan2(-m[1], m[0]) - if theta < 0.0 { - theta += 2 * math.Pi - } - return theta / math.Pi * 180.0 - -} - -// fixup forces `m` to have reasonable values. It is a guard against crazy values in corrupt PDF -// files. -// Currently it clamps elements to [-maxAbsNumber, -maxAbsNumber] to avoid floating point exceptions. -func (m *Matrix) fixup() { - for i, x := range m { - if x > maxAbsNumber { - m[i] = maxAbsNumber - } else if x < -maxAbsNumber { - m[i] = -maxAbsNumber - } - } -} - -// largest numbers needed in PDF transforms. Is this correct? -const maxAbsNumber = 1e9