From da8544e68b2c5b4d93571b4c63161c74052df41d Mon Sep 17 00:00:00 2001 From: Peter Williams Date: Wed, 28 Nov 2018 22:29:35 +1100 Subject: [PATCH] Moved Matrix code to model/matrix.go --- pdf/contentstream/processor.go | 149 +------------------------------ pdf/extractor/point.go | 6 +- pdf/extractor/text.go | 24 ++--- pdf/model/matrix.go | 154 +++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 161 deletions(-) create mode 100644 pdf/model/matrix.go diff --git a/pdf/contentstream/processor.go b/pdf/contentstream/processor.go index 52d3a920..98e88b4a 100644 --- a/pdf/contentstream/processor.go +++ b/pdf/contentstream/processor.go @@ -7,8 +7,6 @@ package contentstream import ( "errors" - "fmt" - "math" "github.com/unidoc/unidoc/common" "github.com/unidoc/unidoc/pdf/core" @@ -22,7 +20,7 @@ type GraphicsState struct { ColorspaceNonStroking model.PdfColorspace ColorStroking model.PdfColor ColorNonStroking model.PdfColor - CTM Matrix + CTM model.Matrix } type GraphicStateStack []GraphicsState @@ -208,7 +206,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 = IdentityMatrix() + proc.graphicsState.CTM = model.IdentityMatrix() for _, op := range proc.operations { var err error @@ -568,149 +566,8 @@ func (proc *ContentStreamProcessor) handleCommand_cm(op *ContentStreamOperation, if err != nil { return err } - m := NewMatrix(f[0], f[1], f[2], f[3], f[4], f[5]) + m := model.NewMatrix(f[0], f[1], f[2], f[3], f[4], f[5]) proc.graphicsState.CTM.Concat(m) return nil } - -// 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] -} - -// 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] - yp := x*m[3] + y*m[4] + m[7] - return xp, yp -} - -// ScalingFactorX returns X scaling of the affine transform. -func (m *Matrix) ScalingFactorX() float64 { - return math.Sqrt(m[0]*m[0] + m[1]*m[1]) -} - -// ScalingFactorY returns X scaling of the affine transform. -func (m *Matrix) ScalingFactorY() float64 { - return math.Sqrt(m[3]*m[3] + m[4]*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 - } - common.Log.Debug("ERROR: Angle not a mulitple of 90°. m=%s", m) - return 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 diff --git a/pdf/extractor/point.go b/pdf/extractor/point.go index 5561f453..66736232 100644 --- a/pdf/extractor/point.go +++ b/pdf/extractor/point.go @@ -13,7 +13,7 @@ import ( "fmt" "github.com/unidoc/unidoc/common" - "github.com/unidoc/unidoc/pdf/contentstream" + "github.com/unidoc/unidoc/pdf/model" ) // Point defines a point in Cartesian coordinates @@ -34,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 := contentstream.NewMatrix(a, b, c, d, tx, ty) + m := model.NewMatrix(a, b, c, d, tx, ty) p.transformByMatrix(m) } @@ -61,7 +61,7 @@ func (p Point) Rotate(theta int) Point { } // transformByMatrix transforms `p` by the affine transformation `m`. -func (p *Point) transformByMatrix(m contentstream.Matrix) { +func (p *Point) transformByMatrix(m model.Matrix) { p.X, p.Y = m.Transform(p.X, p.Y) } diff --git a/pdf/extractor/text.go b/pdf/extractor/text.go index 4dca5ecc..641a52c3 100644 --- a/pdf/extractor/text.go +++ b/pdf/extractor/text.go @@ -314,7 +314,7 @@ func (to *textObject) nextLine() { // in `f` (page 250). func (to *textObject) setTextMatrix(f []float64) { a, b, c, d, tx, ty := f[0], f[1], f[2], f[3], f[4], f[5] - to.Tm = contentstream.NewMatrix(a, b, c, d, tx, ty) + to.Tm = model.NewMatrix(a, b, c, d, tx, ty) to.Tlm = to.Tm } @@ -570,9 +570,9 @@ type textObject struct { gs contentstream.GraphicsState fontStack *fontStacker State *textState - 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. + 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. } // newTextState returns a default textState. @@ -591,8 +591,8 @@ func newTextObject(e *Extractor, gs contentstream.GraphicsState, state *textStat gs: gs, fontStack: fontStack, State: state, - Tm: contentstream.IdentityMatrix(), - Tlm: contentstream.IdentityMatrix(), + Tm: model.IdentityMatrix(), + Tlm: model.IdentityMatrix(), } } @@ -620,7 +620,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 := contentstream.NewMatrix( + stateMatrix := model.NewMatrix( tfs*th, 0, 0, tfs, 0, state.Trise) @@ -692,14 +692,14 @@ func (to *textObject) renderText(data []byte) error { const glyphTextRatio = 1.0 / 1000.0 // translation returns the translation part of `m`. -func translation(m contentstream.Matrix) Point { +func translation(m model.Matrix) Point { tx, ty := m.Translation() return Point{tx, ty} } // translationMatrix returns a matrix that translates by `p`. -func translationMatrix(p Point) contentstream.Matrix { - return contentstream.TranslationMatrix(p.X, p.Y) +func translationMatrix(p Point) model.Matrix { + return model.TranslationMatrix(p.X, p.Y) } // moveTo moves the start of line pointer by `tx`,`ty` and sets the text pointer to the @@ -707,7 +707,7 @@ func translationMatrix(p Point) contentstream.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 = contentstream.NewMatrix(1, 0, 0, 1, tx, ty).Mult(to.Tlm) + to.Tlm = model.NewMatrix(1, 0, 0, 1, tx, ty).Mult(to.Tlm) to.Tm = to.Tlm } @@ -726,7 +726,7 @@ type XYText struct { // 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 contentstream.Matrix, end Point, +func (to *textObject) newXYText(text string, trm model.Matrix, end Point, height, spaceWidth float64) XYText { to.e.textCount++ theta := trm.Angle() diff --git a/pdf/model/matrix.go b/pdf/model/matrix.go new file mode 100644 index 00000000..abe80dbc --- /dev/null +++ b/pdf/model/matrix.go @@ -0,0 +1,154 @@ +/* + * 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" + + "github.com/unidoc/unidoc/common" +) + +// 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] +} + +// 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] + yp := x*m[3] + y*m[4] + m[7] + return xp, yp +} + +// ScalingFactorX returns X scaling of the affine transform. +func (m *Matrix) ScalingFactorX() float64 { + return math.Sqrt(m[0]*m[0] + m[1]*m[1]) +} + +// ScalingFactorY returns X scaling of the affine transform. +func (m *Matrix) ScalingFactorY() float64 { + return math.Sqrt(m[3]*m[3] + m[4]*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 + } + common.Log.Debug("ERROR: Angle not a mulitple of 90°. m=%s", m) + return 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