diff --git a/common/license/key.go b/common/license/key.go index 8d53b7af3..a51080a96 100644 --- a/common/license/key.go +++ b/common/license/key.go @@ -79,7 +79,11 @@ func (k *LicenseKey) Validate() error { } if k.ExpiresAt == nil { - k.ExpiresAt = &noLicenseExpiry + expiresAt := k.CreatedAt.AddDate(1, 0, 0) + if noLicenseExpiry.After(expiresAt) { + expiresAt = noLicenseExpiry + } + k.ExpiresAt = &expiresAt } if k.CreatedAt.After(*k.ExpiresAt) { diff --git a/common/version.go b/common/version.go index d2bcf1a60..0a2b0e34d 100644 --- a/common/version.go +++ b/common/version.go @@ -11,12 +11,12 @@ import ( ) const releaseYear = 2020 -const releaseMonth = 2 -const releaseDay = 10 -const releaseHour = 8 -const releaseMin = 50 +const releaseMonth = 3 +const releaseDay = 8 +const releaseHour = 23 +const releaseMin = 40 // Version holds version information, when bumping this make sure to bump the released at stamp also. -const Version = "3.4.0" +const Version = "3.5.0" var ReleasedAt = time.Date(releaseYear, releaseMonth, releaseDay, releaseHour, releaseMin, 0, 0, time.UTC) diff --git a/go.mod b/go.mod index 3e19193e5..cb4632217 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 1a70098ed..5147dba05 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/internal/cmap/cmap.go b/internal/cmap/cmap.go index cf1a969ec..a6d8b6238 100644 --- a/internal/cmap/cmap.go +++ b/internal/cmap/cmap.go @@ -366,6 +366,11 @@ func (cmap *CMap) Type() int { return cmap.ctype } +// Nbits returns 8 bits for simple font CMaps and 16 bits for CID font CMaps. +func (cmap *CMap) NBits() int { + return cmap.nbits +} + // String returns a human readable description of `cmap`. func (cmap *CMap) String() string { si := cmap.systemInfo diff --git a/internal/e2etest/extract_images_test.go b/internal/e2etest/extract_images_test.go index f66310170..6d2cc4703 100644 --- a/internal/e2etest/extract_images_test.go +++ b/internal/e2etest/extract_images_test.go @@ -16,6 +16,8 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" + "github.com/unidoc/unipdf/v3/common" "github.com/unidoc/unipdf/v3/extractor" "github.com/unidoc/unipdf/v3/model" @@ -38,7 +40,7 @@ var knownExtrImgsHashes = map[string]string{ "7eee345c983461d44ae939b3f800a97642817c8d.pdf": "5a4cd00537f8a1f39fff2d4c6dd5cc53", "52ab322c1697aca9bad37288f7c502e37fa657af.pdf": "2bddee02dff89a38c08322c9d2c779a6", "0edf09fd438db2f18c1bb08fccc1f81a7b280bf2.pdf": "583f755b3fb1bd5697036616442687ab", - "cafe55316a45435c3817f4c1b6a19c9cd52db825.pdf": "b199badff0abb0311a2cbe35c7fce580", + "cafe55316a45435c3817f4c1b6a19c9cd52db825.pdf": "8b8c2eaa9b3a64eb5941348280c9c2a0", "6773e6aa5d8a2d26362cf3fca2874b3a81025bae.pdf": "f052e3e333839508a8bdd8d1a3ba1973", "d11a3ca55664828b69d7c39d83d5c0a63fcea89d.pdf": "29287cd44f009dce5aa9c2a0dc9a3c83", "483933bf73cc4fcc264eb69214ff763ccf299e49.pdf": "627dcf88805786d03b2e76d367b42642", @@ -46,12 +48,13 @@ var knownExtrImgsHashes = map[string]string{ "f856baf7ffcd96003b6bda800171cb0e5680f78e.pdf": "a9505d8c22f1fd063fbe0b05aa33a5fc", "201c20676fe8da14a8130852c91ed58b48cba8fb.pdf": "ffcb78d126c04be9ca2497bb43b6e964", "f0152456494aa09e5cf82c4afe9ecd2fdc2e8d72.pdf": "d0e68157aaa7f9f4406807512db3e676", - "d95643acea1ec3f6215bda35e4cd89dbd8898c44.pdf": "1739aed3e1cbfa5e98f8d7fef17a614b", + "d95643acea1ec3f6215bda35e4cd89dbd8898c44.pdf": "b4c7c2ae9671af69f71e9965e9cf67e8", "110d793aeaa7accbe40b5ab9db249d5a103d3b50.pdf": "a57e347edddfd3f6032b85553b3537cd", "d15a0aa289524619a971188372dd05fb712f1b2c.pdf": "380907273bb8ea64943d986491d827ec", "932e0dfa52c20ffe83b8178fb98296a0dab177d1.pdf": "b44d8b073f99ac3db28d7951e3c7d970", "60a8c28da5c23081834adac4170755904d8c4166.pdf": "9167f381d5eed7a2e5fd10eca567c519", "e51296be2615b9389482c9c16505286619b6cf36.pdf": "ec6e1f6297dd1cbda6ccba39e0c7d3d2", + "d3dd65300785dcf2114663397e475376bac88e75.pdf": "ea848ed888176368e697f577934f0452", // From unipdf#258. } func TestExtractImages(t *testing.T) { @@ -93,7 +96,7 @@ func TestExtractImages(t *testing.T) { knownHash, has := knownExtrImgsHashes[file.Name()] if has { - require.Equal(t, knownHash, hash) + assert.Equal(t, knownHash, hash) matchcount++ } else { t.Logf("%s - hash: %s not in the list of known hashes", file.Name(), hash) diff --git a/internal/textencoding/cmap.go b/internal/textencoding/cmap.go index d9924d7e5..b0dfbedfc 100644 --- a/internal/textencoding/cmap.go +++ b/internal/textencoding/cmap.go @@ -32,6 +32,13 @@ func NewCMapEncoder(baseName string, codeToCID, cidToUnicode *cmap.CMap) CMapEnc // Encode converts the Go unicode string to a PDF encoded string. func (enc CMapEncoder) Encode(str string) []byte { + if enc.cidToUnicode == nil { + return []byte{} + } + + if enc.cidToUnicode.NBits() == 8 { + return encodeString8bit(enc, str) + } return encodeString16bit(enc, str) } diff --git a/internal/transform/matrix.go b/internal/transform/matrix.go index cac0db301..84b1c753c 100644 --- a/internal/transform/matrix.go +++ b/internal/transform/matrix.go @@ -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 @@ -74,12 +91,10 @@ 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`. @@ -87,6 +102,26 @@ 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] diff --git a/internal/transform/point.go b/internal/transform/point.go index 647ba9285..792f321ce 100644 --- a/internal/transform/point.go +++ b/internal/transform/point.go @@ -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) +} diff --git a/model/colorspace.go b/model/colorspace.go index 6b974ec4a..797ee28be 100644 --- a/model/colorspace.go +++ b/model/colorspace.go @@ -2217,8 +2217,9 @@ func (cs *PdfColorspaceSpecialPattern) ImageToRGB(img Image) (Image, error) { return img, errors.New("invalid colorspace for image (pattern)") } -// PdfColorspaceSpecialIndexed is an indexed color space is a lookup table, where the input element is an index to the -// lookup table and the output is a color defined in the lookup table in the Base colorspace. +// PdfColorspaceSpecialIndexed is an indexed color space is a lookup table, where the input element +// is an index to the lookup table and the output is a color defined in the lookup table in the Base +// colorspace. // [/Indexed base hival lookup] type PdfColorspaceSpecialIndexed struct { Base PdfColorspace @@ -2232,9 +2233,7 @@ type PdfColorspaceSpecialIndexed struct { // NewPdfColorspaceSpecialIndexed returns a new Indexed color. func NewPdfColorspaceSpecialIndexed() *PdfColorspaceSpecialIndexed { - cs := &PdfColorspaceSpecialIndexed{} - cs.HiVal = 255 - return cs + return &PdfColorspaceSpecialIndexed{HiVal: 255} } func (cs *PdfColorspaceSpecialIndexed) String() string { @@ -2401,24 +2400,30 @@ func (cs *PdfColorspaceSpecialIndexed) ImageToRGB(img Image) (Image, error) { baseImage.Height = img.Height baseImage.Width = img.Width baseImage.alphaData = img.alphaData - baseImage.BitsPerComponent = img.BitsPerComponent + // TODO(peterwilliams97): Add support for other BitsPerComponent values. + // See https://github.com/unidoc/unipdf/issues/260 + baseImage.BitsPerComponent = 8 baseImage.hasAlpha = img.hasAlpha - baseImage.ColorComponents = img.ColorComponents + baseImage.ColorComponents = cs.Base.GetNumComponents() samples := img.GetSamples() N := cs.Base.GetNumComponents() + if N < 1 { + return Image{}, fmt.Errorf("bad base colorspace NumComponents=%d", N) + } + var baseSamples []uint32 // Convert the indexed data to base color map data. for i := 0; i < len(samples); i++ { // Each data point represents an index location. // For each entry there are N values. - index := int(samples[i]) * N + index := int(samples[i]) common.Log.Trace("Indexed: index=%d N=%d lut=%d", index, N, len(cs.colorLookup)) // Ensure does not go out of bounds. - if index+N-1 >= len(cs.colorLookup) { + if (index+1)*N > len(cs.colorLookup) { // Clip to the end value. - index = len(cs.colorLookup) - N - 1 + index = len(cs.colorLookup)/N - 1 common.Log.Trace("Clipping to index: %d", index) if index < 0 { common.Log.Debug("ERROR: Can't clip index. Is PDF file damaged?") @@ -2426,7 +2431,7 @@ func (cs *PdfColorspaceSpecialIndexed) ImageToRGB(img Image) (Image, error) { } } - cvals := cs.colorLookup[index : index+N] + cvals := cs.colorLookup[index*N : (index+1)*N] common.Log.Trace("C Vals: % d", cvals) for _, val := range cvals { baseSamples = append(baseSamples, uint32(val)) diff --git a/model/font.go b/model/font.go index 6dd3b2b9a..4ba5f750b 100644 --- a/model/font.go +++ b/model/font.go @@ -467,6 +467,59 @@ func (font *PdfFont) CharcodesToUnicode(charcodes []textencoding.CharCode) []run return strlist } +// RunesToCharcodeBytes maps the provided runes to charcode bytes and it +// returns the resulting slice of bytes, along with the number of runes which +// could not be converted. If the number of misses is 0, all runes were +// successfully converted. +func (font *PdfFont) RunesToCharcodeBytes(data []rune) ([]byte, int) { + // Create collection of encoders used for rune to charcode mapping: + // - if the font has a to Unicode CMap, use it first. + // - if the font has an encoder, use it as a fallback. + var encoders []textencoding.TextEncoder + if toUnicode := font.baseFields().toUnicodeCmap; toUnicode != nil { + encoders = append(encoders, textencoding.NewCMapEncoder("", nil, toUnicode)) + } + if encoder := font.Encoder(); encoder != nil { + encoders = append(encoders, encoder) + } + + var buffer bytes.Buffer + var numMisses int + for _, r := range data { + // Attempt to encode the current rune using each of the encoders, + // falling back to the next one in case of failure. + var encoded bool + for _, encoder := range encoders { + if encBytes := encoder.Encode(string(r)); len(encBytes) > 0 { + buffer.Write(encBytes) + encoded = true + break + } + } + + if !encoded { + common.Log.Debug("ERROR: failed to map rune `%+q` to charcode", r) + numMisses++ + } + } + + if numMisses != 0 { + common.Log.Debug("ERROR: could not convert all runes to charcodes.\n"+ + "\tnumRunes=%d numMisses=%d\n"+ + "\tfont=%s encoders=%+v", len(data), numMisses, font, encoders) + } + + return buffer.Bytes(), numMisses +} + +// StringToCharcodeBytes maps the provided string runes to charcode bytes and +// it returns the resulting slice of bytes, along with the number of runes +// which could not be converted. If the number of misses is 0, all string runes +// were successfully converted. +func (font *PdfFont) StringToCharcodeBytes(str string) ([]byte, int) { + return font.RunesToCharcodeBytes([]rune(str)) +} + // ToPdfObject converts the PdfFont object to its PDF representation. func (font *PdfFont) ToPdfObject() core.PdfObject { if font.context == nil { diff --git a/model/font_composite.go b/model/font_composite.go index a68b5decc..06eec9786 100644 --- a/model/font_composite.go +++ b/model/font_composite.go @@ -249,8 +249,18 @@ type pdfCIDFontType0 struct { encoder textencoding.TextEncoder // Table 117 – Entries in a CIDFont dictionary (page 269) - CIDSystemInfo *core.PdfObjectDictionary // (Required) Dictionary that defines the character - // collection of the CIDFont. See Table 116. + // (Required) Dictionary that defines the character collection of the CIDFont. + // See Table 116. + CIDSystemInfo *core.PdfObjectDictionary + + // Glyph metrics fields (optional). + DW core.PdfObject // default glyph width. + W core.PdfObject // glyph widths array. + DW2 core.PdfObject // default glyph metrics for CID fonts used for vertical writing. + W2 core.PdfObject // glyph metrics for CID fonts used for vertical writing. + + widths map[textencoding.CharCode]float64 + defaultWidth float64 } // pdfCIDFontType0FromSkeleton returns a pdfCIDFontType0 with its common fields initalized. @@ -277,12 +287,17 @@ func (font pdfCIDFontType0) Encoder() textencoding.TextEncoder { // GetRuneMetrics returns the character metrics for the specified rune. // A bool flag is returned to indicate whether or not the entry was found. func (font pdfCIDFontType0) GetRuneMetrics(r rune) (fonts.CharMetrics, bool) { - return fonts.CharMetrics{}, true + return fonts.CharMetrics{Wx: font.defaultWidth}, true } // GetCharMetrics returns the char metrics for character code `code`. func (font pdfCIDFontType0) GetCharMetrics(code textencoding.CharCode) (fonts.CharMetrics, bool) { - return fonts.CharMetrics{}, true + width := font.defaultWidth + if w, ok := font.widths[code]; ok { + width = w + } + + return fonts.CharMetrics{Wx: width}, true } // ToPdfObject converts the pdfCIDFontType0 to a PDF representation. @@ -308,6 +323,28 @@ func newPdfCIDFontType0FromPdfObject(d *core.PdfObjectDictionary, base *fontComm } font.CIDSystemInfo = obj + // Optional attributes. + font.DW = d.Get("DW") + font.W = d.Get("W") + font.DW2 = d.Get("DW2") + font.W2 = d.Get("W2") + + // Get font default glyph width. + font.defaultWidth = 1000.0 + if dw, err := core.GetNumberAsFloat(font.DW); err == nil { + font.defaultWidth = dw + } + + // Parse glyph widths array, if one is present. + fontWidths, err := parseCIDFontWidthsArray(font.W) + if err != nil { + return nil, err + } + if fontWidths == nil { + fontWidths = map[textencoding.CharCode]float64{} + } + font.widths = fontWidths + return font, nil } @@ -322,12 +359,19 @@ type pdfCIDFontType2 struct { // These fields are specific to Type 0 fonts. encoder textencoding.TextEncoder + // Table 117 – Entries in a CIDFont dictionary (page 269) + // Dictionary that defines the character collection of the CIDFont (required). + // See Table 116. CIDSystemInfo *core.PdfObjectDictionary - DW core.PdfObject - W core.PdfObject - DW2 core.PdfObject - W2 core.PdfObject - CIDToGIDMap core.PdfObject + + // Glyph metrics fields (optional). + DW core.PdfObject // default glyph width. + W core.PdfObject // glyph widths array. + DW2 core.PdfObject // default glyph metrics for CID fonts used for vertical writing. + W2 core.PdfObject // glyph metrics for CID fonts used for vertical writing. + + // CIDs to glyph indices mapping (optional). + CIDToGIDMap core.PdfObject widths map[textencoding.CharCode]float64 defaultWidth float64 @@ -443,58 +487,84 @@ func newPdfCIDFontType2FromPdfObject(d *core.PdfObjectDictionary, base *fontComm font.W2 = d.Get("W2") font.CIDToGIDMap = d.Get("CIDToGIDMap") - if arr2, ok := core.GetArray(font.W); ok { - font.widths = make(map[textencoding.CharCode]float64) - for i := 0; i < arr2.Len()-1; i++ { - obj0 := (*arr2).Get(i) - n, ok0 := core.GetIntVal(obj0) - if !ok0 { - return nil, fmt.Errorf("Bad font W obj0: i=%d %#v", i, obj0) + // Get font default glyph width. + font.defaultWidth = 1000.0 + if dw, err := core.GetNumberAsFloat(font.DW); err == nil { + font.defaultWidth = dw + } + + // Parse glyph widths array, if one is present. + fontWidths, err := parseCIDFontWidthsArray(font.W) + if err != nil { + return nil, err + } + if fontWidths == nil { + fontWidths = map[textencoding.CharCode]float64{} + } + font.widths = fontWidths + + return font, nil +} + +func parseCIDFontWidthsArray(w core.PdfObject) (map[textencoding.CharCode]float64, error) { + if w == nil { + return nil, nil + } + + wArr, ok := core.GetArray(w) + if !ok { + return nil, nil + } + + fontWidths := map[textencoding.CharCode]float64{} + wArrLen := wArr.Len() + for i := 0; i < wArrLen-1; i++ { + obj0 := wArr.Get(i) + n, ok0 := core.GetIntVal(obj0) + if !ok0 { + return nil, fmt.Errorf("Bad font W obj0: i=%d %#v", i, obj0) + } + i++ + if i > wArrLen-1 { + return nil, fmt.Errorf("Bad font W array: arr2=%+v", wArr) + } + + obj1 := wArr.Get(i) + switch obj1.(type) { + case *core.PdfObjectArray: + arr, _ := core.GetArray(obj1) + if widths, err := arr.ToFloat64Array(); err == nil { + for j := 0; j < len(widths); j++ { + fontWidths[textencoding.CharCode(n+j)] = widths[j] + } + } else { + return nil, fmt.Errorf("Bad font W array obj1: i=%d %#v", i, obj1) + } + case *core.PdfObjectInteger: + n1, ok1 := core.GetIntVal(obj1) + if !ok1 { + return nil, fmt.Errorf("Bad font W int obj1: i=%d %#v", i, obj1) } i++ - if i > arr2.Len()-1 { - return nil, fmt.Errorf("Bad font W array: arr2=%+v", arr2) + if i > wArrLen-1 { + return nil, fmt.Errorf("Bad font W array: arr2=%+v", wArr) } - obj1 := (*arr2).Get(i) - switch obj1.(type) { - case *core.PdfObjectArray: - arr, _ := core.GetArray(obj1) - if widths, err := arr.ToFloat64Array(); err == nil { - for j := 0; j < len(widths); j++ { - font.widths[textencoding.CharCode(n+j)] = widths[j] - } - } else { - return nil, fmt.Errorf("Bad font W array obj1: i=%d %#v", i, obj1) - } - case *core.PdfObjectInteger: - n1, ok1 := core.GetIntVal(obj1) - if !ok1 { - return nil, fmt.Errorf("Bad font W int obj1: i=%d %#v", i, obj1) - } - i++ - if i > arr2.Len()-1 { - return nil, fmt.Errorf("Bad font W array: arr2=%+v", arr2) - } - obj2 := (*arr2).Get(i) - v, err := core.GetNumberAsFloat(obj2) - if err != nil { - return nil, fmt.Errorf("Bad font W int obj2: i=%d %#v", i, obj2) - } - for j := n; j <= n1; j++ { - font.widths[textencoding.CharCode(j)] = v - } - default: - return nil, fmt.Errorf("Bad font W obj1 type: i=%d %#v", i, obj1) + + obj2 := wArr.Get(i) + v, err := core.GetNumberAsFloat(obj2) + if err != nil { + return nil, fmt.Errorf("Bad font W int obj2: i=%d %#v", i, obj2) + } + + for j := n; j <= n1; j++ { + fontWidths[textencoding.CharCode(j)] = v } + default: + return nil, fmt.Errorf("Bad font W obj1 type: i=%d %#v", i, obj1) } } - if defaultWidth, err := core.GetNumberAsFloat(font.DW); err == nil { - font.defaultWidth = defaultWidth - } else { - font.defaultWidth = 1000.0 - } - return font, nil + return fontWidths, nil } // NewCompositePdfFontFromTTFFile loads a composite font from a TTF font file. Composite fonts can diff --git a/model/font_test.go b/model/font_test.go index b0f3c151e..4592005a6 100644 --- a/model/font_test.go +++ b/model/font_test.go @@ -580,6 +580,19 @@ func (f *fontFragmentTest) check(t *testing.T) { t.Errorf("Some codes not decoded %s. font=%s numMisses=%d", f, font, numMisses) return } + + // Test test encoding. + // Encode previously decoded text and then decode the result. + // The decoded result `decodedText` should be the same with the original + // result `actualText`. + encoded, numMisses := font.StringToCharcodeBytes(actualText) + require.Equal(t, numMisses, 0) + encoded = core.MakeStringFromBytes(encoded).Bytes() + + decodedText, _, numMisses := font.CharcodeBytesToUnicode(encoded) + require.Equal(t, numMisses, 0) + require.Equal(t, actualText, decodedText) + if actualText != f.expected { t.Errorf("Incorrect decoding. %s encoding=%s\nexpected=%q\n actual=%q", f, font.Encoder(), f.expected, actualText) diff --git a/model/internal/fonts/ttfparser.go b/model/internal/fonts/ttfparser.go index 9092a08cb..cab077676 100644 --- a/model/internal/fonts/ttfparser.go +++ b/model/internal/fonts/ttfparser.go @@ -490,19 +490,20 @@ func (t *ttfParser) ParseCmap() error { } } - // Latin font support based on (3,1) table encoding. - if offset31 != 0 { - if err := t.parseCmapSubtable31(offset31); err != nil { + // Many non-Latin fonts (including asian fonts) use subtable (1,0). + if offset10 != 0 { + if err := t.parseCmapVersion(offset10); err != nil { return err } } - // Many non-Latin fonts (including asian fonts) use subtable (1,0). - if offset10 != 0 { - if err := t.parseCmapVersion(offset10); err != nil { + // Latin font support based on (3,1) table encoding. + if offset31 != 0 { + if err := t.parseCmapSubtable31(offset31); err != nil { return err } } + if offset31 == 0 && offset10 == 0 { common.Log.Debug("ttfParser.ParseCmap. No 31 or 10 table.") } diff --git a/render/image_device.go b/render/image_device.go new file mode 100644 index 000000000..b31cdf1e3 --- /dev/null +++ b/render/image_device.go @@ -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}) +} diff --git a/render/internal/context/const.go b/render/internal/context/const.go new file mode 100644 index 000000000..250bffad8 --- /dev/null +++ b/render/internal/context/const.go @@ -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) +} diff --git a/render/internal/context/context.go b/render/internal/context/context.go new file mode 100644 index 000000000..95613cabb --- /dev/null +++ b/render/internal/context/context.go @@ -0,0 +1,193 @@ +/* + * 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" + + "github.com/unidoc/unipdf/v3/internal/transform" +) + +// Context defines operations for rendering to a particular target. +type Context interface { + // + // Graphics state operations + // + + // Push adds the current context state on the stack. + Push() + + // Pop removes the most recent context state from the stack. + Pop() + + // + // Matrix operations + // + + // Matrix returns the current transformation matrix. + Matrix() transform.Matrix + + // SetMatrix modifies the transformation matrix. + SetMatrix(m transform.Matrix) + + // Translate updates the current matrix with a translation. + Translate(x, y float64) + + // Scale updates the current matrix with a scaling factor. + // Scaling occurs about the origin. + Scale(x, y float64) + + // Rotate updates the current matrix with a anticlockwise rotation. + // Rotation occurs about the origin. Angle is specified in radians. + Rotate(angle float64) + + // + // Path operations + // + + // MoveTo starts a new subpath within the current path starting at + // the specified point. + MoveTo(x, y float64) + + // LineTo adds a line segment to the current path starting at the current + // point. + LineTo(x, y float64) + + // CubicTo adds a cubic bezier curve to the current path starting at the + // current point. + CubicTo(x1, y1, x2, y2, x3, y3 float64) + + // QuadraticTo adds a quadratic bezier curve to the current path starting + // at the current point. + QuadraticTo(x1, y1, x2, y2 float64) + + // NewSubPath starts a new subpath within the current path. + NewSubPath() + + // ClosePath adds a line segment from the current point to the beginning + // of the current subpath. + ClosePath() + + // ClearPath clears the current path. + ClearPath() + + // Clip updates the clipping region by intersecting the current + // clipping region with the current path as it would be filled by Fill(). + // The path is cleared after this operation. + Clip() + + // ClipPreserve updates the clipping region by intersecting the current + // clipping region with the current path as it would be filled by Fill(). + // The path is preserved after this operation. + ClipPreserve() + + // ResetClip clears the clipping region. + ResetClip() + + // + // Line style operations + // + + // LineWidth returns the current line width. + LineWidth() float64 + + // SetLineWidth sets the line width. + SetLineWidth(lineWidth float64) + + // SetLineCap sets the line cap style. + SetLineCap(lineCap LineCap) + + // SetLineJoin sets the line join style. + SetLineJoin(lineJoin LineJoin) + + // SetDash sets the line dash pattern. + SetDash(dashes ...float64) + + // SetDashOffset sets the initial offset into the dash pattern to use when + // stroking dashed paths. + SetDashOffset(offset float64) + + // + // Fill and stroke operations + // + + // Fill fills the current path with the current color. Open subpaths + // are implicity closed. + Fill() + + // FillPreserve fills the current path with the current color. Open subpaths + // are implicity closed. The path is preserved after this operation. + FillPreserve() + + // Stroke strokes the current path with the current color, line width, + // line cap, line join and dash settings. The path is cleared after this + // operation. + Stroke() + + // StrokePreserve strokes the current path with the current color, + // line width, line cap, line join and dash settings. The path is preserved + // after this operation. + StrokePreserve() + + // SetRGBA sets the both the fill and stroke colors. + // r, g, b, a values should be in range 0-1. + SetRGBA(r, g, b, a float64) + + // SetRGBA sets the fill color. + // r, g, b, a values should be in range 0-1. + SetFillRGBA(r, g, b, a float64) + + // SetStrokeStyle sets current fill pattern. + SetFillStyle(pattern Pattern) + + // SetFillRule sets the fill rule. + SetFillRule(fillRule FillRule) + + // SetRGBA sets the stroke color. + // r, g, b, a values should be in range 0-1. + SetStrokeRGBA(r, g, b, a float64) + + // SetStrokeStyle sets current stroke pattern. + SetStrokeStyle(pattern Pattern) + + // + // Text operations + // + + // TextState returns the current text state. + TextState() *TextState + + // DrawString renders the specified string and the specified position. + DrawString(s string, x, y float64) + + // Measure string returns the width and height of the specified string. + MeasureString(s string) (w, h float64) + + // + // Draw operations + // + + // DrawRectangle draws the specified rectangle. + DrawRectangle(x, y, w, h float64) + + // DrawImage draws the specified image at the specified point. + DrawImage(image image.Image, x, y int) + + // DrawImageAnchored draws the specified image at the specified anchor point. + // The anchor point is x - w * ax, y - h * ay, where w, h is the size of the + // image. Use ax=0.5, ay=0.5 to center the image at the specified point. + DrawImageAnchored(image image.Image, x, y int, ax, ay float64) + + // + // Misc operations + // + + // Width returns the width of the rendering area. + Height() int + + // Height returns the height of the rendering area. + Width() int +} diff --git a/render/internal/context/imagerender/bezier.go b/render/internal/context/imagerender/bezier.go new file mode 100644 index 000000000..37cd4f943 --- /dev/null +++ b/render/internal/context/imagerender/bezier.go @@ -0,0 +1,68 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "math" + + "github.com/unidoc/unipdf/v3/internal/transform" +) + +func quadratic(x0, y0, x1, y1, x2, y2, t float64) (x, y float64) { + u := 1 - t + a := u * u + b := 2 * u * t + c := t * t + x = a*x0 + b*x1 + c*x2 + y = a*y0 + b*y1 + c*y2 + return +} + +func quadraticBezier(x0, y0, x1, y1, x2, y2 float64) []transform.Point { + l := (math.Hypot(x1-x0, y1-y0) + + math.Hypot(x2-x1, y2-y1)) + n := int(l + 0.5) + if n < 4 { + n = 4 + } + d := float64(n) - 1 + result := make([]transform.Point, n) + for i := 0; i < n; i++ { + t := float64(i) / d + x, y := quadratic(x0, y0, x1, y1, x2, y2, t) + result[i] = transform.NewPoint(x, y) + } + return result +} + +func cubic(x0, y0, x1, y1, x2, y2, x3, y3, t float64) (x, y float64) { + u := 1 - t + a := u * u * u + b := 3 * u * u * t + c := 3 * u * t * t + d := t * t * t + x = a*x0 + b*x1 + c*x2 + d*x3 + y = a*y0 + b*y1 + c*y2 + d*y3 + return +} + +func cubicBezier(x0, y0, x1, y1, x2, y2, x3, y3 float64) []transform.Point { + l := (math.Hypot(x1-x0, y1-y0) + + math.Hypot(x2-x1, y2-y1) + + math.Hypot(x3-x2, y3-y2)) + n := int(l + 0.5) + if n < 4 { + n = 4 + } + d := float64(n) - 1 + result := make([]transform.Point, n) + for i := 0; i < n; i++ { + t := float64(i) / d + x, y := cubic(x0, y0, x1, y1, x2, y2, x3, y3, t) + result[i] = transform.NewPoint(x, y) + } + return result +} diff --git a/render/internal/context/imagerender/context.go b/render/internal/context/imagerender/context.go new file mode 100644 index 000000000..465016301 --- /dev/null +++ b/render/internal/context/imagerender/context.go @@ -0,0 +1,834 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "errors" + "image" + "image/color" + "math" + + "github.com/golang/freetype/raster" + "golang.org/x/image/draw" + "golang.org/x/image/font" + "golang.org/x/image/math/f64" + + "github.com/unidoc/unipdf/v3/internal/transform" + "github.com/unidoc/unipdf/v3/render/internal/context" +) + +var ( + defaultFillStyle = newSolidPattern(color.White) + defaultStrokeStyle = newSolidPattern(color.Black) +) + +// Context represents an image rendering context. +type Context struct { + width int + height int + rasterizer *raster.Rasterizer + im *image.RGBA + mask *image.Alpha + color color.Color + fillPattern context.Pattern + strokePattern context.Pattern + strokePath raster.Path + fillPath raster.Path + start transform.Point + current transform.Point + hasCurrent bool + dashes []float64 + dashOffset float64 + lineWidth float64 + lineCap context.LineCap + lineJoin context.LineJoin + fillRule context.FillRule + matrix transform.Matrix + textState *context.TextState + stack []*Context +} + +// NewContext creates a new image.RGBA with the specified width and height +// and prepares a context for rendering onto that image. +func NewContext(width, height int) *Context { + return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height))) +} + +// NewContextForImage copies the specified image into a new image.RGBA +// and prepares a context for rendering onto that image. +func NewContextForImage(im image.Image) *Context { + return NewContextForRGBA(imageToRGBA(im)) +} + +// NewContextForRGBA prepares a context for rendering onto the specified image. +// No copy is made. +func NewContextForRGBA(im *image.RGBA) *Context { + w := im.Bounds().Size().X + h := im.Bounds().Size().Y + + return &Context{ + width: w, + height: h, + rasterizer: raster.NewRasterizer(w, h), + im: im, + color: color.Transparent, + fillPattern: defaultFillStyle, + strokePattern: defaultStrokeStyle, + lineWidth: 1, + fillRule: context.FillRuleWinding, + matrix: transform.IdentityMatrix(), + textState: context.NewTextState(), + } +} + +// Image returns the image that has been drawn by this context. +func (dc *Context) Image() image.Image { + return dc.im +} + +// Width returns the width of the image in pixels. +func (dc *Context) Width() int { + return dc.width +} + +// Height returns the height of the image in pixels. +func (dc *Context) Height() int { + return dc.height +} + +// SetDash sets the current dash pattern to use. Call with zero arguments to +// disable dashes. The values specify the lengths of each dash, with +// alternating on and off lengths. +func (dc *Context) SetDash(dashes ...float64) { + dc.dashes = dashes +} + +// SetDashOffset sets the initial offset into the dash pattern to use when +// stroking dashed paths. +func (dc *Context) SetDashOffset(offset float64) { + dc.dashOffset = offset +} + +// LineWidth returns the line width of the context. +func (dc *Context) LineWidth() float64 { + return dc.lineWidth +} + +// SetLineWidth sets the line width of the context. +func (dc *Context) SetLineWidth(lineWidth float64) { + dc.lineWidth = lineWidth +} + +// SetLineCap sets the line cap style. +func (dc *Context) SetLineCap(lineCap context.LineCap) { + dc.lineCap = lineCap +} + +// SetLineJoin sets the line join style. +func (dc *Context) SetLineJoin(lineJoin context.LineJoin) { + dc.lineJoin = lineJoin +} + +// SetFillRule sets the fill rule. +func (dc *Context) SetFillRule(fillRule context.FillRule) { + dc.fillRule = fillRule +} + +// +// Color setters +// + +func (dc *Context) setFillAndStrokeColor(c color.Color) { + dc.color = c + dc.fillPattern = newSolidPattern(c) + dc.strokePattern = newSolidPattern(c) +} + +// SetFillStyle sets current fill style +func (dc *Context) SetFillStyle(pattern context.Pattern) { + // if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString) + if fillStyle, ok := pattern.(*solidPattern); ok { + dc.color = fillStyle.color + } + dc.fillPattern = pattern +} + +// SetStrokeStyle sets current stroke style +func (dc *Context) SetStrokeStyle(pattern context.Pattern) { + dc.strokePattern = pattern +} + +// SetColor sets the current color(for both fill and stroke). +func (dc *Context) SetColor(c color.Color) { + dc.setFillAndStrokeColor(c) +} + +// SetStrokeRGBA sets the current color for stroking operations. +// r, g, b, a values must be in range 0-1. +func (dc *Context) SetStrokeRGBA(r, g, b, a float64) { + color := color.NRGBA{ + uint8(r * 255), + uint8(g * 255), + uint8(b * 255), + uint8(a * 255), + } + dc.strokePattern = newSolidPattern(color) +} + +// SetFillRGBA sets the current color for fill operations. +// r, g, b, a values must be in range 0-1. +func (dc *Context) SetFillRGBA(r, g, b, a float64) { + color := color.NRGBA{ + uint8(r * 255), + uint8(g * 255), + uint8(b * 255), + uint8(a * 255), + } + dc.color = color + dc.fillPattern = newSolidPattern(color) +} + +// SetHexColor sets the current color using a hex string. The leading pound +// sign (#) is optional. Both 3- and 6-digit variations are supported. 8 digits +// may be provided to set the alpha value as well. +func (dc *Context) SetHexColor(x string) { + r, g, b, a := parseHexColor(x) + dc.SetRGBA255(r, g, b, a) +} + +// SetRGBA255 sets the current color. r, g, b, a values should be between 0 and +// 255, inclusive. +func (dc *Context) SetRGBA255(r, g, b, a int) { + dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} + dc.setFillAndStrokeColor(dc.color) +} + +// SetRGB255 sets the current color. r, g, b values should be between 0 and 255, +// inclusive. Alpha will be set to 255 (fully opaque). +func (dc *Context) SetRGB255(r, g, b int) { + dc.SetRGBA255(r, g, b, 255) +} + +// SetRGBA sets the current color. r, g, b, a values should be between 0 and 1, +// inclusive. +func (dc *Context) SetRGBA(r, g, b, a float64) { + dc.color = color.NRGBA{ + uint8(r * 255), + uint8(g * 255), + uint8(b * 255), + uint8(a * 255), + } + dc.setFillAndStrokeColor(dc.color) +} + +// SetRGB sets the current color. r, g, b values should be between 0 and 1, +// inclusive. Alpha will be set to 1 (fully opaque). +func (dc *Context) SetRGB(r, g, b float64) { + dc.SetRGBA(r, g, b, 1) +} + +// +// Path manipulation +// + +// MoveTo starts a new subpath within the current path starting at the +// specified point. +func (dc *Context) MoveTo(x, y float64) { + if dc.hasCurrent { + dc.fillPath.Add1(fixedPoint(dc.start)) + } + + x, y = dc.Transform(x, y) + p := transform.NewPoint(x, y) + fp := fixedPoint(p) + + dc.strokePath.Start(fp) + dc.fillPath.Start(fp) + dc.start = p + dc.current = p + dc.hasCurrent = true +} + +// LineTo adds a line segment to the current path starting at the current +// point. If there is no current point, it is equivalent to MoveTo(x, y) +func (dc *Context) LineTo(x, y float64) { + if !dc.hasCurrent { + dc.MoveTo(x, y) + } else { + x, y = dc.Transform(x, y) + p := transform.NewPoint(x, y) + fp := fixedPoint(p) + + dc.strokePath.Add1(fp) + dc.fillPath.Add1(fp) + dc.current = p + } +} + +// QuadraticTo adds a quadratic bezier curve to the current path starting at +// the current point. If there is no current point, it first performs +// MoveTo(x1, y1) +func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) { + if !dc.hasCurrent { + dc.MoveTo(x1, y1) + } + + x1, y1 = dc.Transform(x1, y1) + x2, y2 = dc.Transform(x2, y2) + p1 := transform.NewPoint(x1, y1) + p2 := transform.NewPoint(x2, y2) + fp1 := fixedPoint(p1) + fp2 := fixedPoint(p2) + + dc.strokePath.Add2(fp1, fp2) + dc.fillPath.Add2(fp1, fp2) + dc.current = p2 +} + +// CubicTo adds a cubic bezier curve to the current path starting at the +// current point. If there is no current point, it first performs +// MoveTo(x1, y1). Because freetype/raster does not support cubic beziers, +// this is emulated with many small line segments. +func (dc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float64) { + if !dc.hasCurrent { + dc.MoveTo(x1, y1) + } + x0, y0 := dc.current.X, dc.current.Y + x1, y1 = dc.Transform(x1, y1) + x2, y2 = dc.Transform(x2, y2) + x3, y3 = dc.Transform(x3, y3) + points := cubicBezier(x0, y0, x1, y1, x2, y2, x3, y3) + previous := fixedPoint(dc.current) + for _, p := range points[1:] { + f := fixedPoint(p) + if f == previous { + // TODO: this fixes some rendering issues but not all + continue + } + previous = f + dc.strokePath.Add1(f) + dc.fillPath.Add1(f) + dc.current = p + } +} + +// ClosePath adds a line segment from the current point to the beginning +// of the current subpath. If there is no current point, this is a no-op. +func (dc *Context) ClosePath() { + if dc.hasCurrent { + fp := fixedPoint(dc.start) + dc.strokePath.Add1(fp) + dc.fillPath.Add1(fp) + dc.current = dc.start + } +} + +// ClearPath clears the current path. There is no current point after this +// operation. +func (dc *Context) ClearPath() { + dc.strokePath.Clear() + dc.fillPath.Clear() + dc.hasCurrent = false +} + +// NewSubPath starts a new subpath within the current path. There is no current +// point after this operation. +func (dc *Context) NewSubPath() { + if dc.hasCurrent { + dc.fillPath.Add1(fixedPoint(dc.start)) + } + dc.hasCurrent = false +} + +// +// Path drawing +// + +func (dc *Context) capper() raster.Capper { + switch dc.lineCap { + case context.LineCapButt: + return raster.ButtCapper + case context.LineCapRound: + return raster.RoundCapper + case context.LineCapSquare: + return raster.SquareCapper + } + return nil +} + +func (dc *Context) joiner() raster.Joiner { + switch dc.lineJoin { + case context.LineJoinBevel: + return raster.BevelJoiner + case context.LineJoinRound: + return raster.RoundJoiner + } + return nil +} + +func (dc *Context) stroke(painter raster.Painter) { + path := dc.strokePath + if len(dc.dashes) > 0 { + path = dashed(path, dc.dashes, dc.dashOffset) + } else { + // TODO: this is a temporary workaround to remove tiny segments + // that result in rendering issues + path = rasterPath(flattenPath(path)) + } + r := dc.rasterizer + r.UseNonZeroWinding = true + r.Clear() + r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner()) + r.Rasterize(painter) +} + +func (dc *Context) fill(painter raster.Painter) { + path := dc.fillPath + if dc.hasCurrent { + path = make(raster.Path, len(dc.fillPath)) + copy(path, dc.fillPath) + path.Add1(fixedPoint(dc.start)) + } + r := dc.rasterizer + r.UseNonZeroWinding = dc.fillRule == context.FillRuleWinding + r.Clear() + r.AddPath(path) + r.Rasterize(painter) +} + +// StrokePreserve strokes the current path with the current color, line width, +// line cap, line join and dash settings. The path is preserved after this +// operation. +func (dc *Context) StrokePreserve() { + var painter raster.Painter + if dc.mask == nil { + if pattern, ok := dc.strokePattern.(*solidPattern); ok { + // with a nil mask and a solid color pattern, we can be more efficient + // TODO: refactor so we don't have to do this type assertion stuff? + p := raster.NewRGBAPainter(dc.im) + p.SetColor(pattern.color) + painter = p + } + } + if painter == nil { + painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern) + } + dc.stroke(painter) +} + +// Stroke strokes the current path with the current color, line width, +// line cap, line join and dash settings. The path is cleared after this +// operation. +func (dc *Context) Stroke() { + dc.StrokePreserve() + dc.ClearPath() +} + +// FillPreserve fills the current path with the current color. Open subpaths +// are implicity closed. The path is preserved after this operation. +func (dc *Context) FillPreserve() { + var painter raster.Painter + if dc.mask == nil { + if pattern, ok := dc.fillPattern.(*solidPattern); ok { + // with a nil mask and a solid color pattern, we can be more efficient + // TODO: refactor so we don't have to do this type assertion stuff? + p := raster.NewRGBAPainter(dc.im) + p.SetColor(pattern.color) + painter = p + } + } + if painter == nil { + painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern) + } + dc.fill(painter) +} + +// Fill fills the current path with the current color. Open subpaths +// are implicity closed. The path is cleared after this operation. +func (dc *Context) Fill() { + dc.FillPreserve() + dc.ClearPath() +} + +// ClipPreserve updates the clipping region by intersecting the current +// clipping region with the current path as it would be filled by dc.Fill(). +// The path is preserved after this operation. +func (dc *Context) ClipPreserve() { + clip := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) + painter := raster.NewAlphaOverPainter(clip) + dc.fill(painter) + if dc.mask == nil { + dc.mask = clip + } else { + mask := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height)) + draw.DrawMask(mask, mask.Bounds(), clip, image.ZP, dc.mask, image.ZP, draw.Over) + dc.mask = mask + } +} + +// SetMask allows you to directly set the *image.Alpha to be used as a clipping +// mask. It must be the same size as the context, else an error is returned +// and the mask is unchanged. +func (dc *Context) SetMask(mask *image.Alpha) error { + if mask.Bounds().Size() != dc.im.Bounds().Size() { + return errors.New("mask size must match context size") + } + dc.mask = mask + return nil +} + +// AsMask returns an *image.Alpha representing the alpha channel of this +// context. This can be useful for advanced clipping operations where you first +// render the mask geometry and then use it as a mask. +func (dc *Context) AsMask() *image.Alpha { + mask := image.NewAlpha(dc.im.Bounds()) + draw.Draw(mask, dc.im.Bounds(), dc.im, image.ZP, draw.Src) + return mask +} + +// InvertMask inverts the alpha values in the current clipping mask such that +// a fully transparent region becomes fully opaque and vice versa. +func (dc *Context) InvertMask() { + if dc.mask == nil { + dc.mask = image.NewAlpha(dc.im.Bounds()) + } else { + for i, a := range dc.mask.Pix { + dc.mask.Pix[i] = 255 - a + } + } +} + +// Clip updates the clipping region by intersecting the current +// clipping region with the current path as it would be filled by dc.Fill(). +// The path is cleared after this operation. +func (dc *Context) Clip() { + dc.ClipPreserve() + dc.ClearPath() +} + +// ResetClip clears the clipping region. +func (dc *Context) ResetClip() { + dc.mask = nil +} + +// +// Drawing operations +// + +// Clear fills the entire image with the current color. +func (dc *Context) Clear() { + src := image.NewUniform(dc.color) + draw.Draw(dc.im, dc.im.Bounds(), src, image.ZP, draw.Src) +} + +// SetPixel sets the color of the specified pixel using the current color. +func (dc *Context) SetPixel(x, y int) { + dc.im.Set(x, y, dc.color) +} + +// DrawPoint is like DrawCircle but ensures that a circle of the specified +// size is drawn regardless of the current transformation matrix. The position +// is still transformed, but not the shape of the point. +func (dc *Context) DrawPoint(x, y, r float64) { + dc.Push() + tx, ty := dc.Transform(x, y) + dc.Identity() + dc.DrawCircle(tx, ty, r) + dc.Pop() +} + +// DrawLine draws the line described by points x1,y1 and x2,y2. +func (dc *Context) DrawLine(x1, y1, x2, y2 float64) { + dc.MoveTo(x1, y1) + dc.LineTo(x2, y2) +} + +// DrawRectangle draws a rectangle of size w,h at position x,y. +func (dc *Context) DrawRectangle(x, y, w, h float64) { + dc.NewSubPath() + dc.MoveTo(x, y) + dc.LineTo(x+w, y) + dc.LineTo(x+w, y+h) + dc.LineTo(x, y+h) + dc.ClosePath() +} + +// DrawRoundedRectangle draws a rounded rectangle of size w,h at position x,y. +func (dc *Context) DrawRoundedRectangle(x, y, w, h, r float64) { + x0, x1, x2, x3 := x, x+r, x+w-r, x+w + y0, y1, y2, y3 := y, y+r, y+h-r, y+h + dc.NewSubPath() + dc.MoveTo(x1, y0) + dc.LineTo(x2, y0) + dc.DrawArc(x2, y1, r, degreesToRadians(270), degreesToRadians(360)) + dc.LineTo(x3, y2) + dc.DrawArc(x2, y2, r, degreesToRadians(0), degreesToRadians(90)) + dc.LineTo(x1, y3) + dc.DrawArc(x1, y2, r, degreesToRadians(90), degreesToRadians(180)) + dc.LineTo(x0, y1) + dc.DrawArc(x1, y1, r, degreesToRadians(180), degreesToRadians(270)) + dc.ClosePath() +} + +// DrawArc draws an arc described by r, angle1, angle2 at position x,y. +func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) { + dc.DrawEllipticalArc(x, y, r, r, angle1, angle2) +} + +// DrawEllipticalArc draws an elliptical arc described by r, angle1, angle2 at +// position x,y. +func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) { + const n = 16 + for i := 0; i < n; i++ { + p1 := float64(i+0) / n + p2 := float64(i+1) / n + a1 := angle1 + (angle2-angle1)*p1 + a2 := angle1 + (angle2-angle1)*p2 + x0 := x + rx*math.Cos(a1) + y0 := y + ry*math.Sin(a1) + x1 := x + rx*math.Cos((a1+a2)/2) + y1 := y + ry*math.Sin((a1+a2)/2) + x2 := x + rx*math.Cos(a2) + y2 := y + ry*math.Sin(a2) + cx := 2*x1 - x0/2 - x2/2 + cy := 2*y1 - y0/2 - y2/2 + if i == 0 { + if dc.hasCurrent { + dc.LineTo(x0, y0) + } else { + dc.MoveTo(x0, y0) + } + } + dc.QuadraticTo(cx, cy, x2, y2) + } +} + +// DrawEllipse draws an ellipse of size rx,ry at position x,y. +func (dc *Context) DrawEllipse(x, y, rx, ry float64) { + dc.NewSubPath() + dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi) + dc.ClosePath() +} + +// DrawCircle draws a circle of radius r at position x,y. +func (dc *Context) DrawCircle(x, y, r float64) { + dc.NewSubPath() + dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi) + dc.ClosePath() +} + +func (dc *Context) drawRegularPolygon(n int, x, y, r, rotation float64) { + angle := 2 * math.Pi / float64(n) + rotation -= math.Pi / 2 + if n%2 == 0 { + rotation += angle / 2 + } + dc.NewSubPath() + for i := 0; i < n; i++ { + a := rotation + angle*float64(i) + dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a)) + } + dc.ClosePath() +} + +// DrawImage draws the specified image at the specified point. +func (dc *Context) DrawImage(im image.Image, x, y int) { + dc.DrawImageAnchored(im, x, y, 0, 0) +} + +// DrawImageAnchored draws the specified image at the specified anchor point. +// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the +// image. Use ax=0.5, ay=0.5 to center the image at the specified point. +func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) { + s := im.Bounds().Size() + x -= int(ax * float64(s.X)) + y -= int(ay * float64(s.Y)) + transformer := draw.BiLinear + m := dc.matrix.Clone() + m.Translate(float64(x), float64(y)) + s2d := f64.Aff3{m[0], m[3], m[6], m[1], m[4], m[7]} + if dc.mask == nil { + transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, nil) + } else { + transformer.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, &draw.Options{ + DstMask: dc.mask, + DstMaskP: image.ZP, + }) + } +} + +// +// Text operations +// + +// TextState returns the current text state. +func (dc *Context) TextState() *context.TextState { + return dc.textState +} + +func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) { + d := &font.Drawer{ + Dst: im, + Src: image.NewUniform(dc.color), + Face: dc.textState.Tf.Face, + Dot: fixedPoint(transform.NewPoint(x, y)), + } + // based on Drawer.DrawString() in golang.org/x/image/font/font.go + prevC := rune(-1) + for _, c := range s { + if prevC >= 0 { + d.Dot.X += d.Face.Kern(prevC, c) + } + dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c) + if !ok { + // TODO: is falling back on the U+FFFD glyph the responsibility of + // the Drawer or the Face? + // TODO: set prevC = '\ufffd'? + continue + } + sr := dr.Sub(dr.Min) + transformer := draw.BiLinear + m := dc.matrix.Clone() + m.Translate(float64(dr.Min.X), float64(dr.Min.Y)) + s2d := f64.Aff3{m[0], m[3], m[6], m[1], m[4], m[7]} + transformer.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{ + SrcMask: mask, + SrcMaskP: maskp, + }) + d.Dot.X += advance + prevC = c + } +} + +// DrawString draws the specified text at the specified point. +func (dc *Context) DrawString(s string, x, y float64) { + dc.DrawStringAnchored(s, x, y, 0, 0) +} + +// DrawStringAnchored draws the specified text at the specified anchor point. +// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the +// text. Use ax=0.5, ay=0.5 to center the text at the specified point. +func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) { + w, h := dc.MeasureString(s) + x -= ax * w + y += ay * h + if dc.mask == nil { + dc.drawString(dc.im, s, x, y) + } else { + im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height)) + dc.drawString(im, s, x, y) + draw.DrawMask(dc.im, dc.im.Bounds(), im, image.ZP, dc.mask, image.ZP, draw.Over) + } +} + +// MeasureString returns the rendered width and height of the specified text +// given the current font face. +func (dc *Context) MeasureString(s string) (w, h float64) { + d := &font.Drawer{ + Face: dc.textState.Tf.Face, + } + a := d.MeasureString(s) + return float64(a >> 6), dc.textState.Tf.Size +} + +// +// Transformation matrix operations +// + +// Matrix returns the current transformation matrix. +func (dc *Context) Matrix() transform.Matrix { + return dc.matrix +} + +// SetMatrix modifies the transformation matrix. +func (dc *Context) SetMatrix(m transform.Matrix) { + dc.matrix = m +} + +// Identity resets the current transformation matrix to the identity matrix. +// This results in no translating, scaling, rotating, or shearing. +func (dc *Context) Identity() { + dc.matrix = transform.IdentityMatrix() +} + +// Translate updates the current matrix with a translation. +func (dc *Context) Translate(x, y float64) { + dc.matrix.Translate(x, y) +} + +// Scale updates the current matrix with a scaling factor. +// Scaling occurs about the origin. +func (dc *Context) Scale(x, y float64) { + dc.matrix.Scale(x, y) +} + +// ScaleAbout updates the current matrix with a scaling factor. +// Scaling occurs about the specified point. +func (dc *Context) ScaleAbout(sx, sy, x, y float64) { + dc.Translate(x, y) + dc.Scale(sx, sy) + dc.Translate(-x, -y) +} + +// Rotate updates the current matrix with a anticlockwise rotation. +// Rotation occurs about the origin. Angle is specified in radians. +func (dc *Context) Rotate(angle float64) { + dc.matrix.Rotate(angle) +} + +// RotateAbout updates the current matrix with a anticlockwise rotation. +// Rotation occurs about the specified point. Angle is specified in radians. +func (dc *Context) RotateAbout(angle, x, y float64) { + dc.Translate(x, y) + dc.Rotate(angle) + dc.Translate(-x, -y) +} + +// Shear updates the current matrix with a shearing angle. +// Shearing occurs about the origin. +func (dc *Context) Shear(x, y float64) { + dc.matrix.Shear(x, y) +} + +// ShearAbout updates the current matrix with a shearing angle. +// Shearing occurs about the specified point. +func (dc *Context) ShearAbout(sx, sy, x, y float64) { + dc.Translate(x, y) + dc.Shear(sx, sy) + dc.Translate(-x, -y) +} + +// Transform multiplies the specified point by the current matrix, +// returning a transformed position. +func (dc *Context) Transform(x, y float64) (tx, ty float64) { + return dc.matrix.Transform(x, y) +} + +// +// Stack operations +// + +// Push saves the current state of the context for later retrieval. These +// can be nested. +func (dc *Context) Push() { + x := *dc + dc.stack = append(dc.stack, &x) +} + +// Pop restores the last saved context state from the stack. +func (dc *Context) Pop() { + before := *dc + s := dc.stack + x, s := s[len(s)-1], s[:len(s)-1] + *dc = *x + //dc.mask = before.mask + dc.strokePath = before.strokePath + dc.fillPath = before.fillPath + dc.start = before.start + dc.current = before.current + dc.hasCurrent = before.hasCurrent + dc.textState = before.textState +} diff --git a/render/internal/context/imagerender/gradient.go b/render/internal/context/imagerender/gradient.go new file mode 100644 index 000000000..f258a3ac4 --- /dev/null +++ b/render/internal/context/imagerender/gradient.go @@ -0,0 +1,204 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "image/color" + "math" + "sort" + + "github.com/unidoc/unipdf/v3/render/internal/context" +) + +type stop struct { + pos float64 + color color.Color +} + +type stops []stop + +// Len satisfies the Sort interface. +func (s stops) Len() int { + return len(s) +} + +// Less satisfies the Sort interface. +func (s stops) Less(i, j int) bool { + return s[i].pos < s[j].pos +} + +// Swap satisfies the Sort interface. +func (s stops) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Linear Gradient +type linearGradient struct { + x0, y0, x1, y1 float64 + stops stops +} + +func (g *linearGradient) ColorAt(x, y int) color.Color { + if len(g.stops) == 0 { + return color.Transparent + } + + fx, fy := float64(x), float64(y) + x0, y0, x1, y1 := g.x0, g.y0, g.x1, g.y1 + dx, dy := x1-x0, y1-y0 + + // Horizontal + if dy == 0 && dx != 0 { + return getColor((fx-x0)/dx, g.stops) + } + + // Vertical + if dx == 0 && dy != 0 { + return getColor((fy-y0)/dy, g.stops) + } + + // Dot product + s0 := dx*(fx-x0) + dy*(fy-y0) + if s0 < 0 { + return g.stops[0].color + } + // Calculate distance to (x0,y0) alone (x0,y0)->(x1,y1) + mag := math.Hypot(dx, dy) + u := ((fx-x0)*-dy + (fy-y0)*dx) / (mag * mag) + x2, y2 := x0+u*-dy, y0+u*dx + d := math.Hypot(fx-x2, fy-y2) / mag + return getColor(d, g.stops) +} + +func (g *linearGradient) AddColorStop(offset float64, color color.Color) { + g.stops = append(g.stops, stop{pos: offset, color: color}) + sort.Sort(g.stops) +} + +func newLinearGradient(x0, y0, x1, y1 float64) context.Gradient { + g := &linearGradient{ + x0: x0, y0: y0, + x1: x1, y1: y1, + } + return g +} + +// Radial Gradient +type circle struct { + x, y, r float64 +} + +type radialGradient struct { + c0, c1, cd circle + a, inva float64 + mindr float64 + stops stops +} + +func dot3(x0, y0, z0, x1, y1, z1 float64) float64 { + return x0*x1 + y0*y1 + z0*z1 +} + +func (g *radialGradient) ColorAt(x, y int) color.Color { + if len(g.stops) == 0 { + return color.Transparent + } + + // copy from pixman's pixman-radial-gradient.c + + dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y + b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r) + c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r) + + if g.a == 0 { + if b == 0 { + return color.Transparent + } + t := 0.5 * c / b + if t*g.cd.r >= g.mindr { + return getColor(t, g.stops) + } + return color.Transparent + } + + discr := dot3(b, g.a, 0, b, -c, 0) + if discr >= 0 { + sqrtdiscr := math.Sqrt(discr) + t0 := (b + sqrtdiscr) * g.inva + t1 := (b - sqrtdiscr) * g.inva + + if t0*g.cd.r >= g.mindr { + return getColor(t0, g.stops) + } else if t1*g.cd.r >= g.mindr { + return getColor(t1, g.stops) + } + } + + return color.Transparent +} + +func (g *radialGradient) AddColorStop(offset float64, color color.Color) { + g.stops = append(g.stops, stop{pos: offset, color: color}) + sort.Sort(g.stops) +} + +func newRadialGradient(x0, y0, r0, x1, y1, r1 float64) context.Gradient { + c0 := circle{x0, y0, r0} + c1 := circle{x1, y1, r1} + cd := circle{x1 - x0, y1 - y0, r1 - r0} + a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r) + var inva float64 + if a != 0 { + inva = 1.0 / a + } + mindr := -c0.r + g := &radialGradient{ + c0: c0, + c1: c1, + cd: cd, + a: a, + inva: inva, + mindr: mindr, + } + return g +} + +func getColor(pos float64, stops stops) color.Color { + if pos <= 0.0 || len(stops) == 1 { + return stops[0].color + } + + last := stops[len(stops)-1] + + if pos >= last.pos { + return last.color + } + + for i, stop := range stops[1:] { + if pos < stop.pos { + pos = (pos - stops[i].pos) / (stop.pos - stops[i].pos) + return colorLerp(stops[i].color, stop.color, pos) + } + } + + return last.color +} + +func colorLerp(c0, c1 color.Color, t float64) color.Color { + r0, g0, b0, a0 := c0.RGBA() + r1, g1, b1, a1 := c1.RGBA() + + return color.RGBA{ + lerp(r0, r1, t), + lerp(g0, g1, t), + lerp(b0, b1, t), + lerp(a0, a1, t), + } +} + +func lerp(a, b uint32, t float64) uint8 { + return uint8(int32(float64(a)*(1.0-t)+float64(b)*t) >> 8) +} diff --git a/render/internal/context/imagerender/path.go b/render/internal/context/imagerender/path.go new file mode 100644 index 000000000..de92bd5dc --- /dev/null +++ b/render/internal/context/imagerender/path.go @@ -0,0 +1,170 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "math" + + "github.com/golang/freetype/raster" + "golang.org/x/image/math/fixed" + + "github.com/unidoc/unipdf/v3/internal/transform" +) + +func flattenPath(p raster.Path) [][]transform.Point { + var result [][]transform.Point + var path []transform.Point + var cx, cy float64 + for i := 0; i < len(p); { + switch p[i] { + case 0: + if len(path) > 0 { + result = append(result, path) + path = nil + } + x := unfix(p[i+1]) + y := unfix(p[i+2]) + path = append(path, transform.NewPoint(x, y)) + cx, cy = x, y + i += 4 + case 1: + x := unfix(p[i+1]) + y := unfix(p[i+2]) + path = append(path, transform.NewPoint(x, y)) + cx, cy = x, y + i += 4 + case 2: + x1 := unfix(p[i+1]) + y1 := unfix(p[i+2]) + x2 := unfix(p[i+3]) + y2 := unfix(p[i+4]) + points := quadraticBezier(cx, cy, x1, y1, x2, y2) + path = append(path, points...) + cx, cy = x2, y2 + i += 6 + case 3: + x1 := unfix(p[i+1]) + y1 := unfix(p[i+2]) + x2 := unfix(p[i+3]) + y2 := unfix(p[i+4]) + x3 := unfix(p[i+5]) + y3 := unfix(p[i+6]) + points := cubicBezier(cx, cy, x1, y1, x2, y2, x3, y3) + path = append(path, points...) + cx, cy = x3, y3 + i += 8 + default: + panic("bad path") + } + } + if len(path) > 0 { + result = append(result, path) + } + return result +} + +func dashPath(paths [][]transform.Point, dashes []float64, offset float64) [][]transform.Point { + var result [][]transform.Point + if len(dashes) == 0 { + return paths + } + if len(dashes) == 1 { + dashes = append(dashes, dashes[0]) + } + for _, path := range paths { + if len(path) < 2 { + continue + } + previous := path[0] + pathIndex := 1 + dashIndex := 0 + segmentLength := 0.0 + + // offset + if offset != 0 { + var totalLength float64 + for _, dashLength := range dashes { + totalLength += dashLength + } + offset = math.Mod(offset, totalLength) + if offset < 0 { + offset += totalLength + } + for i, dashLength := range dashes { + offset -= dashLength + if offset < 0 { + dashIndex = i + segmentLength = dashLength + offset + break + } + } + } + + var segment []transform.Point + segment = append(segment, previous) + for pathIndex < len(path) { + dashLength := dashes[dashIndex] + point := path[pathIndex] + d := previous.Distance(point) + maxd := dashLength - segmentLength + if d > maxd { + t := maxd / d + p := previous.Interpolate(point, t) + segment = append(segment, p) + if dashIndex%2 == 0 && len(segment) > 1 { + result = append(result, segment) + } + segment = nil + segment = append(segment, p) + segmentLength = 0 + previous = p + dashIndex = (dashIndex + 1) % len(dashes) + } else { + segment = append(segment, point) + previous = point + segmentLength += d + pathIndex++ + } + } + if dashIndex%2 == 0 && len(segment) > 1 { + result = append(result, segment) + } + } + return result +} + +func rasterPath(paths [][]transform.Point) raster.Path { + var result raster.Path + for _, path := range paths { + var previous fixed.Point26_6 + for i, point := range path { + f := fixedPoint(point) + if i == 0 { + result.Start(f) + } else { + dx := f.X - previous.X + dy := f.Y - previous.Y + if dx < 0 { + dx = -dx + } + if dy < 0 { + dy = -dy + } + if dx+dy > 8 { + // TODO: this is a hack for cases where two points are + // too close - causes rendering issues with joins / caps + result.Add1(f) + } + } + previous = f + } + } + return result +} + +func dashed(path raster.Path, dashes []float64, offset float64) raster.Path { + return rasterPath(dashPath(flattenPath(path), dashes, offset)) +} diff --git a/render/internal/context/imagerender/pattern.go b/render/internal/context/imagerender/pattern.go new file mode 100644 index 000000000..d6f7eafc2 --- /dev/null +++ b/render/internal/context/imagerender/pattern.go @@ -0,0 +1,126 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "image" + "image/color" + + "github.com/golang/freetype/raster" + + "github.com/unidoc/unipdf/v3/render/internal/context" +) + +type repeatOp int + +const ( + repeatBoth repeatOp = iota + repeatX + repeatY + repeatNone +) + +// Solid Pattern +type solidPattern struct { + color color.Color +} + +func (p *solidPattern) ColorAt(x, y int) color.Color { + return p.color +} + +func newSolidPattern(color color.Color) context.Pattern { + return &solidPattern{color: color} +} + +// Surface Pattern +type surfacePattern struct { + im image.Image + op repeatOp +} + +func (p *surfacePattern) ColorAt(x, y int) color.Color { + b := p.im.Bounds() + switch p.op { + case repeatX: + if y >= b.Dy() { + return color.Transparent + } + case repeatY: + if x >= b.Dx() { + return color.Transparent + } + case repeatNone: + if x >= b.Dx() || y >= b.Dy() { + return color.Transparent + } + } + x = x%b.Dx() + b.Min.X + y = y%b.Dy() + b.Min.Y + return p.im.At(x, y) +} + +func newSurfacePattern(im image.Image, op repeatOp) context.Pattern { + return &surfacePattern{im: im, op: op} +} + +type patternPainter struct { + im *image.RGBA + mask *image.Alpha + p context.Pattern +} + +// Paint satisfies the Painter interface. +func (r *patternPainter) Paint(ss []raster.Span, done bool) { + b := r.im.Bounds() + for _, s := range ss { + if s.Y < b.Min.Y { + continue + } + if s.Y >= b.Max.Y { + return + } + if s.X0 < b.Min.X { + s.X0 = b.Min.X + } + if s.X1 > b.Max.X { + s.X1 = b.Max.X + } + if s.X0 >= s.X1 { + continue + } + const m = 1<<16 - 1 + y := s.Y - r.im.Rect.Min.Y + x0 := s.X0 - r.im.Rect.Min.X + // RGBAPainter.Paint() in $GOPATH/src/github.com/golang/freetype/raster/paint.go + i0 := (s.Y-r.im.Rect.Min.Y)*r.im.Stride + (s.X0-r.im.Rect.Min.X)*4 + i1 := i0 + (s.X1-s.X0)*4 + for i, x := i0, x0; i < i1; i, x = i+4, x+1 { + ma := s.Alpha + if r.mask != nil { + ma = ma * uint32(r.mask.AlphaAt(x, y).A) / 255 + if ma == 0 { + continue + } + } + c := r.p.ColorAt(x, y) + cr, cg, cb, ca := c.RGBA() + dr := uint32(r.im.Pix[i+0]) + dg := uint32(r.im.Pix[i+1]) + db := uint32(r.im.Pix[i+2]) + da := uint32(r.im.Pix[i+3]) + a := (m - (ca * ma / m)) * 0x101 + r.im.Pix[i+0] = uint8((dr*a + cr*ma) / m >> 8) + r.im.Pix[i+1] = uint8((dg*a + cg*ma) / m >> 8) + r.im.Pix[i+2] = uint8((db*a + cb*ma) / m >> 8) + r.im.Pix[i+3] = uint8((da*a + ca*ma) / m >> 8) + } + } +} + +func newPatternPainter(im *image.RGBA, mask *image.Alpha, p context.Pattern) *patternPainter { + return &patternPainter{im, mask, p} +} diff --git a/render/internal/context/imagerender/util.go b/render/internal/context/imagerender/util.go new file mode 100644 index 000000000..d26f1aedf --- /dev/null +++ b/render/internal/context/imagerender/util.go @@ -0,0 +1,73 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package imagerender + +import ( + "fmt" + "image" + "image/draw" + "math" + "strings" + + "golang.org/x/image/math/fixed" + + "github.com/unidoc/unipdf/v3/internal/transform" +) + +func degreesToRadians(degrees float64) float64 { + return degrees * math.Pi / 180 +} + +func imageToRGBA(src image.Image) *image.RGBA { + bounds := src.Bounds() + dst := image.NewRGBA(bounds) + draw.Draw(dst, bounds, src, bounds.Min, draw.Src) + return dst +} + +func parseHexColor(x string) (r, g, b, a int) { + x = strings.TrimPrefix(x, "#") + a = 255 + if len(x) == 3 { + format := "%1x%1x%1x" + fmt.Sscanf(x, format, &r, &g, &b) + r |= r << 4 + g |= g << 4 + b |= b << 4 + } + if len(x) == 6 { + format := "%02x%02x%02x" + fmt.Sscanf(x, format, &r, &g, &b) + } + if len(x) == 8 { + format := "%02x%02x%02x%02x" + fmt.Sscanf(x, format, &r, &g, &b, &a) + } + return +} + +func fixedPoint(p transform.Point) fixed.Point26_6 { + return fixed.Point26_6{ + X: fix(p.X), + Y: fix(p.Y), + } +} + +func fix(x float64) fixed.Int26_6 { + return fixed.Int26_6(x * 64) +} + +func unfix(x fixed.Int26_6) float64 { + const shift, mask = 6, 1<<6 - 1 + if x >= 0 { + return float64(x>>shift) + float64(x&mask)/64 + } + x = -x + if x >= 0 { + return -(float64(x>>shift) + float64(x&mask)/64) + } + return 0 +} diff --git a/render/internal/context/text_font.go b/render/internal/context/text_font.go new file mode 100644 index 000000000..c6f785a88 --- /dev/null +++ b/render/internal/context/text_font.go @@ -0,0 +1,139 @@ +/* + * 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 ( + "errors" + + "github.com/golang/freetype/truetype" + "golang.org/x/image/font" + + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" + + "github.com/unidoc/unipdf/v3/internal/textencoding" +) + +// TextFont represents a font used to draw text to a target, through a +// rendering context. +type TextFont struct { + Font *model.PdfFont + Face font.Face + Size float64 + + ttf *truetype.Font + origFont *model.PdfFont +} + +// NewTextFont returns a new text font instance based on the specified PDF font +// and the specified font size. +func NewTextFont(font *model.PdfFont, size float64) (*TextFont, error) { + descriptor := font.FontDescriptor() + if descriptor == nil { + return nil, errors.New("could not get font descriptor") + } + + fontStream, ok := core.GetStream(descriptor.FontFile2) + if !ok { + return nil, errors.New("missing font file stream") + } + + fontData, err := core.DecodeStream(fontStream) + if err != nil { + return nil, err + } + + ttfFont, err := truetype.Parse(fontData) + if err != nil { + return nil, err + } + + if size <= 1 { + size = 10 + } + + return &TextFont{ + Font: font, + Face: truetype.NewFace(ttfFont, &truetype.Options{Size: size}), + Size: size, + ttf: ttfFont, + }, nil +} + +// NewTextFontFromPath returns a new text font instance based on the specified +// font file and the specified font size. +func NewTextFontFromPath(filePath string, size float64) (*TextFont, error) { + font, err := model.NewPdfFontFromTTFFile(filePath) + if err != nil { + return nil, err + } + + return NewTextFont(font, size) +} + +// WithSize returns a new text font instance based on the current text font, +// with the specified font size. +func (tf *TextFont) WithSize(size float64, originalFont *model.PdfFont) *TextFont { + if size <= 1 { + size = 10 + } + + return &TextFont{ + Font: tf.Font, + Face: truetype.NewFace(tf.ttf, &truetype.Options{Size: size}), + Size: size, + ttf: tf.ttf, + origFont: originalFont, + } +} + +// BytesToCharcodes converts the specified byte data to character codes, using +// the encapsulated PDF font instance. +func (tf *TextFont) BytesToCharcodes(data []byte) []textencoding.CharCode { + if tf.origFont != nil { + return tf.origFont.BytesToCharcodes(data) + } + + return tf.Font.BytesToCharcodes(data) +} + +// CharcodesToUnicode converts the specified character codes to a slice of +// runes, using the encapsulated PDF font instance. +func (tf *TextFont) CharcodesToUnicode(charcodes []textencoding.CharCode) []rune { + if tf.origFont != nil { + return tf.origFont.CharcodesToUnicode(charcodes) + } + + return tf.Font.CharcodesToUnicode(charcodes) +} + +// GetCharMetrics returns the metrics of the specified character code. The +// character metrics are calculated by the internal PDF font. +func (tf *TextFont) GetCharMetrics(code textencoding.CharCode) (float64, float64, bool) { + if metrics, ok := tf.Font.GetCharMetrics(code); ok && metrics.Wx != 0 { + return metrics.Wx, metrics.Wy, ok + } + if tf.origFont == nil { + return 0, 0, false + } + + metrics, ok := tf.origFont.GetCharMetrics(code) + return metrics.Wx, metrics.Wy, ok && metrics.Wx != 0 +} + +// GetRuneMetrics returns the metrics of the specified rune. The character +// metrics are calculated by the internal PDF font. +func (tf *TextFont) GetRuneMetrics(r rune) (float64, float64, bool) { + if metrics, ok := tf.Font.GetRuneMetrics(r); ok && metrics.Wx != 0 { + return metrics.Wx, metrics.Wy, ok + } + if tf.origFont == nil { + return 0, 0, false + } + + metrics, ok := tf.origFont.GetRuneMetrics(r) + return metrics.Wx, metrics.Wy, ok && metrics.Wx != 0 +} diff --git a/render/internal/context/text_state.go b/render/internal/context/text_state.go new file mode 100644 index 000000000..e7f580211 --- /dev/null +++ b/render/internal/context/text_state.go @@ -0,0 +1,160 @@ +/* + * 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 ( + "github.com/unidoc/unipdf/v3/internal/transform" +) + +// TextState holds a representation of a PDF text state. The text state +// processes different text related operations which may occur in PDF content +// streams. It is used as a part of a renderding context in order to manipulate +// and display text. +type TextState struct { + Tc float64 // Character spacing. + Tw float64 // Word spacing. + Th float64 // Horizontal scaling. + Tl float64 // Leading. + Tf *TextFont // Font + Ts float64 // Text rise. + Tm transform.Matrix // Text matrix. + Tlm transform.Matrix // Text line matrix. +} + +// NewTextState returns a new TextState instance. +func NewTextState() *TextState { + return &TextState{ + Th: 100, + Tm: transform.IdentityMatrix(), + Tlm: transform.IdentityMatrix(), + } +} + +// ProcTm processes a `Tm` operation, which sets the current text matrix. +// +// See section 9.4.2 "Text Positioning Operators" and +// Table 108 (pp. 257-258 PDF32000_2008). +func (ts *TextState) ProcTm(a, b, c, d, e, f float64) { + ts.Tm = transform.NewMatrix(a, b, c, d, e, -f) + ts.Tlm = ts.Tm.Clone() +} + +// ProcTd processes a `Td` operation, which advances the text state to a new +// line with offsets `tx`,`ty`. +// +// See section 9.4.2 "Text Positioning Operators" and +// Table 108 (pp. 257-258 PDF32000_2008). +func (ts *TextState) ProcTd(tx, ty float64) { + ts.Tlm.Concat(transform.TranslationMatrix(tx, -ty)) + ts.Tm = ts.Tlm.Clone() +} + +// ProcTD processes a `TD` operation, which advances the text state to a new +// line with offsets `tx`,`ty`. +// +// See section 9.4.2 "Text Positioning Operators" and +// Table 108 (pp. 257-258 PDF32000_2008). +func (ts *TextState) ProcTD(tx, ty float64) { + ts.Tl = -ty + ts.ProcTd(tx, ty) +} + +// ProcTStar processes a `T*` operation, which advances the text state to a +// new line. +// +// See section 9.4.2 "Text Positioning Operators" and +// Table 108 (pp. 257-258 PDF32000_2008). +func (ts *TextState) ProcTStar() { + ts.ProcTd(0, -ts.Tl) +} + +// ProcTj processes a `Tj` operation, which displays a text string. +// +// See section 9.4.3 "Text Showing Operators" and +// Table 209 (pp. 258-259 PDF32000_2008). +func (ts *TextState) ProcTj(data []byte, ctx Context) { + tfs := ts.Tf.Size + th := ts.Th / 100.0 + stateMatrix := transform.NewMatrix(tfs*th, 0, 0, tfs, 0, ts.Ts) + + runes := ts.Tf.CharcodesToUnicode(ts.Tf.BytesToCharcodes(data)) + for _, r := range runes { + if r == '\x00' { + continue + } + + // Calculate text rendering matrix. + tm := ts.Tm.Clone() + ts.Tm.Concat(stateMatrix) + + // Draw rune. + x, y := ts.Tm.Transform(0, 0) + ctx.Scale(1, -1) + ctx.DrawString(string(r), x, y) + ctx.Scale(1, -1) + + // Calculate word spacing. + tw := 0.0 + if r == ' ' { + tw = ts.Tw + } + + // Calculate rune spacing. + var w float64 + if wX, _, ok := ts.Tf.GetRuneMetrics(r); ok { + w = wX * 0.001 * tfs + } else { + w, _ = ctx.MeasureString(string(r)) + } + + // Calculate displacement offset. + tx := (w + ts.Tc + tw) * th + + // Generate new text matrix. + ts.Tm = transform.TranslationMatrix(tx, 0).Mult(tm) + } +} + +// ProcQ processes a `'` operation, which advances the text state to a new line +// and then displays a text string. +// +// See section 9.4.3 "Text Showing Operators" and +// Table 209 (pp. 258-259 PDF32000_2008). +func (ts *TextState) ProcQ(data []byte, ctx Context) { + ts.ProcTStar() + ts.ProcTj(data, ctx) +} + +// ProcDQ processes a `''` operation, which advances the text state to a new +// line and then displays a text string using aw and ac as word and character +// spacing. +// +// See section 9.4.3 "Text Showing Operators" and +// Table 209 (pp. 258-259 PDF32000_2008). +func (ts *TextState) ProcDQ(data []byte, aw, ac float64, ctx Context) { + ts.Tw = aw + ts.Tc = ac + ts.ProcQ(data, ctx) +} + +// ProcTf processes a `Tf` operation which sets the font and its size. +// +// See section 9.3 "Text State Parameters and Operators" and +// Table 105 (pp. 251-252 PDF32000_2008). +func (ts *TextState) ProcTf(font *TextFont) { + ts.Tf = font +} + +// Translate translates the current text matrix with `tx`,`ty`. +func (ts *TextState) Translate(tx, ty float64) { + ts.Tm = transform.TranslationMatrix(tx, ty).Mult(ts.Tm) +} + +// Reset resets both the text matrix and the line matrix. +func (ts *TextState) Reset() { + ts.Tm = transform.IdentityMatrix() + ts.Tlm = transform.IdentityMatrix() +} diff --git a/render/renderer.go b/render/renderer.go new file mode 100644 index 000000000..0b82c29e9 --- /dev/null +++ b/render/renderer.go @@ -0,0 +1,1071 @@ +/* + * 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" + + "github.com/adrg/sysfont" + + "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/contentstream" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/model" + "github.com/unidoc/unipdf/v3/render/internal/context" + + "github.com/unidoc/unipdf/v3/internal/transform" +) + +var ( + errType = errors.New("type check error") + errRange = errors.New("range check error") +) + +type renderer struct { +} + +func (r renderer) renderPage(ctx context.Context, page *model.PdfPage) error { + contents, err := page.GetAllContentStreams() + if err != nil { + return err + } + + // Change coordinate system. + ctx.Translate(0, float64(ctx.Height())) + ctx.Scale(1, -1) + + // Create white background. + ctx.Push() + ctx.SetRGBA(1, 1, 1, 1) + ctx.DrawRectangle(0, 0, float64(ctx.Width()), float64(ctx.Height())) + ctx.Fill() + ctx.Pop() + + // Set defaults. + ctx.SetLineWidth(1.0) + ctx.SetRGBA(0, 0, 0, 1) + + return r.renderContentStream(ctx, contents, page.Resources) +} + +func (r renderer) renderContentStream(ctx context.Context, contents string, resources *model.PdfPageResources) error { + operations, err := contentstream.NewContentStreamParser(contents).Parse() + if err != nil { + return err + } + + textState := ctx.TextState() + fontCache := map[string]*context.TextFont{} + fontFinder := sysfont.NewFinder(&sysfont.FinderOpts{ + Extensions: []string{".ttf", ".ttc"}, + }) + + processor := contentstream.NewContentStreamProcessor(*operations) + processor.AddHandler(contentstream.HandlerConditionEnumAllOperands, "", + func(op *contentstream.ContentStreamOperation, gs contentstream.GraphicsState, resources *model.PdfPageResources) error { + common.Log.Debug("Processing %s", op.Operand) + switch op.Operand { + // + // Graphics stage operators + // + + // Push current graphics state to the stack. + case "q": + ctx.Push() + // Pop graphics state from the stack. + case "Q": + ctx.Pop() + // Modify graphics state matrix. + case "cm": + if len(op.Params) != 6 { + return errRange + } + + fv, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + m := transform.NewMatrix(fv[0], fv[1], fv[2], fv[3], fv[4], fv[5]) + common.Log.Debug("Graphics state matrix: %+v", m) + ctx.SetMatrix(ctx.Matrix().Mult(m)) + + // TODO: Take angle into account for line widths (8.4.3.2 Line Width). + s := (gs.CTM.ScalingFactorX() + gs.CTM.ScalingFactorY()) / 2.0 + ctx.SetLineWidth(s * ctx.LineWidth()) + // Set line width. + case "w": + if len(op.Params) != 1 { + return errRange + } + + fw, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + // TODO: Take angle into account for line widths (8.4.3.2 Line Width). + s := (gs.CTM.ScalingFactorX() + gs.CTM.ScalingFactorY()) / 2.0 + ctx.SetLineWidth(s * fw[0]) + // Set line cap style. + case "J": + if len(op.Params) != 1 { + return errRange + } + + val, ok := core.GetIntVal(op.Params[0]) + if !ok { + return errType + } + + switch val { + // Butt cap. + case 0: + ctx.SetLineCap(context.LineCapButt) + // Round cap. + case 1: + ctx.SetLineCap(context.LineCapRound) + // Projecting square cap. + case 2: + ctx.SetLineCap(context.LineCapSquare) + default: + common.Log.Debug("Invalid line cap style: %d", val) + return errRange + } + // Set line join style. + case "j": + if len(op.Params) != 1 { + return errRange + } + + val, ok := core.GetIntVal(op.Params[0]) + if !ok { + return errType + } + + switch val { + // Miter join. + case 0: + ctx.SetLineJoin(context.LineJoinBevel) + // Round join. + case 1: + ctx.SetLineJoin(context.LineJoinRound) + // Bevel join. + case 2: + ctx.SetLineJoin(context.LineJoinBevel) + default: + common.Log.Debug("Invalid line join style: %d", val) + return errRange + } + // Set miter limit. + case "M": + if len(op.Params) != 1 { + return errRange + } + + fw, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + // TODO: Add miter support in context. + // ctx.SetMiterLimit(fw[0]) + _ = fw + common.Log.Debug("Miter limit not supported") + // Set line dash pattern. + case "d": + if len(op.Params) != 2 { + return errRange + } + + dashArray, ok := core.GetArray(op.Params[0]) + if !ok { + return errType + } + + phase, ok := core.GetIntVal(op.Params[1]) + if !ok { + return errType + } + + dashes, err := core.GetNumbersAsFloat(dashArray.Elements()) + if err != nil { + return err + } + ctx.SetDash(dashes...) + + // TODO: Add support for dash phase in context. + //ctx.SetDashPhase(phase) + _ = phase + common.Log.Debug("Line dash phase not supported") + // Set color rendering intent. + case "ri": + // TODO: Add rendering intent support. + common.Log.Debug("Rendering intent not supported") + // Set flatness tolerance. + case "i": + // TODO: Add flatness tolerance support. + common.Log.Debug("Flatness tolerance not supported") + // Set graphics state from dictionary. + case "gs": + if len(op.Params) != 1 { + return errRange + } + + rname, ok := core.GetName(op.Params[0]) + if !ok { + return errType + } + if rname == nil { + return errRange + } + + extobj, ok := resources.GetExtGState(*rname) + if !ok { + common.Log.Debug("ERROR: could not find resource: %s", *rname) + return errors.New("resource not found") + } + + extdict, ok := core.GetDict(extobj) + if !ok { + common.Log.Debug("ERROR: could get graphics state dict") + return errType + } + common.Log.Debug("GS dict: %s", extdict.String()) + + // + // Path operators + // + + // Move to. + case "m": + if len(op.Params) != 2 { + common.Log.Debug("WARN: error while processing `m` operator: %s. Output may be incorrect.", errRange) + return nil + } + + xy, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("Move to: %v", xy) + ctx.NewSubPath() + ctx.MoveTo(xy[0], xy[1]) + // Line to. + case "l": + if len(op.Params) != 2 { + common.Log.Debug("WARN: error while processing `l` operator: %s. Output may be incorrect.", errRange) + return nil + } + + xy, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + ctx.LineTo(xy[0], xy[1]) + // Cubic bezier. + case "c": + if len(op.Params) != 6 { + return errRange + } + + cbp, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("Cubic bezier params: %+v", cbp) + ctx.CubicTo(cbp[0], cbp[1], cbp[2], cbp[3], cbp[4], cbp[5]) + // Cubic bezier. + case "v", "y": + if len(op.Params) != 4 { + return errRange + } + + cbp, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("Cubic bezier params: %+v", cbp) + ctx.QuadraticTo(cbp[0], cbp[1], cbp[2], cbp[3]) + // Close current subpath. + case "h": + ctx.ClosePath() + ctx.NewSubPath() + // Rectangle. + case "re": + if len(op.Params) != 4 { + return errRange + } + + xywh, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + ctx.DrawRectangle(xywh[0], xywh[1], xywh[2], xywh[3]) + ctx.NewSubPath() + + // + // Path painting operators + // + + // Set path stroke. + case "S": + color, err := gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color") + return err + } + + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // Close and stroke. + case "s": + color, err := gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color") + return err + } + + ctx.ClosePath() + ctx.NewSubPath() + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // Fill path using non-zero winding number rule. + case "f", "F": + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color") + return err + } + + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.SetFillRule(context.FillRuleWinding) + ctx.Fill() + // Fill path using even-odd rule. + case "f*": + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color") + return err + } + + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.SetFillRule(context.FillRuleEvenOdd) + ctx.Fill() + // Fill then stroke the path using non-zero winding rule. + case "B": + // Fill path. + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor := color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.SetFillRule(context.FillRuleWinding) + ctx.FillPreserve() + + // Stroke path. + color, err = gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor = color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // Fill then stroke the path using even-odd rule. + case "B*": + // Fill path. + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor := color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.SetFillRule(context.FillRuleEvenOdd) + ctx.FillPreserve() + + // Stroke path. + color, err = gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor = color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // Close, fill and stroke the path using non-zero winding rule. + case "b": + // Fill path. + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor := color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.ClosePath() + ctx.NewSubPath() + ctx.SetFillRule(context.FillRuleWinding) + ctx.FillPreserve() + + // Stroke path. + color, err = gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor = color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // Close, fill and stroke the path using even-odd rule. + case "b*": + // Close current subpath. + ctx.ClosePath() + + // Fill path. + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor := color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.NewSubPath() + ctx.SetFillRule(context.FillRuleEvenOdd) + ctx.FillPreserve() + + // Stroke path. + color, err = gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", err) + return err + } + + rgbColor = color.(*model.PdfColorDeviceRGB) + ctx.SetRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + ctx.Stroke() + // End the current path without filling or stroking. + case "n": + ctx.ClearPath() + + // + // Path clipping operators + // + + // Modify current clipping path using non-zero winding rule. + case "W": + // TODO: fix clipping. + //ctx.StrokePreserve() + //ctx.Clip() + ctx.SetFillRule(context.FillRuleWinding) + ctx.ClipPreserve() + // Modify current clipping path using even-odd rule. + case "W*": + // TODO: fix clipping. + //ctx.StrokePreserve() + //ctx.Clip() + ctx.SetFillRule(context.FillRuleEvenOdd) + ctx.ClipPreserve() + + // + // Color operators + // + + // Set RGB non-stroking color. + case "rg": + rgbColor, ok := gs.ColorNonStroking.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + ctx.SetFillRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + // Set RGB stroking color. + case "RG": + rgbColor, ok := gs.ColorStroking.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + ctx.SetStrokeRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + // Set CMYK non-stroking color. + case "k": + cmykColor, ok := gs.ColorNonStroking.(*model.PdfColorDeviceCMYK) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + color, err := gs.ColorspaceNonStroking.ColorToRGB(cmykColor) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetFillRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + // Set CMYK stroking color. + case "K": + cmykColor, ok := gs.ColorStroking.(*model.PdfColorDeviceCMYK) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + color, err := gs.ColorspaceStroking.ColorToRGB(cmykColor) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetStrokeRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + // Set Grayscale non-stroking color. + case "g": + grayColor, ok := gs.ColorNonStroking.(*model.PdfColorDeviceGray) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + color, err := gs.ColorspaceNonStroking.ColorToRGB(grayColor) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetFillRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + // Set Grayscale stroking color. + case "G": + grayColor, ok := gs.ColorStroking.(*model.PdfColorDeviceGray) + if !ok { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + color, err := gs.ColorspaceStroking.ColorToRGB(grayColor) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetStrokeRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + case "cs", "sc", "scn": + color, err := gs.ColorspaceNonStroking.ColorToRGB(gs.ColorNonStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorNonStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetFillRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + case "CS", "SC", "SCN": + color, err := gs.ColorspaceStroking.ColorToRGB(gs.ColorStroking) + if err != nil { + common.Log.Debug("Error converting color: %v", gs.ColorStroking) + return nil + } + rgbColor, ok := color.(*model.PdfColorDeviceRGB) + if !ok { + common.Log.Debug("Error converting color: %v", color) + return nil + } + ctx.SetStrokeRGBA(rgbColor.R(), rgbColor.G(), rgbColor.B(), 1) + + // + // Image operators + // + + // Display xobjects. + case "Do": + if len(op.Params) != 1 { + return errRange + } + + name, ok := core.GetName(op.Params[0]) + if !ok { + return errType + } + + _, xtype := resources.GetXObjectByName(*name) + switch xtype { + case model.XObjectTypeImage: + common.Log.Debug("XObject image: %s", name.String()) + + ximg, err := resources.GetXObjectImageByName(*name) + if err != nil { + return err + } + + img, err := ximg.ToImage() + if err != nil { + return err + } + + goImg, err := img.ToGoImage() + if err != nil { + return err + } + bounds := goImg.Bounds() + + // TODO: Handle soft masks. + ctx.Push() + ctx.Scale(1.0/float64(bounds.Dx()), -1.0/float64(bounds.Dy())) + ctx.DrawImageAnchored(goImg, 0, 0, 0, 1) + ctx.Pop() + case model.XObjectTypeForm: + common.Log.Debug("XObject form: %s", name.String()) + + // Go through the XObject Form content stream. + xform, err := resources.GetXObjectFormByName(*name) + if err != nil { + return err + } + + formContent, err := xform.GetContentStream() + if err != nil { + return err + } + + formResources := xform.Resources + if formResources == nil { + formResources = resources + } + + ctx.Push() + if xform.Matrix != nil { + array, ok := core.GetArray(xform.Matrix) + if !ok { + return errType + } + + mf, err := core.GetNumbersAsFloat(array.Elements()) + if err != nil { + return err + } + if len(mf) != 6 { + return errRange + } + + m := transform.NewMatrix(mf[0], mf[1], mf[2], mf[3], mf[4], mf[5]) + ctx.SetMatrix(ctx.Matrix().Mult(m)) + } + + if xform.BBox != nil { + array, ok := core.GetArray(xform.BBox) + if !ok { + return errType + } + + bf, err := core.GetNumbersAsFloat(array.Elements()) + if err != nil { + return err + } + if len(bf) != 4 { + common.Log.Debug("Len = %d", len(bf)) + return errRange + } + + // Set clipping region. + ctx.DrawRectangle(bf[0], bf[1], bf[2]-bf[0], bf[3]-bf[1]) + ctx.SetRGBA(1, 0, 0, 1) + ctx.Clip() + } else { + common.Log.Debug("ERROR: Required BBox missing on XObject Form") + } + + // Process the content stream in the Form object. + err = r.renderContentStream(ctx, string(formContent), formResources) + if err != nil { + return err + } + ctx.Pop() + } + // Display inline image. + case "BI": + if len(op.Params) != 1 { + return errRange + } + + iimg, ok := op.Params[0].(*contentstream.ContentStreamInlineImage) + if !ok { + return nil + } + + img, err := iimg.ToImage(resources) + if err != nil { + return err + } + + goImg, err := img.ToGoImage() + if err != nil { + return err + } + bounds := goImg.Bounds() + + ctx.Push() + ctx.Scale(1.0/float64(bounds.Dx()), -1.0/float64(bounds.Dy())) + ctx.DrawImageAnchored(goImg, 0, 0, 0, 1) + ctx.Pop() + + // + // Text operators + // + + // Begin text. + case "BT": + textState.Reset() + // End text. + case "ET": + textState.Reset() + // Set text leading. + case "TL": + if len(op.Params) != 1 { + return errRange + } + + tl, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + textState.Tl = tl + // Set character spacing. + case "Tc": + if len(op.Params) != 1 { + return errRange + } + + tc, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + textState.Tc = tc + // Set word spacing. + case "Tw": + if len(op.Params) != 1 { + return errRange + } + + tw, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + textState.Tw = tw + // Set horizontal scaling. + case "Tz": + if len(op.Params) != 1 { + return errRange + } + + th, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + textState.Th = th + // Set text rise. + case "Ts": + if len(op.Params) != 1 { + return errRange + } + + ts, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + textState.Ts = ts + // Move to the next line with specified offsets. + case "Td": + if len(op.Params) != 2 { + return errRange + } + + fv, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("Td: %v", fv) + textState.ProcTd(fv[0], fv[1]) + // Move to the next line with specified offsets. + case "TD": + if len(op.Params) != 2 { + return errRange + } + + fv, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("TD: %v", fv) + textState.ProcTD(fv[0], fv[1]) + // Move to the start of the next line. + case "T*": + textState.ProcTStar() + // Set text line matrix. + case "Tm": + if len(op.Params) != 6 { + return errRange + } + fv, err := core.GetNumbersAsFloat(op.Params) + if err != nil { + return err + } + + common.Log.Debug("Text matrix: %+v", fv) + textState.ProcTm(fv[0], fv[1], fv[2], fv[3], fv[4], fv[5]) + // Move to the next line and show text string. + case `'`: + if len(op.Params) != 1 { + return errRange + } + + charcodes, ok := core.GetStringBytes(op.Params[0]) + if !ok { + return errType + } + common.Log.Debug("' string: %s", string(charcodes)) + + textState.ProcQ(charcodes, ctx) + // Move to the next line and show text string. + case `''`: + if len(op.Params) != 3 { + return errRange + } + + aw, err := core.GetNumberAsFloat(op.Params[0]) + if err != nil { + return err + } + + ac, err := core.GetNumberAsFloat(op.Params[1]) + if err != nil { + return err + } + + charcodes, ok := core.GetStringBytes(op.Params[2]) + if !ok { + return errType + } + + textState.ProcDQ(charcodes, aw, ac, ctx) + // Show text string. + case "Tj": + if len(op.Params) != 1 { + return errRange + } + + charcodes, ok := core.GetStringBytes(op.Params[0]) + if !ok { + return errType + } + common.Log.Debug("Tj string: `%s`", string(charcodes)) + + textState.ProcTj(charcodes, ctx) + // Show array of text strings. + case "TJ": + if len(op.Params) != 1 { + return errRange + } + + array, ok := core.GetArray(op.Params[0]) + if !ok { + common.Log.Debug("Type: %T", array) + return errType + } + common.Log.Debug("TJ array: %+v", array) + + for _, obj := range array.Elements() { + switch t := obj.(type) { + case *core.PdfObjectString: + if t != nil { + textState.ProcTj(t.Bytes(), ctx) + } + case *core.PdfObjectFloat, *core.PdfObjectInteger: + val, err := core.GetNumberAsFloat(t) + if err == nil { + textState.Translate(-val*0.001*textState.Tf.Size, 0) + } + } + } + // Set font and font size. + case "Tf": + if len(op.Params) != 2 { + return errRange + } + common.Log.Debug("%#v", op.Params) + + // Get font name. + fontName, ok := core.GetName(op.Params[0]) + if !ok || fontName == nil { + common.Log.Debug("invalid font name object: %v", op.Params[0]) + return errType + } + common.Log.Debug("Font name: %s", fontName.String()) + + // Get font size. + fontSize, err := core.GetNumberAsFloat(op.Params[1]) + if err != nil { + common.Log.Debug("invalid font size object: %v", op.Params[1]) + return errType + } + common.Log.Debug("Font size: %v", fontSize) + + // Search font in resources. + fObj, has := resources.GetFontByName(*fontName) + if !has { + common.Log.Debug("ERROR: Font %s not found", fontName.String()) + return errors.New("font not found") + } + common.Log.Debug("Font: %T", fObj) + + fontDict, ok := core.GetDict(fObj) + if !ok { + common.Log.Debug("ERROR: could not get font dict") + return errType + } + + pdfFont, err := model.NewPdfFontFromPdfObject(fontDict) + if err != nil { + common.Log.Debug("ERROR: could not load font from object") + return err + } + + baseFont := pdfFont.BaseFont() + if baseFont == "" { + baseFont = fontName.String() + } + + textFont, ok := fontCache[baseFont] + if !ok { + textFont, err = context.NewTextFont(pdfFont, fontSize) + if err != nil { + common.Log.Debug("ERROR: %v", err) + } + } + + if textFont == nil { + // Treat cases such as: OPEIOA+ArialMT + if len(baseFont) > 7 && baseFont[6] == '+' { + baseFont = baseFont[7:] + } + + substitutes := []string{baseFont, "Times New Roman", "Arial", "DejaVu Sans"} + for _, name := range substitutes { + common.Log.Debug("DEBUG: searching system font `%s`", name) + + // Check if font is cached. + if textFont, ok = fontCache[name]; ok { + break + } + + // Find font or suitable alternative. + fontInfo := fontFinder.Match(name) + if fontInfo == nil { + common.Log.Debug("could not find font file %s", name) + continue + } + + // Load matched font. + textFont, err = context.NewTextFontFromPath(fontInfo.Filename, fontSize) + if err != nil { + common.Log.Debug("could not load font file %s", fontInfo.Filename) + continue + } + + // Update font cache. + common.Log.Debug("Substituting font %s with %s (%s)", baseFont, fontInfo.Name, fontInfo.Filename) + fontCache[name] = textFont + break + } + } + + if textFont == nil { + common.Log.Debug("ERROR: could not find any suitable font") + return errors.New("could not find any suitable font") + } + + // Set font. + textState.ProcTf(textFont.WithSize(fontSize, pdfFont)) + + // + // Marked content operators + // + + // Begin a marked-content sequence. + case "BMC", "BDC": + // End a marked-content sequence. + case "EMC": + default: + common.Log.Debug("ERROR: unsupported operand: %s", op.Operand) + } + + return nil + }) + + err = processor.Process(resources) + if err != nil { + return err + } + + return nil +}