-
Notifications
You must be signed in to change notification settings - Fork 256
/
Copy pathencoding_jbig2.go
437 lines (396 loc) · 16.1 KB
/
encoding_jbig2.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
/*
* 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 (
"bytes"
"image"
"image/color"
"github.com/unidoc/unipdf/v3/common"
"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 {
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{}
}
// 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
//noinspection GoPreferNilSlice
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 len(data) == 0 {
return nil, errors.Errorf(processName, "input 'data' not defined")
}
i, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, errors.Wrap(err, processName, "decode input image")
}
encoded, err := enc.encodeImage(i)
if err != nil {
return nil, errors.Wrap(err, processName, "")
}
return encoded, nil
}
// 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)
}
// 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.
func (enc *JBIG2Encoder) UpdateParams(params *PdfObjectDictionary) {
}
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 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")
//noinspection GoNilness
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
}
}
}
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 := bitmap.ImgToGray(i)
histogram := bitmap.GrayImageHistogram(gray)
th = bitmap.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 := bitmap.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
}