mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-01 22:17:29 +08:00
JBIG2 Encoder support for inserting binary images into PDF (#288)
* Added JBIG2 PDF support * Added JBIG2 Encoder binary image requirements * PR #288 revision r1 fixes * PR #288 revision r2 fixes
This commit is contained in:
parent
64a43b38d2
commit
29efa30439
@ -37,6 +37,7 @@ func NewInlineImageFromImage(img model.Image, encoder core.StreamEncoder) (*Cont
|
|||||||
if encoder == nil {
|
if encoder == nil {
|
||||||
encoder = core.NewRawEncoder()
|
encoder = core.NewRawEncoder()
|
||||||
}
|
}
|
||||||
|
encoder.UpdateParams(img.GetParamsDict())
|
||||||
|
|
||||||
inlineImage := ContentStreamInlineImage{}
|
inlineImage := ContentStreamInlineImage{}
|
||||||
if img.ColorComponents == 1 {
|
if img.ColorComponents == 1 {
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/unidoc/unipdf/v3/common"
|
"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"
|
||||||
"github.com/unidoc/unipdf/v3/internal/jbig2/bitmap"
|
"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
|
// 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.
|
// matches to single class, thus the compression is better, but the result might become lossy.
|
||||||
type JBIG2Encoder struct {
|
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
|
d *document.Document
|
||||||
// Globals are the JBIG2 global segments.
|
// Globals are the JBIG2 global segments.
|
||||||
Globals jbig2.Globals
|
Globals jbig2.Globals
|
||||||
@ -69,7 +80,9 @@ type JBIG2Encoder struct {
|
|||||||
|
|
||||||
// NewJBIG2Encoder creates a new JBIG2Encoder.
|
// NewJBIG2Encoder creates a new JBIG2Encoder.
|
||||||
func NewJBIG2Encoder() *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.
|
// 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.
|
// to encode given image.
|
||||||
func (enc *JBIG2Encoder) EncodeBytes(data []byte) ([]byte, error) {
|
func (enc *JBIG2Encoder) EncodeBytes(data []byte) ([]byte, error) {
|
||||||
const processName = "JBIG2Encoder.EncodeBytes"
|
const processName = "JBIG2Encoder.EncodeBytes"
|
||||||
if len(data) == 0 {
|
if enc.ColorComponents != 1 || enc.BitsPerComponent != 1 {
|
||||||
return nil, errors.Errorf(processName, "input 'data' not defined")
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, processName, "decode input image")
|
return nil, err
|
||||||
}
|
}
|
||||||
encoded, err := enc.encodeImage(i)
|
settings := enc.DefaultPageSettings
|
||||||
if err != nil {
|
if err = settings.Validate(); err != nil {
|
||||||
return nil, errors.Wrap(err, processName, "")
|
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.
|
// 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)
|
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.
|
// Encode encodes previously prepare jbig2 document and stores it as the byte slice.
|
||||||
func (enc *JBIG2Encoder) Encode() (data []byte, err error) {
|
func (enc *JBIG2Encoder) Encode() (data []byte, err error) {
|
||||||
const processName = "JBIG2Document.Encode"
|
const processName = "JBIG2Document.Encode"
|
||||||
@ -217,8 +252,24 @@ func (enc *JBIG2Encoder) MakeStreamDict() *PdfObjectDictionary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateParams updates the parameter values of the encoder.
|
// 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) {
|
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) {
|
func (enc *JBIG2Encoder) encodeImage(i image.Image) ([]byte, error) {
|
||||||
@ -262,25 +313,30 @@ func newJBIG2DecoderFromStream(streamObj *PdfObjectStream, decodeParams *PdfObje
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if no decode params provided - end fast.
|
||||||
if decodeParams != nil {
|
if decodeParams == nil {
|
||||||
if globals := decodeParams.Get("JBIG2Globals"); globals != 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
|
var err error
|
||||||
|
|
||||||
globalsStream, ok := globals.(*PdfObjectStream)
|
globalsStream, ok := globals.(*PdfObjectStream)
|
||||||
if !ok {
|
if !ok {
|
||||||
err = errors.Error(processName, "jbig2.Globals stream should be an Object Stream")
|
err = errors.Error(processName, "jbig2.Globals stream should be an Object Stream")
|
||||||
common.Log.Debug("ERROR: %s", err.Error())
|
common.Log.Debug("ERROR: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
encoder.Globals, err = jbig2.DecodeGlobals(globalsStream.Stream)
|
encoder.Globals, err = jbig2.DecodeGlobals(globalsStream.Stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errors.Wrap(err, processName, "corrupted jbig2 encoded data")
|
err = errors.Wrap(err, processName, "corrupted jbig2 encoded data")
|
||||||
common.Log.Debug("ERROR: %s", err)
|
common.Log.Debug("ERROR: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
return encoder, nil
|
return encoder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,9 +404,9 @@ func GoImageToJBIG2(i image.Image, bwThreshold float64) (*JBIG2Image, error) {
|
|||||||
var th uint8
|
var th uint8
|
||||||
if bwThreshold == JB2ImageAutoThreshold {
|
if bwThreshold == JB2ImageAutoThreshold {
|
||||||
// autoThreshold using triangle method
|
// autoThreshold using triangle method
|
||||||
gray := bitmap.ImgToGray(i)
|
gray := imageutil.ImgToGray(i)
|
||||||
histogram := bitmap.GrayImageHistogram(gray)
|
histogram := imageutil.GrayImageHistogram(gray)
|
||||||
th = bitmap.AutoThresholdTriangle(histogram)
|
th = imageutil.AutoThresholdTriangle(histogram)
|
||||||
i = gray
|
i = gray
|
||||||
} else if bwThreshold > 1.0 || bwThreshold < 0.0 {
|
} else if bwThreshold > 1.0 || bwThreshold < 0.0 {
|
||||||
// check if bwThreshold is unknown - set to 0.0 is not in the allowed range.
|
// 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 {
|
} else {
|
||||||
th = uint8(255 * bwThreshold)
|
th = uint8(255 * bwThreshold)
|
||||||
}
|
}
|
||||||
gray := bitmap.ImgToBinary(i, th)
|
gray := imageutil.ImgToBinary(i, th)
|
||||||
return bwToJBIG2Image(gray), nil
|
return bwToJBIG2Image(gray), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
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.
|
// makeXObject makes the encoded XObject Image that will be used in the PDF.
|
||||||
func (img *Image) makeXObject() error {
|
func (img *Image) makeXObject() error {
|
||||||
encoder := img.encoder
|
encoder := img.encoder
|
||||||
@ -181,7 +188,10 @@ func (img *Image) makeXObject() error {
|
|||||||
func (img *Image) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
|
func (img *Image) GeneratePageBlocks(ctx DrawContext) ([]*Block, DrawContext, error) {
|
||||||
if img.xobj == nil {
|
if img.xobj == nil {
|
||||||
// Build the XObject Image if not already prepared.
|
// Build the XObject Image if not already prepared.
|
||||||
img.makeXObject()
|
if err := img.makeXObject(); err != nil {
|
||||||
|
return nil, ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var blocks []*Block
|
var blocks []*Block
|
||||||
|
1
go.sum
1
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/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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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/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 h1:saj5dTV7eQ1wFg/gVZr1SfbkOmg8CYO9R8frHgQiyR4=
|
||||||
github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83/go.mod h1:xaGEIRenAiJcGgd9p62zbiP4993KaV3PdjczwGnP50I=
|
github.com/gunnsth/pkcs7 v0.0.0-20181213175627-3cffc6fbfe83/go.mod h1:xaGEIRenAiJcGgd9p62zbiP4993KaV3PdjczwGnP50I=
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package bitmap
|
package imageutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
@ -131,6 +131,11 @@ func ImgToGray(i image.Image) *image.Gray {
|
|||||||
return g
|
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 {
|
func blackOrWhite(c, threshold uint8) uint8 {
|
||||||
if c < threshold {
|
if c < threshold {
|
||||||
return 255
|
return 255
|
@ -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.
|
// addPadBits creates new data byte slice that contains extra padding on the last byte for each row.
|
||||||
func (b *Bitmap) addPadBits() (err error) {
|
func (b *Bitmap) addPadBits() (err error) {
|
||||||
const processName = "addPadBits"
|
const processName = "bitmap.addPadBits"
|
||||||
endbits := b.Width % 8
|
endbits := b.Width % 8
|
||||||
if endbits == 0 {
|
if endbits == 0 {
|
||||||
// no partial words
|
// no partial words
|
||||||
@ -559,19 +559,17 @@ func (b *Bitmap) addPadBits() (err error) {
|
|||||||
w := writer.NewMSB(data)
|
w := writer.NewMSB(data)
|
||||||
temp := make([]byte, fullBytes)
|
temp := make([]byte, fullBytes)
|
||||||
var (
|
var (
|
||||||
i, j int
|
i int
|
||||||
bits uint64
|
bits uint64
|
||||||
)
|
)
|
||||||
for i = 0; i < b.Height; i++ {
|
for i = 0; i < b.Height; i++ {
|
||||||
// iterate over full bytes
|
// iterate over full bytes
|
||||||
for j = 0; j < fullBytes; j++ {
|
|
||||||
if _, err = r.Read(temp); err != nil {
|
if _, err = r.Read(temp); err != nil {
|
||||||
return errors.Wrap(err, processName, "full byte")
|
return errors.Wrap(err, processName, "full byte")
|
||||||
}
|
}
|
||||||
if _, err = w.Write(temp); err != nil {
|
if _, err = w.Write(temp); err != nil {
|
||||||
return errors.Wrap(err, processName, "full bytes")
|
return errors.Wrap(err, processName, "full bytes")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// read unused bits
|
// read unused bits
|
||||||
if bits, err = r.ReadBits(byte(endbits)); err != nil {
|
if bits, err = r.ReadBits(byte(endbits)); err != nil {
|
||||||
return errors.Wrap(err, processName, "skipping bits")
|
return errors.Wrap(err, processName, "skipping bits")
|
||||||
|
@ -98,7 +98,7 @@ func (d *Document) AddGenericPage(bm *bitmap.Bitmap, duplicateLineRemoval bool)
|
|||||||
const processName = "Document.AddGenericPage"
|
const processName = "Document.AddGenericPage"
|
||||||
// check if this is PDFMode and there is already a page
|
// check if this is PDFMode and there is already a page
|
||||||
if !d.FullHeaders && d.NumberOfPages != 0 {
|
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
|
// initialize page
|
||||||
page := &Page{
|
page := &Page{
|
||||||
|
1
internal/jbig2/tests/.gitignore
vendored
1
internal/jbig2/tests/.gitignore
vendored
@ -3,3 +3,4 @@ jbig2files
|
|||||||
.test
|
.test
|
||||||
*.jbig2
|
*.jbig2
|
||||||
.envrc
|
.envrc
|
||||||
|
.env
|
||||||
|
155
internal/jbig2/tests/encode_pdf_test.go
Normal file
155
internal/jbig2/tests/encode_pdf_test.go
Normal file
@ -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...)
|
||||||
|
}
|
@ -11,14 +11,15 @@ import (
|
|||||||
goimage "image"
|
goimage "image"
|
||||||
gocolor "image/color"
|
gocolor "image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"io"
|
|
||||||
|
|
||||||
// Imported for initialization side effects.
|
// Imported for initialization side effects.
|
||||||
_ "image/gif"
|
_ "image/gif"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/unidoc/unipdf/v3/common"
|
"github.com/unidoc/unipdf/v3/common"
|
||||||
"github.com/unidoc/unipdf/v3/core"
|
"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"
|
"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.
|
// 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.
|
// 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
|
// 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)
|
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.
|
// ToGoImage converts the unidoc Image to a golang Image structure.
|
||||||
func (img *Image) ToGoImage() (goimage.Image, error) {
|
func (img *Image) ToGoImage() (goimage.Image, error) {
|
||||||
common.Log.Trace("Converting to go image")
|
common.Log.Trace("Converting to go image")
|
||||||
|
@ -240,20 +240,17 @@ func NewXObjectImageFromImage(img *Image, cs PdfColorspace, encoder core.StreamE
|
|||||||
// If `encoder` is nil, uses raw encoding (none).
|
// If `encoder` is nil, uses raw encoding (none).
|
||||||
func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorspace,
|
func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorspace,
|
||||||
encoder core.StreamEncoder) (*XObjectImage, error) {
|
encoder core.StreamEncoder) (*XObjectImage, error) {
|
||||||
xobj := NewXObjectImage()
|
|
||||||
|
|
||||||
if encoder == nil {
|
if encoder == nil {
|
||||||
encoder = core.NewRawEncoder()
|
encoder = core.NewRawEncoder()
|
||||||
}
|
}
|
||||||
|
encoder.UpdateParams(img.GetParamsDict())
|
||||||
|
|
||||||
encoded, err := encoder.EncodeBytes(img.Data)
|
encoded, err := encoder.EncodeBytes(img.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.Log.Debug("Error with encoding: %v", err)
|
common.Log.Debug("Error with encoding: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
xobj := NewXObjectImage()
|
||||||
xobj.Filter = encoder
|
|
||||||
xobj.Stream = encoded
|
|
||||||
|
|
||||||
// Width and height.
|
// Width and height.
|
||||||
imWidth := img.Width
|
imWidth := img.Width
|
||||||
@ -261,8 +258,12 @@ func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorsp
|
|||||||
xobj.Width = &imWidth
|
xobj.Width = &imWidth
|
||||||
xobj.Height = &imHeight
|
xobj.Height = &imHeight
|
||||||
|
|
||||||
// Bits.
|
// Bits per Component.
|
||||||
xobj.BitsPerComponent = &img.BitsPerComponent
|
imBPC := img.BitsPerComponent
|
||||||
|
xobj.BitsPerComponent = &imBPC
|
||||||
|
|
||||||
|
xobj.Filter = encoder
|
||||||
|
xobj.Stream = encoded
|
||||||
|
|
||||||
// Guess colorspace if not explicitly set.
|
// Guess colorspace if not explicitly set.
|
||||||
if cs == nil {
|
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
|
// Has same width and height as original and stored in same
|
||||||
// bits per component (1 component, hence the DeviceGray channel).
|
// bits per component (1 component, hence the DeviceGray channel).
|
||||||
smask := NewXObjectImage()
|
smask := NewXObjectImage()
|
||||||
|
|
||||||
smask.Filter = encoder
|
smask.Filter = encoder
|
||||||
encoded, err := encoder.EncodeBytes(img.alphaData)
|
encoded, err := encoder.EncodeBytes(img.alphaData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -291,7 +293,7 @@ func UpdateXObjectImageFromImage(xobjIn *XObjectImage, img *Image, cs PdfColorsp
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
smask.Stream = encoded
|
smask.Stream = encoded
|
||||||
smask.BitsPerComponent = &img.BitsPerComponent
|
smask.BitsPerComponent = xobj.BitsPerComponent
|
||||||
smask.Width = &img.Width
|
smask.Width = &img.Width
|
||||||
smask.Height = &img.Height
|
smask.Height = &img.Height
|
||||||
smask.ColorSpace = NewPdfColorspaceDeviceGray()
|
smask.ColorSpace = NewPdfColorspaceDeviceGray()
|
||||||
@ -448,6 +450,8 @@ func NewXObjectImageFromStream(stream *core.PdfObjectStream) (*XObjectImage, err
|
|||||||
|
|
||||||
// SetImage updates XObject Image with new image data.
|
// SetImage updates XObject Image with new image data.
|
||||||
func (ximg *XObjectImage) SetImage(img *Image, cs PdfColorspace) error {
|
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)
|
encoded, err := ximg.Filter.EncodeBytes(img.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -493,6 +497,7 @@ func (ximg *XObjectImage) SetFilter(encoder core.StreamEncoder) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ximg.Filter = encoder
|
ximg.Filter = encoder
|
||||||
|
encoder.UpdateParams(ximg.getParamsDict())
|
||||||
encoded, err = encoder.EncodeBytes(decoded)
|
encoded, err = encoder.EncodeBytes(decoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -596,3 +601,13 @@ func (ximg *XObjectImage) ToPdfObject() core.PdfObject {
|
|||||||
|
|
||||||
return stream
|
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
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user