2017-04-05 18:05:38 +00:00
|
|
|
|
/*
|
|
|
|
|
* This file is subject to the terms and conditions defined in
|
|
|
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
|
|
|
*/
|
|
|
|
|
|
2017-03-14 13:04:51 +00:00
|
|
|
|
package contentstream
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2018-08-22 12:29:34 +10:00
|
|
|
|
"fmt"
|
2018-09-19 11:12:59 +10:00
|
|
|
|
"math"
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
"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
|
2018-08-22 12:29:34 +10:00
|
|
|
|
CTM Matrix
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
type Orientation int
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
OrientationPortrait Orientation = iota
|
|
|
|
|
OrientationLandscape
|
|
|
|
|
)
|
|
|
|
|
|
2017-03-14 13:04:51 +00:00
|
|
|
|
type GraphicStateStack []GraphicsState
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// Transform returns coordinates x, y transformed by the CTM
|
|
|
|
|
func (gs *GraphicsState) Transform(x, y float64) (float64, float64) {
|
|
|
|
|
// xp, yp := gs.CTM.Transform(x, y)
|
|
|
|
|
// fmt.Printf("Transform. %5.1f,%5.1f->%5.1f,%5.1f %+v\n", x, y, xp, yp, gs.CTM)
|
|
|
|
|
return gs.CTM.Transform(x, y)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the likely page orientation given the CTM
|
|
|
|
|
func (gs *GraphicsState) PageOrientation() Orientation {
|
|
|
|
|
return gs.CTM.pageOrientation()
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-14 13:04:51 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp HandlerConditionEnum) All() bool {
|
|
|
|
|
return csp == HandlerConditionEnumAllOperands
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp HandlerConditionEnum) Operand() bool {
|
|
|
|
|
return csp == HandlerConditionEnumOperand
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2017-08-04 17:47:55 +10:00
|
|
|
|
case *PdfColorspaceCalGray:
|
|
|
|
|
return NewPdfColorCalGray(0.0), nil
|
|
|
|
|
case *PdfColorspaceCalRGB:
|
|
|
|
|
return NewPdfColorCalRGB(0.0, 0.0, 0.0), nil
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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 {
|
2017-03-24 11:31:20 +00:00
|
|
|
|
// Alternate not defined.
|
|
|
|
|
// Try to fall back to DeviceGray, DeviceRGB or DeviceCMYK.
|
|
|
|
|
common.Log.Trace("ICC Based not defined - attempting fall back (N = %d)", cs.N)
|
|
|
|
|
if cs.N == 1 {
|
|
|
|
|
common.Log.Trace("Falling back to DeviceGray")
|
|
|
|
|
return csp.getInitialColor(NewPdfColorspaceDeviceGray())
|
|
|
|
|
} else if cs.N == 3 {
|
|
|
|
|
common.Log.Trace("Falling back to DeviceRGB")
|
|
|
|
|
return csp.getInitialColor(NewPdfColorspaceDeviceRGB())
|
|
|
|
|
} else if cs.N == 4 {
|
|
|
|
|
common.Log.Trace("Falling back to DeviceCMYK")
|
|
|
|
|
return csp.getInitialColor(NewPdfColorspaceDeviceCMYK())
|
|
|
|
|
} else {
|
|
|
|
|
return nil, errors.New("Alternate space not defined for ICC")
|
|
|
|
|
}
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
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:
|
2017-04-04 05:51:58 +00:00
|
|
|
|
// FIXME/check: A pattern does not have an initial color...
|
|
|
|
|
return nil, nil
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
common.Log.Debug("Unable to determine initial color for unknown colorspace: %T", cs)
|
|
|
|
|
return nil, errors.New("Unsupported colorspace")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the entire operations.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) Process(resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
// Initialize graphics state
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceStroking = NewPdfColorspaceDeviceGray()
|
|
|
|
|
csp.graphicsState.ColorspaceNonStroking = NewPdfColorspaceDeviceGray()
|
|
|
|
|
csp.graphicsState.ColorStroking = NewPdfColorDeviceGray(0)
|
|
|
|
|
csp.graphicsState.ColorNonStroking = NewPdfColorDeviceGray(0)
|
|
|
|
|
csp.graphicsState.CTM = IdentityMatrix()
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
for _, op := range csp.operations {
|
2017-08-07 20:21:35 +00:00
|
|
|
|
var err error
|
|
|
|
|
|
2017-03-14 13:04:51 +00:00
|
|
|
|
// Internal handling.
|
|
|
|
|
switch op.Operand {
|
|
|
|
|
case "q":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsStack.Push(csp.graphicsState)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "Q":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState = csp.graphicsStack.Pop()
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
// Color operations (Table 74 p. 179)
|
|
|
|
|
case "CS":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_CS(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "cs":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_cs(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "SC":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_SC(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "SCN":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_SCN(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "sc":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_sc(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "scn":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_scn(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "G":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_G(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "g":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_g(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "RG":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_RG(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "rg":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_rg(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "K":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_K(op, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
case "k":
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = csp.handleCommand_k(op, resources)
|
2017-08-07 20:21:35 +00:00
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
common.Log.Debug("Processor handling error (%s): %v", op.Operand, err)
|
|
|
|
|
common.Log.Debug("Operand: %#v", op.Operand)
|
|
|
|
|
return err
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if have external handler also, and process if so.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
for _, entry := range csp.handlers {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
var err error
|
|
|
|
|
if entry.Condition.All() {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = entry.Handler(op, csp.graphicsState, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
} else if entry.Condition.Operand() && op.Operand == entry.Operand {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
err = entry.Handler(op, csp.graphicsState, resources)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
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)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_SC(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
// For DeviceGray, CalGray, Indexed: one operand is required
|
|
|
|
|
// For DeviceRGB, CalRGB, Lab: 3 operands required
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
cs := csp.graphicsState.ColorspaceStroking
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-04 05:51:58 +00:00
|
|
|
|
func isPatternCS(cs PdfColorspace) bool {
|
|
|
|
|
_, isPattern := cs.(*PdfColorspaceSpecialPattern)
|
|
|
|
|
return isPattern
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-14 13:04:51 +00:00
|
|
|
|
// SCN: Same as SC but also supports Pattern, Separation, DeviceN and ICCBased color spaces.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_SCN(op *ContentStreamOperation, resources *PdfPageResources) error {
|
|
|
|
|
cs := csp.graphicsState.ColorspaceStroking
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
2017-04-04 05:51:58 +00:00
|
|
|
|
if !isPatternCS(cs) {
|
|
|
|
|
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")
|
|
|
|
|
}
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color, err := cs.ColorFromPdfObjects(op.Params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sc: Same as SC except used for non-stroking operations.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_sc(op *ContentStreamOperation, resources *PdfPageResources) error {
|
|
|
|
|
cs := csp.graphicsState.ColorspaceNonStroking
|
2017-04-04 05:51:58 +00:00
|
|
|
|
|
|
|
|
|
if !isPatternCS(cs) {
|
|
|
|
|
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")
|
|
|
|
|
}
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color, err := cs.ColorFromPdfObjects(op.Params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorNonStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// scn: Same as SCN except used for non-stroking operations.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_scn(op *ContentStreamOperation, resources *PdfPageResources) error {
|
|
|
|
|
cs := csp.graphicsState.ColorspaceNonStroking
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
2017-04-04 05:51:58 +00:00
|
|
|
|
if !isPatternCS(cs) {
|
|
|
|
|
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")
|
|
|
|
|
}
|
2017-03-14 13:04:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color, err := cs.ColorFromPdfObjects(op.Params)
|
|
|
|
|
if err != nil {
|
2017-08-07 20:21:35 +00:00
|
|
|
|
common.Log.Debug("ERROR: Fail to get color from params: %+v (CS is %+v)", op.Params, cs)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorNonStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// G: Set the stroking colorspace to DeviceGray, and the color to the specified graylevel (range [0-1]).
|
|
|
|
|
// gray G
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_G(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceStroking = cs
|
|
|
|
|
csp.graphicsState.ColorStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// g: Same as G, but for non-stroking colorspace and color (range [0-1]).
|
|
|
|
|
// gray g
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_g(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
cs := NewPdfColorspaceDeviceGray()
|
|
|
|
|
if len(op.Params) != cs.GetNumComponents() {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
common.Log.Debug("Invalid number of parameters for g")
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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 {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
common.Log.Debug("ERROR: handleCommand_g Invalid params. cs=%T op=%s err=%v", cs, op, err)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceNonStroking = cs
|
|
|
|
|
csp.graphicsState.ColorNonStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RG: Sets the stroking colorspace to DeviceRGB and the stroking color to r,g,b. [0-1] ranges.
|
|
|
|
|
// r g b RG
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_RG(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
cs := NewPdfColorspaceDeviceRGB()
|
|
|
|
|
if len(op.Params) != cs.GetNumComponents() {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
common.Log.Debug("Invalid number of parameters for RG")
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceStroking = cs
|
|
|
|
|
csp.graphicsState.ColorStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// rg: Same as RG but for non-stroking colorspace, color.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_rg(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceNonStroking = cs
|
|
|
|
|
csp.graphicsState.ColorNonStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
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
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_K(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceStroking = cs
|
|
|
|
|
csp.graphicsState.ColorStroking = color
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// k: Same as K but for non-stroking colorspace, color.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_k(op *ContentStreamOperation, resources *PdfPageResources) error {
|
2017-03-14 13:04:51 +00:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
csp.graphicsState.ColorspaceNonStroking = cs
|
|
|
|
|
csp.graphicsState.ColorNonStroking = color
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// cm: concatenates an affine transform to the CTM.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (csp *ContentStreamProcessor) handleCommand_cm(op *ContentStreamOperation,
|
|
|
|
|
resources *PdfPageResources) error {
|
|
|
|
|
if len(op.Params) != 6 {
|
|
|
|
|
common.Log.Debug("Invalid number of parameters for cm: %d", len(op.Params))
|
|
|
|
|
return errors.New("Invalid number of parameters")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f, err := GetNumbersAsFloat(op.Params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
m := NewMatrix(f[0], f[1], f[2], f[3], f[4], f[5])
|
|
|
|
|
csp.graphicsState.CTM.Concat(m)
|
2017-03-14 13:04:51 +00:00
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// Matrix is a linear transform matrix in homogenous coordinates.
|
|
|
|
|
// PDF coordinate transforms are always affine so we only need 6 of these. See newMatrix.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
type Matrix [9]float64
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// IdentityMatrix returns the identity transform.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func IdentityMatrix() Matrix {
|
|
|
|
|
return NewMatrix(1, 0, 0, 1, 0, 0)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-19 11:12:59 +10:00
|
|
|
|
// TranslationMatrix returns a matrix that translates by `tx`, `ty`.
|
|
|
|
|
func TranslationMatrix(tx, ty float64) Matrix {
|
|
|
|
|
return NewMatrix(1, 0, 0, 1, tx, ty)
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// NewMatrix returns an affine transform matrix laid out in homogenous coordinates as
|
|
|
|
|
// a b 0
|
|
|
|
|
// c d 0
|
|
|
|
|
// tx ty 1
|
|
|
|
|
func NewMatrix(a, b, c, d, tx, ty float64) Matrix {
|
|
|
|
|
m := Matrix{
|
|
|
|
|
a, b, 0,
|
|
|
|
|
c, d, 0,
|
|
|
|
|
tx, ty, 1,
|
|
|
|
|
}
|
|
|
|
|
m.fixup()
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// String returns a string describing `m`.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (m Matrix) String() string {
|
|
|
|
|
a, b, c, d, tx, ty := m[0], m[1], m[3], m[4], m[6], m[7]
|
2018-10-09 11:49:59 +11:00
|
|
|
|
return fmt.Sprintf("[%.1f,%.1f,%.1f,%.1f:%.1f,%.1f]", a, b, c, d, tx, ty)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// Set sets `m` to affine transform a,b,c,d,tx,ty.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (m *Matrix) Set(a, b, c, d, tx, ty float64) {
|
|
|
|
|
m[0], m[1] = a, b
|
|
|
|
|
m[3], m[4] = c, d
|
|
|
|
|
m[6], m[7] = tx, ty
|
|
|
|
|
m.fixup()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// Concat sets `m` to `m` × `b`.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// `b` needs to be created by newMatrix. i.e. It must be an affine transform
|
|
|
|
|
func (m *Matrix) Concat(b Matrix) {
|
|
|
|
|
*m = Matrix{
|
|
|
|
|
m[0]*b[0] + m[1]*b[3], m[0]*b[1] + m[1]*b[4], 0,
|
|
|
|
|
m[3]*b[0] + m[4]*b[3], m[3]*b[1] + m[4]*b[4], 0,
|
|
|
|
|
m[6]*b[0] + m[7]*b[3] + b[6], m[6]*b[1] + m[7]*b[4] + b[7], 1,
|
|
|
|
|
}
|
|
|
|
|
m.fixup()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-19 11:12:59 +10:00
|
|
|
|
// Mult returns `m` × `b`.
|
|
|
|
|
func (m Matrix) Mult(b Matrix) Matrix {
|
|
|
|
|
m.Concat(b)
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// Translate appends a translation of `dx`,`dy` to `m`.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// m.Translate(dx, dy) is equivalent to m.Concat(NewMatrix(1, 0, 0, 1, dx, dy))
|
|
|
|
|
func (m *Matrix) Translate(dx, dy float64) {
|
|
|
|
|
m[6] += dx
|
|
|
|
|
m[7] += dy
|
|
|
|
|
m.fixup()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-19 11:12:59 +10:00
|
|
|
|
// Translation returns the translation part of `m`.
|
|
|
|
|
func (m *Matrix) Translation() (float64, float64) {
|
|
|
|
|
return m[6], m[7]
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-09 11:49:59 +11:00
|
|
|
|
// Translation returns the translation part of `m`.
|
|
|
|
|
func (m *Matrix) ScalingX() float64 {
|
|
|
|
|
return math.Hypot(m[0], m[1])
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// Transform returns coordinates `x`,`y` transformed by `m`.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (m *Matrix) Transform(x, y float64) (float64, float64) {
|
|
|
|
|
xp := x*m[0] + y*m[1] + m[6]
|
|
|
|
|
yp := x*m[3] + y*m[4] + m[7]
|
|
|
|
|
return xp, yp
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-19 11:12:59 +10:00
|
|
|
|
func (m *Matrix) ScalingFactorX() float64 {
|
|
|
|
|
scale := m[0]
|
|
|
|
|
if !(m[1] == 0.0 && m[3] == 0.0) {
|
|
|
|
|
scale = math.Sqrt(m[0]*m[0] + m[1]*m[1])
|
|
|
|
|
}
|
|
|
|
|
return scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Matrix) ScalingFactorY() float64 {
|
|
|
|
|
scale := m[4]
|
|
|
|
|
if !(m[1] == 0.0 && m[3] == 0.0) {
|
|
|
|
|
scale = math.Sqrt(m[3]*m[3] + m[4]*m[4])
|
|
|
|
|
}
|
|
|
|
|
return scale
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// pageOrientation returns a guess at the pdf page orientation when text is printed with CTM `m`.
|
2018-10-09 19:05:38 +11:00
|
|
|
|
// XXX(peterwilliams97) Use pageRotate flag instead
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (m *Matrix) pageOrientation() Orientation {
|
|
|
|
|
switch {
|
|
|
|
|
case m[1]*m[1]+m[3]*m[3] > m[0]*m[0]+m[4]*m[4]:
|
|
|
|
|
return OrientationLandscape
|
|
|
|
|
default:
|
|
|
|
|
return OrientationPortrait
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fixup forces `m` to have reasonable values. It is a guard against crazy values in corrupt PDF
|
2018-09-06 15:17:41 +10:00
|
|
|
|
// files.
|
|
|
|
|
// Currently it clamps elements to [-maxAbsNumber, -maxAbsNumber] to avoid floating point exceptions.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (m *Matrix) fixup() {
|
|
|
|
|
for i, x := range m {
|
|
|
|
|
if x > maxAbsNumber {
|
|
|
|
|
m[i] = maxAbsNumber
|
|
|
|
|
} else if x < -maxAbsNumber {
|
|
|
|
|
m[i] = -maxAbsNumber
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// largest numbers needed in PDF transforms. Is this correct?
|
|
|
|
|
const maxAbsNumber = 1e9
|