diff --git a/contentstream/inline-image.go b/contentstream/inline-image.go index 442c9806..ba6e144e 100644 --- a/contentstream/inline-image.go +++ b/contentstream/inline-image.go @@ -37,6 +37,7 @@ func NewInlineImageFromImage(img model.Image, encoder core.StreamEncoder) (*Cont if encoder == nil { encoder = core.NewRawEncoder() } + encoder.UpdateParams(img.GetParamsDict()) inlineImage := ContentStreamInlineImage{} if img.ColorComponents == 1 { diff --git a/core/encoding_jbig2.go b/core/encoding_jbig2.go index 0e43b3ab..62c797da 100644 --- a/core/encoding_jbig2.go +++ b/core/encoding_jbig2.go @@ -6,11 +6,11 @@ package core import ( - "bytes" "image" "image/color" "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/internal/imageutil" "github.com/unidoc/unipdf/v3/internal/jbig2" "github.com/unidoc/unipdf/v3/internal/jbig2/bitmap" @@ -55,6 +55,17 @@ const JB2ImageAutoThreshold = -1.0 // The similarity is defined by the 'Threshold' variable (default: 0.95). The less the value is, the more components // matches to single class, thus the compression is better, but the result might become lossy. type JBIG2Encoder struct { + // These values are required to be set for the 'EncodeBytes' method. + // ColorComponents defines the number of color components for provided image. + ColorComponents int + // BitsPerComponent is the number of bits that stores per color component + BitsPerComponent int + // Width is the width of the image to encode + Width int + // Height is the height of the image to encode. + Height int + + // Encode Page and Decode parameters d *document.Document // Globals are the JBIG2 global segments. Globals jbig2.Globals @@ -69,7 +80,9 @@ type JBIG2Encoder struct { // NewJBIG2Encoder creates a new JBIG2Encoder. func NewJBIG2Encoder() *JBIG2Encoder { - return &JBIG2Encoder{} + return &JBIG2Encoder{ + d: document.InitEncodeDocument(false), + } } // AddPageImage adds the page with the image 'img' to the encoder context in order to encode it jbig2 document. @@ -165,18 +178,31 @@ func (enc *JBIG2Encoder) DecodeStream(streamObj *PdfObjectStream) ([]byte, error // to encode given image. func (enc *JBIG2Encoder) EncodeBytes(data []byte) ([]byte, error) { const processName = "JBIG2Encoder.EncodeBytes" - if len(data) == 0 { - return nil, errors.Errorf(processName, "input 'data' not defined") + if enc.ColorComponents != 1 || enc.BitsPerComponent != 1 { + return nil, errors.Errorf(processName, "provided invalid input image. JBIG2 Encoder requires binary images data") } - i, _, err := image.Decode(bytes.NewReader(data)) + b, err := bitmap.NewWithUnpaddedData(enc.Width, enc.Height, data) if err != nil { - return nil, errors.Wrap(err, processName, "decode input image") + return nil, err } - encoded, err := enc.encodeImage(i) - if err != nil { + settings := enc.DefaultPageSettings + if err = settings.Validate(); err != nil { return nil, errors.Wrap(err, processName, "") } - return encoded, nil + + switch settings.Compression { + case JB2Generic: + if err = enc.d.AddGenericPage(b, settings.DuplicatedLinesRemoval); err != nil { + return nil, errors.Wrap(err, processName, "") + } + case JB2SymbolCorrelation: + return nil, errors.Error(processName, "symbol correlation encoding not implemented yet") + case JB2SymbolRankHaus: + return nil, errors.Error(processName, "symbol rank haus encoding not implemented yet") + default: + return nil, errors.Error(processName, "provided invalid compression") + } + return enc.Encode() } // EncodeImage encodes 'img' golang image.Image into jbig2 encoded bytes document using default encoder settings. @@ -184,6 +210,15 @@ func (enc *JBIG2Encoder) EncodeImage(img image.Image) ([]byte, error) { return enc.encodeImage(img) } +// EncodeJBIG2Image encodes 'img' into jbig2 encoded bytes stream, using default encoder settings. +func (enc *JBIG2Encoder) EncodeJBIG2Image(img *JBIG2Image) ([]byte, error) { + const processName = "core.EncodeJBIG2Image" + if err := enc.AddPageImage(img, &enc.DefaultPageSettings); err != nil { + return nil, errors.Wrap(err, processName, "") + } + return enc.Encode() +} + // Encode encodes previously prepare jbig2 document and stores it as the byte slice. func (enc *JBIG2Encoder) Encode() (data []byte, err error) { const processName = "JBIG2Document.Encode" @@ -217,8 +252,24 @@ func (enc *JBIG2Encoder) MakeStreamDict() *PdfObjectDictionary { } // UpdateParams updates the parameter values of the encoder. -// The body of this method is empty but required to implement StreamEncoder interface. +// Implements StreamEncoder interface. func (enc *JBIG2Encoder) UpdateParams(params *PdfObjectDictionary) { + bpc, err := GetNumberAsInt64(params.Get("BitsPerComponent")) + if err == nil { + enc.BitsPerComponent = int(bpc) + } + width, err := GetNumberAsInt64(params.Get("Width")) + if err == nil { + enc.Width = int(width) + } + height, err := GetNumberAsInt64(params.Get("Height")) + if err == nil { + enc.Height = int(height) + } + colorComponents, err := GetNumberAsInt64(params.Get("ColorComponents")) + if err == nil { + enc.ColorComponents = int(colorComponents) + } } func (enc *JBIG2Encoder) encodeImage(i image.Image) ([]byte, error) { @@ -262,24 +313,29 @@ func newJBIG2DecoderFromStream(streamObj *PdfObjectStream, decodeParams *PdfObje } } } - - if decodeParams != nil { - if globals := decodeParams.Get("JBIG2Globals"); globals != nil { - var err error - - globalsStream, ok := globals.(*PdfObjectStream) - if !ok { - err = errors.Error(processName, "jbig2.Globals stream should be an Object Stream") - common.Log.Debug("ERROR: %s", err.Error()) - return nil, err - } - encoder.Globals, err = jbig2.DecodeGlobals(globalsStream.Stream) - if err != nil { - err = errors.Wrap(err, processName, "corrupted jbig2 encoded data") - common.Log.Debug("ERROR: %s", err) - return nil, err - } - } + // if no decode params provided - end fast. + if decodeParams == nil { + return encoder, nil + } + // set image parameters. + encoder.UpdateParams(decodeParams) + globals := decodeParams.Get("JBIG2Globals") + if globals == nil { + return encoder, nil + } + // decode and set JBIG2 Globals. + var err error + globalsStream, ok := globals.(*PdfObjectStream) + if !ok { + err = errors.Error(processName, "jbig2.Globals stream should be an Object Stream") + common.Log.Debug("ERROR: %v", err) + return nil, err + } + encoder.Globals, err = jbig2.DecodeGlobals(globalsStream.Stream) + if err != nil { + err = errors.Wrap(err, processName, "corrupted jbig2 encoded data") + common.Log.Debug("ERROR: %v", err) + return nil, err } return encoder, nil } @@ -348,9 +404,9 @@ func GoImageToJBIG2(i image.Image, bwThreshold float64) (*JBIG2Image, error) { var th uint8 if bwThreshold == JB2ImageAutoThreshold { // autoThreshold using triangle method - gray := bitmap.ImgToGray(i) - histogram := bitmap.GrayImageHistogram(gray) - th = bitmap.AutoThresholdTriangle(histogram) + gray := imageutil.ImgToGray(i) + histogram := imageutil.GrayImageHistogram(gray) + th = imageutil.AutoThresholdTriangle(histogram) i = gray } else if bwThreshold > 1.0 || bwThreshold < 0.0 { // check if bwThreshold is unknown - set to 0.0 is not in the allowed range. @@ -358,7 +414,7 @@ func GoImageToJBIG2(i image.Image, bwThreshold float64) (*JBIG2Image, error) { } else { th = uint8(255 * bwThreshold) } - gray := bitmap.ImgToBinary(i, th) + gray := imageutil.ImgToBinary(i, th) return bwToJBIG2Image(gray), nil } diff --git a/creator/image.go b/creator/image.go index f112bc0f..1738a518 100644 --- a/creator/image.go +++ b/creator/image.go @@ -158,6 +158,13 @@ func (img *Image) GetMargins() (float64, float64, float64, float64) { return img.margins.left, img.margins.right, img.margins.top, img.margins.bottom } +// ConvertToBinary converts current image data into binary (Bi-level image) format. +// If provided image is RGB or GrayScale the function converts it into binary image +// using histogram auto threshold method. +func (img *Image) ConvertToBinary() error { + return img.img.ConvertToBinary() +} + // makeXObject makes the encoded XObject Image that will be used in the PDF. func (img *Image) makeXObject() error { encoder := img.encoder @@ -181,7 +188,10 @@ func (img *Image) makeXObject() error { func (img *Image) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) { if img.xobj == nil { // Build the XObject Image if not already prepared. - img.makeXObject() + if err := img.makeXObject(); err != nil { + return nil, ctx, err + } + } var blocks []*Block diff --git a/go.sum b/go.sum index 5147dba0..99e1cd77 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,7 @@ github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDd github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83 h1:saj5dTV7eQ1wFg/gVZr1SfbkOmg8CYO9R8frHgQiyR4= github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83/go.mod h1:xaGEIRenAiJcGgd9p62zbiP4993KaV3PdjczwGnP50I= diff --git a/internal/jbig2/bitmap/rgbtobw.go b/internal/imageutil/rgbtobw.go similarity index 95% rename from internal/jbig2/bitmap/rgbtobw.go rename to internal/imageutil/rgbtobw.go index 1b84df17..ee2bba65 100644 --- a/internal/jbig2/bitmap/rgbtobw.go +++ b/internal/imageutil/rgbtobw.go @@ -1,4 +1,4 @@ -package bitmap +package imageutil import ( "image" @@ -131,6 +131,11 @@ func ImgToGray(i image.Image) *image.Gray { return g } +// IsGrayImgBlackAndWhite checks if provided gray image is BlackAndWhite - Binary image. +func IsGrayImgBlackAndWhite(i *image.Gray) bool { + return isGrayBlackWhite(i) +} + func blackOrWhite(c, threshold uint8) uint8 { if c < threshold { return 255 diff --git a/internal/jbig2/bitmap/bitmap.go b/internal/jbig2/bitmap/bitmap.go index 35f18121..ae58ed1b 100644 --- a/internal/jbig2/bitmap/bitmap.go +++ b/internal/jbig2/bitmap/bitmap.go @@ -545,7 +545,7 @@ func (b *Bitmap) addBorderGeneral(left, right, top, bot int, val int) (*Bitmap, // addPadBits creates new data byte slice that contains extra padding on the last byte for each row. func (b *Bitmap) addPadBits() (err error) { - const processName = "addPadBits" + const processName = "bitmap.addPadBits" endbits := b.Width % 8 if endbits == 0 { // no partial words @@ -559,18 +559,16 @@ func (b *Bitmap) addPadBits() (err error) { w := writer.NewMSB(data) temp := make([]byte, fullBytes) var ( - i, j int + i int bits uint64 ) for i = 0; i < b.Height; i++ { // iterate over full bytes - for j = 0; j < fullBytes; j++ { - if _, err = r.Read(temp); err != nil { - return errors.Wrap(err, processName, "full byte") - } - if _, err = w.Write(temp); err != nil { - return errors.Wrap(err, processName, "full bytes") - } + if _, err = r.Read(temp); err != nil { + return errors.Wrap(err, processName, "full byte") + } + if _, err = w.Write(temp); err != nil { + return errors.Wrap(err, processName, "full bytes") } // read unused bits if bits, err = r.ReadBits(byte(endbits)); err != nil { diff --git a/internal/jbig2/document/document.go b/internal/jbig2/document/document.go index 92ab6657..e943ed60 100644 --- a/internal/jbig2/document/document.go +++ b/internal/jbig2/document/document.go @@ -98,7 +98,7 @@ func (d *Document) AddGenericPage(bm *bitmap.Bitmap, duplicateLineRemoval bool) const processName = "Document.AddGenericPage" // check if this is PDFMode and there is already a page if !d.FullHeaders && d.NumberOfPages != 0 { - return errors.Error(processName, "document already contains page. FileMode disallows addoing more than one page") + return errors.Error(processName, "document already contains page. FileMode disallows adding more than one page") } // initialize page page := &Page{ diff --git a/internal/jbig2/tests/.gitignore b/internal/jbig2/tests/.gitignore index 20735b39..3e5e0ddd 100644 --- a/internal/jbig2/tests/.gitignore +++ b/internal/jbig2/tests/.gitignore @@ -3,3 +3,4 @@ jbig2files .test *.jbig2 .envrc +.env diff --git a/internal/jbig2/tests/encode_pdf_test.go b/internal/jbig2/tests/encode_pdf_test.go new file mode 100644 index 00000000..ed489494 --- /dev/null +++ b/internal/jbig2/tests/encode_pdf_test.go @@ -0,0 +1,155 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package tests + +import ( + "bytes" + "crypto/md5" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/creator" +) + +// TestImageEncodeJBIG2PDF tests the encode process for the JBIG2 encoder into PDF file. +func TestImageEncodeJBIG2PDF(t *testing.T) { + dirName := os.Getenv(EnvImageDirectory) + if dirName == "" { + t.Skipf("no environment variable: '%s' provided", EnvImageDirectory) + } + + // get the file names within given directory + fileNames, err := readFileNames(dirName, "jpg") + require.NoError(t, err) + + if len(fileNames) == 0 { + t.Skipf("no files found in the '%s' directory", dirName) + } + + // prepare temporary directory where the jbig2 files would be stored + tempDir := filepath.Join(os.TempDir(), "unipdf", "jbig2", "encoded-pdf") + err = os.MkdirAll(tempDir, 0700) + require.NoError(t, err) + + var f *os.File + switch { + case logToFile: + fileName := filepath.Join(tempDir, fmt.Sprintf("log_%s.txt", time.Now().Format("20060102"))) + f, err = os.Create(fileName) + require.NoError(t, err) + common.SetLogger(common.NewWriterLogger(common.LogLevelTrace, f)) + case testing.Verbose(): + common.SetLogger(common.NewConsoleLogger(common.LogLevelDebug)) + } + + // clear all the temporary files + defer func() { + if f != nil { + f.Close() + } + + switch { + case !keepEncodedFile && !logToFile: + err = os.RemoveAll(filepath.Join(tempDir)) + case !keepEncodedFile: + err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(info.Name(), "zip") { + return os.Remove(path) + } + return nil + }) + } + if err != nil { + common.Log.Error(err.Error()) + } + }() + + defer func() { + if !keepEncodedFile { + os.RemoveAll(tempDir) + } + }() + + buf := &bytes.Buffer{} + h := md5.New() + edp := []goldenValuePair{} + + for _, fileName := range fileNames { + var duplicateLinesRemoval bool + duplicateLinesName := "NoDuplicateLinesRemoval" + for i := 0; i < 2; i++ { + if i == 1 { + duplicateLinesRemoval = true + duplicateLinesName = duplicateLinesName[2:] + } + t.Run(rawFileName(fileName)+duplicateLinesName, func(t *testing.T) { + // read the file + c := creator.New() + + img, err := c.NewImageFromFile(filepath.Join(dirName, fileName)) + require.NoError(t, err) + + // conver an image to binary image + err = img.ConvertToBinary() + require.NoError(t, err) + + img.ScaleToWidth(612.0) + + e := core.NewJBIG2Encoder() + if duplicateLinesRemoval { + e.DefaultPageSettings.DuplicatedLinesRemoval = true + } + img.SetEncoder(e) + + // Use page width of 612 points, and calculate the height proportionally based on the image. + // Standard PPI is 72 points per inch, thus a width of 8.5" + height := 612.0 * img.Height() / img.Width() + c.NewPage() + c.SetPageSize(creator.PageSize{612, height}) + // c.SetPageSize(creator.PageSize{img.Width() * 1.2, img.Height() * 1.2}) + img.SetPos(0, 0) + + err = c.Draw(img) + require.NoError(t, err) + + err = c.Write(buf) + require.NoError(t, err) + + _, err = h.Write(buf.Bytes()) + require.NoError(t, err) + + if keepEncodedFile { + f, err := os.Create(filepath.Join(tempDir, rawFileName(fileName)+duplicateLinesName+".pdf")) + require.NoError(t, err) + defer f.Close() + + _, err = f.Write(buf.Bytes()) + require.NoError(t, err) + } + hashEncoded := h.Sum(nil) + buf.Reset() + + edp = append(edp, goldenValuePair{ + Filename: rawFileName(fileName) + duplicateLinesName, + Hash: hashEncoded, + }) + }) + } + } + const goldenFileName = "encoded-pdf" + checkGoldenValuePairs(t, dirName, goldenFileName, edp...) +} diff --git a/model/image.go b/model/image.go index 0845b96e..ddc61b74 100644 --- a/model/image.go +++ b/model/image.go @@ -11,14 +11,15 @@ import ( goimage "image" gocolor "image/color" "image/draw" - "io" - // Imported for initialization side effects. _ "image/gif" _ "image/png" + "io" "github.com/unidoc/unipdf/v3/common" "github.com/unidoc/unipdf/v3/core" + "github.com/unidoc/unipdf/v3/internal/imageutil" + "github.com/unidoc/unipdf/v3/internal/jbig2/bitmap" "github.com/unidoc/unipdf/v3/internal/sampling" ) @@ -50,6 +51,59 @@ func (img *Image) AlphaMap(mapFunc AlphaMapFunc) { } } +// ConvertToBinary converts current image into binary (bi-level) format. +// Binary images are composed of single bits per pixel (only black or white). +// If provided image has more color components, then it would be converted into binary image using +// histogram auto threshold function. +func (img *Image) ConvertToBinary() error { + // check if given image is already a binary image (1 bit per component - 1 color component - the size of the data + // is equal to the multiplication of width and height. + if img.ColorComponents == 1 && img.BitsPerComponent == 1 { + return nil + } + i, err := img.ToGoImage() + if err != nil { + return err + } + gray := imageutil.ImgToGray(i) + // check if 'img' is already a binary image. + if !imageutil.IsGrayImgBlackAndWhite(gray) { + threshold := imageutil.AutoThresholdTriangle(imageutil.GrayImageHistogram(gray)) + gray = imageutil.ImgToBinary(i, threshold) + } + // use JBIG2 bitmap as the temporary binary data converter - by default it uses + tmpBM := bitmap.New(int(img.Width), int(img.Height)) + for y := 0; y < tmpBM.Height; y++ { + for x := 0; x < tmpBM.Width; x++ { + c := gray.GrayAt(x, y) + // set only the white pixel - c.Y != 0 + if c.Y != 0 { + if err = tmpBM.SetPixel(x, y, 1); err != nil { + return err + } + } + } + } + unpaddedData, err := tmpBM.GetUnpaddedData() + if err != nil { + return err + } + img.BitsPerComponent = 1 + img.ColorComponents = 1 + img.Data = unpaddedData + return nil +} + +// GetParamsDict returns *core.PdfObjectDictionary with a set of basic image parameters. +func (img *Image) GetParamsDict() *core.PdfObjectDictionary { + params := core.MakeDict() + params.Set("Width", core.MakeInteger(img.Width)) + params.Set("Height", core.MakeInteger(img.Height)) + params.Set("ColorComponents", core.MakeInteger(int64(img.ColorComponents))) + params.Set("BitsPerComponent", core.MakeInteger(img.BitsPerComponent)) + return params +} + // GetSamples converts 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. // NOTE: The method resamples the image byte data before returning the result and @@ -294,6 +348,15 @@ func (img *Image) Resample(targetBitsPerComponent int64) { img.BitsPerComponent = int64(targetBitsPerComponent) } +// ToJBIG2Image converts current image to the core.JBIG2Image. +func (img *Image) ToJBIG2Image() (*core.JBIG2Image, error) { + goImg, err := img.ToGoImage() + if err != nil { + return nil, err + } + return core.GoImageToJBIG2(goImg, core.JB2ImageAutoThreshold) +} + // ToGoImage converts the unidoc Image to a golang Image structure. func (img *Image) ToGoImage() (goimage.Image, error) { common.Log.Trace("Converting to go image") diff --git a/model/xobject.go b/model/xobject.go index 612e43ac..2c1bba9e 100644 --- a/model/xobject.go +++ b/model/xobject.go @@ -240,20 +240,17 @@ func NewXObjectImageFromImage(img *Image, cs PdfColorspace, encoder core.StreamE // If `encoder` is nil, uses raw encoding (none). func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorspace, encoder core.StreamEncoder) (*XObjectImage, error) { - xobj := NewXObjectImage() - if encoder == nil { encoder = core.NewRawEncoder() } + encoder.UpdateParams(img.GetParamsDict()) encoded, err := encoder.EncodeBytes(img.Data) if err != nil { common.Log.Debug("Error with encoding: %v", err) return nil, err } - - xobj.Filter = encoder - xobj.Stream = encoded + xobj := NewXObjectImage() // Width and height. imWidth := img.Width @@ -261,8 +258,12 @@ func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorsp xobj.Width = &imWidth xobj.Height = &imHeight - // Bits. - xobj.BitsPerComponent = &img.BitsPerComponent + // Bits per Component. + imBPC := img.BitsPerComponent + xobj.BitsPerComponent = &imBPC + + xobj.Filter = encoder + xobj.Stream = encoded // Guess colorspace if not explicitly set. if cs == nil { @@ -284,6 +285,7 @@ func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorsp // Has same width and height as original and stored in same // bits per component (1 component, hence the DeviceGray channel). smask := NewXObjectImage() + smask.Filter = encoder encoded, err := encoder.EncodeBytes(img.alphaData) if err != nil { @@ -291,7 +293,7 @@ func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorsp return nil, err } smask.Stream = encoded - smask.BitsPerComponent = &img.BitsPerComponent + smask.BitsPerComponent = xobj.BitsPerComponent smask.Width = &img.Width smask.Height = &img.Height smask.ColorSpace = NewPdfColorspaceDeviceGray() @@ -448,6 +450,8 @@ func NewXObjectImageFromStream(stream *core.PdfObjectStream) (*XObjectImage, err // SetImage updates XObject Image with new image data. func (ximg *XObjectImage) SetImage(img *Image, cs PdfColorspace) error { + // update image parameters of the filter encoder. + ximg.Filter.UpdateParams(img.GetParamsDict()) encoded, err := ximg.Filter.EncodeBytes(img.Data) if err != nil { return err @@ -493,6 +497,7 @@ func (ximg *XObjectImage) SetFilter(encoder core.StreamEncoder) error { } ximg.Filter = encoder + encoder.UpdateParams(ximg.getParamsDict()) encoded, err = encoder.EncodeBytes(decoded) if err != nil { return err @@ -596,3 +601,13 @@ func (ximg *XObjectImage) ToPdfObject() core.PdfObject { return stream } + +// getParamsDict returns *core.PdfObjectDictionary with a set of basic image parameters. +func (ximg *XObjectImage) getParamsDict() *core.PdfObjectDictionary { + params := core.MakeDict() + params.Set("Width", core.MakeInteger(*ximg.Width)) + params.Set("Height", core.MakeInteger(*ximg.Height)) + params.Set("ColorComponents", core.MakeInteger(int64(ximg.ColorSpace.GetNumComponents()))) + params.Set("BitsPerComponent", core.MakeInteger(*ximg.BitsPerComponent)) + return params +}