mirror of
https://github.com/unidoc/unipdf.git
synced 2025-04-27 13:48:51 +08:00
Initial colorspace and functions support.
Implementation of ps parser for function type 4. Function type 3 not implemented yet and type 0 needs a better interpolation. Functions and colorspaces need more testing.
This commit is contained in:
parent
80fa077d15
commit
b8a3ec7180
@ -776,6 +776,7 @@ func newMultiEncoderFromStream(streamObj *PdfObjectStream) (*MultiEncoder, error
|
||||
}
|
||||
|
||||
// Prepare the decode params array (one for each filter type)
|
||||
// Optional, not always present.
|
||||
var decodeParamsDict *PdfObjectDictionary
|
||||
decodeParamsArray := []PdfObject{}
|
||||
obj := (*encDict)["DecodeParms"]
|
||||
@ -821,10 +822,14 @@ func newMultiEncoderFromStream(streamObj *PdfObjectStream) (*MultiEncoder, error
|
||||
if decodeParamsDict != nil {
|
||||
dp = decodeParamsDict
|
||||
} else {
|
||||
if idx >= len(decodeParamsArray) {
|
||||
return nil, fmt.Errorf("Missing elements in decode params array")
|
||||
// Only get the dp if provided. Oftentimes there is no decode params dict
|
||||
// provided.
|
||||
if len(decodeParamsArray) > 0 {
|
||||
if idx >= len(decodeParamsArray) {
|
||||
return nil, fmt.Errorf("Missing elements in decode params array")
|
||||
}
|
||||
dp = decodeParamsArray[idx]
|
||||
}
|
||||
dp = decodeParamsArray[idx]
|
||||
}
|
||||
|
||||
var dParams *PdfObjectDictionary
|
||||
@ -849,6 +854,7 @@ func newMultiEncoderFromStream(streamObj *PdfObjectStream) (*MultiEncoder, error
|
||||
encoder := NewASCIIHexEncoder()
|
||||
mencoder.AddEncoder(encoder)
|
||||
} else {
|
||||
common.Log.Error("Unsupported filter %s", *name)
|
||||
return nil, fmt.Errorf("Invalid filter in multi filter array")
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,14 @@ func MakeInteger(val int64) *PdfObjectInteger {
|
||||
return &num
|
||||
}
|
||||
|
||||
func MakeArray(objects ...PdfObject) *PdfObjectArray {
|
||||
array := PdfObjectArray{}
|
||||
for _, obj := range objects {
|
||||
array = append(array, obj)
|
||||
}
|
||||
return &array
|
||||
}
|
||||
|
||||
func MakeFloat(val float64) *PdfObjectFloat {
|
||||
num := PdfObjectFloat(val)
|
||||
return &num
|
||||
@ -162,6 +170,36 @@ func (this *PdfObjectName) DefaultWriteString() string {
|
||||
return output.String()
|
||||
}
|
||||
|
||||
func (this *PdfObjectArray) ToFloat64Array() ([]float64, error) {
|
||||
vals := []float64{}
|
||||
|
||||
for _, obj := range *this {
|
||||
if number, is := obj.(*PdfObjectInteger); is {
|
||||
vals = append(vals, float64(*number))
|
||||
} else if number, is := obj.(*PdfObjectFloat); is {
|
||||
vals = append(vals, float64(*number))
|
||||
} else {
|
||||
return nil, fmt.Errorf("Type error")
|
||||
}
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (this *PdfObjectArray) ToIntegerArray() ([]int, error) {
|
||||
vals := []int{}
|
||||
|
||||
for _, obj := range *this {
|
||||
if number, is := obj.(*PdfObjectInteger); is {
|
||||
vals = append(vals, int(*number))
|
||||
} else {
|
||||
return nil, fmt.Errorf("Type error")
|
||||
}
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (this *PdfObjectArray) String() string {
|
||||
outStr := "["
|
||||
for ind, o := range *this {
|
||||
@ -186,6 +224,39 @@ func (this *PdfObjectArray) DefaultWriteString() string {
|
||||
return outStr
|
||||
}
|
||||
|
||||
func (this *PdfObjectArray) Append(obj PdfObject) {
|
||||
*this = append(*this, obj)
|
||||
}
|
||||
|
||||
func getNumberAsFloat(obj PdfObject) (float64, error) {
|
||||
if fObj, ok := obj.(*PdfObjectFloat); ok {
|
||||
return float64(*fObj), nil
|
||||
}
|
||||
|
||||
if iObj, ok := obj.(*PdfObjectInteger); ok {
|
||||
return float64(*iObj), nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("Not a number")
|
||||
}
|
||||
|
||||
// For numeric array: Get the array in []float64 slice representation.
|
||||
// Will return error if not entirely numeric.
|
||||
func (this *PdfObjectArray) GetAsFloat64Slice() ([]float64, error) {
|
||||
slice := []float64{}
|
||||
|
||||
for _, obj := range *this {
|
||||
obj := TraceToDirectObject(obj)
|
||||
number, err := getNumberAsFloat(obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Array element not a number")
|
||||
}
|
||||
slice = append(slice, number)
|
||||
}
|
||||
|
||||
return slice, nil
|
||||
}
|
||||
|
||||
func (this *PdfObjectDictionary) String() string {
|
||||
outStr := "Dict("
|
||||
for k, v := range *this {
|
||||
@ -198,7 +269,7 @@ func (this *PdfObjectDictionary) String() string {
|
||||
func (this *PdfObjectDictionary) DefaultWriteString() string {
|
||||
outStr := "<<"
|
||||
for k, v := range *this {
|
||||
common.Log.Debug("Writing k: %s %T", k, v)
|
||||
common.Log.Debug("Writing k: %s %T %v %v", k, v, k, v)
|
||||
outStr += k.DefaultWriteString()
|
||||
outStr += " "
|
||||
outStr += v.DefaultWriteString()
|
||||
@ -259,11 +330,19 @@ func (this *PdfObjectNull) DefaultWriteString() string {
|
||||
//
|
||||
// Note: This function does not trace/resolve references.
|
||||
// That needs to be done beforehand.
|
||||
const TraceMaxDepth = 20
|
||||
|
||||
func TraceToDirectObject(obj PdfObject) PdfObject {
|
||||
iobj, isIndirectObj := obj.(*PdfIndirectObject)
|
||||
depth := 0
|
||||
for isIndirectObj == true {
|
||||
obj = iobj.PdfObject
|
||||
iobj, isIndirectObj = obj.(*PdfIndirectObject)
|
||||
depth++
|
||||
if depth > TraceMaxDepth {
|
||||
common.Log.Error("Trace depth level beyond 20 - error!")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
@ -34,8 +34,13 @@ func NewEncoderFromStream(streamObj *PdfObjectStream) (StreamEncoder, error) {
|
||||
|
||||
if len(*array) != 1 {
|
||||
menc, err := newMultiEncoderFromStream(streamObj)
|
||||
if err != nil {
|
||||
common.Log.Error("Failed creating multi encoder: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common.Log.Debug("Multi enc: %s\n", menc)
|
||||
return menc, err
|
||||
return menc, nil
|
||||
}
|
||||
|
||||
// Single element.
|
||||
|
1635
pdf/model/colorspace.go
Normal file
1635
pdf/model/colorspace.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -90,6 +90,11 @@ func (this *ContentStreamInlineImage) DefaultWriteString() string {
|
||||
return output.String()
|
||||
}
|
||||
|
||||
// Export the inline image to Image which can be transformed or exported easily.
|
||||
func (this *ContentStreamInlineImage) ToImage() (*Image, error) {
|
||||
return nil, fmt.Errorf("Not implemented yet")
|
||||
}
|
||||
|
||||
// Parse an inline image from a content stream, both read its properties and
|
||||
// binary data.
|
||||
// When called, "BI" has already been read from the stream. This function
|
||||
|
895
pdf/model/functions.go
Normal file
895
pdf/model/functions.go
Normal file
@ -0,0 +1,895 @@
|
||||
/*
|
||||
* This file is subject to the terms and conditions defined in
|
||||
* file 'LICENSE.md', which is part of this source code package.
|
||||
*/
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
|
||||
"github.com/unidoc/unidoc/common"
|
||||
. "github.com/unidoc/unidoc/pdf/core"
|
||||
"github.com/unidoc/unidoc/pdf/ps"
|
||||
)
|
||||
|
||||
type PdfValue interface{}
|
||||
|
||||
type PdfFunction interface {
|
||||
Evaluate([]float64) ([]float64, error)
|
||||
ToPdfObject() PdfObject
|
||||
}
|
||||
|
||||
// In PDF: A function object may be a dictionary or a stream, depending on the type of function.
|
||||
// - Stream: Type 0, Type 4
|
||||
// - Dictionary: Type 2, Type 3.
|
||||
|
||||
// Loads a PDF Function from a PdfObject (can be either stream or dictionary).
|
||||
func newPdfFunctionFromPdfObject(obj PdfObject) (PdfFunction, error) {
|
||||
if stream, is := obj.(*PdfObjectStream); is {
|
||||
dict := stream.PdfObjectDictionary
|
||||
|
||||
ftype, ok := (*dict)["FunctionType"].(*PdfObjectInteger)
|
||||
if !ok {
|
||||
common.Log.Error("FunctionType number missing")
|
||||
return nil, errors.New("Invalid parameter or missing")
|
||||
}
|
||||
|
||||
if *ftype == 0 {
|
||||
return newPdfFunctionType0FromStream(stream)
|
||||
} else if *ftype == 4 {
|
||||
return newPdfFunctionType4FromStream(stream)
|
||||
} else {
|
||||
return nil, errors.New("Invalid function type")
|
||||
}
|
||||
} else if indObj, is := obj.(*PdfIndirectObject); is {
|
||||
// Indirect object containing a dictionary.
|
||||
// The indirect object is the container (which is tracked).
|
||||
dict, ok := indObj.PdfObject.(*PdfObjectDictionary)
|
||||
if !ok {
|
||||
common.Log.Error("Function Indirect object not containing dictionary")
|
||||
return nil, errors.New("Invalid parameter or missing")
|
||||
}
|
||||
|
||||
ftype, ok := (*dict)["FunctionType"].(*PdfObjectInteger)
|
||||
if !ok {
|
||||
common.Log.Error("FunctionType number missing")
|
||||
return nil, errors.New("Invalid parameter or missing")
|
||||
}
|
||||
|
||||
if *ftype == 2 {
|
||||
return newPdfFunctionType2FromPdfObject(indObj)
|
||||
} else if *ftype == 3 {
|
||||
return newPdfFunctionType3FromPdfObject(indObj)
|
||||
} else {
|
||||
return nil, errors.New("Invalid function type")
|
||||
}
|
||||
} else if dict, is := obj.(*PdfObjectDictionary); is {
|
||||
ftype, ok := (*dict)["FunctionType"].(*PdfObjectInteger)
|
||||
if !ok {
|
||||
common.Log.Error("FunctionType number missing")
|
||||
return nil, errors.New("Invalid parameter or missing")
|
||||
}
|
||||
|
||||
if *ftype == 2 {
|
||||
return newPdfFunctionType2FromPdfObject(dict)
|
||||
} else if *ftype == 3 {
|
||||
return newPdfFunctionType3FromPdfObject(dict)
|
||||
} else {
|
||||
return nil, errors.New("Invalid function type")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("Type error")
|
||||
}
|
||||
}
|
||||
|
||||
// Simple linear interpolation from the PDF manual.
|
||||
func interpolate(x, xmin, xmax, ymin, ymax float64) float64 {
|
||||
if math.Abs(xmax-xmin) < 0.000001 {
|
||||
return ymin
|
||||
}
|
||||
|
||||
y := ymin + (x-xmin)*(ymax-ymin)/(xmax-xmin)
|
||||
return y
|
||||
}
|
||||
|
||||
//
|
||||
// Type 0 functions use a sequence of sample values (contained in a stream) to provide an approximation
|
||||
// for functions whose domains and ranges are bounded. The samples are organized as an m-dimensional
|
||||
// table in which each entry has n components
|
||||
//
|
||||
type PdfFunctionType0 struct {
|
||||
Domain []float64 // required; 2*m length; where m is the number of input values
|
||||
Range []float64 // required (type 0); 2*n length; where n is the number of output values
|
||||
|
||||
NumInputs int
|
||||
NumOutputs int
|
||||
|
||||
Size []int
|
||||
BitsPerSample int
|
||||
Order int // Values 1 or 3 (linear or cubic spline interpolation)
|
||||
Encode []float64
|
||||
Decode []float64
|
||||
|
||||
rawData []byte
|
||||
data []uint32
|
||||
|
||||
container *PdfObjectStream
|
||||
}
|
||||
|
||||
// Construct the PDF function object from a stream object (typically loaded from a PDF file).
|
||||
func newPdfFunctionType0FromStream(stream *PdfObjectStream) (*PdfFunctionType0, error) {
|
||||
fun := &PdfFunctionType0{}
|
||||
|
||||
fun.container = stream
|
||||
|
||||
dict := stream.PdfObjectDictionary
|
||||
|
||||
// Domain
|
||||
array, has := TraceToDirectObject((*dict)["Domain"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Domain not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
common.Log.Error("Domain invalid")
|
||||
return nil, errors.New("Invalid domain range")
|
||||
}
|
||||
fun.NumInputs = len(*array) / 2
|
||||
domain, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Domain = domain
|
||||
|
||||
// Range
|
||||
array, has = TraceToDirectObject((*dict)["Range"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Range not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
return nil, errors.New("Invalid range")
|
||||
}
|
||||
fun.NumOutputs = len(*array) / 2
|
||||
rang, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Range = rang
|
||||
|
||||
// Number of samples in each input dimension
|
||||
array, has = TraceToDirectObject((*dict)["Size"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Size not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
tablesize, err := array.ToIntegerArray()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(tablesize) != fun.NumInputs {
|
||||
common.Log.Error("Table size not matching number of inputs")
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
fun.Size = tablesize
|
||||
|
||||
// BitsPerSample
|
||||
bps, has := TraceToDirectObject((*dict)["BitsPerSample"]).(*PdfObjectInteger)
|
||||
if !has {
|
||||
common.Log.Error("BitsPerSample not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if *bps != 1 && *bps != 2 && *bps != 4 && *bps != 8 && *bps != 12 && *bps != 16 && *bps != 24 && *bps != 32 {
|
||||
common.Log.Error("Bits per sample outside range (%d)", *bps)
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
fun.BitsPerSample = int(*bps)
|
||||
|
||||
fun.Order = 1
|
||||
order, has := TraceToDirectObject((*dict)["Order"]).(*PdfObjectInteger)
|
||||
if has {
|
||||
if *order != 1 && *order != 3 {
|
||||
common.Log.Error("Invalid order (%d)", *order)
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
fun.Order = int(*order)
|
||||
}
|
||||
|
||||
// Encode: is a 2*m array specifying the linear mapping of input values into the domain of the function's
|
||||
// sample table.
|
||||
array, has = TraceToDirectObject((*dict)["Encode"]).(*PdfObjectArray)
|
||||
if has {
|
||||
encode, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Encode = encode
|
||||
}
|
||||
|
||||
// Decode
|
||||
array, has = TraceToDirectObject((*dict)["Decode"]).(*PdfObjectArray)
|
||||
if has {
|
||||
decode, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Decode = decode
|
||||
}
|
||||
|
||||
data, err := DecodeStream(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.rawData = data
|
||||
|
||||
return fun, nil
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType0) ToPdfObject() PdfObject {
|
||||
container := this.container
|
||||
if container != nil {
|
||||
this.container = &PdfObjectStream{}
|
||||
}
|
||||
|
||||
dict := &PdfObjectDictionary{}
|
||||
|
||||
(*dict)["FunctionType"] = MakeInteger(0)
|
||||
|
||||
// Domain (required).
|
||||
domainArray := &PdfObjectArray{}
|
||||
for _, val := range this.Domain {
|
||||
domainArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Domain"] = domainArray
|
||||
|
||||
// Range (required).
|
||||
rangeArray := &PdfObjectArray{}
|
||||
for _, val := range this.Range {
|
||||
rangeArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Range"] = rangeArray
|
||||
|
||||
// Size (required).
|
||||
sizeArray := &PdfObjectArray{}
|
||||
for _, val := range this.Size {
|
||||
sizeArray.Append(MakeInteger(int64(val)))
|
||||
}
|
||||
(*dict)["Size"] = sizeArray
|
||||
|
||||
(*dict)["BitsPerSample"] = MakeInteger(int64(this.BitsPerSample))
|
||||
|
||||
if this.Order != 1 {
|
||||
(*dict)["Order"] = MakeInteger(int64(this.Order))
|
||||
}
|
||||
|
||||
// TODO: Encode.
|
||||
// Either here, or automatically later on when writing out.
|
||||
(*dict)["Length"] = MakeInteger(int64(len(this.rawData)))
|
||||
container.Stream = this.rawData
|
||||
|
||||
container.PdfObjectDictionary = dict
|
||||
return container
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType0) Evaluate(x []float64) ([]float64, error) {
|
||||
if len(x) != this.NumInputs {
|
||||
common.Log.Error("Number of inputs not matching what is needed")
|
||||
return nil, errors.New("Range check error")
|
||||
}
|
||||
|
||||
if this.data == nil {
|
||||
// Process the samples if not already done.
|
||||
err := this.processSamples()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
indices := []int{}
|
||||
// Start with nearest neighbour interpolation.
|
||||
for i := 0; i < len(x); i++ {
|
||||
xi := x[i]
|
||||
|
||||
xip := math.Min(math.Max(xi, this.Domain[2*i]), this.Domain[2*i+1])
|
||||
|
||||
ei := interpolate(xip, this.Domain[2*i], this.Domain[2*i+1], this.Encode[2*i], this.Encode[2*i+1])
|
||||
eip := math.Min(math.Max(ei, 0), float64(this.Size[i]))
|
||||
// eip represents coordinate into the data table.
|
||||
// At this point it is real values.
|
||||
|
||||
// Interpolation shall be used to to determine output values
|
||||
// from the nearest surrounding values in the sample table.
|
||||
|
||||
// Initial implementation is simply nearest neighbour.
|
||||
// Then will add the linear and possibly bicubic/spline.
|
||||
index := int(math.Floor(eip + 0.5))
|
||||
if index < 0 {
|
||||
index = 0
|
||||
} else if index > this.Size[i] {
|
||||
index = this.Size[i] - 1
|
||||
}
|
||||
indices = append(indices, index)
|
||||
|
||||
}
|
||||
|
||||
// Calculate the index
|
||||
m := indices[0]
|
||||
for i := 1; i < this.NumInputs; i++ {
|
||||
add := indices[i]
|
||||
for j := 0; j < i; j++ {
|
||||
add *= this.Size[j]
|
||||
}
|
||||
m += add
|
||||
}
|
||||
m *= this.NumOutputs
|
||||
|
||||
// Output values.
|
||||
outputs := []float64{}
|
||||
for j := 0; j < this.NumOutputs; j++ {
|
||||
rj := this.data[m+j]
|
||||
rjp := interpolate(float64(rj), 0, math.Pow(2, float64(this.BitsPerSample)), this.Decode[2*j], this.Decode[2*j+1])
|
||||
yj := math.Min(math.Max(rjp, this.Range[2*j]), this.Range[2*j+1])
|
||||
outputs = append(outputs, yj)
|
||||
}
|
||||
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
// Convert raw data to data table. The maximum supported BitsPerSample is 32, so we store the resulting data
|
||||
// in a uint32 array. This is somewhat wasteful in the case of a small BitsPerSample, but these tables are
|
||||
// presumably not huge at any rate.
|
||||
func (this *PdfFunctionType0) processSamples() error {
|
||||
this.data = []uint32{}
|
||||
|
||||
bitsLeftPerSample := this.BitsPerSample
|
||||
var sample uint32
|
||||
var remainder byte
|
||||
remainderBits := 0
|
||||
|
||||
index := 0
|
||||
|
||||
i := 0
|
||||
for i < len(this.rawData) {
|
||||
// Start with the remainder.
|
||||
if remainderBits > 0 {
|
||||
take := remainderBits
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
|
||||
sample = (sample << uint(take)) | uint32(remainder>>uint(8-take))
|
||||
remainderBits -= take
|
||||
if remainderBits > 0 {
|
||||
remainder = remainder << uint(take)
|
||||
} else {
|
||||
remainder = 0
|
||||
}
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
this.data[index] = sample
|
||||
bitsLeftPerSample = this.BitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
// Take next byte
|
||||
b := this.rawData[i]
|
||||
i++
|
||||
|
||||
// 8 bits.
|
||||
take := 8
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
remainderBits = 8 - take
|
||||
sample = (sample << uint(take)) | uint32(b>>uint(remainderBits))
|
||||
|
||||
if take < 8 {
|
||||
remainder = b << uint(take)
|
||||
}
|
||||
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
this.data[index] = sample
|
||||
bitsLeftPerSample = this.BitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//
|
||||
// Type 2 functions define an exponential interpolation of one input value and n
|
||||
// output values:
|
||||
// f(x) = y_0, ..., y_(n-1)
|
||||
// y_j = C0_j + x^N * (C1_j - C0_j); for 0 <= j < n
|
||||
// When N=1 ; linear interpolation between C0 and C1.
|
||||
//
|
||||
type PdfFunctionType2 struct {
|
||||
Domain []float64
|
||||
Range []float64
|
||||
|
||||
C0 []float64
|
||||
C1 []float64
|
||||
N float64
|
||||
|
||||
container *PdfIndirectObject
|
||||
}
|
||||
|
||||
// Can be either indirect object or dictionary. If indirect, then must be holding a dictionary,
|
||||
// i.e. acting as a container. When converting back to pdf object, will use the container provided.
|
||||
|
||||
func newPdfFunctionType2FromPdfObject(obj PdfObject) (*PdfFunctionType2, error) {
|
||||
fun := &PdfFunctionType2{}
|
||||
|
||||
var dict *PdfObjectDictionary
|
||||
if indObj, is := obj.(*PdfIndirectObject); is {
|
||||
d, ok := indObj.PdfObject.(*PdfObjectDictionary)
|
||||
if !ok {
|
||||
return nil, errors.New("Type check error")
|
||||
}
|
||||
fun.container = indObj
|
||||
dict = d
|
||||
} else if d, is := obj.(*PdfObjectDictionary); is {
|
||||
dict = d
|
||||
} else {
|
||||
return nil, errors.New("Type check error")
|
||||
}
|
||||
|
||||
common.Log.Debug("FUNC2: %s", dict.String())
|
||||
|
||||
// Domain
|
||||
array, has := TraceToDirectObject((*dict)["Domain"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Domain not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
common.Log.Error("Domain range invalid")
|
||||
return nil, errors.New("Invalid domain range")
|
||||
}
|
||||
domain, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Domain = domain
|
||||
|
||||
// Range
|
||||
array, has = TraceToDirectObject((*dict)["Range"]).(*PdfObjectArray)
|
||||
if has {
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
return nil, errors.New("Invalid range")
|
||||
}
|
||||
|
||||
rang, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Range = rang
|
||||
}
|
||||
|
||||
// C0.
|
||||
array, has = TraceToDirectObject((*dict)["C0"]).(*PdfObjectArray)
|
||||
if has {
|
||||
c0, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.C0 = c0
|
||||
}
|
||||
|
||||
// C1.
|
||||
array, has = TraceToDirectObject((*dict)["C1"]).(*PdfObjectArray)
|
||||
if has {
|
||||
c1, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.C1 = c1
|
||||
}
|
||||
|
||||
if len(fun.C0) != len(fun.C1) {
|
||||
common.Log.Error("C0 and C1 not matching")
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
|
||||
// Exponent.
|
||||
N, err := getNumberAsFloat(TraceToDirectObject((*dict)["N"]))
|
||||
if err != nil {
|
||||
common.Log.Error("N missing or invalid, dict: %s", dict.String())
|
||||
return nil, err
|
||||
}
|
||||
fun.N = N
|
||||
|
||||
return fun, nil
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType2) ToPdfObject() PdfObject {
|
||||
dict := &PdfObjectDictionary{}
|
||||
|
||||
(*dict)["FunctionType"] = MakeInteger(2)
|
||||
|
||||
// Domain (required).
|
||||
domainArray := &PdfObjectArray{}
|
||||
for _, val := range this.Domain {
|
||||
domainArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Domain"] = domainArray
|
||||
|
||||
// Range (required).
|
||||
if this.Range != nil {
|
||||
rangeArray := &PdfObjectArray{}
|
||||
for _, val := range this.Range {
|
||||
rangeArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Range"] = rangeArray
|
||||
}
|
||||
|
||||
// C0.
|
||||
if this.C0 != nil {
|
||||
c0Array := &PdfObjectArray{}
|
||||
for _, val := range this.C0 {
|
||||
c0Array.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["C0"] = c0Array
|
||||
}
|
||||
|
||||
// C1.
|
||||
if this.C1 != nil {
|
||||
c1Array := &PdfObjectArray{}
|
||||
for _, val := range this.C1 {
|
||||
c1Array.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["C1"] = c1Array
|
||||
}
|
||||
|
||||
// exponent
|
||||
(*dict)["N"] = MakeFloat(this.N)
|
||||
|
||||
// Wrap in a container if we have one already specified.
|
||||
if this.container != nil {
|
||||
this.container.PdfObject = dict
|
||||
return this.container
|
||||
} else {
|
||||
return dict
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType2) Evaluate(x []float64) ([]float64, error) {
|
||||
if len(x) != 1 {
|
||||
common.Log.Error("Only one input allowed")
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
|
||||
// Prepare.
|
||||
c0 := []float64{0.0}
|
||||
if this.C0 != nil {
|
||||
c0 = this.C0
|
||||
}
|
||||
c1 := []float64{1.0}
|
||||
if this.C1 != nil {
|
||||
c1 = this.C1
|
||||
}
|
||||
|
||||
y := []float64{}
|
||||
for i := 0; i < len(c0); i++ {
|
||||
yi := c0[i] + math.Pow(x[0], this.N)*(c1[i]-c0[i])
|
||||
y = append(y, yi)
|
||||
}
|
||||
|
||||
return y, nil
|
||||
}
|
||||
|
||||
//
|
||||
// Type 3 functions define stitching of the subdomains of serveral 1-input functions to produce
|
||||
// a single new 1-input function.
|
||||
//
|
||||
type PdfFunctionType3 struct {
|
||||
Domain []float64
|
||||
Range []float64
|
||||
|
||||
Functions []PdfFunction // k-1 input functions
|
||||
Bounds []float64 // k-1 numbers; defines the intervals where each function applies
|
||||
Encode []float64 // Array of 2k numbers..
|
||||
|
||||
container *PdfIndirectObject
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType3) Evaluate(x []float64) ([]float64, error) {
|
||||
if len(x) != 1 {
|
||||
common.Log.Error("Only one input allowed")
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
|
||||
// Determine which function to use
|
||||
|
||||
// Encode
|
||||
|
||||
return nil, errors.New("Not implemented yet")
|
||||
}
|
||||
|
||||
func newPdfFunctionType3FromPdfObject(obj PdfObject) (*PdfFunctionType3, error) {
|
||||
fun := &PdfFunctionType3{}
|
||||
|
||||
var dict *PdfObjectDictionary
|
||||
if indObj, is := obj.(*PdfIndirectObject); is {
|
||||
d, ok := indObj.PdfObject.(*PdfObjectDictionary)
|
||||
if !ok {
|
||||
return nil, errors.New("Type check error")
|
||||
}
|
||||
fun.container = indObj
|
||||
dict = d
|
||||
} else if d, is := obj.(*PdfObjectDictionary); is {
|
||||
dict = d
|
||||
} else {
|
||||
return nil, errors.New("Type check error")
|
||||
}
|
||||
|
||||
// Domain
|
||||
array, has := TraceToDirectObject((*dict)["Domain"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Domain not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if len(*array) != 2 {
|
||||
common.Log.Error("Domain invalid")
|
||||
return nil, errors.New("Invalid domain range")
|
||||
}
|
||||
domain, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Domain = domain
|
||||
|
||||
// Range
|
||||
array, has = TraceToDirectObject((*dict)["Range"]).(*PdfObjectArray)
|
||||
if has {
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
return nil, errors.New("Invalid range")
|
||||
}
|
||||
rang, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Range = rang
|
||||
}
|
||||
|
||||
// Functions.
|
||||
array, has = TraceToDirectObject((*dict)["Functions"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Functions not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
fun.Functions = []PdfFunction{}
|
||||
for _, obj := range *array {
|
||||
subf, err := newPdfFunctionFromPdfObject(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Functions = append(fun.Functions, subf)
|
||||
}
|
||||
|
||||
// Bounds
|
||||
array, has = TraceToDirectObject((*dict)["Bounds"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Bounds not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
bounds, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Bounds = bounds
|
||||
if len(fun.Bounds) != len(fun.Functions)-1 {
|
||||
common.Log.Error("Bounds (%d) and num functions (%d) not matching", len(fun.Bounds), len(fun.Functions))
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
|
||||
// Encode.
|
||||
array, has = TraceToDirectObject((*dict)["Encode"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Encode not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
encode, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Encode = encode
|
||||
if len(fun.Encode) != 2*len(fun.Functions) {
|
||||
common.Log.Error("Len encode (%d) and num functions (%d) not matching up", len(fun.Encode), len(fun.Functions))
|
||||
return nil, errors.New("Range check")
|
||||
}
|
||||
|
||||
return fun, nil
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType3) ToPdfObject() PdfObject {
|
||||
dict := &PdfObjectDictionary{}
|
||||
|
||||
(*dict)["FunctionType"] = MakeInteger(3)
|
||||
|
||||
// Domain (required).
|
||||
domainArray := &PdfObjectArray{}
|
||||
for _, val := range this.Domain {
|
||||
domainArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Domain"] = domainArray
|
||||
|
||||
// Range (required).
|
||||
if this.Range != nil {
|
||||
rangeArray := &PdfObjectArray{}
|
||||
for _, val := range this.Range {
|
||||
rangeArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Range"] = rangeArray
|
||||
}
|
||||
|
||||
// Functions
|
||||
if this.Functions != nil {
|
||||
fArray := &PdfObjectArray{}
|
||||
for _, fun := range this.Functions {
|
||||
fArray.Append(fun.ToPdfObject())
|
||||
}
|
||||
(*dict)["Functions"] = fArray
|
||||
}
|
||||
|
||||
// Bounds.
|
||||
if this.Bounds != nil {
|
||||
bArray := &PdfObjectArray{}
|
||||
for _, val := range this.Bounds {
|
||||
bArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Bounds"] = bArray
|
||||
}
|
||||
|
||||
// Encode.
|
||||
if this.Encode != nil {
|
||||
eArray := &PdfObjectArray{}
|
||||
for _, val := range this.Encode {
|
||||
eArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Encode"] = eArray
|
||||
}
|
||||
|
||||
// Wrap in a container if we have one already specified.
|
||||
if this.container != nil {
|
||||
this.container.PdfObject = dict
|
||||
return this.container
|
||||
} else {
|
||||
return dict
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Type 4. Postscript calculator functions.
|
||||
//
|
||||
type PdfFunctionType4 struct {
|
||||
Domain []float64
|
||||
Range []float64
|
||||
Program *ps.PSProgram
|
||||
|
||||
executor *ps.PSExecutor
|
||||
decodedData []byte
|
||||
|
||||
container *PdfObjectStream
|
||||
}
|
||||
|
||||
// Input [x1 x2 x3]
|
||||
func (this *PdfFunctionType4) Evaluate(xVec []float64) ([]float64, error) {
|
||||
if this.executor == nil {
|
||||
this.executor = ps.NewPSExecutor(this.Program)
|
||||
}
|
||||
|
||||
inputs := []ps.PSObject{}
|
||||
for _, val := range xVec {
|
||||
inputs = append(inputs, ps.MakeReal(val))
|
||||
}
|
||||
|
||||
outputs, err := this.executor.Execute(inputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// After execution the outputs are on the stack [y1 ... yM]
|
||||
// Convert to floats.
|
||||
yVec, err := ps.PSObjectArrayToFloat64Array(outputs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return yVec, nil
|
||||
}
|
||||
|
||||
// Load a type 4 function from a PDF stream object.
|
||||
func newPdfFunctionType4FromStream(stream *PdfObjectStream) (*PdfFunctionType4, error) {
|
||||
fun := &PdfFunctionType4{}
|
||||
|
||||
fun.container = stream
|
||||
|
||||
dict := stream.PdfObjectDictionary
|
||||
|
||||
// Domain
|
||||
array, has := TraceToDirectObject((*dict)["Domain"]).(*PdfObjectArray)
|
||||
if !has {
|
||||
common.Log.Error("Domain not specified")
|
||||
return nil, errors.New("Required attribute missing or invalid")
|
||||
}
|
||||
if len(*array)%2 != 0 {
|
||||
common.Log.Error("Domain invalid")
|
||||
return nil, errors.New("Invalid domain range")
|
||||
}
|
||||
domain, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Domain = domain
|
||||
|
||||
// Range
|
||||
array, has = TraceToDirectObject((*dict)["Range"]).(*PdfObjectArray)
|
||||
if has {
|
||||
if len(*array) < 0 || len(*array)%2 != 0 {
|
||||
return nil, errors.New("Invalid range")
|
||||
}
|
||||
rang, err := array.ToFloat64Array()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Range = rang
|
||||
}
|
||||
|
||||
// Program. Decode the program and parse the PS code.
|
||||
decoded, err := DecodeStream(stream)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.decodedData = decoded
|
||||
|
||||
psParser := ps.NewPSParser([]byte(decoded))
|
||||
prog, err := psParser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fun.Program = prog
|
||||
|
||||
return fun, nil
|
||||
}
|
||||
|
||||
func (this *PdfFunctionType4) ToPdfObject() PdfObject {
|
||||
container := this.container
|
||||
if container != nil {
|
||||
this.container = &PdfObjectStream{}
|
||||
}
|
||||
|
||||
dict := &PdfObjectDictionary{}
|
||||
(*dict)["FunctionType"] = MakeInteger(4)
|
||||
|
||||
// Domain (required).
|
||||
domainArray := &PdfObjectArray{}
|
||||
for _, val := range this.Domain {
|
||||
domainArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Domain"] = domainArray
|
||||
|
||||
// Range (required).
|
||||
rangeArray := &PdfObjectArray{}
|
||||
for _, val := range this.Range {
|
||||
rangeArray.Append(MakeFloat(val))
|
||||
}
|
||||
(*dict)["Range"] = rangeArray
|
||||
|
||||
// TODO: Encode.
|
||||
// Either here, or automatically later on when writing out.
|
||||
(*dict)["Length"] = MakeInteger(int64(len(this.decodedData)))
|
||||
|
||||
container.Stream = this.decodedData
|
||||
container.PdfObjectDictionary = dict
|
||||
|
||||
return container
|
||||
}
|
103
pdf/model/functions_test.go
Normal file
103
pdf/model/functions_test.go
Normal file
@ -0,0 +1,103 @@
|
||||
// Test functions
|
||||
// XXX: We need tests of type 0, type 2, type 3, type 4 functions. Particularly type 0 is complex and
|
||||
// needs comprehensive tests.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/unidoc/unidoc/common"
|
||||
. "github.com/unidoc/unidoc/pdf/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
//common.SetLogger(common.ConsoleLogger{})
|
||||
common.SetLogger(common.DummyLogger{})
|
||||
}
|
||||
|
||||
type Type4TestCase struct {
|
||||
Inputs []float64
|
||||
Expected []float64
|
||||
}
|
||||
|
||||
// TODO/XXX: Implement example 2 from page 167.
|
||||
|
||||
func TestType4Function1(t *testing.T) {
|
||||
rawText := `
|
||||
10 0 obj
|
||||
<<
|
||||
/FunctionType 4
|
||||
/Domain [ -1.0 1.0 -1.0 1.0]
|
||||
/Range [ -1.0 1.0 ]
|
||||
/Length 48
|
||||
>>
|
||||
stream
|
||||
{ 360 mul sin
|
||||
2 div
|
||||
exch 360 mul
|
||||
sin 2 div
|
||||
add
|
||||
} endstream
|
||||
endobj
|
||||
`
|
||||
/*
|
||||
* Inputs: [x y] where -1<x<1, -1<y<1
|
||||
* Outputs: z where -1 < z < 1
|
||||
* z = sin(y * 360)/2 + sin(x * 360)/2
|
||||
*/
|
||||
|
||||
parser := NewParserFromString(rawText)
|
||||
|
||||
obj, err := parser.ParseIndirectObject()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to parse indirect obj (%s)", err)
|
||||
return
|
||||
}
|
||||
|
||||
stream, ok := obj.(*PdfObjectStream)
|
||||
if !ok {
|
||||
t.Errorf("Invalid object type (%q)", obj)
|
||||
return
|
||||
}
|
||||
|
||||
fun, err := newPdfFunctionFromPdfObject(obj)
|
||||
if err != nil {
|
||||
t.Errorf("Failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// z = sin(360*x)/2 + sin(360*y)/2
|
||||
testcases := []Type4TestCase{
|
||||
{[]float64{0.5, 0.5}, []float64{0}},
|
||||
{[]float64{0.25, 0.25}, []float64{1.0}},
|
||||
{[]float64{0.25, 0.5}, []float64{0.5}},
|
||||
{[]float64{0.5, 0.25}, []float64{0.5}},
|
||||
{[]float64{-0.5, -0.25}, []float64{-0.50}},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
outputs, err := fun.Evaluate(testcase.Inputs)
|
||||
if err != nil {
|
||||
t.Errorf("Failed: %v", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(testcase)
|
||||
fmt.Println(outputs)
|
||||
|
||||
if len(outputs) != len(testcase.Expected) {
|
||||
t.Errorf("Failed, output length mismatch")
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(outputs); i++ {
|
||||
if math.Abs(outputs[i]-testcase.Expected[i]) > 0.000001 {
|
||||
t.Errorf("Failed, output and expected mismatch")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%s", stream.Stream)
|
||||
}
|
@ -15,14 +15,33 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/unidoc/unidoc/common"
|
||||
"github.com/unidoc/unidoc/pdf/model/sampling"
|
||||
)
|
||||
|
||||
// Basic representation of an image. The images are stored in the JPEG
|
||||
// format.
|
||||
// Basic representation of an image.
|
||||
// The colorspace is not specified, but must be known when handling the image.
|
||||
type Image struct {
|
||||
Width int64
|
||||
Height int64
|
||||
Data *bytes.Buffer
|
||||
Width int64 // The width of the image in samples
|
||||
Height int64 // The height of the image in samples
|
||||
BitsPerComponent int64 // The number of bits per color component
|
||||
Data []byte // Image data stored as bytes.
|
||||
}
|
||||
|
||||
// Convert the raw byte slice into samples which are stored in a uint32 bit array.
|
||||
// Each sample is represented by BitsPerComponent consecutive bits in the raw data.
|
||||
func (this *Image) GetSamples() []uint32 {
|
||||
samples := sampling.ResampleBytes(this.Data, int(this.BitsPerComponent))
|
||||
return samples
|
||||
}
|
||||
|
||||
// Convert samples to byte-data.
|
||||
func (this *Image) SetSamples(samples []uint32) {
|
||||
resampled := sampling.ResampleUint32(samples, 8)
|
||||
data := []byte{}
|
||||
for _, val := range resampled {
|
||||
data = append(data, byte(val))
|
||||
}
|
||||
this.Data = data
|
||||
}
|
||||
|
||||
type ImageHandler interface {
|
||||
@ -36,6 +55,8 @@ type ImageHandler interface {
|
||||
|
||||
type DefaultImageHandler struct{}
|
||||
|
||||
// Reads an image and loads into a new Image object with an RGB
|
||||
// colormap and 8 bits per component.
|
||||
func (this DefaultImageHandler) Read(reader io.Reader) (*Image, error) {
|
||||
// Load the image with the native implementation.
|
||||
img, _, err := image.Decode(reader)
|
||||
@ -63,7 +84,8 @@ func (this DefaultImageHandler) Read(reader io.Reader) (*Image, error) {
|
||||
imag := Image{}
|
||||
imag.Width = int64(b.Dx())
|
||||
imag.Height = int64(b.Dy())
|
||||
imag.Data = &buf
|
||||
imag.BitsPerComponent = 8 // RGBA colormap
|
||||
imag.Data = buf.Bytes()
|
||||
|
||||
return &imag, nil
|
||||
}
|
||||
@ -77,5 +99,4 @@ var ImageHandling ImageHandler = DefaultImageHandler{}
|
||||
|
||||
func SetImageHandler(imgHandling ImageHandler) {
|
||||
ImageHandling = imgHandling
|
||||
|
||||
}
|
||||
|
@ -748,11 +748,61 @@ func (this *PdfPage) GetContentStreams() ([]string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
type PdfPageResourcesColorspaces struct {
|
||||
Names []string
|
||||
Colorspaces map[string]PdfColorspace
|
||||
|
||||
container *PdfIndirectObject
|
||||
}
|
||||
|
||||
func newPdfPageResourcesColorspacesFromPdfObject(obj PdfObject) (*PdfPageResourcesColorspaces, error) {
|
||||
colorspaces := &PdfPageResourcesColorspaces{}
|
||||
|
||||
if indObj, isIndirect := obj.(*PdfIndirectObject); isIndirect {
|
||||
colorspaces.container = indObj
|
||||
obj = indObj.PdfObject
|
||||
}
|
||||
|
||||
dict, ok := obj.(*PdfObjectDictionary)
|
||||
if !ok {
|
||||
return nil, errors.New("CS attribute type error")
|
||||
}
|
||||
|
||||
colorspaces.Names = []string{}
|
||||
colorspaces.Colorspaces = map[string]PdfColorspace{}
|
||||
|
||||
for csName, csObj := range *dict {
|
||||
colorspaces.Names = append(colorspaces.Names, string(csName))
|
||||
cs, err := newPdfColorspaceFromPdfObject(csObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
colorspaces.Colorspaces[string(csName)] = cs
|
||||
}
|
||||
|
||||
return colorspaces, nil
|
||||
}
|
||||
|
||||
func (this *PdfPageResourcesColorspaces) ToPdfObject() PdfObject {
|
||||
dict := &PdfObjectDictionary{}
|
||||
for _, csName := range this.Names {
|
||||
(*dict)[PdfObjectName(csName)] = this.Colorspaces[csName].ToPdfObject()
|
||||
}
|
||||
|
||||
if this.container != nil {
|
||||
this.container.PdfObject = dict
|
||||
return this.container
|
||||
}
|
||||
|
||||
return dict
|
||||
}
|
||||
|
||||
// Page resources model.
|
||||
// Implements PdfModel.
|
||||
type PdfPageResources struct {
|
||||
ExtGState PdfObject
|
||||
ColorSpace PdfObject
|
||||
ExtGState PdfObject
|
||||
//ColorSpace PdfObject
|
||||
ColorSpace *PdfPageResourcesColorspaces
|
||||
Pattern PdfObject
|
||||
Shading PdfObject
|
||||
XObject PdfObject
|
||||
@ -776,7 +826,13 @@ func NewPdfPageResourcesFromDict(dict *PdfObjectDictionary) (*PdfPageResources,
|
||||
r.ExtGState = obj
|
||||
}
|
||||
if obj, isDefined := (*dict)["ColorSpace"]; isDefined {
|
||||
r.ColorSpace = obj
|
||||
//r.ColorSpace = obj
|
||||
|
||||
colorspaces, err := newPdfPageResourcesColorspacesFromPdfObject(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.ColorSpace = colorspaces
|
||||
}
|
||||
if obj, isDefined := (*dict)["Pattern"]; isDefined {
|
||||
r.Pattern = obj
|
||||
@ -807,197 +863,15 @@ func (r *PdfPageResources) GetContainingPdfObject() PdfObject {
|
||||
func (r *PdfPageResources) ToPdfObject() PdfObject {
|
||||
d := r.primitive
|
||||
d.SetIfNotNil("ExtGState", r.ExtGState)
|
||||
d.SetIfNotNil("ColorSpace", r.ColorSpace)
|
||||
if r.ColorSpace != nil {
|
||||
d.SetIfNotNil("ColorSpace", r.ColorSpace.ToPdfObject())
|
||||
}
|
||||
d.SetIfNotNil("Pattern", r.Pattern)
|
||||
d.SetIfNotNil("Shading", r.Shading)
|
||||
d.SetIfNotNil("XObject", r.XObject)
|
||||
d.SetIfNotNil("Font", r.Font)
|
||||
d.SetIfNotNil("ProcSet", r.ProcSet)
|
||||
d.SetIfNotNil("Properties", r.Properties)
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
// XObjectImage (Table 89 in 8.9.5.1).
|
||||
// Implements PdfModel interface.
|
||||
type XObjectImage struct {
|
||||
Width *int64
|
||||
Height *int64
|
||||
ColorSpace PdfObject
|
||||
BitsPerComponent *int64
|
||||
Intent PdfObject
|
||||
ImageMask PdfObject
|
||||
Mask PdfObject
|
||||
Decode PdfObject
|
||||
Interpolate PdfObject
|
||||
Alternatives PdfObject
|
||||
SMask PdfObject
|
||||
SMaskInData PdfObject
|
||||
Name PdfObject
|
||||
StructParent PdfObject
|
||||
ID PdfObject
|
||||
OPI PdfObject
|
||||
Metadata PdfObject
|
||||
OC PdfObject
|
||||
Stream []byte
|
||||
// Primitive
|
||||
primitive *PdfObjectStream
|
||||
}
|
||||
|
||||
func NewXObjectImage() *XObjectImage {
|
||||
xobj := &XObjectImage{}
|
||||
stream := &PdfObjectStream{}
|
||||
stream.PdfObjectDictionary = &PdfObjectDictionary{}
|
||||
xobj.primitive = stream
|
||||
return xobj
|
||||
}
|
||||
|
||||
// Creates a new XObject Image from an image object with default
|
||||
// options.
|
||||
func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, error) {
|
||||
xobj := NewXObjectImage()
|
||||
|
||||
xobj.Name = &name
|
||||
xobj.Stream = img.Data.Bytes()
|
||||
|
||||
// Width and height.
|
||||
imWidth := img.Width
|
||||
imHeight := img.Height
|
||||
xobj.Width = &imWidth
|
||||
xobj.Height = &imHeight
|
||||
|
||||
// Bits.
|
||||
bitDepth := int64(8)
|
||||
xobj.BitsPerComponent = &bitDepth
|
||||
|
||||
xobj.ColorSpace = MakeName("DeviceRGB")
|
||||
|
||||
return xobj, nil
|
||||
}
|
||||
|
||||
// Build the image xobject from a stream object.
|
||||
func NewXObjectImageFromStream(stream PdfObjectStream) (*XObjectImage, error) {
|
||||
img := NewXObjectImage()
|
||||
|
||||
dict := *(stream.PdfObjectDictionary)
|
||||
|
||||
if obj, isDefined := dict["Width"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image width object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.Width = &iVal
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["Height"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image height object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.Height = &iVal
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["ColorSpace"]; isDefined {
|
||||
img.ColorSpace = obj
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["BitsPerComponent"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image height object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.BitsPerComponent = &iVal
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["Intent"]; isDefined {
|
||||
img.Intent = obj
|
||||
}
|
||||
if obj, isDefined := dict["ImageMask"]; isDefined {
|
||||
img.ImageMask = obj
|
||||
}
|
||||
if obj, isDefined := dict["Mask"]; isDefined {
|
||||
img.Mask = obj
|
||||
}
|
||||
if obj, isDefined := dict["Decode"]; isDefined {
|
||||
img.Decode = obj
|
||||
}
|
||||
if obj, isDefined := dict["Interpolate"]; isDefined {
|
||||
img.Interpolate = obj
|
||||
}
|
||||
if obj, isDefined := dict["Alternatives"]; isDefined {
|
||||
img.Alternatives = obj
|
||||
}
|
||||
if obj, isDefined := dict["SMask"]; isDefined {
|
||||
img.SMask = obj
|
||||
}
|
||||
if obj, isDefined := dict["SMaskInData"]; isDefined {
|
||||
img.SMaskInData = obj
|
||||
}
|
||||
if obj, isDefined := dict["Name"]; isDefined {
|
||||
img.Name = obj
|
||||
}
|
||||
if obj, isDefined := dict["StructParent"]; isDefined {
|
||||
img.StructParent = obj
|
||||
}
|
||||
if obj, isDefined := dict["ID"]; isDefined {
|
||||
img.ID = obj
|
||||
}
|
||||
if obj, isDefined := dict["OPI"]; isDefined {
|
||||
img.OPI = obj
|
||||
}
|
||||
if obj, isDefined := dict["Metadata"]; isDefined {
|
||||
img.Metadata = obj
|
||||
}
|
||||
if obj, isDefined := dict["OC"]; isDefined {
|
||||
img.OC = obj
|
||||
}
|
||||
|
||||
img.Stream = stream.Stream
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (ximg *XObjectImage) GetContainingPdfObject() PdfObject {
|
||||
return ximg.primitive
|
||||
}
|
||||
|
||||
// Return a stream object.
|
||||
func (ximg *XObjectImage) ToPdfObject() PdfObject {
|
||||
stream := ximg.primitive
|
||||
stream.Stream = ximg.Stream
|
||||
|
||||
dict := stream.PdfObjectDictionary
|
||||
|
||||
dict.Set("Type", MakeName("XObject"))
|
||||
dict.Set("Subtype", MakeName("Image"))
|
||||
dict.Set("Width", MakeInteger(*(ximg.Width)))
|
||||
dict.Set("Height", MakeInteger(*(ximg.Height)))
|
||||
dict.Set("Filter", MakeName("DCTDecode"))
|
||||
|
||||
if ximg.BitsPerComponent != nil {
|
||||
dict.Set("BitsPerComponent", MakeInteger(*(ximg.BitsPerComponent)))
|
||||
}
|
||||
|
||||
dict.SetIfNotNil("ColorSpace", ximg.ColorSpace)
|
||||
dict.SetIfNotNil("Intent", ximg.Intent)
|
||||
dict.SetIfNotNil("ImageMask", ximg.ImageMask)
|
||||
dict.SetIfNotNil("Mask", ximg.Mask)
|
||||
dict.SetIfNotNil("Decode", ximg.Decode)
|
||||
dict.SetIfNotNil("Interpolate", ximg.Interpolate)
|
||||
dict.SetIfNotNil("Alternatives", ximg.Alternatives)
|
||||
dict.SetIfNotNil("SMask", ximg.SMask)
|
||||
dict.SetIfNotNil("SMaskInData", ximg.SMaskInData)
|
||||
dict.SetIfNotNil("Name", ximg.Name)
|
||||
dict.SetIfNotNil("StructParent", ximg.StructParent)
|
||||
dict.SetIfNotNil("ID", ximg.ID)
|
||||
dict.SetIfNotNil("OPI", ximg.OPI)
|
||||
dict.SetIfNotNil("Metadata", ximg.Metadata)
|
||||
dict.SetIfNotNil("OC", ximg.OC)
|
||||
|
||||
dict.Set("Length", MakeInteger(int64(len(ximg.Stream))))
|
||||
stream.Stream = ximg.Stream
|
||||
|
||||
return stream
|
||||
}
|
||||
|
187
pdf/model/sampling/resample.go
Normal file
187
pdf/model/sampling/resample.go
Normal file
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* This file is subject to the terms and conditions defined in
|
||||
* file 'LICENSE.md', which is part of this source code package.
|
||||
*/
|
||||
|
||||
package sampling
|
||||
|
||||
// Resample the raw data which is in 8-bit (byte) format as a different
|
||||
// bit count per sample, up to 32 bits (uint32).
|
||||
func ResampleBytes(data []byte, bitsPerSample int) []uint32 {
|
||||
samples := []uint32{}
|
||||
|
||||
bitsLeftPerSample := bitsPerSample
|
||||
var sample uint32
|
||||
var remainder byte
|
||||
remainderBits := 0
|
||||
|
||||
index := 0
|
||||
|
||||
i := 0
|
||||
for i < len(data) {
|
||||
// Start with the remainder.
|
||||
if remainderBits > 0 {
|
||||
take := remainderBits
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
|
||||
sample = (sample << uint(take)) | uint32(remainder>>uint(8-take))
|
||||
remainderBits -= take
|
||||
if remainderBits > 0 {
|
||||
remainder = remainder << uint(take)
|
||||
} else {
|
||||
remainder = 0
|
||||
}
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
//samples[index] = sample
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
// Take next byte
|
||||
b := data[i]
|
||||
i++
|
||||
|
||||
// 8 bits.
|
||||
take := 8
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
remainderBits = 8 - take
|
||||
sample = (sample << uint(take)) | uint32(b>>uint(remainderBits))
|
||||
|
||||
if take < 8 {
|
||||
remainder = b << uint(take)
|
||||
}
|
||||
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
//samples[index] = sample
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take care of remaining samples (if enough data available).
|
||||
for remainderBits >= bitsPerSample {
|
||||
take := remainderBits
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
|
||||
sample = (sample << uint(take)) | uint32(remainder>>uint(8-take))
|
||||
remainderBits -= take
|
||||
if remainderBits > 0 {
|
||||
remainder = remainder << uint(take)
|
||||
} else {
|
||||
remainder = 0
|
||||
}
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
//samples[index] = sample
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
||||
|
||||
// Resample the raw data which is in 32-bit (uint 32) format as a different
|
||||
// bit count per sample, up to 32 bits (uint32).
|
||||
func ResampleUint32(data []uint32, bitsPerSample int) []uint32 {
|
||||
samples := []uint32{}
|
||||
|
||||
bitsLeftPerSample := bitsPerSample
|
||||
var sample uint32
|
||||
var remainder uint32
|
||||
remainderBits := 0
|
||||
|
||||
index := 0
|
||||
|
||||
i := 0
|
||||
for i < len(data) {
|
||||
// Start with the remainder.
|
||||
if remainderBits > 0 {
|
||||
take := remainderBits
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
|
||||
sample = (sample << uint(take)) | uint32(remainder>>uint(32-take))
|
||||
remainderBits -= take
|
||||
if remainderBits > 0 {
|
||||
remainder = remainder << uint(take)
|
||||
} else {
|
||||
remainder = 0
|
||||
}
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
//samples[index] = sample
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
} else {
|
||||
// Take next byte
|
||||
b := data[i]
|
||||
i++
|
||||
|
||||
// 32 bits.
|
||||
take := 32
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
remainderBits = 32 - take
|
||||
sample = (sample << uint(take)) | uint32(b>>uint(remainderBits))
|
||||
|
||||
if take < 32 {
|
||||
remainder = b << uint(take)
|
||||
}
|
||||
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
//samples[index] = sample
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take care of remaining samples (if enough data available).
|
||||
for remainderBits >= bitsPerSample {
|
||||
take := remainderBits
|
||||
if bitsLeftPerSample < take {
|
||||
take = bitsLeftPerSample
|
||||
}
|
||||
|
||||
sample = (sample << uint(take)) | uint32(remainder>>uint(32-take))
|
||||
remainderBits -= take
|
||||
if remainderBits > 0 {
|
||||
remainder = remainder << uint(take)
|
||||
} else {
|
||||
remainder = 0
|
||||
}
|
||||
bitsLeftPerSample -= take
|
||||
if bitsLeftPerSample == 0 {
|
||||
samples = append(samples, sample)
|
||||
bitsLeftPerSample = bitsPerSample
|
||||
sample = 0
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
return samples
|
||||
}
|
147
pdf/model/sampling/resample_test.go
Normal file
147
pdf/model/sampling/resample_test.go
Normal file
@ -0,0 +1,147 @@
|
||||
/*
|
||||
* This file is subject to the terms and conditions defined in
|
||||
* file 'LICENSE.md', which is part of this source code package.
|
||||
*/
|
||||
|
||||
package sampling
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type TestResamplingTest1 struct {
|
||||
InputData []byte
|
||||
BitsPerSample int
|
||||
Expected []uint32
|
||||
}
|
||||
|
||||
func samplesEqual(d1 []uint32, d2 []uint32) bool {
|
||||
if len(d1) != len(d2) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(d1); i++ {
|
||||
if d1[i] != d2[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Test resampling example data with different bits per sample.
|
||||
// Input is in bytes.
|
||||
func TestResamplingBytes(t *testing.T) {
|
||||
// 0xB5 0x5D 0x2A
|
||||
// 10110101 01011101 00101010
|
||||
// 2-bit resampling:
|
||||
// 10 11 01 01 01 01 11 01 00 10 10 10
|
||||
// 2 3 1 1 1 1 3 1 0 2 2 2
|
||||
// 3-bit resampling:
|
||||
// 101 101 010 101 110 100 101 010
|
||||
// 5 5 2 5 6 4 5 2
|
||||
// 4-bit resampling:
|
||||
// 1011 0101 0101 1101 0010 1010
|
||||
// 11 5 5 13 2 10
|
||||
// 5-bit resampling
|
||||
// 10110 10101 01110 10010 (1010)<- the remainder is dumped
|
||||
// 22 21 14 18
|
||||
// 12-bit resampling
|
||||
// 101101010101 110100101010
|
||||
// 2901 3370
|
||||
// 13-bit resampling
|
||||
// 1011010101011 (10100101010)
|
||||
// 16-bit resampling
|
||||
// 1011010101011101 (00101010)
|
||||
// 46429
|
||||
// 24-bit resampling
|
||||
// 101101010101110100101010
|
||||
//
|
||||
// 0xde 0xad 0xbe 0xef 0x15 0x13 0x37
|
||||
|
||||
testcases := []TestResamplingTest1{
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 1, []uint32{1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 2, []uint32{2, 3, 1, 1, 1, 1, 3, 1, 0, 2, 2, 2}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 3, []uint32{5, 5, 2, 5, 6, 4, 5, 2}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 4, []uint32{11, 5, 5, 13, 2, 10}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 5, []uint32{22, 21, 14, 18}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 8, []uint32{0xB5, 0x5D, 0x2A}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 12, []uint32{2901, 3370}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 13, []uint32{5803}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 16, []uint32{0xB55D}},
|
||||
{[]byte{0xB5, 0x5D, 0x2A}, 24, []uint32{0xB55D2A}},
|
||||
{[]byte{0xde, 0xad, 0xbe, 0xef, 0x15, 0x13, 0x37, 0x20}, 24, []uint32{0xdeadbe, 0xef1513}},
|
||||
{[]byte{0xde, 0xad, 0xbe, 0xef, 0x15, 0x13, 0x37, 0x20, 0x21}, 32, []uint32{0xdeadbeef, 0x15133720}},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
b := ResampleBytes(testcase.InputData, testcase.BitsPerSample)
|
||||
fmt.Println(b)
|
||||
if !samplesEqual(b, testcase.Expected) {
|
||||
t.Errorf("Test case failed. Got: % d, expected: % d", b, testcase.Expected)
|
||||
t.Errorf("Test case failed. Got: % X, expected: % X", b, testcase.Expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TestResamplingTest2 struct {
|
||||
InputData []uint32
|
||||
BitsPerSample int
|
||||
Expected []uint32
|
||||
}
|
||||
|
||||
// Test resampling example data with different bits per sample.
|
||||
// Input is in uint32.
|
||||
func TestResamplingUint32(t *testing.T) {
|
||||
// 0xB5 0x5D 0x2A 0x00
|
||||
// 10110101 01011101 00101010 00000000
|
||||
// 2-bit resampling:
|
||||
// 10 11 01 01 01 01 11 01 00 10 10 10 00 00 00 00
|
||||
// 2 3 1 1 1 1 3 1 0 2 2 2 0 0 0 0
|
||||
// 3-bit resampling:
|
||||
// 101 101 010 101 110 100 101 010 000 000 (00)
|
||||
// 5 5 2 5 6 4 5 2 0 0
|
||||
// 4-bit resampling:
|
||||
// 1011 0101 0101 1101 0010 1010 0000 0000
|
||||
// 11 5 5 13 2 10 0 0
|
||||
// 5-bit resampling
|
||||
// 10110 10101 01110 10010 10100 00000 (00)<- the remainder is dumped
|
||||
// 22 21 14 18 20 0
|
||||
// 12-bit resampling
|
||||
// 101101010101 110100101010 (00000000)
|
||||
// 2901 3370
|
||||
// 13-bit resampling
|
||||
// 1011010101011 1010010101000 (0000000)
|
||||
// 16-bit resampling
|
||||
// 1011010101011101 0010101000000000
|
||||
// 0xB55D 0x2A00
|
||||
// 24-bit resampling
|
||||
// 101101010101110100101010 (00000000)
|
||||
//
|
||||
// 0xde 0xad 0xbe 0xef 0x15 0x13 0x37
|
||||
|
||||
testcases := []TestResamplingTest2{
|
||||
{[]uint32{0xB55D2A00}, 1, []uint32{1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
|
||||
{[]uint32{0xB55D2A00}, 2, []uint32{2, 3, 1, 1, 1, 1, 3, 1, 0, 2, 2, 2, 0, 0, 0, 0}},
|
||||
{[]uint32{0xB55D2A00}, 3, []uint32{5, 5, 2, 5, 6, 4, 5, 2, 0, 0}},
|
||||
{[]uint32{0xB55D2A00}, 4, []uint32{11, 5, 5, 13, 2, 10, 0, 0}},
|
||||
{[]uint32{0xB55D2A00}, 5, []uint32{22, 21, 14, 18, 20, 0}},
|
||||
{[]uint32{0xB55D2A00}, 8, []uint32{0xB5, 0x5D, 0x2A, 0x00}},
|
||||
{[]uint32{0xB55D2A00}, 12, []uint32{2901, 3370}},
|
||||
{[]uint32{0xB55D2A00}, 13, []uint32{5803, 5288}},
|
||||
{[]uint32{0xB55D2A00}, 16, []uint32{0xB55D, 0x2A00}},
|
||||
{[]uint32{0xB55D2A00}, 24, []uint32{0xB55D2A}},
|
||||
{[]uint32{0xdeadbeef, 0x15133720}, 24, []uint32{0xdeadbe, 0xef1513}},
|
||||
{[]uint32{0xdeadbeef, 0x15133720}, 32, []uint32{0xdeadbeef, 0x15133720c}},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
b := ResampleUint32(testcase.InputData, testcase.BitsPerSample)
|
||||
fmt.Println(b)
|
||||
if !samplesEqual(b, testcase.Expected) {
|
||||
t.Errorf("Test case failed. Got: % d, expected: % d", b, testcase.Expected)
|
||||
t.Errorf("Test case failed. Got: % X, expected: % X", b, testcase.Expected)
|
||||
}
|
||||
}
|
||||
}
|
@ -183,7 +183,7 @@ func (this *PdfWriter) addObjects(obj PdfObject) error {
|
||||
|
||||
if so, isStreamObj := obj.(*PdfObjectStream); isStreamObj {
|
||||
common.Log.Debug("Stream")
|
||||
common.Log.Debug("- %s", obj)
|
||||
common.Log.Debug("- %s %p", obj, obj)
|
||||
if this.addObject(so) {
|
||||
err := this.addObjects(so.PdfObjectDictionary)
|
||||
if err != nil {
|
||||
@ -477,6 +477,8 @@ func (this *PdfWriter) writeObject(num int, obj PdfObject) {
|
||||
return
|
||||
}
|
||||
|
||||
// XXX/TODO: Add a default encoder if Filter not specified?
|
||||
// Still need to make sure is encrypted.
|
||||
if pobj, isStream := obj.(*PdfObjectStream); isStream {
|
||||
outStr := fmt.Sprintf("%d 0 obj\n", num)
|
||||
outStr += pobj.PdfObjectDictionary.DefaultWriteString()
|
||||
|
253
pdf/model/xobject.go
Normal file
253
pdf/model/xobject.go
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* This file is subject to the terms and conditions defined in
|
||||
* file 'LICENSE.md', which is part of this source code package.
|
||||
*/
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
. "github.com/unidoc/unidoc/pdf/core"
|
||||
)
|
||||
|
||||
// XObjectImage (Table 89 in 8.9.5.1).
|
||||
// Implements PdfModel interface.
|
||||
type XObjectImage struct {
|
||||
//ColorSpace PdfObject
|
||||
Width *int64
|
||||
Height *int64
|
||||
ColorSpace PdfColorspace
|
||||
BitsPerComponent *int64
|
||||
Filter *PdfObjectName
|
||||
Intent PdfObject
|
||||
ImageMask PdfObject
|
||||
Mask PdfObject
|
||||
Decode PdfObject
|
||||
Interpolate PdfObject
|
||||
Alternatives PdfObject
|
||||
SMask PdfObject
|
||||
SMaskInData PdfObject
|
||||
Name PdfObject
|
||||
StructParent PdfObject
|
||||
ID PdfObject
|
||||
OPI PdfObject
|
||||
Metadata PdfObject
|
||||
OC PdfObject
|
||||
Stream []byte
|
||||
// Primitive
|
||||
primitive *PdfObjectStream
|
||||
}
|
||||
|
||||
func NewXObjectImage() *XObjectImage {
|
||||
xobj := &XObjectImage{}
|
||||
stream := &PdfObjectStream{}
|
||||
stream.PdfObjectDictionary = &PdfObjectDictionary{}
|
||||
xobj.primitive = stream
|
||||
return xobj
|
||||
}
|
||||
|
||||
// Creates a new XObject Image from an image object with default
|
||||
// options.
|
||||
func NewXObjectImageFromImage(name PdfObjectName, img *Image) (*XObjectImage, error) {
|
||||
xobj := NewXObjectImage()
|
||||
|
||||
xobj.Name = &name
|
||||
xobj.Stream = img.Data
|
||||
|
||||
// Width and height.
|
||||
imWidth := img.Width
|
||||
imHeight := img.Height
|
||||
xobj.Width = &imWidth
|
||||
xobj.Height = &imHeight
|
||||
|
||||
// Bits.
|
||||
xobj.BitsPerComponent = &img.BitsPerComponent
|
||||
|
||||
//xobj.ColorSpace = MakeName("DeviceRGB")
|
||||
xobj.ColorSpace = NewPdfColorspaceDeviceRGB()
|
||||
|
||||
// define color space?
|
||||
|
||||
return xobj, nil
|
||||
}
|
||||
|
||||
// Build the image xobject from a stream object.
|
||||
// An image dictionary is the dictionary portion of a stream object representing an image XObject.
|
||||
func NewXObjectImageFromStream(stream *PdfObjectStream) (*XObjectImage, error) {
|
||||
img := &XObjectImage{}
|
||||
img.primitive = stream
|
||||
|
||||
dict := *(stream.PdfObjectDictionary)
|
||||
|
||||
if obj, isDefined := dict["Filter"]; isDefined {
|
||||
name, ok := obj.(*PdfObjectName)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image Filter (not name)")
|
||||
}
|
||||
img.Filter = name
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["Width"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image width object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.Width = &iVal
|
||||
} else {
|
||||
return nil, errors.New("Width missing")
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["Height"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image height object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.Height = &iVal
|
||||
} else {
|
||||
return nil, errors.New("Height missing")
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["ColorSpace"]; isDefined {
|
||||
//img.ColorSpace = obj
|
||||
cs, err := newPdfColorspaceFromPdfObject(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
img.ColorSpace = cs
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["BitsPerComponent"]; isDefined {
|
||||
iObj, ok := obj.(*PdfObjectInteger)
|
||||
if !ok {
|
||||
return nil, errors.New("Invalid image height object")
|
||||
}
|
||||
iVal := int64(*iObj)
|
||||
img.BitsPerComponent = &iVal
|
||||
}
|
||||
|
||||
if obj, isDefined := dict["Intent"]; isDefined {
|
||||
img.Intent = obj
|
||||
}
|
||||
if obj, isDefined := dict["ImageMask"]; isDefined {
|
||||
img.ImageMask = obj
|
||||
}
|
||||
if obj, isDefined := dict["Mask"]; isDefined {
|
||||
img.Mask = obj
|
||||
}
|
||||
if obj, isDefined := dict["Decode"]; isDefined {
|
||||
img.Decode = obj
|
||||
}
|
||||
if obj, isDefined := dict["Interpolate"]; isDefined {
|
||||
img.Interpolate = obj
|
||||
}
|
||||
if obj, isDefined := dict["Alternatives"]; isDefined {
|
||||
img.Alternatives = obj
|
||||
}
|
||||
if obj, isDefined := dict["SMask"]; isDefined {
|
||||
img.SMask = obj
|
||||
}
|
||||
if obj, isDefined := dict["SMaskInData"]; isDefined {
|
||||
img.SMaskInData = obj
|
||||
}
|
||||
if obj, isDefined := dict["Name"]; isDefined {
|
||||
img.Name = obj
|
||||
}
|
||||
if obj, isDefined := dict["StructParent"]; isDefined {
|
||||
img.StructParent = obj
|
||||
}
|
||||
if obj, isDefined := dict["ID"]; isDefined {
|
||||
img.ID = obj
|
||||
}
|
||||
if obj, isDefined := dict["OPI"]; isDefined {
|
||||
img.OPI = obj
|
||||
}
|
||||
if obj, isDefined := dict["Metadata"]; isDefined {
|
||||
img.Metadata = obj
|
||||
}
|
||||
if obj, isDefined := dict["OC"]; isDefined {
|
||||
img.OC = obj
|
||||
}
|
||||
|
||||
img.Stream = stream.Stream
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// This will convert to an Image which can be transformed or saved out.
|
||||
// The image is returned in RGB colormap.
|
||||
func (ximg *XObjectImage) ToImage() (*Image, error) {
|
||||
image := &Image{}
|
||||
|
||||
if ximg.Height == nil {
|
||||
return nil, errors.New("Height attribute missing")
|
||||
}
|
||||
image.Height = *ximg.Height
|
||||
|
||||
if ximg.Width == nil {
|
||||
return nil, errors.New("Width attribute missing")
|
||||
}
|
||||
image.Width = *ximg.Width
|
||||
|
||||
if ximg.BitsPerComponent == nil {
|
||||
return nil, errors.New("Bits per component missing")
|
||||
}
|
||||
image.BitsPerComponent = *ximg.BitsPerComponent
|
||||
|
||||
decoded, err := DecodeStream(ximg.primitive)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
image.Data = decoded
|
||||
|
||||
// Convert to RGB image representation.
|
||||
rgbImage, err := ximg.ColorSpace.ToRGB(*image)
|
||||
return &rgbImage, err
|
||||
}
|
||||
|
||||
func (ximg *XObjectImage) GetContainingPdfObject() PdfObject {
|
||||
return ximg.primitive
|
||||
}
|
||||
|
||||
// Return a stream object.
|
||||
func (ximg *XObjectImage) ToPdfObject() PdfObject {
|
||||
stream := ximg.primitive
|
||||
stream.Stream = ximg.Stream
|
||||
|
||||
dict := stream.PdfObjectDictionary
|
||||
|
||||
dict.Set("Type", MakeName("XObject"))
|
||||
dict.Set("Subtype", MakeName("Image"))
|
||||
dict.Set("Width", MakeInteger(*(ximg.Width)))
|
||||
dict.Set("Height", MakeInteger(*(ximg.Height)))
|
||||
dict.Set("Filter", MakeName("DCTDecode"))
|
||||
|
||||
if ximg.BitsPerComponent != nil {
|
||||
dict.Set("BitsPerComponent", MakeInteger(*(ximg.BitsPerComponent)))
|
||||
}
|
||||
|
||||
if ximg.ColorSpace != nil {
|
||||
dict.SetIfNotNil("ColorSpace", ximg.ColorSpace.ToPdfObject())
|
||||
}
|
||||
dict.SetIfNotNil("Intent", ximg.Intent)
|
||||
dict.SetIfNotNil("ImageMask", ximg.ImageMask)
|
||||
dict.SetIfNotNil("Mask", ximg.Mask)
|
||||
dict.SetIfNotNil("Decode", ximg.Decode)
|
||||
dict.SetIfNotNil("Interpolate", ximg.Interpolate)
|
||||
dict.SetIfNotNil("Alternatives", ximg.Alternatives)
|
||||
dict.SetIfNotNil("SMask", ximg.SMask)
|
||||
dict.SetIfNotNil("SMaskInData", ximg.SMaskInData)
|
||||
dict.SetIfNotNil("Name", ximg.Name)
|
||||
dict.SetIfNotNil("StructParent", ximg.StructParent)
|
||||
dict.SetIfNotNil("ID", ximg.ID)
|
||||
dict.SetIfNotNil("OPI", ximg.OPI)
|
||||
dict.SetIfNotNil("Metadata", ximg.Metadata)
|
||||
dict.SetIfNotNil("OC", ximg.OC)
|
||||
|
||||
dict.Set("Length", MakeInteger(int64(len(ximg.Stream))))
|
||||
stream.Stream = ximg.Stream
|
||||
|
||||
return stream
|
||||
}
|
15
pdf/ps/const.go
Normal file
15
pdf/ps/const.go
Normal file
@ -0,0 +1,15 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Tolerance for comparing real values.
|
||||
const TOLERANCE = 0.000001
|
||||
|
||||
// Common errors.
|
||||
var ErrStackUnderflow = errors.New("Stack underflow")
|
||||
var ErrStackOverflow = errors.New("Stack overflow")
|
||||
var ErrTypeCheck = errors.New("Type check error")
|
||||
var ErrRangeCheck = errors.New("Range check error")
|
||||
var ErrUndefinedResult = errors.New("Undefined result error")
|
57
pdf/ps/exec.go
Normal file
57
pdf/ps/exec.go
Normal file
@ -0,0 +1,57 @@
|
||||
package ps
|
||||
|
||||
// A limited postscript parser for PDF function type 4.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A PSExecutor has its own execution stack and is used to executre a PS routine (program).
|
||||
type PSExecutor struct {
|
||||
Stack *PSStack
|
||||
program *PSProgram
|
||||
}
|
||||
|
||||
func NewPSExecutor(program *PSProgram) *PSExecutor {
|
||||
executor := &PSExecutor{}
|
||||
executor.Stack = NewPSStack()
|
||||
executor.program = program
|
||||
return executor
|
||||
}
|
||||
|
||||
func PSObjectArrayToFloat64Array(objects []PSObject) ([]float64, error) {
|
||||
vals := []float64{}
|
||||
|
||||
for _, obj := range objects {
|
||||
if number, is := obj.(*PSInteger); is {
|
||||
vals = append(vals, float64(number.Val))
|
||||
} else if number, is := obj.(*PSReal); is {
|
||||
vals = append(vals, number.Val)
|
||||
} else {
|
||||
return nil, fmt.Errorf("Type error")
|
||||
}
|
||||
}
|
||||
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func (this *PSExecutor) Execute(objects []PSObject) ([]PSObject, error) {
|
||||
// Add the arguments on stack
|
||||
// [obj1 obj2 ...]
|
||||
for _, obj := range objects {
|
||||
err := this.Stack.Push(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err := this.program.Exec(this.Stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := []PSObject(*this.Stack)
|
||||
this.Stack.Empty()
|
||||
|
||||
return result, nil
|
||||
}
|
31
pdf/ps/handy.go
Normal file
31
pdf/ps/handy.go
Normal file
@ -0,0 +1,31 @@
|
||||
package ps
|
||||
|
||||
func MakeReal(val float64) PSObject {
|
||||
obj := PSReal{}
|
||||
obj.Val = val
|
||||
return &obj
|
||||
}
|
||||
|
||||
func MakeInteger(val int) PSObject {
|
||||
obj := PSInteger{}
|
||||
obj.Val = val
|
||||
return &obj
|
||||
}
|
||||
|
||||
func MakeBool(val bool) *PSBoolean {
|
||||
obj := PSBoolean{}
|
||||
obj.Val = val
|
||||
return &obj
|
||||
}
|
||||
|
||||
func MakeOperand(val string) *PSOperand {
|
||||
obj := PSOperand(val)
|
||||
return &obj
|
||||
}
|
||||
|
||||
func abs(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
1336
pdf/ps/object.go
Normal file
1336
pdf/ps/object.go
Normal file
File diff suppressed because it is too large
Load Diff
244
pdf/ps/parser.go
Normal file
244
pdf/ps/parser.go
Normal file
@ -0,0 +1,244 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/unidoc/unidoc/common"
|
||||
pdfcore "github.com/unidoc/unidoc/pdf/core"
|
||||
)
|
||||
|
||||
type PSParser struct {
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
// Create a new instance of the PDF Postscript parser from input data.
|
||||
func NewPSParser(content []byte) *PSParser {
|
||||
parser := PSParser{}
|
||||
|
||||
buffer := bytes.NewBuffer(content)
|
||||
parser.reader = bufio.NewReader(buffer)
|
||||
|
||||
return &parser
|
||||
}
|
||||
|
||||
// Parse the postscript and store as a program that can be executed.
|
||||
func (this *PSParser) Parse() (*PSProgram, error) {
|
||||
this.skipSpaces()
|
||||
bb, err := this.reader.Peek(2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bb[0] != '{' {
|
||||
return nil, fmt.Errorf("Invalid PS Program not starting with {")
|
||||
}
|
||||
|
||||
program, err := this.parseFunction()
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return program, err
|
||||
}
|
||||
|
||||
// Detect the signature at the current parse position and parse
|
||||
// the corresponding object.
|
||||
func (this *PSParser) parseFunction() (*PSProgram, error) {
|
||||
c, _ := this.reader.ReadByte()
|
||||
if c != '{' {
|
||||
return nil, errors.New("Invalid function")
|
||||
}
|
||||
|
||||
function := NewPSProgram()
|
||||
|
||||
for {
|
||||
this.skipSpaces()
|
||||
bb, err := this.reader.Peek(2)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
common.Log.Debug("Peek string: %s", string(bb))
|
||||
// Determine type.
|
||||
if bb[0] == '}' {
|
||||
common.Log.Debug("EOF function")
|
||||
this.reader.ReadByte()
|
||||
break
|
||||
} else if bb[0] == '{' {
|
||||
common.Log.Debug("Function!")
|
||||
inlineF, err := this.parseFunction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
function.Append(inlineF)
|
||||
} else if pdfcore.IsDecimalDigit(bb[0]) || (bb[0] == '-' && pdfcore.IsDecimalDigit(bb[1])) {
|
||||
common.Log.Debug("->Number!")
|
||||
number, err := this.parseNumber()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
function.Append(number)
|
||||
} else {
|
||||
common.Log.Debug("->Operand or bool?")
|
||||
// Let's peek farther to find out.
|
||||
bb, _ = this.reader.Peek(5)
|
||||
peekStr := string(bb)
|
||||
common.Log.Debug("Peek str: %s", peekStr)
|
||||
|
||||
if (len(peekStr) > 4) && (peekStr[:5] == "false") {
|
||||
b, err := this.parseBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
function.Append(b)
|
||||
} else if (len(peekStr) > 3) && (peekStr[:4] == "true") {
|
||||
b, err := this.parseBool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
function.Append(b)
|
||||
} else {
|
||||
operand, err := this.parseOperand()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
function.Append(operand)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return function, nil
|
||||
}
|
||||
|
||||
// Skip over any spaces. Returns the number of spaces skipped and
|
||||
// an error if any.
|
||||
func (this *PSParser) skipSpaces() (int, error) {
|
||||
cnt := 0
|
||||
for {
|
||||
bb, err := this.reader.Peek(1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if pdfcore.IsWhiteSpace(bb[0]) {
|
||||
this.reader.ReadByte()
|
||||
cnt++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return cnt, nil
|
||||
}
|
||||
|
||||
// Numeric objects.
|
||||
// Integer or Real numbers.
|
||||
func (this *PSParser) parseNumber() (PSObject, error) {
|
||||
isFloat := false
|
||||
allowSigns := true
|
||||
numStr := ""
|
||||
for {
|
||||
common.Log.Debug("Parsing number \"%s\"", numStr)
|
||||
bb, err := this.reader.Peek(1)
|
||||
if err == io.EOF {
|
||||
// GH: EOF handling. Handle EOF like end of line. Can happen with
|
||||
// encoded object streams that the object is at the end.
|
||||
// In other cases, we will get the EOF error elsewhere at any rate.
|
||||
break // Handle like EOF
|
||||
}
|
||||
if err != nil {
|
||||
common.Log.Error("ERROR %s", err)
|
||||
return nil, err
|
||||
}
|
||||
if allowSigns && (bb[0] == '-' || bb[0] == '+') {
|
||||
// Only appear in the beginning, otherwise serves as a delimiter.
|
||||
b, _ := this.reader.ReadByte()
|
||||
numStr += string(b)
|
||||
allowSigns = false // Only allowed in beginning, and after e (exponential).
|
||||
} else if pdfcore.IsDecimalDigit(bb[0]) {
|
||||
b, _ := this.reader.ReadByte()
|
||||
numStr += string(b)
|
||||
} else if bb[0] == '.' {
|
||||
b, _ := this.reader.ReadByte()
|
||||
numStr += string(b)
|
||||
isFloat = true
|
||||
} else if bb[0] == 'e' {
|
||||
// Exponential number format.
|
||||
// XXX Is this supported in PS?
|
||||
b, _ := this.reader.ReadByte()
|
||||
numStr += string(b)
|
||||
isFloat = true
|
||||
allowSigns = true
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if isFloat {
|
||||
fVal, err := strconv.ParseFloat(numStr, 64)
|
||||
o := MakeReal(fVal)
|
||||
return o, err
|
||||
} else {
|
||||
intVal, err := strconv.ParseInt(numStr, 10, 64)
|
||||
o := MakeInteger(int(intVal))
|
||||
return o, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse bool object.
|
||||
func (this *PSParser) parseBool() (*PSBoolean, error) {
|
||||
bb, err := this.reader.Peek(4)
|
||||
if err != nil {
|
||||
return MakeBool(false), err
|
||||
}
|
||||
if (len(bb) >= 4) && (string(bb[:4]) == "true") {
|
||||
this.reader.Discard(4)
|
||||
return MakeBool(true), nil
|
||||
}
|
||||
|
||||
bb, err = this.reader.Peek(5)
|
||||
if err != nil {
|
||||
return MakeBool(false), err
|
||||
}
|
||||
if (len(bb) >= 5) && (string(bb[:5]) == "false") {
|
||||
this.reader.Discard(5)
|
||||
return MakeBool(false), nil
|
||||
}
|
||||
|
||||
return MakeBool(false), errors.New("Unexpected boolean string")
|
||||
}
|
||||
|
||||
// An operand is a text command represented by a word.
|
||||
func (this *PSParser) parseOperand() (*PSOperand, error) {
|
||||
bytes := []byte{}
|
||||
for {
|
||||
bb, err := this.reader.Peek(1)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if pdfcore.IsDelimiter(bb[0]) {
|
||||
break
|
||||
}
|
||||
if pdfcore.IsWhiteSpace(bb[0]) {
|
||||
break
|
||||
}
|
||||
|
||||
b, _ := this.reader.ReadByte()
|
||||
bytes = append(bytes, b)
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return nil, fmt.Errorf("Invalid operand (empty)")
|
||||
}
|
||||
|
||||
return MakeOperand(string(bytes)), nil
|
||||
}
|
401
pdf/ps/parser_test.go
Normal file
401
pdf/ps/parser_test.go
Normal file
@ -0,0 +1,401 @@
|
||||
package ps
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/unidoc/unidoc/common"
|
||||
)
|
||||
|
||||
func init() {
|
||||
common.SetLogger(common.ConsoleLogger{})
|
||||
}
|
||||
|
||||
func makeReaderForText(txt string) *bufio.Reader {
|
||||
buf := []byte(txt)
|
||||
bufReader := bytes.NewReader(buf)
|
||||
bufferedReader := bufio.NewReader(bufReader)
|
||||
return bufferedReader
|
||||
}
|
||||
|
||||
func quickEval(progText string) (PSObject, error) {
|
||||
parser := NewPSParser([]byte(progText))
|
||||
|
||||
prog, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("%s\n", progText)
|
||||
fmt.Printf("-> Program: %s\n", prog.ToString())
|
||||
|
||||
exec := NewPSExecutor(prog)
|
||||
|
||||
outputs, err := exec.Execute(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(outputs) != 1 {
|
||||
return nil, fmt.Errorf("Stack result has too many values (>1)")
|
||||
}
|
||||
|
||||
stack := PSStack(outputs)
|
||||
fmt.Printf("=> Result Stack: %s\n", stack.ToString())
|
||||
|
||||
return outputs[0], nil
|
||||
}
|
||||
|
||||
func quickTest(progText string) (*PSStack, error) {
|
||||
parser := NewPSParser([]byte(progText))
|
||||
|
||||
prog, err := parser.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("%s\n", progText)
|
||||
fmt.Printf("-> Program: %s\n", prog.ToString())
|
||||
|
||||
exec := NewPSExecutor(prog)
|
||||
|
||||
outputs, err := exec.Execute(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stack := PSStack(outputs)
|
||||
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
func TestAdd1(t *testing.T) {
|
||||
progText := "{ 1 1 add }"
|
||||
|
||||
obj, err := quickEval(progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
val, ok := obj.(*PSInteger)
|
||||
if !ok {
|
||||
t.Errorf("Wrong output type")
|
||||
return
|
||||
}
|
||||
|
||||
if val.Val != 2 {
|
||||
t.Errorf("Wrong result")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdd2(t *testing.T) {
|
||||
progText := "{ 1.1 1 add 3 4 add add }"
|
||||
|
||||
obj, err := quickEval(progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
val, ok := obj.(*PSReal)
|
||||
if !ok {
|
||||
t.Errorf("Wrong output type")
|
||||
return
|
||||
}
|
||||
if math.Abs(val.Val-9.1) > TOLERANCE {
|
||||
t.Errorf("Wrong result")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
//// 8.3 6.6 sub -> 1.7 (real)
|
||||
// 8 6.3 sub -> 1.7 (real)
|
||||
// 8 6 sub -> 2 (int)
|
||||
func TestSub1(t *testing.T) {
|
||||
progText := "{ 8.3 6.6 sub }"
|
||||
|
||||
obj, err := quickEval(progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
val, ok := obj.(*PSReal)
|
||||
if !ok {
|
||||
t.Errorf("Wrong output type")
|
||||
return
|
||||
}
|
||||
if math.Abs(val.Val-1.7) > TOLERANCE {
|
||||
t.Errorf("Wrong result")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub2(t *testing.T) {
|
||||
progText := "{ 8 6.3 sub }"
|
||||
|
||||
obj, err := quickEval(progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
val, ok := obj.(*PSReal)
|
||||
if !ok {
|
||||
t.Errorf("Wrong output type")
|
||||
return
|
||||
}
|
||||
if math.Abs(val.Val-1.7) > TOLERANCE {
|
||||
t.Errorf("Wrong result")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestSub3(t *testing.T) {
|
||||
progText := "{ 8 6 sub }"
|
||||
|
||||
obj, err := quickEval(progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
val, ok := obj.(*PSInteger)
|
||||
if !ok {
|
||||
t.Errorf("Wrong output type")
|
||||
return
|
||||
}
|
||||
if val.Val != 2 {
|
||||
t.Errorf("Wrong result")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 6 + (3/8) -> 6.375
|
||||
// 3 8 div 6 add
|
||||
// 6 3 8 div add
|
||||
//
|
||||
// 8 - (7*3) -> -13
|
||||
// 8 7 3 mul sub
|
||||
// 7 3 mul 8 exch sub
|
||||
// Simple test entry with a single expected PSObject output.
|
||||
type SimpleTestEntry struct {
|
||||
progText string
|
||||
expected PSObject
|
||||
}
|
||||
|
||||
func TestArithmetics(t *testing.T) {
|
||||
testcases := []SimpleTestEntry{
|
||||
{progText: "{ 3 8 div 6 add }", expected: MakeReal(6.375)},
|
||||
{progText: "{ 6 3 8 div add }", expected: MakeReal(6.375)},
|
||||
{progText: "{ 8 7 3 mul sub }", expected: MakeInteger(-13)},
|
||||
{progText: "{ 7 3 mul 8 exch sub }", expected: MakeInteger(-13)},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
obj, err := quickEval(testcase.progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Maybe not the most robust test (comparing the strings), but should do.
|
||||
if obj.ToString() != testcase.expected.ToString() {
|
||||
t.Errorf("Wrong result: %s != %s", obj.ToString(), testcase.expected.ToString())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complex test entry can have a more complex output.
|
||||
type ComplexTestEntry struct {
|
||||
progText string
|
||||
expected string
|
||||
}
|
||||
|
||||
func TestStackOperations(t *testing.T) {
|
||||
testcases := []ComplexTestEntry{
|
||||
{progText: "{ 7 8 9 3 1 roll }", expected: "[ int:9 int:7 int:8 ]"},
|
||||
{progText: "{ 7 8 9 3 -1 roll }", expected: "[ int:8 int:9 int:7 ]"},
|
||||
{progText: "{ 9 7 8 3 -1 roll }", expected: "[ int:7 int:8 int:9 ]"},
|
||||
{progText: "{ 1 1 0.2 7 8 9 3 1 roll }", expected: "[ int:1 int:1 real:0.20000 int:9 int:7 int:8 ]"},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
stack, err := quickTest(testcase.progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Maybe not the most robust test (comparing the strings), but should do.
|
||||
if stack.ToString() != testcase.expected {
|
||||
t.Errorf("Wrong result: '%s' != '%s'", stack.ToString(), testcase.expected)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionOperations(t *testing.T) {
|
||||
testcases := []ComplexTestEntry{
|
||||
// atan
|
||||
{progText: "{ 0 1 atan }", expected: "[ real:0.00000 ]"},
|
||||
{progText: "{ 1 0 atan }", expected: "[ real:90.00000 ]"},
|
||||
{progText: "{ -100 0 atan }", expected: "[ real:270.00000 ]"},
|
||||
{progText: "{ 4 4 atan }", expected: "[ real:45.00000 ]"},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
stack, err := quickTest(testcase.progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Maybe not the most robust test (comparing the strings), but should do.
|
||||
if stack.ToString() != testcase.expected {
|
||||
t.Errorf("Wrong result: '%s' != '%s'", stack.ToString(), testcase.expected)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVariousCases(t *testing.T) {
|
||||
testcases := []ComplexTestEntry{
|
||||
// ceiling
|
||||
{progText: "{ 3.2 ceiling }", expected: "[ real:4.00000 ]"},
|
||||
{progText: "{ -4.8 ceiling }", expected: "[ real:-4.00000 ]"},
|
||||
{progText: "{ 99 ceiling }", expected: "[ int:99 ]"},
|
||||
// floor
|
||||
{progText: "{ 3.2 floor }", expected: "[ real:3.00000 ]"},
|
||||
{progText: "{ -4.8 floor }", expected: "[ real:-5.00000 ]"},
|
||||
{progText: "{ 99 floor }", expected: "[ int:99 ]"},
|
||||
// exp
|
||||
{progText: "{ 9 0.5 exp }", expected: "[ real:3.00000 ]"},
|
||||
{progText: "{ -9 -1 exp }", expected: "[ real:-0.11111 ]"},
|
||||
// and
|
||||
{progText: "{ true true and }", expected: "[ bool:true ]"},
|
||||
{progText: "{ true false and }", expected: "[ bool:false ]"},
|
||||
{progText: "{ false true and }", expected: "[ bool:false ]"},
|
||||
{progText: "{ false false and }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 99 1 and }", expected: "[ int:1 ]"},
|
||||
{progText: "{ 52 7 and }", expected: "[ int:4 ]"},
|
||||
// bitshift
|
||||
{progText: "{ 7 3 bitshift }", expected: "[ int:56 ]"},
|
||||
{progText: "{ 142 -3 bitshift }", expected: "[ int:17 ]"},
|
||||
// copy
|
||||
{progText: "{ 7 3 2 copy }", expected: "[ int:7 int:3 int:7 int:3 ]"},
|
||||
{progText: "{ 7 3 0 copy }", expected: "[ int:7 int:3 ]"},
|
||||
// cos
|
||||
{progText: "{ 0 cos }", expected: "[ real:1.00000 ]"},
|
||||
{progText: "{ 90 cos }", expected: "[ real:0.00000 ]"},
|
||||
// eq.
|
||||
{progText: "{ 4.0 4 eq }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 4 4.0 eq }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 4.0 4.0 eq }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 4 4 eq }", expected: "[ bool:true ]"},
|
||||
{progText: "{ -4 4 eq }", expected: "[ bool:false ]"},
|
||||
{progText: "{ false false eq }", expected: "[ bool:true ]"},
|
||||
{progText: "{ true false eq }", expected: "[ bool:false ]"},
|
||||
{progText: "{ true 4 eq }", expected: "[ bool:false ]"},
|
||||
// ge
|
||||
{progText: "{ 4.2 4 ge }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 4 4 ge }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 3.9 4 ge }", expected: "[ bool:false ]"},
|
||||
// gt
|
||||
{progText: "{ 4.2 4 gt }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 4 4 gt }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 3.9 4 gt }", expected: "[ bool:false ]"},
|
||||
// if
|
||||
{progText: "{ 4.2 4 gt {5} if }", expected: "[ int:5 ]"},
|
||||
{progText: "{ 4.2 4 gt {4.0 4.0 ge {3} if} if}", expected: "[ int:3 ]"},
|
||||
{progText: "{ 4.0 4.0 gt {5} if }", expected: "[ ]"},
|
||||
// ifelse
|
||||
{progText: "{ 4.2 4 gt {5} {4} ifelse }", expected: "[ int:5 ]"},
|
||||
{progText: "{ 3 4 gt {5} {4} ifelse }", expected: "[ int:4 ]"},
|
||||
// index
|
||||
{progText: "{ 0 1 2 3 4 5 2 index }", expected: "[ int:0 int:1 int:2 int:3 int:4 int:5 int:3 ]"},
|
||||
{progText: "{ 9 8 7 2 index }", expected: "[ int:9 int:8 int:7 int:9 ]"},
|
||||
// le
|
||||
{progText: "{ 4.2 4 le }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 4 4 le }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 3.9 4 le }", expected: "[ bool:true ]"},
|
||||
// ln
|
||||
{progText: "{ 10 ln }", expected: "[ real:2.30259 ]"},
|
||||
{progText: "{ 100 ln }", expected: "[ real:4.60517 ]"},
|
||||
// log
|
||||
{progText: "{ 10 log }", expected: "[ real:1.00000 ]"},
|
||||
{progText: "{ 100 log }", expected: "[ real:2.00000 ]"},
|
||||
// lt
|
||||
{progText: "{ 4.2 4 lt }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 4 4 lt }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 3.9 4 lt }", expected: "[ bool:true ]"},
|
||||
// ne
|
||||
{progText: "{ 4.0 4 ne }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 4 4.0 ne }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 4.0 4.0 ne }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 4 4 ne }", expected: "[ bool:false ]"},
|
||||
{progText: "{ -4 4 ne }", expected: "[ bool:true ]"},
|
||||
{progText: "{ false false ne }", expected: "[ bool:false ]"},
|
||||
{progText: "{ true false ne }", expected: "[ bool:true ]"},
|
||||
{progText: "{ true 4 ne }", expected: "[ bool:true ]"},
|
||||
// neg
|
||||
// not
|
||||
{progText: "{ true not }", expected: "[ bool:false ]"},
|
||||
{progText: "{ false not }", expected: "[ bool:true ]"},
|
||||
{progText: "{ 52 not }", expected: "[ int:-53 ]"},
|
||||
// or
|
||||
{progText: "{ true true or }", expected: "[ bool:true ]"},
|
||||
{progText: "{ true false or }", expected: "[ bool:true ]"},
|
||||
{progText: "{ false true or }", expected: "[ bool:true ]"},
|
||||
{progText: "{ false false or }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 17 5 or }", expected: "[ int:21 ]"},
|
||||
// pop
|
||||
{progText: "{ 1 2 3 pop }", expected: "[ int:1 int:2 ]"},
|
||||
{progText: "{ 1 2 pop }", expected: "[ int:1 ]"},
|
||||
{progText: "{ 1 pop }", expected: "[ ]"},
|
||||
// round
|
||||
{progText: "{ 3.2 round }", expected: "[ real:3.00000 ]"},
|
||||
{progText: "{ 6.5 round }", expected: "[ real:7.00000 ]"},
|
||||
{progText: "{ -4.8 round }", expected: "[ real:-5.00000 ]"},
|
||||
{progText: "{ -6.5 round }", expected: "[ real:-6.00000 ]"},
|
||||
{progText: "{ 99 round }", expected: "[ int:99 ]"},
|
||||
// roll
|
||||
{progText: "{ 1 2 3 3 -1 roll }", expected: "[ int:2 int:3 int:1 ]"},
|
||||
{progText: "{ 1 2 3 3 1 roll }", expected: "[ int:3 int:1 int:2 ]"},
|
||||
{progText: "{ 1 2 3 3 0 roll }", expected: "[ int:1 int:2 int:3 ]"},
|
||||
// sin
|
||||
{progText: "{ 0 sin }", expected: "[ real:0.00000 ]"},
|
||||
{progText: "{ 90 sin }", expected: "[ real:1.00000 ]"},
|
||||
// sqrt
|
||||
{progText: "{ 4 sqrt }", expected: "[ real:2.00000 ]"},
|
||||
{progText: "{ 2 sqrt }", expected: "[ real:1.41421 ]"},
|
||||
// truncate
|
||||
{progText: "{ 3.2 truncate }", expected: "[ real:3.00000 ]"},
|
||||
{progText: "{ -4.8 truncate }", expected: "[ real:-4.00000 ]"},
|
||||
{progText: "{ 99 truncate }", expected: "[ int:99 ]"},
|
||||
// xor
|
||||
{progText: "{ true true xor }", expected: "[ bool:false ]"},
|
||||
{progText: "{ true false xor }", expected: "[ bool:true ]"},
|
||||
{progText: "{ false true xor }", expected: "[ bool:true ]"},
|
||||
{progText: "{ false false xor }", expected: "[ bool:false ]"},
|
||||
{progText: "{ 7 3 xor }", expected: "[ int:4 ]"},
|
||||
{progText: "{ 12 3 xor }", expected: "[ int:15 ]"},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
stack, err := quickTest(testcase.progText)
|
||||
if err != nil {
|
||||
t.Errorf("Error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Maybe not the most robust test (comparing the strings), but should do.
|
||||
if stack.ToString() != testcase.expected {
|
||||
t.Errorf("Wrong result: '%s' != '%s'", stack.ToString(), testcase.expected)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
72
pdf/ps/stack.go
Normal file
72
pdf/ps/stack.go
Normal file
@ -0,0 +1,72 @@
|
||||
package ps
|
||||
|
||||
type PSStack []PSObject
|
||||
|
||||
func NewPSStack() *PSStack {
|
||||
return &PSStack{}
|
||||
}
|
||||
|
||||
func (stack *PSStack) Empty() {
|
||||
*stack = []PSObject{}
|
||||
}
|
||||
|
||||
func (stack *PSStack) Push(obj PSObject) error {
|
||||
if len(*stack) > 100 {
|
||||
return ErrStackOverflow
|
||||
}
|
||||
|
||||
*stack = append(*stack, obj)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (stack *PSStack) Pop() (PSObject, error) {
|
||||
if len(*stack) < 1 {
|
||||
return nil, ErrStackUnderflow
|
||||
}
|
||||
|
||||
obj := (*stack)[len(*stack)-1]
|
||||
*stack = (*stack)[0 : len(*stack)-1]
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (stack *PSStack) PopInteger() (int, error) {
|
||||
obj, err := stack.Pop()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if number, is := obj.(*PSInteger); is {
|
||||
return number.Val, nil
|
||||
} else {
|
||||
return 0, ErrTypeCheck
|
||||
}
|
||||
}
|
||||
|
||||
// Pop and return the numeric value of the top of the stack as a float64.
|
||||
// Real or integer only.
|
||||
func (stack *PSStack) PopNumberAsFloat64() (float64, error) {
|
||||
obj, err := stack.Pop()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if number, is := obj.(*PSReal); is {
|
||||
return number.Val, nil
|
||||
} else if number, is := obj.(*PSInteger); is {
|
||||
return float64(number.Val), nil
|
||||
} else {
|
||||
return 0, ErrTypeCheck
|
||||
}
|
||||
}
|
||||
|
||||
func (this *PSStack) ToString() string {
|
||||
s := "[ "
|
||||
for _, obj := range *this {
|
||||
s += obj.ToString()
|
||||
s += " "
|
||||
}
|
||||
s += "]"
|
||||
|
||||
return s
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user