mirror of
https://github.com/unidoc/unipdf.git
synced 2025-05-02 22:17:06 +08:00

* Added JBIG2 PDF support * Added JBIG2 Encoder binary image requirements * PR #288 revision r1 fixes * PR #288 revision r2 fixes
493 lines
18 KiB
Go
493 lines
18 KiB
Go
/*
|
|
* This file is subject to the terms and conditions defined in
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
*/
|
|
|
|
package core
|
|
|
|
import (
|
|
"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"
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/decoder"
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/document"
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/errors"
|
|
)
|
|
|
|
// JBIG2CompressionType defines the enum compression type used by the JBIG2Encoder.
|
|
type JBIG2CompressionType int
|
|
|
|
const (
|
|
// JB2Generic is the JBIG2 compression type that uses generic region see 6.2.
|
|
JB2Generic JBIG2CompressionType = iota
|
|
// JB2SymbolCorrelation is the JBIG2 compression type that uses symbol dictionary and text region encoding procedure
|
|
// with the correlation classification.
|
|
// NOT IMPLEMENTED YET.
|
|
JB2SymbolCorrelation
|
|
// JB2SymbolRankHaus is the JBIG2 compression type that uses symbol dictionary and text region encoding procedure
|
|
// with the rank hausdorff classification. RankHausMode uses the rank Hausdorff method that classifies the input images.
|
|
// It is more robust, more susceptible to confusing components that should be in different classes.
|
|
// NOT IMPLEMENTED YET.
|
|
JB2SymbolRankHaus
|
|
)
|
|
|
|
// JB2ImageAutoThreshold is the const value used by the 'GoImageToJBIG2Image function' used to set auto threshold
|
|
// for the histogram.
|
|
const JB2ImageAutoThreshold = -1.0
|
|
|
|
//
|
|
// JBIG2Encoder/Decoder
|
|
//
|
|
|
|
// JBIG2Encoder implements both jbig2 encoder and the decoder. The encoder allows to encode
|
|
// provided images (best used document scans) in multiple way. By default it uses single page generic
|
|
// encoder. It allows to store lossless data as a single segment.
|
|
// In order to store multiple image pages use the 'FileMode' which allows to store more pages within single jbig2 document.
|
|
// WIP: In order to obtain better compression results the encoder would allow to encode the input in a
|
|
// lossy or lossless way with a component (symbol) mode. It divides the image into components.
|
|
// Then checks if any component is 'similar' to the others and maps them together. The symbol classes are stored
|
|
// in the dictionary. Then the encoder creates text regions which uses the related symbol classes to fill it's space.
|
|
// 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
|
|
// IsChocolateData defines if the data is encoded such that
|
|
// binary data '1' means black and '0' white.
|
|
// otherwise the data is called vanilla.
|
|
// Naming convention taken from: 'https://en.wikipedia.org/wiki/Binary_image#Interpretation'
|
|
IsChocolateData bool
|
|
// DefaultPageSettings are the settings parameters used by the jbig2 encoder.
|
|
DefaultPageSettings JBIG2EncoderSettings
|
|
}
|
|
|
|
// NewJBIG2Encoder creates a new JBIG2Encoder.
|
|
func NewJBIG2Encoder() *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.
|
|
// The 'settings' defines what encoding type should be used by the encoder.
|
|
func (enc *JBIG2Encoder) AddPageImage(img *JBIG2Image, settings *JBIG2EncoderSettings) (err error) {
|
|
const processName = "JBIG2Document.AddPageImage"
|
|
if enc == nil {
|
|
return errors.Error(processName, "JBIG2Document is nil")
|
|
}
|
|
if settings == nil {
|
|
settings = &enc.DefaultPageSettings
|
|
}
|
|
if enc.d == nil {
|
|
enc.d = document.InitEncodeDocument(settings.FileMode)
|
|
}
|
|
|
|
if err = settings.Validate(); err != nil {
|
|
return errors.Wrap(err, processName, "")
|
|
}
|
|
|
|
// convert input 'img' to the bitmap.Bitmap
|
|
b, err := img.toBitmap()
|
|
if err != nil {
|
|
return errors.Wrap(err, processName, "")
|
|
}
|
|
|
|
switch settings.Compression {
|
|
case JB2Generic:
|
|
if err = enc.d.AddGenericPage(b, settings.DuplicatedLinesRemoval); err != nil {
|
|
return errors.Wrap(err, processName, "")
|
|
}
|
|
case JB2SymbolCorrelation:
|
|
return errors.Error(processName, "symbol correlation encoding not implemented yet")
|
|
case JB2SymbolRankHaus:
|
|
return errors.Error(processName, "symbol rank haus encoding not implemented yet")
|
|
default:
|
|
return errors.Error(processName, "provided invalid compression")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DecodeBytes decodes a slice of JBIG2 encoded bytes and returns the results.
|
|
func (enc *JBIG2Encoder) DecodeBytes(encoded []byte) ([]byte, error) {
|
|
parameters := decoder.Parameters{UnpaddedData: true}
|
|
return jbig2.DecodeBytes(encoded, parameters, enc.Globals)
|
|
}
|
|
|
|
// DecodeGlobals decodes 'encoded' byte stream and returns their Globally defined segments ('Globals').
|
|
func (enc *JBIG2Encoder) DecodeGlobals(encoded []byte) (jbig2.Globals, error) {
|
|
return jbig2.DecodeGlobals(encoded)
|
|
}
|
|
|
|
// DecodeImages decodes the page images from the jbig2 'encoded' data input.
|
|
// The jbig2 document may contain multiple pages, thus the function can return multiple
|
|
// images. The images order corresponds to the page number.
|
|
func (enc *JBIG2Encoder) DecodeImages(encoded []byte) ([]image.Image, error) {
|
|
const processName = "JBIG2Encoder.DecodeImages"
|
|
parameters := decoder.Parameters{UnpaddedData: true}
|
|
// create decoded document.
|
|
d, err := decoder.Decode(encoded, parameters, enc.Globals.ToDocumentGlobals())
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
|
|
// get page number in the document.
|
|
pageNumber, err := d.PageNumber()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
|
|
// decode all images
|
|
images := []image.Image{}
|
|
var img image.Image
|
|
for i := 1; i <= pageNumber; i++ {
|
|
img, err = d.DecodePageImage(i)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, processName, "page: '%d'", i)
|
|
}
|
|
images = append(images, img)
|
|
}
|
|
return images, nil
|
|
}
|
|
|
|
// DecodeStream decodes a JBIG2 encoded stream and returns the result as a slice of bytes.
|
|
func (enc *JBIG2Encoder) DecodeStream(streamObj *PdfObjectStream) ([]byte, error) {
|
|
return enc.DecodeBytes(streamObj.Stream)
|
|
}
|
|
|
|
// EncodeBytes encodes slice of bytes into JBIG2 encoding format.
|
|
// The input 'data' must be an image. In order to Decode it a user is responsible to
|
|
// load the codec ('png', 'jpg').
|
|
// Returns jbig2 single page encoded document byte slice. The encoder uses DefaultPageSettings
|
|
// to encode given image.
|
|
func (enc *JBIG2Encoder) EncodeBytes(data []byte) ([]byte, error) {
|
|
const processName = "JBIG2Encoder.EncodeBytes"
|
|
if enc.ColorComponents != 1 || enc.BitsPerComponent != 1 {
|
|
return nil, errors.Errorf(processName, "provided invalid input image. JBIG2 Encoder requires binary images data")
|
|
}
|
|
b, err := bitmap.NewWithUnpaddedData(enc.Width, enc.Height, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
settings := enc.DefaultPageSettings
|
|
if err = settings.Validate(); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
|
|
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.
|
|
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"
|
|
if enc.d == nil {
|
|
return nil, errors.Errorf(processName, "document input data not defined")
|
|
}
|
|
enc.d.FullHeaders = enc.DefaultPageSettings.FileMode
|
|
// encode the document
|
|
data, err = enc.d.Encode()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// GetFilterName returns the name of the encoding filter.
|
|
func (enc *JBIG2Encoder) GetFilterName() string {
|
|
return StreamEncodingFilterNameJBIG2
|
|
}
|
|
|
|
// MakeDecodeParams makes a new instance of an encoding dictionary based on the current encoder settings.
|
|
func (enc *JBIG2Encoder) MakeDecodeParams() PdfObject {
|
|
return MakeDict()
|
|
}
|
|
|
|
// MakeStreamDict makes a new instance of an encoding dictionary for a stream object.
|
|
func (enc *JBIG2Encoder) MakeStreamDict() *PdfObjectDictionary {
|
|
dict := MakeDict()
|
|
dict.Set("Filter", MakeName(enc.GetFilterName()))
|
|
return dict
|
|
}
|
|
|
|
// UpdateParams updates the parameter values of the encoder.
|
|
// 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) {
|
|
const processName = "encodeImage"
|
|
// convert the input into jbig2 image
|
|
jbig2Image, err := GoImageToJBIG2(i, JB2ImageAutoThreshold)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "convert input image to jbig2 img")
|
|
}
|
|
if err = enc.AddPageImage(jbig2Image, &enc.DefaultPageSettings); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return enc.Encode()
|
|
}
|
|
|
|
func newJBIG2DecoderFromStream(streamObj *PdfObjectStream, decodeParams *PdfObjectDictionary) (*JBIG2Encoder, error) {
|
|
const processName = "newJBIG2DecoderFromStream"
|
|
encoder := &JBIG2Encoder{}
|
|
encDict := streamObj.PdfObjectDictionary
|
|
if encDict == nil {
|
|
// No encoding dictionary.
|
|
return encoder, nil
|
|
}
|
|
|
|
// If decodeParams not provided, see if we can get from the stream.
|
|
if decodeParams == nil {
|
|
obj := encDict.Get("DecodeParms")
|
|
if obj != nil {
|
|
switch t := obj.(type) {
|
|
case *PdfObjectDictionary:
|
|
decodeParams = t
|
|
case *PdfObjectArray:
|
|
if t.Len() == 1 {
|
|
if dp, ok := GetDict(t.Get(0)); ok {
|
|
decodeParams = dp
|
|
}
|
|
}
|
|
default:
|
|
common.Log.Error("DecodeParams not a dictionary %#v", obj)
|
|
return nil, errors.Errorf(processName, "invalid DecodeParms type: %T", t)
|
|
}
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
|
|
//
|
|
// JBIG2Image
|
|
//
|
|
|
|
// JBIG2Image is the image structure used by the jbig2 encoder. Its Data must be in a
|
|
// 1 bit per component and 1 component per pixel (1bpp). In order to create binary image
|
|
// use GoImageToJBIG2 function. If the image data contains the row bytes padding set the HasPadding to true.
|
|
type JBIG2Image struct {
|
|
// Width and Height defines the image boundaries.
|
|
Width, Height int
|
|
// Data is the byte slice data for the input image
|
|
Data []byte
|
|
// HasPadding is the attribute that defines if the last byte of the data in the row contains
|
|
// 0 bits padding.
|
|
HasPadding bool
|
|
}
|
|
|
|
// ToGoImage converts the JBIG2Image to the golang image.Image.
|
|
func (j *JBIG2Image) ToGoImage() (image.Image, error) {
|
|
const processName = "JBIG2Image.ToGoImage"
|
|
bm, err := j.toBitmap()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return bm.ToImage(), nil
|
|
}
|
|
|
|
func (j *JBIG2Image) toBitmap() (b *bitmap.Bitmap, err error) {
|
|
const processName = "JBIG2Image.toBitmap"
|
|
if j.Data == nil {
|
|
return nil, errors.Error(processName, "image data not defined")
|
|
}
|
|
if j.Width == 0 || j.Height == 0 {
|
|
return nil, errors.Error(processName, "image height or width not defined")
|
|
}
|
|
// check if the data already has padding
|
|
if j.HasPadding {
|
|
b, err = bitmap.NewWithData(j.Width, j.Height, j.Data)
|
|
} else {
|
|
b, err = bitmap.NewWithUnpaddedData(j.Width, j.Height, j.Data)
|
|
}
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// GoImageToJBIG2 creates a binary image on the base of 'i' golang image.Image.
|
|
// If the image is not a black/white image then the function converts provided input into
|
|
// JBIG2Image with 1bpp. For non grayscale images the function performs the conversion to the grayscale temp image.
|
|
// Then it checks the value of the gray image value if it's within bounds of the black white threshold.
|
|
// This 'bwThreshold' value should be in range (0.0, 1.0). The threshold checks if the grayscale pixel (uint) value
|
|
// is greater or smaller than 'bwThreshold' * 255. Pixels inside the range will be white, and the others will be black.
|
|
// If the 'bwThreshold' is equal to -1.0 - JB2ImageAutoThreshold then it's value would be set on the base of
|
|
// it's histogram using Triangle method. For more information go to:
|
|
// https://www.mathworks.com/matlabcentral/fileexchange/28047-gray-image-thresholding-using-the-triangle-method
|
|
func GoImageToJBIG2(i image.Image, bwThreshold float64) (*JBIG2Image, error) {
|
|
const processName = "GoImageToJBIG2"
|
|
if i == nil {
|
|
return nil, errors.Error(processName, "image 'i' not defined")
|
|
}
|
|
var th uint8
|
|
if bwThreshold == JB2ImageAutoThreshold {
|
|
// autoThreshold using triangle method
|
|
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.
|
|
return nil, errors.Error(processName, "provided threshold is not in a range {0.0, 1.0}")
|
|
} else {
|
|
th = uint8(255 * bwThreshold)
|
|
}
|
|
gray := imageutil.ImgToBinary(i, th)
|
|
return bwToJBIG2Image(gray), nil
|
|
}
|
|
|
|
func bwToJBIG2Image(i *image.Gray) *JBIG2Image {
|
|
bounds := i.Bounds()
|
|
// compute the rowStride - number of bytes in the row.
|
|
bm := bitmap.New(bounds.Dx(), bounds.Dy())
|
|
ji := &JBIG2Image{Height: bounds.Dy(), Width: bounds.Dx(), HasPadding: true}
|
|
// allocate the byte slice data
|
|
var pix color.Gray
|
|
for y := 0; y < bounds.Dy(); y++ {
|
|
for x := 0; x < bounds.Dx(); x++ {
|
|
pix = i.GrayAt(x, y)
|
|
// check if the pixel is black or white
|
|
// where black pixel would be stored as '1' bit
|
|
// and the white as '0' bit.
|
|
|
|
// the pix is color.Black if it's Y value is '0'.
|
|
if pix.Y == 0 {
|
|
if err := bm.SetPixel(x, y, 1); err != nil {
|
|
common.Log.Debug("can't set pixel at bitmap: %v", bm)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ji.Data = bm.Data
|
|
return ji
|
|
}
|
|
|
|
// JBIG2EncoderSettings contains the parameters and settings used by the JBIG2Encoder.
|
|
// Current version works only on JB2Generic compression.
|
|
type JBIG2EncoderSettings struct {
|
|
// FileMode defines if the jbig2 encoder should return full jbig2 file instead of
|
|
// shortened pdf mode. This adds the file header to the jbig2 definition.
|
|
FileMode bool
|
|
// Compression is the setting that defines the compression type used for encoding the page.
|
|
Compression JBIG2CompressionType
|
|
// DuplicatedLinesRemoval code generic region in a way such that if the lines are duplicated the encoder
|
|
// doesn't store it twice.
|
|
DuplicatedLinesRemoval bool
|
|
// DefaultPixelValue is the bit value initial for every pixel in the page.
|
|
DefaultPixelValue uint8
|
|
// ResolutionX optional setting that defines the 'x' axis input image resolution - used for single page encoding.
|
|
ResolutionX int
|
|
// ResolutionY optional setting that defines the 'y' axis input image resolution - used for single page encoding.
|
|
ResolutionY int
|
|
// Threshold defines the threshold of the image correlation for
|
|
// non Generic compression.
|
|
// User only for JB2SymbolCorrelation and JB2SymbolRankHaus methods.
|
|
// Best results in range [0.7 - 0.98] - the less the better the compression would be
|
|
// but the more lossy.
|
|
// Default value: 0.95
|
|
Threshold float64
|
|
}
|
|
|
|
// Validate validates the page settings for the JBIG2 encoder.
|
|
func (s JBIG2EncoderSettings) Validate() error {
|
|
const processName = "validateEncoder"
|
|
if s.Threshold < 0 || s.Threshold > 1.0 {
|
|
return errors.Errorf(processName, "provided threshold value: '%v' must be in range [0.0, 1.0]", s.Threshold)
|
|
}
|
|
if s.ResolutionX < 0 {
|
|
return errors.Errorf(processName, "provided x resolution: '%d' must be positive or zero value", s.ResolutionX)
|
|
}
|
|
if s.ResolutionY < 0 {
|
|
return errors.Errorf(processName, "provided y resolution: '%d' must be positive or zero value", s.ResolutionY)
|
|
}
|
|
if s.DefaultPixelValue != 0 && s.DefaultPixelValue != 1 {
|
|
return errors.Errorf(processName, "default pixel value: '%d' must be a value for the bit: {0,1}", s.DefaultPixelValue)
|
|
}
|
|
if s.Compression != JB2Generic {
|
|
return errors.Errorf(processName, "provided compression is not implemented yet")
|
|
}
|
|
return nil
|
|
}
|