Skip to content

Commit

Permalink
Merge eb8162f into 5d279db
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverpool committed Jan 7, 2021
2 parents 5d279db + eb8162f commit eed770c
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 52 deletions.
3 changes: 1 addition & 2 deletions examples/preview/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package main
import (
"fmt"
"image/color"
"image/png"
"os"

"github.com/tdewolff/canvas"
Expand Down Expand Up @@ -122,7 +121,7 @@ func draw(c *canvas.Context) {
if err != nil {
panic(err)
}
img, err := png.Decode(lenna)
img, err := canvas.NewPNGImage(lenna)
if err != nil {
panic(err)
}
Expand Down
75 changes: 75 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package canvas

import (
"bytes"
"image"
"image/jpeg"
"image/png"
"io"
)

// JPEGImage gives access to the raw bytes.
// Should be used with PDF, only for baseline JPEGs
// (progressive might not be displayed properly)
type JPEGImage interface {
image.Image
JPEGBytes() []byte
}

type jpegImage struct {
bufferedImage
}

func (i jpegImage) JPEGBytes() []byte {
return i.bytes
}

// NewJPEGImage parses a reader to later give access to the JPEG raw bytes.
// Should be used with PDF, only for baseline JPEGs
// (progressive might not be displayed properly)
func NewJPEGImage(r io.Reader) (JPEGImage, error) {
bi, err := newBufferedImage(jpeg.Decode, r)
if err != nil {
return nil, err
}
return jpegImage{bi}, nil
}

// PNGImage gives access to the raw bytes
type PNGImage interface {
image.Image
PNGBytes() []byte
}

type pngImage struct {
bufferedImage
}

func (i pngImage) PNGBytes() []byte {
return i.bytes
}

// NewPNGImage parses a reader to later give access to the PNG raw bytes
func NewPNGImage(r io.Reader) (PNGImage, error) {
bi, err := newBufferedImage(png.Decode, r)
if err != nil {
return nil, err
}
return pngImage{bi}, nil
}

// bufferedImage is a generic struct for holding specific decoders
type bufferedImage struct {
image.Image
bytes []byte
}

func newBufferedImage(decode func(io.Reader) (image.Image, error), r io.Reader) (bufferedImage, error) {
var buffer bytes.Buffer
r = io.TeeReader(r, &buffer)
img, err := decode(r)
return bufferedImage{
Image: img,
bytes: buffer.Bytes(),
}, err
}
69 changes: 61 additions & 8 deletions pdf/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ type pdfStream struct {
const (
pdfFilterASCII85 pdfFilter = "ASCII85Decode"
pdfFilterFlate pdfFilter = "FlateDecode"
pdfFilterDCT pdfFilter = "DCTDecode"
)

func (w *pdfWriter) writeVal(i interface{}) {
Expand Down Expand Up @@ -369,6 +370,8 @@ func (w *pdfWriter) writeVal(i interface{}) {
if filter, ok := v.dict["Filter"].(pdfFilter); ok {
filters = append(filters, filter)
} else if filterArray, ok := v.dict["Filter"].(pdfArray); ok {
// filters must be applied in reverse order
// For example, data encoded using LZW and ASCII base-85 encoding (in that order) shall be decoded using the following entry in the stream dictionary: EXAMPLE 2/Filter [ /ASCII85Decode /LZWDecode ]
for i := len(filterArray) - 1; i >= 0; i-- {
if filter, ok := filterArray[i].(pdfFilter); ok {
filters = append(filters, filter)
Expand All @@ -388,6 +391,13 @@ func (w *pdfWriter) writeVal(i interface{}) {
w := zlib.NewWriter(&b2)
w.Write(b)
w.Close()
case pdfFilterDCT:
// This filter is used by JPEG images
// we consider that the buffer is already encoded
if len(filters) > 1 {
panic("pdfFilterDCT can't be combined with other filters")
}
break
}
b = b2.Bytes()
}
Expand Down Expand Up @@ -940,6 +950,56 @@ func (w *pdfPageWriter) DrawImage(img image.Image, enc canvas.ImageEncoding, m c
}

func (w *pdfPageWriter) embedImage(img image.Image, enc canvas.ImageEncoding) pdfName {
var ref pdfStream
if i, ok := img.(canvas.JPEGImage); ok {
ref = w.jpegStream(i)
} else {
ref = w.imageStream(img)
}

if _, ok := w.resources["XObject"]; !ok {
w.resources["XObject"] = pdfDict{}
}
name := pdfName(fmt.Sprintf("Im%d", len(w.resources["XObject"].(pdfDict))))
w.resources["XObject"].(pdfDict)[name] = ref
return name
}

func (w *pdfPageWriter) jpegStream(img canvas.JPEGImage) pdfStream {
size := img.Bounds().Size()
dict := pdfDict{
"Type": pdfName("XObject"),
"Subtype": pdfName("Image"),
"Width": size.X,
"Height": size.Y,

// "ColorSpace": will be set below
"BitsPerComponent": 8, // bpc
// "Interpolate": true,
"Filter": pdfFilterDCT, // f
}

switch img.ColorModel() {
case color.GrayModel:
dict["ColorSpace"] = pdfName("DeviceGray")
case color.YCbCrModel:
dict["ColorSpace"] = pdfName("DeviceRGB")
case color.CMYKModel:
dict["ColorSpace"] = pdfName("DeviceCMYK")
dict["Decode"] = pdfArray([]interface{}{1, 0, 1, 0, 1, 0, 1, 0})
default:
// fallback to generic imageStream
// fmt.Errorf("unsupported JPEG-color space: %s", img.ColorModel())
return w.imageStream(img)
}

return pdfStream{
dict: dict,
stream: img.JPEGBytes(),
}
}

func (w *pdfPageWriter) imageStream(img image.Image) pdfStream {
size := img.Bounds().Size()
sp := img.Bounds().Min // starting point
b := make([]byte, size.X*size.Y*3)
Expand Down Expand Up @@ -989,17 +1049,10 @@ func (w *pdfPageWriter) embedImage(img image.Image, enc canvas.ImageEncoding) pd
}

// TODO: (PDF) implement JPXFilter for lossy image compression
ref := w.pdf.writeObject(pdfStream{
return pdfStream{
dict: dict,
stream: b,
})

if _, ok := w.resources["XObject"]; !ok {
w.resources["XObject"] = pdfDict{}
}
name := pdfName(fmt.Sprintf("Im%d", len(w.resources["XObject"].(pdfDict))))
w.resources["XObject"].(pdfDict)[name] = ref
return name
}

func (w *pdfPageWriter) getOpacityGS(a float64) pdfName {
Expand Down
107 changes: 65 additions & 42 deletions svg/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,42 +333,16 @@ func (r *SVG) RenderText(text *canvas.Text, m canvas.Matrix) {
func (r *SVG) RenderImage(img image.Image, m canvas.Matrix) {
refMask := ""
mimetype := "image/png"
if r.imgEnc == canvas.Lossy {
switch img.(type) {
case canvas.JPEGImage:
mimetype = "image/jpg"
if opaqueImg, ok := img.(interface{ Opaque() bool }); !ok || !opaqueImg.Opaque() {
hasMask := false
size := img.Bounds().Size()
opaque := image.NewRGBA(img.Bounds())
mask := image.NewGray(img.Bounds())
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
R, G, B, A := img.At(x, y).RGBA()
if A != 0 {
r := byte((R * 65535 / A) >> 8)
g := byte((G * 65535 / A) >> 8)
b := byte((B * 65535 / A) >> 8)
opaque.SetRGBA(x, y, color.RGBA{r, g, b, 255})
mask.SetGray(x, y, color.Gray{byte(A >> 8)})
}
if A>>8 != 255 {
hasMask = true
}
}
}
if hasMask {
img = opaque
refMask = fmt.Sprintf("m%v", r.maskID)
r.maskID++

fmt.Fprintf(r.w, `<mask id="%s"><image width="%d" height="%d" xlink:href="data:image/jpg;base64,`, refMask, size.X, size.Y)
encoder := base64.NewEncoder(base64.StdEncoding, r.w)
if err := jpeg.Encode(encoder, mask, nil); err != nil {
panic(err)
}
if err := encoder.Close(); err != nil {
panic(err)
}
fmt.Fprintf(r.w, `"/></mask>`)
case canvas.PNGImage:
mimetype = "image/png"
default:
if r.imgEnc == canvas.Lossy {
mimetype = "image/jpg"
if opaqueImg, ok := img.(interface{ Opaque() bool }); !ok || !opaqueImg.Opaque() {
img, refMask = r.renderOpacityMask(img)
}
}
}
Expand All @@ -378,15 +352,25 @@ func (r *SVG) RenderImage(img image.Image, m canvas.Matrix) {
m.ToSVG(r.height), img.Bounds().Size().X, img.Bounds().Size().Y, mimetype)

encoder := base64.NewEncoder(base64.StdEncoding, r.w)
if mimetype == "image/jpg" {
if err := jpeg.Encode(encoder, img, nil); err != nil {
panic(err)
}
} else {
if err := png.Encode(encoder, img); err != nil {
panic(err)
var err error
switch j := img.(type) {
case canvas.JPEGImage:
_, err = encoder.Write(j.JPEGBytes())
case canvas.PNGImage:
_, err = encoder.Write(j.PNGBytes())
default:
if mimetype == "image/jpg" {
err = jpeg.Encode(encoder, img, nil)
} else if mimetype == "image/png" {
err = png.Encode(encoder, img)
} else {
err = fmt.Errorf("unexpected mimetype: %s", mimetype)
}
}

if err != nil {
panic(err)
}
if err := encoder.Close(); err != nil {
panic(err)
}
Expand All @@ -397,3 +381,42 @@ func (r *SVG) RenderImage(img image.Image, m canvas.Matrix) {
r.writeClasses(r.w)
fmt.Fprintf(r.w, `"/>`)
}

func (r *SVG) renderOpacityMask(img image.Image) (image.Image, string) {
refMask := ""
hasMask := false
size := img.Bounds().Size()
opaque := image.NewRGBA(img.Bounds())
mask := image.NewGray(img.Bounds())
for y := 0; y < size.Y; y++ {
for x := 0; x < size.X; x++ {
R, G, B, A := img.At(x, y).RGBA()
if A != 0 {
r := byte((R * 65535 / A) >> 8)
g := byte((G * 65535 / A) >> 8)
b := byte((B * 65535 / A) >> 8)
opaque.SetRGBA(x, y, color.RGBA{r, g, b, 255})
mask.SetGray(x, y, color.Gray{byte(A >> 8)})
}
if A>>8 != 255 {
hasMask = true
}
}
}
if hasMask {
img = opaque
refMask = fmt.Sprintf("m%v", r.maskID)
r.maskID++

fmt.Fprintf(r.w, `<mask id="%s"><image width="%d" height="%d" xlink:href="data:image/jpg;base64,`, refMask, size.X, size.Y)
encoder := base64.NewEncoder(base64.StdEncoding, r.w)
if err := jpeg.Encode(encoder, mask, nil); err != nil {
panic(err)
}
if err := encoder.Close(); err != nil {
panic(err)
}
fmt.Fprintf(r.w, `"/></mask>`)
}
return img, refMask
}

0 comments on commit eed770c

Please sign in to comment.