Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,8 +353,11 @@ func contentStream(data []byte, extra reader.Dict) *reader.Stream {
func (d *Doc) madeContent(w *reader.Writer, p Page, area [4]float64) ([]byte, reader.Dict) {
var content []byte
resources := reader.Dict{}
if p.tiles != nil {
switch {
case p.tiles != nil:
content, resources = d.composeContent(w, p)
case p.picture != nil:
content, resources = d.pictureContent(w, p, area)
}
if len(p.marks) > 0 {
stamp, fonts, alpha := d.stampContent(p, area)
Expand Down
3 changes: 3 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ type Page struct {
blank bool
size [2]float64

// picture is set when the page is one image made into a page.
picture *picture

// marks is the text drawn on top of whatever the page already shows.
marks []stampInstance
}
Expand Down
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ require (
)

require (
github.com/ajroetker/go-highway v0.0.4 // indirect
github.com/ajroetker/go-jpeg2000 v0.0.2 // indirect
github.com/sergeymakinen/go-bmp v1.0.0 // indirect
github.com/sergeymakinen/go-ico v1.0.0 // indirect
github.com/tannevaled/gobig2 v0.1.0 // indirect
golang.org/x/image v0.45.0 // indirect
golang.org/x/sys v0.47.0 // indirect
)

require (
github.com/go-gfx/gfx v0.16.0
github.com/go-opentype/fonts v0.9.0 // indirect
github.com/go-opentype/opentype v0.12.0 // indirect
github.com/go-pdfkit/pdffont v0.3.1 // indirect
Expand Down
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
github.com/ajroetker/go-highway v0.0.4 h1:RDQo+9OhTXI6BFctLo+5gYpHNbb92VYJ0ObnR4l6xAQ=
github.com/ajroetker/go-highway v0.0.4/go.mod h1:C/zYPNSSpOaraejY89FUTZTyQNEhi5+rEbU0LjlqJeU=
github.com/ajroetker/go-jpeg2000 v0.0.2 h1:ni8brffZrci4Kacx3nM5d92ipmTDfak84KgHYi6IxFw=
github.com/ajroetker/go-jpeg2000 v0.0.2/go.mod h1:7ld88W47lZy0x8gRQesRGAonDPOpr6ev8rckjCAfbzE=
github.com/go-gfx/gfx v0.16.0 h1:HYlQrz2kHm+Vg1aBotnO6O9yfadNqWVrPK+b0uqxtKg=
github.com/go-gfx/gfx v0.16.0/go.mod h1:VAK6hgCgkhT3j3ek2K7G8zDfkCPD3RCF45UbcRlCV8Q=
github.com/go-opentype/fonts v0.9.0 h1:slB6OB3riLyUPrOxqXe0s6/AzdenF1TDvCN8N87hhQk=
github.com/go-opentype/fonts v0.9.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g=
github.com/go-opentype/opentype v0.12.0 h1:wBlcDi+3ZaNZXEt5z+Ixr11/cYYwi5W+jX6yTl/qr1I=
Expand All @@ -10,3 +16,13 @@ github.com/go-pdfkit/pdffont v0.3.1 h1:+K7opHGRERDZ5EMkA12s1GT48t8GpncArhnyzyc5b
github.com/go-pdfkit/pdffont v0.3.1/go.mod h1:eFJ/7t9AvcX76KY5qNJMMKetz8hUUUdVM1qSWLi3S1g=
github.com/go-pdfkit/reader v0.6.0 h1:KAabNOYUcTlZlNBTbG9bEhWP1NiZhjuhzUdavdTdfes=
github.com/go-pdfkit/reader v0.6.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8=
github.com/sergeymakinen/go-bmp v1.0.0 h1:SdGTzp9WvCV0A1V0mBeaS7kQAwNLdVJbmHlqNWq0R+M=
github.com/sergeymakinen/go-bmp v1.0.0/go.mod h1:/mxlAQZRLxSvJFNIEGGLBE/m40f3ZnUifpgVDlcUIEY=
github.com/sergeymakinen/go-ico v1.0.0 h1:uL3khgvKkY6WfAetA+RqsguClBuu7HpvBB/nq/Jvr80=
github.com/sergeymakinen/go-ico v1.0.0/go.mod h1:wQ47mTczswBO5F0NoDt7O0IXgnV4Xy3ojrroMQzyhUk=
github.com/tannevaled/gobig2 v0.1.0 h1:9PdMvmnmYQURlUF40zt8t35Wnrz3KhZrwfxgCMvQc04=
github.com/tannevaled/gobig2 v0.1.0/go.mod h1:X0S1H+N35kg6zYgVYRqGMsGj3C35zs+iA2r1MXyPcV4=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
155 changes: 155 additions & 0 deletions picture.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) 2026, the go-pdfkit/ops authors
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

package ops

import (
"bytes"
"compress/zlib"
"fmt"

"github.com/go-gfx/gfx/codec"
"github.com/go-gfx/gfx/raster"
"github.com/go-pdfkit/reader"
)

// picture is one image made into a page.
type picture struct {
// stored is the image as the file held it, when that is a format a PDF
// can carry as it stands. filter names it.
stored []byte
filter reader.Name
// pix is the decoded image, for everything else.
pix *raster.Image
width, height int
}

// Picture adds a page holding one image, the size of the image.
//
// This is the other half of drawing a page: a scanner, a camera and a
// screenshot all produce a picture, and what people want of a PDF toolkit is
// to be handed one back with the picture in it.
//
// Every format go-gfx/gfx reads is accepted, which is more than a PDF can
// carry. A JPEG is put in as it stands, because a PDF carries JPEG and
// re-encoding one loses a little of it for nothing; everything else is decoded
// and written as samples, compressed. An image with any transparency in it
// gets a soft mask, so a PNG drawn over the page's white does not come out
// with black behind it.
//
// dpi says how many of the image's pixels go into an inch of paper; 0 means
// 72, one point per pixel. A photograph from a telephone at 72 is a page the
// size of a wall, and at 300 it is a photograph.
func (d *Doc) Picture(data []byte, dpi float64) error {
if dpi <= 0 {
dpi = 72
}
p, err := readPicture(data)
if err != nil {
return err
}
scale := 72 / dpi
d.pages = append(d.pages, Page{
picture: p,
size: [2]float64{float64(p.width) * scale, float64(p.height) * scale},
})
return nil
}

// readPicture works out how an image will be carried.
//
// Every format is decoded, even the one that is then carried undecoded, because
// the page has to be the size of the picture and only the picture says what
// that is.
func readPicture(data []byte) (*picture, error) {
format := codec.Sniff(data)
if format == codec.Unknown {
return nil, fmt.Errorf("ops: these bytes are not a picture in any format that can be read")
}
img, err := codec.Decode(data)
if err != nil {
return nil, fmt.Errorf("ops: this picture cannot be read: %w", err)
}
// A picture of no size is a page of no size. GIF, BMP and JPEG all decode
// one without complaining — they are not malformed files, they are empty
// ones — so the check is here rather than left to the decoders.
if img.W <= 0 || img.H <= 0 {
return nil, fmt.Errorf("ops: a picture of %d by %d is not one", img.W, img.H)
}
if format == codec.JPEG {
// A PDF carries JPEG itself, and re-encoding one would lose a little
// of it for nothing.
return &picture{stored: data, filter: "DCTDecode",
width: img.W, height: img.H}, nil
}
return &picture{pix: img, width: img.W, height: img.H}, nil
}

// pictureContent writes the image and returns the operators that draw it over
// the whole page, with the resources they need.
func (d *Doc) pictureContent(w *reader.Writer, p Page, area [4]float64) ([]byte, reader.Dict) {
pic := p.picture
dict := reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(pic.width), "Height": reader.Integer(pic.height),
"BitsPerComponent": reader.Integer(8),
"ColorSpace": reader.Name("DeviceRGB"),
}
var ref reader.Object
if pic.stored != nil {
dict["Filter"] = pic.filter
ref = w.Add(&reader.Stream{Dict: dict, Raw: pic.stored})
} else {
rgb, alpha := split(pic.pix)
if alpha != nil {
ref = w.Add(&reader.Stream{Dict: reader.Dict{
"Type": reader.Name("XObject"), "Subtype": reader.Name("Image"),
"Width": reader.Integer(pic.width), "Height": reader.Integer(pic.height),
"BitsPerComponent": reader.Integer(8),
"ColorSpace": reader.Name("DeviceGray"),
"Filter": reader.Name("FlateDecode"),
}, Raw: deflate(alpha)})
dict["SMask"] = ref
}
dict["Filter"] = reader.Name("FlateDecode")
ref = w.Add(&reader.Stream{Dict: dict, Raw: deflate(rgb)})
}
content := fmt.Sprintf("q %g 0 0 %g %g %g cm /Pic Do Q",
area[2]-area[0], area[3]-area[1], area[0], area[1])
return []byte(content), reader.Dict{"XObject": reader.Dict{"Pic": ref}}
}

// split separates an image into its colours and its transparency. The alpha is
// nil when every pixel is opaque, which is most pictures and saves carrying a
// mask that says nothing.
func split(img *raster.Image) (rgb, alpha []byte) {
n := img.W * img.H
rgb = make([]byte, n*3)
opaque := true
for i := 0; i < n; i++ {
rgb[i*3], rgb[i*3+1], rgb[i*3+2] = img.Pix[i*4], img.Pix[i*4+1], img.Pix[i*4+2]
if img.Pix[i*4+3] != 255 {
opaque = false
}
}
if opaque {
return rgb, nil
}
alpha = make([]byte, n)
for i := 0; i < n; i++ {
alpha[i] = img.Pix[i*4+3]
}
return rgb, alpha
}

// deflate compresses samples the way a PDF carries them.
func deflate(data []byte) []byte {
var buf bytes.Buffer
zw := zlib.NewWriter(&buf)
// A bytes.Buffer never fails to take bytes, and Close only flushes.
zw.Write(data)
zw.Close()
return buf.Bytes()
}
Loading
Loading