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
1217 lines
31 KiB
Go
1217 lines
31 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 bitmap
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"math"
|
|
|
|
"github.com/unidoc/unipdf/v3/common"
|
|
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/errors"
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/reader"
|
|
"github.com/unidoc/unipdf/v3/internal/jbig2/writer"
|
|
)
|
|
|
|
// tab8 contains number of '1' bits in each possible 8 bit value stored at it's index.
|
|
var tab8 [256]uint8
|
|
|
|
func init() {
|
|
for i := 0; i < 256; i++ {
|
|
tab8[i] = uint8(i&0x1) +
|
|
(uint8(i>>1) & 0x1) +
|
|
(uint8(i>>2) & 0x1) +
|
|
(uint8(i>>3) & 0x1) +
|
|
(uint8(i>>4) & 0x1) +
|
|
(uint8(i>>5) & 0x1) +
|
|
(uint8(i>>6) & 0x1) +
|
|
(uint8(i>>7) & 0x1)
|
|
}
|
|
}
|
|
|
|
// Bitmap is the jbig2 bitmap representation.
|
|
type Bitmap struct {
|
|
// Width and Height represents bitmap dimensions.
|
|
Width, Height int
|
|
// BitmapNumber is the bitmap's id number.
|
|
BitmapNumber int
|
|
// RowStride is the number of bytes set per row.
|
|
RowStride int
|
|
// Data saves the bits data for the bitmap.
|
|
Data []byte
|
|
// Color is the bitmap's color interpretation.
|
|
Color Color
|
|
|
|
// Special instructions for I/O.
|
|
Special int
|
|
|
|
// Text string associated with the pix.
|
|
Text string
|
|
|
|
// The XResolution and YResolution are the
|
|
// image resolution parameters at width and height.
|
|
XResolution, YResolution int
|
|
}
|
|
|
|
// New creates new bitmap with the parameters as provided in the arguments.
|
|
func New(width, height int) *Bitmap {
|
|
bm := newBitmap(width, height)
|
|
bm.Data = make([]byte, height*bm.RowStride)
|
|
return bm
|
|
}
|
|
|
|
// Copy the bitmap 's' into bitmap 'd'. If 'd' is nil, it is created by the function.
|
|
func Copy(d, s *Bitmap) (*Bitmap, error) {
|
|
return copyBitmap(d, s)
|
|
}
|
|
|
|
func newBitmap(width, height int) *Bitmap {
|
|
return &Bitmap{
|
|
Width: width,
|
|
Height: height,
|
|
RowStride: (width + 7) >> 3,
|
|
}
|
|
}
|
|
|
|
// NewWithData creates new bitmap with the provided 'width', 'height' and the byte slice 'data'.
|
|
func NewWithData(width, height int, data []byte) (*Bitmap, error) {
|
|
const processName = "NewWithData"
|
|
bm := newBitmap(width, height)
|
|
bm.Data = data
|
|
if len(data) < height*bm.RowStride {
|
|
return nil, errors.Errorf(processName, "invalid data length: %d - should be: %d", len(data), height*bm.RowStride)
|
|
}
|
|
return bm, nil
|
|
}
|
|
|
|
// NewWithUnpaddedData creates new bitmap with provided 'width', 'height' and the byte slice 'data' that doesn't
|
|
// contains paddings on the last byte of each row.
|
|
// This function adds the padding to the bitmap data.
|
|
func NewWithUnpaddedData(width, height int, data []byte) (*Bitmap, error) {
|
|
const processName = "NewWithUnpaddedData"
|
|
bm := newBitmap(width, height)
|
|
bm.Data = data
|
|
if dataLen := ((width * height) + 7) >> 3; len(data) < dataLen {
|
|
return nil, errors.Errorf(processName, "invalid data length: '%d'. The data should contain at least: '%d' bytes", len(data), dataLen)
|
|
}
|
|
if err := bm.addPadBits(); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return bm, nil
|
|
}
|
|
|
|
// AddBorder creates a new bitmap with the border of size 'borderSize'. If the 'borderSize' is different than zero
|
|
// the resultant bitmap dimensions would increase by width += 2* borderSize, height += 2*borderSize.
|
|
// The value 'val' represents the binary bit 'value' of the border - '0' and '1'
|
|
func (b *Bitmap) AddBorder(borderSize, val int) (*Bitmap, error) {
|
|
if borderSize == 0 {
|
|
return b.Copy(), nil
|
|
}
|
|
d, err := b.addBorderGeneral(borderSize, borderSize, borderSize, borderSize, val)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "AddBorder", "")
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// AddBorderGeneral creates new bitmap on the base of the bitmap 'b' with the border of size for each side
|
|
// 'left','right','top','bot'. The 'val' sets the border white (0) or black (1).
|
|
func (b *Bitmap) AddBorderGeneral(left, right, top, bot int, val int) (*Bitmap, error) {
|
|
return b.addBorderGeneral(left, right, top, bot, val)
|
|
}
|
|
|
|
// And does the raster operation 'AND' on the provided bitmaps 'b' and 's'.
|
|
func (b *Bitmap) And(s *Bitmap) (d *Bitmap, err error) {
|
|
const processName = "Bitmap.And"
|
|
if b == nil {
|
|
return nil, errors.Error(processName, "'bitmap 'b' is nil")
|
|
}
|
|
|
|
if s == nil {
|
|
return nil, errors.Error(processName, "bitmap 's' is nil")
|
|
}
|
|
|
|
if !b.SizesEqual(s) {
|
|
common.Log.Debug("%s - Bitmap 's' is not equal size with 'b'", processName)
|
|
}
|
|
|
|
if d, err = copyBitmap(d, b); err != nil {
|
|
return nil, errors.Wrap(err, processName, "can't create 'd' bitmap")
|
|
}
|
|
|
|
if err = d.RasterOperation(0, 0, d.Width, d.Height, PixSrcAndDst, s, 0, 0); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// ClipRectangle clips the 'b' Bitmap to the 'box' with relatively defined coordinates.
|
|
// If the box is not contained within the pix the 'box' is clipped to the 'b' size.
|
|
func (b *Bitmap) ClipRectangle(box *image.Rectangle) (d *Bitmap, boxC *image.Rectangle, err error) {
|
|
const processName = "ClipRectangle"
|
|
if box == nil {
|
|
return nil, nil, errors.Error(processName, "box is not defined")
|
|
}
|
|
w, h := b.Width, b.Height
|
|
sRect := image.Rect(0, 0, w, h)
|
|
if !box.Overlaps(sRect) {
|
|
return nil, nil, errors.Error(processName, "box doesn't overlap b")
|
|
}
|
|
boxCT := box.Intersect(sRect)
|
|
|
|
bx, by := boxCT.Min.X, boxCT.Min.Y
|
|
bw, bh := boxCT.Dx(), boxCT.Dy()
|
|
|
|
d = New(bw, bh)
|
|
d.Text = b.Text
|
|
if err = d.RasterOperation(0, 0, bw, bh, PixSrc, b, bx, by); err != nil {
|
|
return nil, nil, errors.Wrap(err, processName, "PixSrc to clipped")
|
|
}
|
|
boxC = &boxCT
|
|
return d, boxC, nil
|
|
}
|
|
|
|
// Copy gets a copy of the 'b' bitmap.
|
|
func (b *Bitmap) Copy() *Bitmap {
|
|
data := make([]byte, len(b.Data))
|
|
copy(data, b.Data)
|
|
return &Bitmap{
|
|
Width: b.Width,
|
|
Height: b.Height,
|
|
RowStride: b.RowStride,
|
|
Data: data,
|
|
Color: b.Color,
|
|
Text: b.Text,
|
|
BitmapNumber: b.BitmapNumber,
|
|
Special: b.Special,
|
|
}
|
|
}
|
|
|
|
// CountPixels counts the pixels for the bitmap 'b'.
|
|
func (b *Bitmap) CountPixels() int {
|
|
return b.countPixels()
|
|
}
|
|
|
|
// CreateTemplate creates a copy template bitmap on the base of the
|
|
// bitmap 'b'. A template has the same parameters as bitmap 'b', but contains empty Data.
|
|
func (b *Bitmap) CreateTemplate() *Bitmap {
|
|
return b.createTemplate()
|
|
}
|
|
|
|
// Equals checks if all the pixels in the 'b' bitmap are equals to the 's' bitmap.
|
|
func (b *Bitmap) Equals(s *Bitmap) bool {
|
|
if len(b.Data) != len(s.Data) || b.Width != s.Width || b.Height != s.Height {
|
|
return false
|
|
}
|
|
|
|
for y := 0; y < b.Height; y++ {
|
|
lineIndex := y * b.RowStride
|
|
for i := 0; i < b.RowStride; i++ {
|
|
if b.Data[lineIndex+i] != s.Data[lineIndex+i] {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Equivalent checks if the bitmaps 'b' and 's' are equivalent
|
|
// from the visual point of view.
|
|
func (b *Bitmap) Equivalent(s *Bitmap) bool {
|
|
return b.equivalent(s)
|
|
}
|
|
|
|
// GetBitOffset gets the bit offset at the 'x' coordinate.
|
|
func (b *Bitmap) GetBitOffset(x int) int {
|
|
return x & 0x07
|
|
}
|
|
|
|
// GetByte gets and returns the byte at given 'index'.
|
|
func (b *Bitmap) GetByte(index int) (byte, error) {
|
|
if index > len(b.Data)-1 || index < 0 {
|
|
return 0, errors.Errorf("GetByte", "index: %d out of range", index)
|
|
}
|
|
return b.Data[index], nil
|
|
}
|
|
|
|
// GetByteIndex gets the byte index from the bitmap at coordinates 'x','y'.
|
|
func (b *Bitmap) GetByteIndex(x, y int) int {
|
|
return y*b.RowStride + (x >> 3)
|
|
}
|
|
|
|
// GetChocolateData gets bitmap data as a byte slice with Chocolate bit interpretation.
|
|
// 'Chocolate' data is the bit interpretation where the 0'th bit means white and the 1'th bit means black.
|
|
// The naming convention based on the: `https://en.wikipedia.org/wiki/Binary_image#Interpretation` page.
|
|
func (b *Bitmap) GetChocolateData() []byte {
|
|
if b.Color == Vanilla {
|
|
b.inverseData()
|
|
}
|
|
return b.Data
|
|
}
|
|
|
|
// GetPixel gets the pixel value at the coordinates 'x', 'y'.
|
|
func (b *Bitmap) GetPixel(x, y int) bool {
|
|
i := b.GetByteIndex(x, y)
|
|
o := b.GetBitOffset(x)
|
|
shift := uint(7 - o)
|
|
if i > len(b.Data)-1 {
|
|
common.Log.Debug("Trying to get pixel out of the data range. x: '%d', y:'%d', bm: '%s'", x, y, b)
|
|
return false
|
|
}
|
|
if (b.Data[i]>>shift)&0x01 >= 1 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetUnpaddedData gets the data without row stride padding.
|
|
// The unpadded data contains bitmap.Height * bitmap.Width bits with
|
|
// optional last byte padding.
|
|
func (b *Bitmap) GetUnpaddedData() ([]byte, error) {
|
|
padding := uint(b.Width & 0x07)
|
|
if padding == 0 {
|
|
return b.Data, nil
|
|
}
|
|
|
|
size := b.Width * b.Height
|
|
if size%8 != 0 {
|
|
size >>= 3
|
|
size++
|
|
} else {
|
|
size >>= 3
|
|
}
|
|
|
|
data := make([]byte, size)
|
|
w := writer.NewMSB(data)
|
|
|
|
const processName = "GetUnpaddedData"
|
|
for y := 0; y < b.Height; y++ {
|
|
// btIndex is the byte index per row.
|
|
for btIndex := 0; btIndex < b.RowStride; btIndex++ {
|
|
bt := b.Data[y*b.RowStride+btIndex]
|
|
if btIndex != b.RowStride-1 {
|
|
err := w.WriteByte(bt)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
continue
|
|
}
|
|
|
|
for i := uint(0); i < padding; i++ {
|
|
err := w.WriteBit(int(bt >> (7 - i) & 0x01))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
// GetVanillaData gets bitmap data as a byte slice with Vanilla bit interpretation.
|
|
// 'Vanilla' is the bit interpretation where the 0'th bit means black and 1'th bit means white.
|
|
// The naming convention based on the `https://en.wikipedia.org/wiki/Binary_image#Interpretation` page.
|
|
func (b *Bitmap) GetVanillaData() []byte {
|
|
if b.Color == Chocolate {
|
|
b.inverseData()
|
|
}
|
|
return b.Data
|
|
}
|
|
|
|
// InverseData inverses the data color interpretation.
|
|
func (b *Bitmap) InverseData() {
|
|
b.inverseData()
|
|
}
|
|
|
|
// RemoveBorder create a new bitmap based on the 'b' with removed border of size 'borderSize'.
|
|
func (b *Bitmap) RemoveBorder(borderSize int) (*Bitmap, error) {
|
|
if borderSize == 0 {
|
|
return b.Copy(), nil
|
|
}
|
|
d, err := b.removeBorderGeneral(borderSize, borderSize, borderSize, borderSize)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "RemoveBorder", "")
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// RemoveBorderGeneral creates a new bitmap with removed border of size 'left', 'right', 'top', 'bot'.
|
|
// The resultant bitmap dimensions would be smaller by the value of border size.
|
|
func (b *Bitmap) RemoveBorderGeneral(left, right, top, bot int) (*Bitmap, error) {
|
|
return b.removeBorderGeneral(left, right, top, bot)
|
|
}
|
|
|
|
// SetPixel sets the pixel at 'x', 'y' coordinates with the value of 'pixel'.
|
|
// Returns an error if the index is out of range.
|
|
func (b *Bitmap) SetPixel(x, y int, pixel byte) error {
|
|
i := b.GetByteIndex(x, y)
|
|
if i > len(b.Data)-1 {
|
|
return errors.Errorf("SetPixel", "index out of range: %d", i)
|
|
}
|
|
o := b.GetBitOffset(x)
|
|
|
|
shift := uint(7 - o)
|
|
src := b.Data[i]
|
|
var result byte
|
|
if pixel == 1 {
|
|
result = src | (pixel & 0x01 << shift)
|
|
} else {
|
|
result = src & ^(1 << shift)
|
|
}
|
|
b.Data[i] = result
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetByte sets the byte at 'index' with value 'v'.
|
|
// Returns an error if the index is out of range.
|
|
func (b *Bitmap) SetByte(index int, v byte) error {
|
|
if index > len(b.Data)-1 || index < 0 {
|
|
return errors.Errorf("SetByte", "index out of range: %d", index)
|
|
}
|
|
|
|
b.Data[index] = v
|
|
return nil
|
|
}
|
|
|
|
// SetDefaultPixel sets all bits within bitmap to '1'.
|
|
func (b *Bitmap) SetDefaultPixel() {
|
|
for i := range b.Data {
|
|
b.Data[i] = byte(0xff)
|
|
}
|
|
}
|
|
|
|
// SetPadBits sets the pad bits for the current bitmap.
|
|
func (b *Bitmap) SetPadBits(value int) {
|
|
b.setPadBits(value)
|
|
}
|
|
|
|
// SizesEqual checks if the bitmaps are of the same size.
|
|
func (b *Bitmap) SizesEqual(s *Bitmap) bool {
|
|
if b == s {
|
|
return true
|
|
}
|
|
|
|
if b.Width != s.Width || b.Height != s.Height {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// String implements the Stringer interface.
|
|
func (b *Bitmap) String() string {
|
|
var s = "\n"
|
|
for y := 0; y < b.Height; y++ {
|
|
var row string
|
|
for x := 0; x < b.Width; x++ {
|
|
pix := b.GetPixel(x, y)
|
|
if pix {
|
|
row += "1"
|
|
} else {
|
|
row += "0"
|
|
}
|
|
}
|
|
s += row + "\n"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// ThresholdPixelSum checks if the number of the 'ON' pixels is above the provided 'thresh' threshold.
|
|
// If the on pixel count > thresh the function returns quckly.
|
|
func (b *Bitmap) ThresholdPixelSum(thresh int, tab8 []int) (above bool, err error) {
|
|
const processName = "Bitmap.ThresholdPixelSum"
|
|
if tab8 == nil {
|
|
tab8 = makePixelSumTab8()
|
|
}
|
|
|
|
fullBytes := b.Width >> 3
|
|
endBits := b.Width & 7
|
|
endMask := byte(0xff << uint(8-endBits))
|
|
var (
|
|
i, j, lineIndex, count int
|
|
bt byte
|
|
)
|
|
for i = 0; i < b.Height; i++ {
|
|
lineIndex = b.RowStride * i
|
|
for j = 0; j < fullBytes; j++ {
|
|
bt, err = b.GetByte(lineIndex + j)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, processName, "fullByte")
|
|
}
|
|
count += tab8[bt]
|
|
}
|
|
if endBits != 0 {
|
|
bt, err = b.GetByte(lineIndex + j)
|
|
if err != nil {
|
|
return false, errors.Wrap(err, processName, "partialByte")
|
|
}
|
|
bt &= endMask
|
|
count += tab8[bt]
|
|
}
|
|
if count > thresh {
|
|
return true, nil
|
|
}
|
|
}
|
|
return above, nil
|
|
}
|
|
|
|
// ToImage gets the bitmap data and store in the image.Image.
|
|
func (b *Bitmap) ToImage() image.Image {
|
|
img := image.NewGray(image.Rect(0, 0, b.Width-1, b.Height-1))
|
|
for x := 0; x < b.Width; x++ {
|
|
for y := 0; y < b.Height; y++ {
|
|
c := color.Black
|
|
if b.GetPixel(x, y) {
|
|
c = color.White
|
|
}
|
|
img.Set(x, y, c)
|
|
}
|
|
}
|
|
return img
|
|
}
|
|
|
|
// Zero check if there is no 'ONE' pixels.
|
|
func (b *Bitmap) Zero() bool {
|
|
fullBytes := b.Width / 8
|
|
endBits := b.Width & 7
|
|
var endMask byte
|
|
if endBits != 0 {
|
|
endMask = byte(0xff << uint(8-endBits))
|
|
}
|
|
|
|
var line, i, j int
|
|
for i = 0; i < b.Height; i++ {
|
|
line = b.RowStride * i
|
|
for j = 0; j < fullBytes; j, line = j+1, line+1 {
|
|
if b.Data[line] != 0 {
|
|
return false
|
|
}
|
|
}
|
|
if endBits > 0 {
|
|
if b.Data[line]&endMask != 0 {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (b *Bitmap) addBorderGeneral(left, right, top, bot int, val int) (*Bitmap, error) {
|
|
const processName = "addBorderGeneral"
|
|
if left < 0 || right < 0 || top < 0 || bot < 0 {
|
|
return nil, errors.Error(processName, "negative border added")
|
|
}
|
|
|
|
ws, hs := b.Width, b.Height
|
|
wd := ws + left + right
|
|
hd := hs + top + bot
|
|
|
|
bd := New(wd, hd)
|
|
bd.Color = b.Color
|
|
|
|
op := PixClr
|
|
if val > 0 {
|
|
op = PixSet
|
|
}
|
|
|
|
err := bd.RasterOperation(0, 0, left, hd, op, nil, 0, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "left")
|
|
}
|
|
err = bd.RasterOperation(wd-right, 0, right, hd, op, nil, 0, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "right")
|
|
}
|
|
err = bd.RasterOperation(0, 0, wd, top, op, nil, 0, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "top")
|
|
}
|
|
err = bd.RasterOperation(0, hd-bot, wd, bot, op, nil, 0, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "bottom")
|
|
}
|
|
|
|
// copy the pixels into the interior
|
|
err = bd.RasterOperation(left, top, ws, hs, PixSrc, b, 0, 0)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "copy")
|
|
}
|
|
return bd, nil
|
|
}
|
|
|
|
// 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 = "bitmap.addPadBits"
|
|
endbits := b.Width % 8
|
|
if endbits == 0 {
|
|
// no partial words
|
|
return nil
|
|
}
|
|
fullBytes := b.Width / 8
|
|
// mask := rmaskByte[endbits]
|
|
|
|
r := reader.New(b.Data)
|
|
data := make([]byte, b.Height*b.RowStride)
|
|
w := writer.NewMSB(data)
|
|
temp := make([]byte, fullBytes)
|
|
var (
|
|
i int
|
|
bits uint64
|
|
)
|
|
for i = 0; i < b.Height; i++ {
|
|
// iterate over 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 {
|
|
return errors.Wrap(err, processName, "skipping bits")
|
|
}
|
|
|
|
if err = w.WriteByte(byte(bits) << uint(8-endbits)); err != nil {
|
|
return errors.Wrap(err, processName, "last byte")
|
|
}
|
|
}
|
|
b.Data = w.Data()
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) clearAll() error {
|
|
return b.RasterOperation(0, 0, b.Width, b.Height, PixClr, nil, 0, 0)
|
|
}
|
|
|
|
// clipRectangle clips the bitmap 'b' with the bounds provided by the 'box' argument.
|
|
// The optional 'clipped' image.Rectangle argument would be changed for the actual box of clipped bitmap.
|
|
// The function returns the clipped bitmap. If the 'box' doesn't intersect with the 'b' bitmap the function returns nil bitmap.
|
|
func (b *Bitmap) clipRectangle(box, boxClipped *image.Rectangle) (clipped *Bitmap, err error) {
|
|
const processName = "clipRectangle"
|
|
if box == nil {
|
|
return nil, errors.Error(processName, "provided nil 'box'")
|
|
}
|
|
w, h := b.Width, b.Height
|
|
clippedTemp, err := ClipBoxToRectangle(box, w, h)
|
|
if err != nil {
|
|
common.Log.Warning("'box' doesn't overlap bitmap 'b': %v", err)
|
|
return nil, nil
|
|
}
|
|
bx, by := clippedTemp.Min.X, clippedTemp.Min.Y
|
|
bw, bh := clippedTemp.Max.X-clippedTemp.Min.X, clippedTemp.Max.Y-clippedTemp.Min.Y
|
|
|
|
clipped = New(bw, bh)
|
|
|
|
clipped.Text = b.Text
|
|
if err = clipped.RasterOperation(0, 0, bw, bh, PixSrc, b, bx, by); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
if boxClipped != nil {
|
|
*boxClipped = *clippedTemp
|
|
}
|
|
return clipped, nil
|
|
}
|
|
|
|
func (b *Bitmap) countPixels() int {
|
|
var (
|
|
sum int
|
|
endmask uint8
|
|
bt byte
|
|
btIndex int
|
|
)
|
|
fullBytes := b.RowStride
|
|
padding := uint(b.Width & 0x07)
|
|
if padding != 0 {
|
|
endmask = uint8((0xff << (8 - padding)) & 0xff)
|
|
fullBytes--
|
|
}
|
|
|
|
for y := 0; y < b.Height; y++ {
|
|
for btIndex = 0; btIndex < fullBytes; btIndex++ {
|
|
bt = b.Data[y*b.RowStride+btIndex]
|
|
sum += int(tab8[bt])
|
|
}
|
|
if padding != 0 {
|
|
sum += int(tab8[b.Data[y*b.RowStride+btIndex]&endmask])
|
|
}
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func (b *Bitmap) createTemplate() *Bitmap {
|
|
return &Bitmap{
|
|
Width: b.Width,
|
|
Height: b.Height,
|
|
RowStride: b.RowStride,
|
|
Color: b.Color,
|
|
Text: b.Text,
|
|
BitmapNumber: b.BitmapNumber,
|
|
Special: b.Special,
|
|
Data: make([]byte, len(b.Data)),
|
|
}
|
|
}
|
|
|
|
func (b *Bitmap) equivalent(s *Bitmap) bool {
|
|
if b == s {
|
|
return true
|
|
}
|
|
|
|
if !b.SizesEqual(s) {
|
|
return false
|
|
}
|
|
|
|
// get the XOR of the bitmaps 'b' and 's'
|
|
xor := combineBitmap(b, s, CmbOpXor)
|
|
|
|
// count the pixels for the first bitmap.
|
|
pixelCount := b.countPixels()
|
|
thresh := int(0.25 * float32(pixelCount))
|
|
|
|
// if the symbols are significantly different end fast.
|
|
if xor.thresholdPixelSum(thresh) {
|
|
return false
|
|
}
|
|
|
|
var (
|
|
parsedPixCounts [9][9]int
|
|
horizontalParsedPix [18][9]int
|
|
verticalParsedPix [9][18]int
|
|
horizontalModuleCtr int
|
|
verticalModuleCtr int
|
|
)
|
|
divider := 9
|
|
verticalPart := b.Height / divider
|
|
horizontalPart := b.Width / divider
|
|
vp, hp := verticalPart/2, horizontalPart/2
|
|
if verticalPart < horizontalPart {
|
|
vp = horizontalPart / 2
|
|
hp = verticalPart / 2
|
|
}
|
|
|
|
pointThresh := float64(vp) * float64(hp) * math.Pi
|
|
vLineThresh := int(float64(verticalPart*horizontalPart/2) * 0.9)
|
|
hLineThresh := int(float64(horizontalPart*verticalPart/2) * 0.9)
|
|
|
|
for horizontalPosition := 0; horizontalPosition < divider; horizontalPosition++ {
|
|
horizontalStart := horizontalPart*horizontalPosition + horizontalModuleCtr
|
|
var horizontalEnd int
|
|
|
|
if horizontalPosition == divider-1 {
|
|
horizontalModuleCtr = 0
|
|
horizontalEnd = b.Width
|
|
} else {
|
|
horizontalEnd = horizontalStart + horizontalPart
|
|
if ((b.Width - horizontalModuleCtr) % divider) > 0 {
|
|
horizontalModuleCtr++
|
|
horizontalEnd++
|
|
}
|
|
}
|
|
|
|
for verticalPosition := 0; verticalPosition < divider; verticalPosition++ {
|
|
verticalStart := verticalPart*verticalPosition + verticalModuleCtr
|
|
var verticalEnd int
|
|
|
|
if verticalPosition == divider-1 {
|
|
verticalModuleCtr = 0
|
|
verticalEnd = b.Height
|
|
} else {
|
|
verticalEnd = verticalStart + verticalPart
|
|
if (b.Height-verticalModuleCtr)%divider > 0 {
|
|
verticalModuleCtr++
|
|
verticalEnd++
|
|
}
|
|
}
|
|
var leftCount, rightCount, downCount, upCount int
|
|
|
|
horizontalCenter := (horizontalStart + horizontalEnd) / 2
|
|
verticalCenter := (verticalStart + verticalEnd) / 2
|
|
|
|
for h := horizontalStart; h < horizontalEnd; h++ {
|
|
for v := verticalStart; v < verticalEnd; v++ {
|
|
if xor.GetPixel(h, v) {
|
|
if h < horizontalCenter {
|
|
leftCount++
|
|
} else {
|
|
rightCount++
|
|
}
|
|
|
|
if v < verticalCenter {
|
|
upCount++
|
|
} else {
|
|
downCount++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parsedPixCounts[horizontalPosition][verticalPosition] = leftCount + rightCount
|
|
|
|
horizontalParsedPix[horizontalPosition*2][verticalPosition] = leftCount
|
|
horizontalParsedPix[horizontalPosition*2+1][verticalPosition] = rightCount
|
|
|
|
verticalParsedPix[horizontalPosition][verticalPosition*2] = upCount
|
|
verticalParsedPix[horizontalPosition][verticalPosition*2+1] = downCount
|
|
}
|
|
}
|
|
|
|
for i := 0; i < divider*2-1; i++ {
|
|
for j := 0; j < (divider - 1); j++ {
|
|
var horizontalSum int
|
|
|
|
for x := 0; x < 2; x++ {
|
|
for y := 0; y < 2; y++ {
|
|
horizontalSum += horizontalParsedPix[i+x][j+y]
|
|
}
|
|
}
|
|
|
|
if horizontalSum > hLineThresh {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := 0; i < (divider - 1); i++ {
|
|
for j := 0; j < ((divider * 2) - 1); j++ {
|
|
var verticalSum int
|
|
|
|
for x := 0; x < 2; x++ {
|
|
for y := 0; y < 2; y++ {
|
|
verticalSum += verticalParsedPix[i+x][j+y]
|
|
}
|
|
}
|
|
|
|
if verticalSum > vLineThresh {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
// check for cross lines
|
|
|
|
for i := 0; i < (divider - 2); i++ {
|
|
for j := 0; j < (divider - 2); j++ {
|
|
var leftCross, rightCross int
|
|
|
|
for x := 0; x < 3; x++ {
|
|
for y := 0; y < 3; y++ {
|
|
if x == y {
|
|
leftCross += parsedPixCounts[i+x][j+y]
|
|
}
|
|
|
|
if (2 - x) == y {
|
|
rightCross += parsedPixCounts[i+x][j+y]
|
|
}
|
|
}
|
|
}
|
|
|
|
if leftCross > hLineThresh || rightCross > hLineThresh {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := 0; i < (divider - 1); i++ {
|
|
for j := 0; j < (divider - 1); j++ {
|
|
var sum int
|
|
|
|
for x := 0; x < 2; x++ {
|
|
for y := 0; y < 2; y++ {
|
|
sum += parsedPixCounts[i+x][j+y]
|
|
}
|
|
}
|
|
|
|
if float64(sum) > pointThresh {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (b *Bitmap) inverseData() {
|
|
if err := b.RasterOperation(0, 0, b.Width, b.Height, PixNotDst, nil, 0, 0); err != nil {
|
|
common.Log.Debug("Inverse data failed: '%v'", err)
|
|
}
|
|
// flip the color interpretation
|
|
if b.Color == Chocolate {
|
|
b.Color = Vanilla
|
|
} else {
|
|
b.Color = Chocolate
|
|
}
|
|
}
|
|
|
|
// nextOnPixel
|
|
func (b *Bitmap) nextOnPixel(xStart, yStart int) (pt image.Point, ok bool, err error) {
|
|
const processName = "nextOnPixel"
|
|
pt, ok, err = b.nextOnPixelLow(b.Width, b.Height, b.RowStride, xStart, yStart)
|
|
if err != nil {
|
|
return pt, false, errors.Wrap(err, processName, "")
|
|
}
|
|
return pt, ok, nil
|
|
}
|
|
|
|
func (b *Bitmap) nextOnPixelLow(w, h, wpl, xStart, yStart int) (pt image.Point, ok bool, err error) {
|
|
const processName = "Bitmap.nextOnPixelLow"
|
|
var (
|
|
x int
|
|
bt byte
|
|
)
|
|
line := yStart * wpl
|
|
index := line + (xStart / 8)
|
|
|
|
if bt, err = b.GetByte(index); err != nil {
|
|
return pt, false, errors.Wrap(err, processName, "xStart and yStart out of range")
|
|
}
|
|
|
|
// if the 'bt' byte is different than 0, it must contain 'ON' pixel.
|
|
// search the byte place.
|
|
if bt != 0 {
|
|
xEnd := xStart - (xStart % 8) + 7
|
|
for x = xStart; x <= xEnd && x < w; x++ {
|
|
if b.GetPixel(x, yStart) {
|
|
pt.X = x
|
|
pt.Y = yStart
|
|
return pt, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// continue with the rest of the line.
|
|
startByte := (xStart / 8) + 1
|
|
x = 8 * startByte
|
|
var i int
|
|
for index = line + startByte; x < w; index, x = index+1, x+8 {
|
|
if bt, err = b.GetByte(index); err != nil {
|
|
return pt, false, errors.Wrap(err, processName, "rest of the line byte")
|
|
}
|
|
if bt == 0 {
|
|
continue
|
|
}
|
|
for i = 0; i < 8 && x < w; i, x = i+1, x+1 {
|
|
if b.GetPixel(x, yStart) {
|
|
pt.X = x
|
|
pt.Y = yStart
|
|
return pt, true, nil
|
|
}
|
|
}
|
|
}
|
|
// search till the end of the bitmap data.
|
|
for y := yStart + 1; y < h; y++ {
|
|
line = y * wpl
|
|
for index, x = line, 0; x < w; index, x = index+1, x+8 {
|
|
if bt, err = b.GetByte(index); err != nil {
|
|
return pt, false, errors.Wrap(err, processName, "following lines")
|
|
}
|
|
|
|
// if the byte is different than 0x00 it must contain at least one 'ON' bit.
|
|
if bt == 0 {
|
|
continue
|
|
}
|
|
for i = 0; i < 8 && x < w; i, x = i+1, x+1 {
|
|
if b.GetPixel(x, y) {
|
|
pt.X = x
|
|
pt.Y = y
|
|
return pt, true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return pt, false, nil
|
|
}
|
|
|
|
func (b *Bitmap) removeBorderGeneral(left, right, top, bot int) (*Bitmap, error) {
|
|
const processName = "removeBorderGeneral"
|
|
if left < 0 || right < 0 || top < 0 || bot < 0 {
|
|
return nil, errors.Error(processName, "negative broder remove values")
|
|
}
|
|
|
|
ws, hs := b.Width, b.Height
|
|
wd := ws - left - right
|
|
hd := hs - top - bot
|
|
|
|
if wd <= 0 {
|
|
return nil, errors.Errorf(processName, "width: %d must be > 0", wd)
|
|
}
|
|
|
|
if hd <= 0 {
|
|
return nil, errors.Errorf(processName, "height: %d must be > 0", hd)
|
|
}
|
|
|
|
bm := New(wd, hd)
|
|
bm.Color = b.Color
|
|
|
|
err := bm.RasterOperation(0, 0, wd, hd, PixSrc, b, left, top)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return bm, nil
|
|
}
|
|
|
|
func (b *Bitmap) resizeImageData(s *Bitmap) error {
|
|
if s == nil {
|
|
return errors.Error("resizeImageData", "src is not defined")
|
|
}
|
|
if b.SizesEqual(s) {
|
|
return nil
|
|
}
|
|
|
|
b.Data = make([]byte, len(s.Data))
|
|
b.Width = s.Width
|
|
b.Height = s.Height
|
|
b.RowStride = s.RowStride
|
|
// NOTE: if resolution included, set also resolution
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) setAll() error {
|
|
err := rasterOperation(b, 0, 0, b.Width, b.Height, PixSet, nil, 0, 0)
|
|
if err != nil {
|
|
return errors.Wrap(err, "setAll", "")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) setBit(index int) {
|
|
b.Data[(index >> 3)] |= 0x80 >> uint(index&7)
|
|
}
|
|
|
|
func (b *Bitmap) setPadBits(val int) {
|
|
endbits := 8 - b.Width%8
|
|
if endbits == 8 {
|
|
// no partial words
|
|
return
|
|
}
|
|
fullBytes := b.Width / 8
|
|
mask := rmaskByte[endbits]
|
|
if val == 0 {
|
|
mask ^= mask
|
|
}
|
|
|
|
var bIndex int
|
|
for i := 0; i < b.Height; i++ {
|
|
bIndex = i*b.RowStride + fullBytes
|
|
if val == 0 {
|
|
b.Data[bIndex] &= mask
|
|
} else {
|
|
b.Data[bIndex] |= mask
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Bitmap) setTwoBytes(index int, tb uint16) error {
|
|
if index+1 > len(b.Data)-1 {
|
|
return errors.Errorf("setTwoBytes", "index: '%d' out of range", index)
|
|
}
|
|
b.Data[index] = byte((tb & 0xff00) >> 8)
|
|
b.Data[index+1] = byte(tb & 0xff)
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) setFourBytes(index int, fb uint32) error {
|
|
if index+3 > len(b.Data)-1 {
|
|
return errors.Errorf("setFourBytes", "index: '%d' out of range", index)
|
|
}
|
|
b.Data[index] = byte((fb & 0xff000000) >> 24)
|
|
b.Data[index+1] = byte((fb & 0xff0000) >> 16)
|
|
b.Data[index+2] = byte((fb & 0xff00) >> 8)
|
|
b.Data[index+3] = byte(fb & 0xff)
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) setEightBytes(index int, eb uint64) error {
|
|
fullBytesNumber := b.RowStride - (index % b.RowStride)
|
|
if b.RowStride != b.Width>>3 {
|
|
fullBytesNumber--
|
|
}
|
|
if fullBytesNumber >= 8 {
|
|
return b.setEightFullBytes(index, eb)
|
|
}
|
|
return b.setEightPartlyBytes(index, fullBytesNumber, eb)
|
|
}
|
|
|
|
func (b *Bitmap) setEightPartlyBytes(index, fullBytesNumber int, eb uint64) (err error) {
|
|
var (
|
|
temp byte
|
|
shift int
|
|
)
|
|
const processName = "setEightPartlyBytes"
|
|
for i := 1; i <= fullBytesNumber; i++ {
|
|
// eb >> 7 * 8 = 56
|
|
// 4 + 7 - 7
|
|
shift = 64 - i*8
|
|
temp = byte(eb >> uint(shift) & 0xff)
|
|
common.Log.Trace("temp: %08b, index: %d, idx: %d, fullBytesNumber: %d, shift: %d", temp, index, index+i-1, fullBytesNumber, shift)
|
|
if err = b.SetByte(index+i-1, temp); err != nil {
|
|
return errors.Wrap(err, processName, "fullByte")
|
|
}
|
|
}
|
|
padding := b.RowStride*8 - b.Width
|
|
if padding == 0 {
|
|
return nil
|
|
}
|
|
shift -= 8
|
|
temp = byte(eb>>uint(shift)&0xff) << uint(padding)
|
|
if err = b.SetByte(index+fullBytesNumber, temp); err != nil {
|
|
return errors.Wrap(err, processName, "padded")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bitmap) setEightFullBytes(index int, eb uint64) error {
|
|
if index+7 > len(b.Data)-1 {
|
|
return errors.Error("setEightBytes", "index out of range")
|
|
}
|
|
|
|
b.Data[index] = byte((eb & 0xff00000000000000) >> 56)
|
|
b.Data[index+1] = byte((eb & 0xff000000000000) >> 48)
|
|
b.Data[index+2] = byte((eb & 0xff0000000000) >> 40)
|
|
b.Data[index+3] = byte((eb & 0xff00000000) >> 32)
|
|
b.Data[index+4] = byte((eb & 0xff000000) >> 24)
|
|
b.Data[index+5] = byte((eb & 0xff0000) >> 16)
|
|
b.Data[index+6] = byte((eb & 0xff00) >> 8)
|
|
b.Data[index+7] = byte(eb & 0xff)
|
|
return nil
|
|
}
|
|
|
|
// thresholdPixelSum this function sums the '1' pixels and returns immediately
|
|
// if the count goes above threshold.
|
|
// Returns false if the sum is greater then 'thresh' threshold.
|
|
func (b *Bitmap) thresholdPixelSum(thresh int) bool {
|
|
var (
|
|
sum int
|
|
endmask uint8
|
|
bt byte
|
|
btIndex int
|
|
)
|
|
fullBytes := b.RowStride
|
|
padding := uint(b.Width & 0x07)
|
|
if padding != 0 {
|
|
endmask = uint8((0xff << (8 - padding)) & 0xff)
|
|
fullBytes--
|
|
}
|
|
|
|
for y := 0; y < b.Height; y++ {
|
|
for btIndex = 0; btIndex < fullBytes; btIndex++ {
|
|
bt = b.Data[y*b.RowStride+btIndex]
|
|
sum += int(tab8[bt])
|
|
}
|
|
|
|
if padding != 0 {
|
|
bt = b.Data[y*b.RowStride+btIndex] & endmask
|
|
sum += int(tab8[bt])
|
|
}
|
|
|
|
if sum > thresh {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func copyBitmap(d, s *Bitmap) (*Bitmap, error) {
|
|
if s == nil {
|
|
return nil, errors.Error("copyBitmap", "source not defined")
|
|
}
|
|
if s == d {
|
|
return d, nil
|
|
}
|
|
|
|
if d == nil {
|
|
// create new bitmap if 'd' is nil.
|
|
d = s.createTemplate()
|
|
copy(d.Data, s.Data)
|
|
return d, nil
|
|
}
|
|
err := d.resizeImageData(s)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "copyBitmap", "")
|
|
}
|
|
d.Text = s.Text
|
|
copy(d.Data, s.Data)
|
|
return d, nil
|
|
}
|
|
|
|
func xor(d, b1, b2 *Bitmap) (*Bitmap, error) {
|
|
const processName = "bitmap.xor"
|
|
|
|
if b1 == nil {
|
|
return nil, errors.Error(processName, "'b1' is nil")
|
|
}
|
|
if b2 == nil {
|
|
return nil, errors.Error(processName, "'b2' is nil")
|
|
}
|
|
|
|
if d == b2 {
|
|
return nil, errors.Error(processName, "'d' == 'b2'")
|
|
}
|
|
|
|
if !b1.SizesEqual(b2) {
|
|
common.Log.Debug("%s - Bitmap 'b1' is not equal size with 'b2'", processName)
|
|
}
|
|
var err error
|
|
if d, err = copyBitmap(d, b1); err != nil {
|
|
return nil, errors.Wrap(err, processName, "can't create 'd'")
|
|
}
|
|
|
|
if err = d.RasterOperation(0, 0, d.Width, d.Height, PixSrcXorDst, b2, 0, 0); err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func abs(input int) int {
|
|
if input < 0 {
|
|
return -input
|
|
}
|
|
return input
|
|
}
|
|
|
|
func max(x, y int) int {
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
func min(x, y int) int {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
func subtract(d, s1, s2 *Bitmap) (*Bitmap, error) {
|
|
const processName = "subtract"
|
|
if s1 == nil {
|
|
return nil, errors.Error(processName, "'s1' is nil")
|
|
}
|
|
if s2 == nil {
|
|
return nil, errors.Error(processName, "'s2' is nil")
|
|
}
|
|
|
|
var err error
|
|
switch {
|
|
case d == s1:
|
|
if err = d.RasterOperation(0, 0, s1.Width, s1.Height, PixNotSrcAndDst, s2, 0, 0); err != nil {
|
|
return nil, errors.Wrap(err, processName, "d == s1")
|
|
}
|
|
case d == s2:
|
|
if err = d.RasterOperation(0, 0, s1.Width, s1.Height, PixNotSrcAndDst, s1, 0, 0); err != nil {
|
|
return nil, errors.Wrap(err, processName, "d == s2")
|
|
}
|
|
default:
|
|
d, err = copyBitmap(d, s1)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, processName, "")
|
|
}
|
|
if err = d.RasterOperation(0, 0, s1.Width, s1.Height, PixNotSrcAndDst, s2, 0, 0); err != nil {
|
|
return nil, errors.Wrap(err, processName, "default")
|
|
}
|
|
}
|
|
return d, nil
|
|
}
|