Skip to content

Commit

Permalink
Add basic image rendering support (unidoc#266)
Browse files Browse the repository at this point in the history
* Add render package
* Add text state
* Add more text operators
* Remove unnecessary files
* Add text font
* Add custom text render method
* Improve text rendering method
* Rename text state methods
* Refactor and document context interface
* Refact text begin/end operators
* Fix graphics state transformations
* Keep original font when doing font substitution
* Take page cropbox into account
* Revert to substitution font if original font measurement is 0
* Add font substitution package
* Implement addition transform.Point methods
* Use transform.Point in the image context package
* Remove unneeded functionality from the render image package
* Fix golint notices in the image rendering package
* Fix go vet notices in the render package
* Fix golint notices in the top-level render package
* Improve render context package documentation
* Document context text state struct.
* Document context text font struct.
* Minor logging improvements
* Add license disclaimer to the render package files
* Avoid using package aliases where possible
* Change style of section comments
* Adapt render package import style to follow the developer guide
* Improve documentation for the internal matrix implementation
* Update render package dependency versions
* Apply crop box post render
* Account for offseted media boxes
* Improve metrics of rendered characters
* Fix text matrix translation
* Change priority of fonts used for measuring rendered characters
* Skip invalid m and l operators on image rendering
* Small fix for v operator
* Fix rendered characters spacing issues
* Refactor naming of internal render packages
  • Loading branch information
adrg authored Mar 2, 2020
1 parent e056c0e commit d961079
Show file tree
Hide file tree
Showing 16 changed files with 3,257 additions and 24 deletions.
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
module github.com/unidoc/unipdf/v3

require (
github.com/adrg/sysfont v0.1.0
github.com/boombuler/barcode v1.0.0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
golang.org/x/net v0.0.0-20190606173856-1492cefac77f // indirect
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444 // indirect
golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20190606174628-0139d5756a7d // indirect
)
18 changes: 7 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
github.com/adrg/strutil v0.1.0 h1:IOQnSOAjbE17+7l1lw4rXgX6JuSeJGdZa7BucTMV3Qg=
github.com/adrg/strutil v0.1.0/go.mod h1:pXRr2+IyX5AEPAF5icj/EeTaiflPSD2hvGjnguilZgE=
github.com/adrg/sysfont v0.1.0 h1:vOk13USVkciGOJj9sPT9Gl9zfHUT2HZgsBnwS1Je4Q8=
github.com/adrg/sysfont v0.1.0/go.mod h1:DzISco90USPZJ+lmtpuz1SOTn1fih6YyB0KG2TEP/0U=
github.com/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83 h1:saj5dTV7eQ1wFg/gVZr1SfbkOmg8CYO9R8frHgQiyR4=
github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83/go.mod h1:xaGEIRenAiJcGgd9p62zbiP4993KaV3PdjczwGnP50I=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand All @@ -10,26 +17,15 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c h1:Vj5n4GlwjmQteupaxJ9+0FNOmBrHfq7vN4btdGoDZgI=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190606173856-1492cefac77f/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190606174628-0139d5756a7d h1:CoaGYJ9a8IXms8Q/NUeypLWbStIszTH0IIwqBUkEB9g=
golang.org/x/tools v0.0.0-20190606174628-0139d5756a7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
47 changes: 41 additions & 6 deletions internal/transform/matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,28 @@ func IdentityMatrix() Matrix {
return NewMatrix(1, 0, 0, 1, 0, 0)
}

// TranslationMatrix returns a matrix that translates by `tx`, `ty`.
// TranslationMatrix returns a matrix that translates by `tx`,`ty`.
func TranslationMatrix(tx, ty float64) Matrix {
return NewMatrix(1, 0, 0, 1, tx, ty)
}

// ScaleMatrix returns a matrix that scales by `x`,`y`.
func ScaleMatrix(x, y float64) Matrix {
return NewMatrix(x, 0, 0, y, 0, 0)
}

// RotationMatrix returns a matrix that rotates by angle `angle`, specified in radians.
func RotationMatrix(angle float64) Matrix {
c := math.Cos(angle)
s := math.Sin(angle)
return NewMatrix(c, s, -s, c, 0, 0)
}

// ShearMatrix returns a matrix that shears `x`,`y`.
func ShearMatrix(x, y float64) Matrix {
return NewMatrix(1, y, x, 1, 0, 0)
}

// NewMatrix returns an affine transform matrix laid out in homogenous coordinates as
// a b 0
// c d 0
Expand Down Expand Up @@ -74,19 +91,37 @@ func (m Matrix) Mult(b Matrix) Matrix {
return m
}

// Translate appends a translation of `dx`,`dy` to `m`.
// Translate appends a translation of `x`,`y` 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.clampRange()
func (m *Matrix) Translate(x, y float64) {
m.Concat(TranslationMatrix(x, y))
}

// Translation returns the translation part of `m`.
func (m *Matrix) Translation() (float64, float64) {
return m[6], m[7]
}

// Scale scales the current matrix by `x`,`y`.
func (m *Matrix) Scale(x, y float64) {
m.Concat(ScaleMatrix(x, y))
}

// Rotate rotates the current matrix by angle `angle`, specified in radians.
func (m *Matrix) Rotate(angle float64) {
m.Concat(RotationMatrix(angle))
}

// Shear shears the current matrix by `x',`y`.
func (m *Matrix) Shear(x, y float64) {
m.Concat(ShearMatrix(x, y))
}

// Clone returns a copy of the current matrix.
func (m *Matrix) Clone() Matrix {
return NewMatrix(m[0], m[1], m[3], m[4], 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]
Expand Down
19 changes: 16 additions & 3 deletions internal/transform/point.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,25 @@ func (p Point) Rotate(theta float64) Point {
return Point{r * cos, r * sin}
}

// transformByMatrix mutates and transforms `p` by the affine transformation `m`.
func (p *Point) transformByMatrix(m Matrix) {
p.X, p.Y = m.Transform(p.X, p.Y)
// Distance returns the distance between `a` and `b`.
func (a Point) Distance(b Point) float64 {
return math.Hypot(a.X-b.X, a.Y-b.Y)
}

// Interpolate does linear interpolation between point `a` and `b` for value `t`.
func (a Point) Interpolate(b Point, t float64) Point {
return Point{
X: (1-t)*a.X + t*b.X,
Y: (1-t)*a.Y + t*b.Y,
}
}

// String returns a string describing `p`.
func (p Point) String() string {
return fmt.Sprintf("(%.2f,%.2f)", p.X, p.Y)
}

// transformByMatrix mutates and transforms `p` by the affine transformation `m`.
func (p *Point) transformByMatrix(m Matrix) {
p.X, p.Y = m.Transform(p.X, p.Y)
}
106 changes: 106 additions & 0 deletions render/image_device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/

package render

import (
"errors"
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"os"
"path/filepath"
"strings"

"github.com/unidoc/unipdf/v3/model"
"github.com/unidoc/unipdf/v3/render/internal/context/imagerender"
)

// ImageDevice is used to render PDF pages to image targets.
type ImageDevice struct {
renderer
}

// NewImageDevice returns a new image device.
func NewImageDevice() *ImageDevice {
return &ImageDevice{}
}

// Render converts the specified PDF page into an image and returns the result.
func (d *ImageDevice) Render(page *model.PdfPage) (image.Image, error) {
// Get page dimensions.
mbox, err := page.GetMediaBox()
if err != nil {
return nil, err
}

// Render page.
width, height := mbox.Llx+mbox.Width(), mbox.Lly+mbox.Height()

ctx := imagerender.NewContext(int(width), int(height))
if err := d.renderPage(ctx, page); err != nil {
return nil, err
}

// Apply crop box, if one exists.
img := ctx.Image()
if box := page.CropBox; box != nil {
// Calculate crop bounds and crop start position.
cropBounds := image.Rect(0, 0, int(box.Width()), int(box.Height()))
cropStart := image.Pt(int(box.Llx), int(height-box.Ury))

// Crop image.
cropImg := image.NewRGBA(cropBounds)
draw.Draw(cropImg, cropBounds, img, cropStart, draw.Src)
img = cropImg
}

return img, nil
}

// RenderToPath converts the specified PDF page into an image and saves the
// result at the specified location.
func (d *ImageDevice) RenderToPath(page *model.PdfPage, outputPath string) error {
image, err := d.Render(page)
if err != nil {
return err
}

extension := strings.ToLower(filepath.Ext(outputPath))
if extension == "" {
return errors.New("could not recognize output file type")
}

switch extension {
case ".png":
return savePNG(outputPath, image)
case ".jpg", ".jpeg":
return saveJPG(outputPath, image, 100)
}

return fmt.Errorf("unrecognized output file type: %s", extension)
}

func savePNG(path string, image image.Image) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()

return png.Encode(file, image)
}

func saveJPG(path string, image image.Image, quality int) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()

return jpeg.Encode(file, image, &jpeg.Options{Quality: quality})
}
47 changes: 47 additions & 0 deletions render/internal/context/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* This file is subject to the terms and conditions defined in
* file 'LICENSE.md', which is part of this source code package.
*/

package context

import "image/color"

// FillRule represents the fill style used by a context instance.
type FillRule int

// Fill rules.
const (
FillRuleWinding FillRule = iota
FillRuleEvenOdd
)

// LineCap represents the line cap style used by a context instance.
type LineCap int

// Line cap styles.
const (
LineCapRound LineCap = iota
LineCapButt
LineCapSquare
)

// LineJoin represents the line join style used by a context instance.
type LineJoin int

// Line join styles.
const (
LineJoinRound LineJoin = iota
LineJoinBevel
)

// Pattern represents a pattern which can be rendered by a context instance.
type Pattern interface {
ColorAt(x, y int) color.Color
}

// Gradient represents a gradient pattern which can be rendered by a context instance.
type Gradient interface {
Pattern
AddColorStop(offset float64, color color.Color)
}
Loading

0 comments on commit d961079

Please sign in to comment.