mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-02 22:17:06 +08:00
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:
parent
915a9c240d
commit
8ae4f6a63a
@ -9,6 +9,7 @@
|
|||||||
package contentstream
|
package contentstream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
. "github.com/unidoc/unidoc/pdf/core"
|
. "github.com/unidoc/unidoc/pdf/core"
|
||||||
@ -19,6 +20,38 @@ type ContentStreamOperation struct {
|
|||||||
Operand string
|
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.
|
// 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.
|
// Does not take into account Encoding table, the output is simply the character codes.
|
||||||
func (this *ContentStreamParser) ExtractText() (string, error) {
|
func (this *ContentStreamParser) ExtractText() (string, error) {
|
||||||
|
557
pdf/contentstream/graphics.go
Normal file
557
pdf/contentstream/graphics.go
Normal 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
|
||||||
|
}
|
@ -31,6 +31,42 @@ type ContentStreamInlineImage struct {
|
|||||||
stream []byte
|
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 {
|
func (this *ContentStreamInlineImage) String() string {
|
||||||
s := fmt.Sprintf("InlineImage(len=%d)\n", len(this.stream))
|
s := fmt.Sprintf("InlineImage(len=%d)\n", len(this.stream))
|
||||||
if this.BitsPerComponent != nil {
|
if this.BitsPerComponent != nil {
|
||||||
@ -107,6 +143,7 @@ func (this *ContentStreamInlineImage) DefaultWriteString() string {
|
|||||||
|
|
||||||
output.WriteString("ID ")
|
output.WriteString("ID ")
|
||||||
output.Write(this.stream)
|
output.Write(this.stream)
|
||||||
|
output.WriteString("\n")
|
||||||
|
|
||||||
return output.String()
|
return output.String()
|
||||||
}
|
}
|
||||||
|
@ -27,18 +27,17 @@ type ContentStreamParser struct {
|
|||||||
// stream string.
|
// stream string.
|
||||||
func NewContentStreamParser(contentStr string) *ContentStreamParser {
|
func NewContentStreamParser(contentStr string) *ContentStreamParser {
|
||||||
// Each command has parameters and an operand (command).
|
// Each command has parameters and an operand (command).
|
||||||
|
|
||||||
parser := ContentStreamParser{}
|
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)
|
parser.reader = bufio.NewReader(buffer)
|
||||||
|
|
||||||
return &parser
|
return &parser
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses all commands in content stream, returning a list of operation data.
|
// Parses all commands in content stream, returning a list of operation data.
|
||||||
func (this *ContentStreamParser) Parse() ([]*ContentStreamOperation, error) {
|
func (this *ContentStreamParser) Parse() (ContentStreamOperations, error) {
|
||||||
operations := []*ContentStreamOperation{}
|
operations := ContentStreamOperations{}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
operation := ContentStreamOperation{}
|
operation := ContentStreamOperation{}
|
||||||
|
@ -704,6 +704,7 @@ type DCTEncoder struct {
|
|||||||
BitsPerComponent int // 8 or 16 bit
|
BitsPerComponent int // 8 or 16 bit
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
|
Quality int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a new DCT encoder with default parameters.
|
// Make a new DCT encoder with default parameters.
|
||||||
@ -713,6 +714,8 @@ func NewDCTEncoder() *DCTEncoder {
|
|||||||
encoder.ColorComponents = 3
|
encoder.ColorComponents = 3
|
||||||
encoder.BitsPerComponent = 8
|
encoder.BitsPerComponent = 8
|
||||||
|
|
||||||
|
encoder.Quality = 75
|
||||||
|
|
||||||
return encoder
|
return encoder
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -796,6 +799,7 @@ func newDCTEncoderFromStream(streamObj *PdfObjectStream, multiEnc *MultiEncoder)
|
|||||||
encoder.Width = cfg.Width
|
encoder.Width = cfg.Width
|
||||||
encoder.Height = cfg.Height
|
encoder.Height = cfg.Height
|
||||||
common.Log.Trace("DCT Encoder: %+v", encoder)
|
common.Log.Trace("DCT Encoder: %+v", encoder)
|
||||||
|
encoder.Quality = 75
|
||||||
|
|
||||||
return encoder, nil
|
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.
|
// The quality is specified from 0-100 (with 100 being the best quality) in the DCT structure.
|
||||||
// TODO: Add option to override the quality, can be used in compression.
|
// 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 := jpeg.Options{}
|
||||||
opt.Quality = 100
|
opt.Quality = this.Quality
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := jpeg.Encode(&buf, img, &opt)
|
err := jpeg.Encode(&buf, img, &opt)
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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) {
|
func (r *PdfPageResources) GetXObjectImageByName(keyName string) (*XObjectImage, error) {
|
||||||
stream, xtype := r.GetXObjectByName(keyName)
|
stream, xtype := r.GetXObjectByName(keyName)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
@ -964,6 +978,12 @@ func (r *PdfPageResources) GetXObjectImageByName(keyName string) (*XObjectImage,
|
|||||||
return ximg, nil
|
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) {
|
func (r *PdfPageResources) GetXObjectFormByName(keyName string) (*XObjectForm, error) {
|
||||||
stream, xtype := r.GetXObjectByName(keyName)
|
stream, xtype := r.GetXObjectByName(keyName)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
@ -980,3 +1000,9 @@ func (r *PdfPageResources) GetXObjectFormByName(keyName string) (*XObjectForm, e
|
|||||||
|
|
||||||
return xform, nil
|
return xform, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *PdfPageResources) SetXObjectFormByName(keyName string, xform *XObjectForm) error {
|
||||||
|
stream := xform.ToPdfObject().(*PdfObjectStream)
|
||||||
|
err := r.setXObjectByName(keyName, stream)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
@ -28,6 +28,20 @@ func getNumberAsFloat(obj PdfObject) (float64, error) {
|
|||||||
return 0, errors.New("Not a number")
|
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
|
// Cases where expecting an integer, but some implementations actually
|
||||||
// store the number in a floating point format.
|
// store the number in a floating point format.
|
||||||
func getNumberAsInt64(obj PdfObject) (int64, error) {
|
func getNumberAsInt64(obj PdfObject) (int64, error) {
|
||||||
|
@ -147,6 +147,23 @@ func (xform *XObjectForm) GetContentStream() ([]byte, error) {
|
|||||||
return decoded, nil
|
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.
|
// Return a stream object.
|
||||||
func (xform *XObjectForm) ToPdfObject() PdfObject {
|
func (xform *XObjectForm) ToPdfObject() PdfObject {
|
||||||
stream := xform.primitive
|
stream := xform.primitive
|
||||||
@ -221,7 +238,7 @@ func NewXObjectImage() *XObjectImage {
|
|||||||
|
|
||||||
// Creates a new XObject Image from an image object with default
|
// Creates a new XObject Image from an image object with default
|
||||||
// options.
|
// options.
|
||||||
func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, error) {
|
func NewXObjectImageFromImage(name PdfObjectName, img *Image, cs PdfColorspace) (*XObjectImage, error) {
|
||||||
xobj := NewXObjectImage()
|
xobj := NewXObjectImage()
|
||||||
|
|
||||||
xobj.Name = &name
|
xobj.Name = &name
|
||||||
@ -236,10 +253,21 @@ func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, er
|
|||||||
// Bits.
|
// Bits.
|
||||||
xobj.BitsPerComponent = &img.BitsPerComponent
|
xobj.BitsPerComponent = &img.BitsPerComponent
|
||||||
|
|
||||||
//xobj.ColorSpace = MakeName("DeviceRGB")
|
// Guess colorspace if not explicitly set.
|
||||||
|
if cs == nil {
|
||||||
|
if img.ColorComponents == 1 {
|
||||||
|
xobj.ColorSpace = NewPdfColorspaceDeviceGray()
|
||||||
|
} else if img.ColorComponents == 3 {
|
||||||
xobj.ColorSpace = NewPdfColorspaceDeviceRGB()
|
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
|
return xobj, nil
|
||||||
}
|
}
|
||||||
@ -350,6 +378,38 @@ func NewXObjectImageFromStream(stream *PdfObjectStream) (*XObjectImage, error) {
|
|||||||
return img, nil
|
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.
|
// Compress with default settings, updating the underlying stream also.
|
||||||
// XXX/TODO: Add flate encoding as an option (although lossy). Need to be able
|
// XXX/TODO: Add flate encoding as an option (although lossy). Need to be able
|
||||||
// to set default settings and override.
|
// to set default settings and override.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user