Initial implementation of a simple graphics state handler / content stream processor. Multiple changes and fixes in colorspace handling. And more.

This commit is contained in:
Gunnsteinn Hall 2017-03-14 13:04:51 +00:00
parent 915a9c240d
commit 8ae4f6a63a
9 changed files with 1609 additions and 39 deletions

View File

@ -9,6 +9,7 @@
package contentstream
import (
"bytes"
"fmt"
. "github.com/unidoc/unidoc/pdf/core"
@ -19,6 +20,38 @@ type ContentStreamOperation struct {
Operand string
}
type ContentStreamOperations []*ContentStreamOperation
// Convert a set of content stream operations to a content stream byte presentation, i.e. the kind that can be
// stored as a PDF stream or string format.
func (this ContentStreamOperations) Bytes() []byte {
var buf bytes.Buffer
for _, op := range this {
if op == nil {
continue
}
if op.Operand == "BI" {
// Inline image requires special handling.
buf.WriteString(op.Operand + "\n")
buf.WriteString(op.Params[0].DefaultWriteString())
} else {
// Default handler.
for _, param := range op.Params {
buf.WriteString(param.DefaultWriteString())
buf.WriteString(" ")
}
buf.WriteString(op.Operand + "\n")
}
}
return buf.Bytes()
}
// Parses and extracts all text data in content streams and returns as a string.
// Does not take into account Encoding table, the output is simply the character codes.
func (this *ContentStreamParser) ExtractText() (string, error) {

View File

@ -0,0 +1,557 @@
package contentstream
import (
"errors"
"fmt"
"github.com/unidoc/unidoc/common"
. "github.com/unidoc/unidoc/pdf/core"
. "github.com/unidoc/unidoc/pdf/model"
)
// Basic graphics state implementation.
// Initially only implementing and tracking a portion of the information specified. Easy to add more.
type GraphicsState struct {
ColorspaceStroking PdfColorspace
ColorspaceNonStroking PdfColorspace
ColorStroking PdfColor
ColorNonStroking PdfColor
}
type GraphicStateStack []GraphicsState
func (gs *GraphicsState) String() string {
return fmt.Sprintf("%+v", gs)
}
func (gsStack *GraphicStateStack) Push(gs GraphicsState) {
*gsStack = append(*gsStack, gs)
}
func (gsStack *GraphicStateStack) Pop() GraphicsState {
gs := (*gsStack)[len(*gsStack)-1]
*gsStack = (*gsStack)[:len(*gsStack)-1]
return gs
}
// ContentStreamProcessor defines a data structure and methods for processing a content stream, keeping track of the
// current graphics state, and allowing external handlers to define their own functions as a part of the processing,
// for example rendering or extracting certain information.
type ContentStreamProcessor struct {
graphicsStack GraphicStateStack
operations []*ContentStreamOperation
graphicsState GraphicsState
handlers []HandlerEntry
currentIndex int
}
type HandlerFunc func(op *ContentStreamOperation, gs GraphicsState, resources *PdfPageResources) error
type HandlerEntry struct {
Condition HandlerConditionEnum
Operand string
Handler HandlerFunc
}
type HandlerConditionEnum int
func (this HandlerConditionEnum) All() bool {
return this == HandlerConditionEnumAllOperands
}
func (this HandlerConditionEnum) Operand() bool {
return this == HandlerConditionEnumOperand
}
const (
HandlerConditionEnumOperand HandlerConditionEnum = iota
HandlerConditionEnumAllOperands HandlerConditionEnum = iota
)
func NewContentStreamProcessor(ops []*ContentStreamOperation) *ContentStreamProcessor {
csp := ContentStreamProcessor{}
csp.graphicsStack = GraphicStateStack{}
// Set defaults..
gs := GraphicsState{}
csp.graphicsState = gs
csp.handlers = []HandlerEntry{}
csp.currentIndex = 0
csp.operations = ops
return &csp
}
func (csp *ContentStreamProcessor) AddHandler(condition HandlerConditionEnum, operand string, handler HandlerFunc) {
entry := HandlerEntry{}
entry.Condition = condition
entry.Operand = operand
entry.Handler = handler
csp.handlers = append(csp.handlers, entry)
}
func (csp *ContentStreamProcessor) getColorspace(name string, resources *PdfPageResources) (PdfColorspace, error) {
switch name {
case "DeviceGray":
return NewPdfColorspaceDeviceGray(), nil
case "DeviceRGB":
return NewPdfColorspaceDeviceRGB(), nil
case "DeviceCMYK":
return NewPdfColorspaceDeviceCMYK(), nil
case "Pattern":
return NewPdfColorspaceSpecialPattern(), nil
}
// Next check the colorspace dictionary.
cs, has := resources.ColorSpace.Colorspaces[name]
if has {
return cs, nil
}
// Lastly check other potential colormaps.
switch name {
case "CalGray":
return NewPdfColorspaceCalGray(), nil
case "CalRGB":
return NewPdfColorspaceCalRGB(), nil
case "Lab":
return NewPdfColorspaceLab(), nil
}
// Otherwise unsupported.
common.Log.Debug("Unknown colorspace requested: %s", name)
return nil, errors.New("Unsupported colorspace")
}
// Get initial color for a given colorspace.
func (csp *ContentStreamProcessor) getInitialColor(cs PdfColorspace) (PdfColor, error) {
switch cs := cs.(type) {
case *PdfColorspaceDeviceGray:
return NewPdfColorDeviceGray(0.0), nil
case *PdfColorspaceDeviceRGB:
return NewPdfColorDeviceRGB(0.0, 0.0, 0.0), nil
case *PdfColorspaceDeviceCMYK:
return NewPdfColorDeviceCMYK(0.0, 0.0, 0.0, 1.0), nil
case *PdfColorspaceLab:
l := 0.0
a := 0.0
b := 0.0
if cs.Range[0] > 0 {
l = cs.Range[0]
}
if cs.Range[2] > 0 {
a = cs.Range[2]
}
return NewPdfColorLab(l, a, b), nil
case *PdfColorspaceICCBased:
if cs.Alternate == nil {
return nil, errors.New("Alternate space not defined for ICC")
}
return csp.getInitialColor(cs.Alternate)
case *PdfColorspaceSpecialIndexed:
if cs.Base == nil {
return nil, errors.New("Indexed base not specified")
}
return csp.getInitialColor(cs.Base)
case *PdfColorspaceSpecialSeparation:
if cs.AlternateSpace == nil {
return nil, errors.New("Alternate space not specified")
}
return csp.getInitialColor(cs.AlternateSpace)
case *PdfColorspaceDeviceN:
if cs.AlternateSpace == nil {
return nil, errors.New("Alternate space not specified")
}
return csp.getInitialColor(cs.AlternateSpace)
case *PdfColorspaceSpecialPattern:
// TODO: Define color for pattern space.
return nil, errors.New("Initial color for pattern space undefined")
}
common.Log.Debug("Unable to determine initial color for unknown colorspace: %T", cs)
return nil, errors.New("Unsupported colorspace")
}
// Process the entire operations.
func (this *ContentStreamProcessor) Process(resources *PdfPageResources) error {
// Initialize graphics state
this.graphicsState.ColorspaceStroking = NewPdfColorspaceDeviceGray()
this.graphicsState.ColorspaceNonStroking = NewPdfColorspaceDeviceGray()
this.graphicsState.ColorStroking = NewPdfColorDeviceGray(0)
this.graphicsState.ColorNonStroking = NewPdfColorDeviceGray(0)
for _, op := range this.operations {
// Internal handling.
switch op.Operand {
case "q":
this.graphicsStack.Push(this.graphicsState)
case "Q":
this.graphicsState = this.graphicsStack.Pop()
// Color operations (Table 74 p. 179)
case "CS":
err := this.handleCommand_CS(op, resources)
if err != nil {
return err
}
case "cs":
err := this.handleCommand_cs(op, resources)
if err != nil {
return err
}
case "SC":
err := this.handleCommand_SC(op, resources)
if err != nil {
return err
}
case "SCN":
err := this.handleCommand_SCN(op, resources)
if err != nil {
return err
}
case "sc":
err := this.handleCommand_sc(op, resources)
if err != nil {
return err
}
case "scn":
err := this.handleCommand_scn(op, resources)
if err != nil {
return err
}
case "G":
err := this.handleCommand_G(op, resources)
if err != nil {
return err
}
case "g":
err := this.handleCommand_g(op, resources)
if err != nil {
return err
}
case "RG":
err := this.handleCommand_RG(op, resources)
if err != nil {
return err
}
case "rg":
err := this.handleCommand_rg(op, resources)
if err != nil {
return err
}
case "K":
err := this.handleCommand_K(op, resources)
if err != nil {
return err
}
case "k":
err := this.handleCommand_k(op, resources)
if err != nil {
return err
}
}
// Check if have external handler also, and process if so.
for _, entry := range this.handlers {
var err error
if entry.Condition.All() {
err = entry.Handler(op, this.graphicsState, resources)
} else if entry.Condition.Operand() && op.Operand == entry.Operand {
err = entry.Handler(op, this.graphicsState, resources)
}
if err != nil {
common.Log.Debug("Processor handler error: %v", err)
return err
}
}
}
return nil
}
// CS: Set the current color space for stroking operations.
func (csp *ContentStreamProcessor) handleCommand_CS(op *ContentStreamOperation, resources *PdfPageResources) error {
if len(op.Params) < 1 {
common.Log.Debug("Invalid cs command, skipping over")
return errors.New("Too few parameters")
}
if len(op.Params) > 1 {
common.Log.Debug("cs command with too many parameters - continuing")
return errors.New("Too many parameters")
}
name, ok := op.Params[0].(*PdfObjectName)
if !ok {
common.Log.Debug("ERROR: cs command with invalid parameter, skipping over")
return errors.New("Type check error")
}
// Set the current color space to use for stroking operations.
// Either device based or referring to resource dict.
cs, err := csp.getColorspace(string(*name), resources)
if err != nil {
return err
}
csp.graphicsState.ColorspaceStroking = cs
// Set initial color.
color, err := csp.getInitialColor(cs)
if err != nil {
return err
}
csp.graphicsState.ColorStroking = color
return nil
}
// cs: Set the current color space for non-stroking operations.
func (csp *ContentStreamProcessor) handleCommand_cs(op *ContentStreamOperation, resources *PdfPageResources) error {
if len(op.Params) < 1 {
common.Log.Debug("Invalid CS command, skipping over")
return errors.New("Too few parameters")
}
if len(op.Params) > 1 {
common.Log.Debug("CS command with too many parameters - continuing")
return errors.New("Too many parameters")
}
name, ok := op.Params[0].(*PdfObjectName)
if !ok {
common.Log.Debug("ERROR: CS command with invalid parameter, skipping over")
return errors.New("Type check error")
}
// Set the current color space to use for non-stroking operations.
// Either device based or referring to resource dict.
cs, err := csp.getColorspace(string(*name), resources)
if err != nil {
return err
}
csp.graphicsState.ColorspaceNonStroking = cs
// Set initial color.
color, err := csp.getInitialColor(cs)
if err != nil {
return err
}
csp.graphicsState.ColorNonStroking = color
return nil
}
// SC: Set the color to use for stroking operations in a device, CIE-based or Indexed colorspace. (not ICC based)
func (this *ContentStreamProcessor) handleCommand_SC(op *ContentStreamOperation, resources *PdfPageResources) error {
// For DeviceGray, CalGray, Indexed: one operand is required
// For DeviceRGB, CalRGB, Lab: 3 operands required
cs := this.graphicsState.ColorspaceStroking
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorStroking = color
return nil
}
// SCN: Same as SC but also supports Pattern, Separation, DeviceN and ICCBased color spaces.
func (this *ContentStreamProcessor) handleCommand_SCN(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := this.graphicsState.ColorspaceStroking
if _, isPattern := cs.(*PdfColorspaceSpecialPattern); isPattern {
// TODO handle pattern space spearately. See p. 180.
common.Log.Debug("Pattern not handled yet")
// Return error?
return errors.New("SCN - pattern not handled yet")
}
if len(op.Params) != this.graphicsState.ColorspaceStroking.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorStroking = color
return nil
}
// sc: Same as SC except used for non-stroking operations.
func (this *ContentStreamProcessor) handleCommand_sc(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := this.graphicsState.ColorspaceNonStroking
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorNonStroking = color
return nil
}
// scn: Same as SCN except used for non-stroking operations.
func (this *ContentStreamProcessor) handleCommand_scn(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := this.graphicsState.ColorspaceNonStroking
if _, isPattern := cs.(*PdfColorspaceSpecialPattern); isPattern {
// TODO handle pattern space spearately. See p. 180.
common.Log.Debug("Pattern not handled yet")
return errors.New("SCN - pattern not handled yet")
}
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorNonStroking = color
return nil
}
// G: Set the stroking colorspace to DeviceGray, and the color to the specified graylevel (range [0-1]).
// gray G
func (this *ContentStreamProcessor) handleCommand_G(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceGray()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceStroking = cs
this.graphicsState.ColorStroking = color
return nil
}
// g: Same as G, but for non-stroking colorspace and color (range [0-1]).
// gray g
func (this *ContentStreamProcessor) handleCommand_g(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceGray()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceNonStroking = cs
this.graphicsState.ColorNonStroking = color
return nil
}
// RG: Sets the stroking colorspace to DeviceRGB and the stroking color to r,g,b. [0-1] ranges.
// r g b RG
func (this *ContentStreamProcessor) handleCommand_RG(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceRGB()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceStroking = cs
this.graphicsState.ColorStroking = color
return nil
}
// rg: Same as RG but for non-stroking colorspace, color.
func (this *ContentStreamProcessor) handleCommand_rg(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceRGB()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceNonStroking = cs
this.graphicsState.ColorNonStroking = color
return nil
}
// K: Sets the stroking colorspace to DeviceCMYK and the stroking color to c,m,y,k. [0-1] ranges.
// c m y k K
func (this *ContentStreamProcessor) handleCommand_K(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceCMYK()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceStroking = cs
this.graphicsState.ColorStroking = color
return nil
}
// k: Same as K but for non-stroking colorspace, color.
func (this *ContentStreamProcessor) handleCommand_k(op *ContentStreamOperation, resources *PdfPageResources) error {
cs := NewPdfColorspaceDeviceCMYK()
if len(op.Params) != cs.GetNumComponents() {
common.Log.Debug("Invalid number of parameters for SC")
common.Log.Debug("Number %d not matching colorspace %T", len(op.Params), cs)
return errors.New("Invalid number of parameters")
}
color, err := cs.ColorFromPdfObjects(op.Params)
if err != nil {
return err
}
this.graphicsState.ColorspaceNonStroking = cs
this.graphicsState.ColorNonStroking = color
return nil
}

View File

@ -31,6 +31,42 @@ type ContentStreamInlineImage struct {
stream []byte
}
// Make a new content stream inline image object from an image.
func NewInlineImageFromImage(img Image, encoder StreamEncoder) (*ContentStreamInlineImage, error) {
if encoder == nil {
encoder = NewRawEncoder()
}
inlineImage := ContentStreamInlineImage{}
if img.ColorComponents == 1 {
inlineImage.ColorSpace = MakeName("DeviceGray")
} else if img.ColorComponents == 3 {
inlineImage.ColorSpace = MakeName("DeviceRGB")
} else if img.ColorComponents == 4 {
inlineImage.ColorSpace = MakeName("DeviceCMYK")
} else {
common.Log.Debug("Invalid number of color components for inline image: %d", img.ColorComponents)
return nil, errors.New("Invalid number of color components")
}
inlineImage.BitsPerComponent = MakeInteger(img.BitsPerComponent)
inlineImage.Width = MakeInteger(img.Width)
inlineImage.Height = MakeInteger(img.Height)
encoded, err := encoder.EncodeBytes(img.Data)
if err != nil {
return nil, err
}
inlineImage.stream = encoded
filterName := encoder.GetFilterName()
if len(filterName) > 0 {
inlineImage.Filter = MakeName(filterName)
}
// XXX/FIXME: Add decode params?
return &inlineImage, nil
}
func (this *ContentStreamInlineImage) String() string {
s := fmt.Sprintf("InlineImage(len=%d)\n", len(this.stream))
if this.BitsPerComponent != nil {
@ -107,6 +143,7 @@ func (this *ContentStreamInlineImage) DefaultWriteString() string {
output.WriteString("ID ")
output.Write(this.stream)
output.WriteString("\n")
return output.String()
}

View File

@ -27,18 +27,17 @@ type ContentStreamParser struct {
// stream string.
func NewContentStreamParser(contentStr string) *ContentStreamParser {
// Each command has parameters and an operand (command).
parser := ContentStreamParser{}
buffer := bytes.NewBufferString(contentStr)
buffer := bytes.NewBufferString(contentStr + "\n") // Add newline at end to get last operand without EOF error.
parser.reader = bufio.NewReader(buffer)
return &parser
}
// Parses all commands in content stream, returning a list of operation data.
func (this *ContentStreamParser) Parse() ([]*ContentStreamOperation, error) {
operations := []*ContentStreamOperation{}
func (this *ContentStreamParser) Parse() (ContentStreamOperations, error) {
operations := ContentStreamOperations{}
for {
operation := ContentStreamOperation{}

View File

@ -704,6 +704,7 @@ type DCTEncoder struct {
BitsPerComponent int // 8 or 16 bit
Width int
Height int
Quality int
}
// Make a new DCT encoder with default parameters.
@ -713,6 +714,8 @@ func NewDCTEncoder() *DCTEncoder {
encoder.ColorComponents = 3
encoder.BitsPerComponent = 8
encoder.Quality = 75
return encoder
}
@ -796,6 +799,7 @@ func newDCTEncoderFromStream(streamObj *PdfObjectStream, multiEnc *MultiEncoder)
encoder.Width = cfg.Width
encoder.Height = cfg.Height
common.Log.Trace("DCT Encoder: %+v", encoder)
encoder.Quality = 75
return encoder, nil
}
@ -987,10 +991,12 @@ func (this *DCTEncoder) EncodeBytes(data []byte) ([]byte, error) {
}
}
// Use full quality. N.B. even 100 is lossy, as still is transformed, but as good as it gets for DCT.
// TODO: Add option to override the quality, can be used in compression.
// The quality is specified from 0-100 (with 100 being the best quality) in the DCT structure.
// N.B. even 100 is lossy, as still is transformed, but as good as it gets for DCT.
// This is not related to the DPI, but rather inherent transformation losses.
opt := jpeg.Options{}
opt.Quality = 100
opt.Quality = this.Quality
var buf bytes.Buffer
err := jpeg.Encode(&buf, img, &opt)

File diff suppressed because it is too large Load Diff

View File

@ -947,6 +947,20 @@ func (r *PdfPageResources) GetXObjectByName(keyName string) (*PdfObjectStream, X
}
}
func (r *PdfPageResources) setXObjectByName(keyName string, stream *PdfObjectStream) error {
if r.XObject == nil {
r.XObject = &PdfObjectDictionary{}
}
xresDict, has := r.XObject.(*PdfObjectDictionary)
if !has {
return errors.New("Type check error")
}
(*xresDict)[PdfObjectName(keyName)] = stream
return nil
}
func (r *PdfPageResources) GetXObjectImageByName(keyName string) (*XObjectImage, error) {
stream, xtype := r.GetXObjectByName(keyName)
if stream == nil {
@ -964,6 +978,12 @@ func (r *PdfPageResources) GetXObjectImageByName(keyName string) (*XObjectImage,
return ximg, nil
}
func (r *PdfPageResources) SetXObjectImageByName(keyName string, ximg *XObjectImage) error {
stream := ximg.ToPdfObject().(*PdfObjectStream)
err := r.setXObjectByName(keyName, stream)
return err
}
func (r *PdfPageResources) GetXObjectFormByName(keyName string) (*XObjectForm, error) {
stream, xtype := r.GetXObjectByName(keyName)
if stream == nil {
@ -980,3 +1000,9 @@ func (r *PdfPageResources) GetXObjectFormByName(keyName string) (*XObjectForm, e
return xform, nil
}
func (r *PdfPageResources) SetXObjectFormByName(keyName string, xform *XObjectForm) error {
stream := xform.ToPdfObject().(*PdfObjectStream)
err := r.setXObjectByName(keyName, stream)
return err
}

View File

@ -28,6 +28,20 @@ func getNumberAsFloat(obj PdfObject) (float64, error) {
return 0, errors.New("Not a number")
}
// Convert a list of pdf objects representing floats or integers to a slice of float64 values.
func getNumbersAsFloat(objects []PdfObject) ([]float64, error) {
floats := []float64{}
for _, obj := range objects {
val, err := getNumberAsFloat(obj)
if err != nil {
return nil, err
}
floats = append(floats, val)
}
return floats, nil
}
// Cases where expecting an integer, but some implementations actually
// store the number in a floating point format.
func getNumberAsInt64(obj PdfObject) (int64, error) {

View File

@ -147,6 +147,23 @@ func (xform *XObjectForm) GetContentStream() ([]byte, error) {
return decoded, nil
}
// Update the content stream, encode if needed.
func (xform *XObjectForm) SetContentStream(content []byte) error {
encoded := content
if xform.Filter != nil {
enc, err := xform.Filter.EncodeBytes(encoded)
if err != nil {
return err
}
encoded = enc
}
xform.primitive.Stream = encoded
xform.primitive.PdfObjectDictionary.Set("Length", MakeInteger(int64(len(encoded))))
return nil
}
// Return a stream object.
func (xform *XObjectForm) ToPdfObject() PdfObject {
stream := xform.primitive
@ -221,7 +238,7 @@ func NewXObjectImage() *XObjectImage {
// Creates a new XObject Image from an image object with default
// options.
func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, error) {
func NewXObjectImageFromImage(name PdfObjectName, img *Image, cs PdfColorspace) (*XObjectImage, error) {
xobj := NewXObjectImage()
xobj.Name = &name
@ -236,10 +253,21 @@ func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, er
// Bits.
xobj.BitsPerComponent = &img.BitsPerComponent
//xobj.ColorSpace = MakeName("DeviceRGB")
xobj.ColorSpace = NewPdfColorspaceDeviceRGB()
// Guess colorspace if not explicitly set.
if cs == nil {
if img.ColorComponents == 1 {
xobj.ColorSpace = NewPdfColorspaceDeviceGray()
} else if img.ColorComponents == 3 {
xobj.ColorSpace = NewPdfColorspaceDeviceRGB()
} else if img.ColorComponents == 4 {
xobj.ColorSpace = NewPdfColorspaceDeviceCMYK()
} else {
return nil, errors.New("Colorspace undefined")
}
} else {
xobj.ColorSpace = cs
// define color space?
}
return xobj, nil
}
@ -350,6 +378,38 @@ func NewXObjectImageFromStream(stream *PdfObjectStream) (*XObjectImage, error) {
return img, nil
}
// Update XObject Image with new image data.
func (ximg *XObjectImage) SetImage(img *Image, cs PdfColorspace) error {
encoded, err := ximg.Filter.EncodeBytes(img.Data)
if err != nil {
return err
}
ximg.Stream = encoded
// Width, height and bits.
ximg.Width = &img.Width
ximg.Height = &img.Height
ximg.BitsPerComponent = &img.BitsPerComponent
// Guess colorspace if not explicitly set.
if cs == nil {
if img.ColorComponents == 1 {
ximg.ColorSpace = NewPdfColorspaceDeviceGray()
} else if img.ColorComponents == 3 {
ximg.ColorSpace = NewPdfColorspaceDeviceRGB()
} else if img.ColorComponents == 4 {
ximg.ColorSpace = NewPdfColorspaceDeviceCMYK()
} else {
return errors.New("Colorspace undefined")
}
} else {
ximg.ColorSpace = cs
}
return nil
}
// Compress with default settings, updating the underlying stream also.
// XXX/TODO: Add flate encoding as an option (although lossy). Need to be able
// to set default settings and override.