Skip to content

Commit

Permalink
Merge 80997cd into 83aba49
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverpool committed Feb 10, 2021
2 parents 83aba49 + 80997cd commit 749bd00
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 61 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
37 changes: 37 additions & 0 deletions image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package canvas

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

// Image allows the renderer to optimize specific cases
type Image struct {
image.Image
Bytes []byte
Mimetype string // image/png or image/jpeg for instance
}

// NewJPEGImage parses a reader to later give access to the JPEG raw bytes.
func NewJPEGImage(r io.Reader) (Image, error) {
return newImage("image/jpeg", jpeg.Decode, r)
}

// NewPNGImage parses a reader to later give access to the PNG raw bytes
func NewPNGImage(r io.Reader) (Image, error) {
return newImage("image/png", png.Decode, r)
}

func newImage(mimetype string, decode func(io.Reader) (image.Image, error), r io.Reader) (Image, error) {
var buffer bytes.Buffer
r = io.TeeReader(r, &buffer)
img, err := decode(r)
return Image{
Image: img,
Bytes: buffer.Bytes(),
Mimetype: mimetype,
}, err
}
80 changes: 72 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 1 < len(filters) {
panic("pdfFilterDCT can't be combined with other filters")
}
b2.Write(b)
}
b = b2.Bytes()
}
Expand Down Expand Up @@ -940,6 +950,67 @@ func (w *pdfPageWriter) DrawImage(img image.Image, enc canvas.ImageEncoding, m c
}

func (w *pdfPageWriter) embedImage(img image.Image, enc canvas.ImageEncoding) pdfName {
var stream pdfStream
if i, ok := img.(canvas.Image); ok && i.Mimetype == "image/jpeg" && 0 < len(i.Bytes) {
stream = w.jpegStream(i)
} else {
stream = w.imageStream(img)
}

ref := w.pdf.writeObject(stream)
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.Image) pdfStream {
// ignore progressive jpeg (contains 0xff 0xc2 marker)
markerStarted := false
for _, b := range img.Bytes {
if markerStarted && b == 0xc2 {
// fallback to generic imageStream
return w.imageStream(img)
}
markerStarted = (b == 0xff)
}

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.Bytes,
}
}

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 +1060,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
136 changes: 85 additions & 51 deletions svg/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,61 +331,17 @@ 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 {
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>`)
}
}
}
size := img.Bounds().Size()
writeTo, refMask, mimetype := r.encodableImage(img)

m = m.Translate(0.0, float64(img.Bounds().Size().Y))
m = m.Translate(0.0, float64(size.Y))
fmt.Fprintf(r.w, `<image transform="%s" width="%d" height="%d" xlink:href="data:%s;base64,`,
m.ToSVG(r.height), img.Bounds().Size().X, img.Bounds().Size().Y, mimetype)
m.ToSVG(r.height), size.X, 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)
}
err := writeTo(encoder)
if err != nil {
panic(err)
}
if err := encoder.Close(); err != nil {
panic(err)
Expand All @@ -397,3 +353,81 @@ func (r *SVG) RenderImage(img image.Image, m canvas.Matrix) {
r.writeClasses(r.w)
fmt.Fprintf(r.w, `"/>`)
}

// return a WriterTo, a refMask and a mimetype
func (r *SVG) encodableImage(img image.Image) (func(io.Writer) error, string, string) {
if cimg, ok := img.(canvas.Image); ok && 0 < len(cimg.Bytes) {
if cimg.Mimetype == "image/jpeg" || cimg.Mimetype == "image/png" {
return func(w io.Writer) error {
_, err := w.Write(cimg.Bytes)
return err
}, "", cimg.Mimetype
}
}

// lossy: jpeg
if r.imgEnc == canvas.Lossy {
var refMask string
if opaqueImg, ok := img.(interface{ Opaque() bool }); !ok || !opaqueImg.Opaque() {
img, refMask = r.renderOpacityMask(img)
}
return func(w io.Writer) error {
return jpeg.Encode(w, img, nil)
}, refMask, "image/jpeg"
}

// lossless: png
return func(w io.Writer) error {
return png.Encode(w, img)
}, "", "image/png"
}

func (r *SVG) renderOpacityMask(img image.Image) (image.Image, string) {
opaque, mask := splitImageAlphaChannel(img)
if mask == nil {
return opaque, ""
}

refMask := fmt.Sprintf("m%v", r.maskID)
r.maskID++

size := img.Bounds().Size()
fmt.Fprintf(r.w, `<mask id="%s"><image width="%d" height="%d" xlink:href="data:image/jpeg;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 opaque, refMask
}

func splitImageAlphaChannel(img image.Image) (image.Image, image.Image) {
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 {
return img, nil
}

return opaque, mask
}

0 comments on commit 749bd00

Please sign in to comment.