package batchsplit import ( "fmt" "image" "image/png" "os" "strings" "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/oned" "github.com/makiuchi-d/gozxing/qrcode" ) // Class ist das Ergebnis der Seitenklassifikation; Value traegt bei einer // Barcode-Trennseite den Barcode-Wert ohne Praefix. type Class struct { Separator bool Value string } // Page ist eine Stapelseite; das Rasterbild wird erst beim Zugriff geladen. type Page struct { Number int Content []byte path string img image.Image loaded bool } func newPage(n int, content []byte, imagePath string) *Page { return &Page{Number: n, Content: content, path: imagePath} } func (p *Page) Image() (image.Image, error) { if p.loaded { return p.img, nil } if p.path == "" { return nil, fmt.Errorf("seite %d ist nicht gerastert", p.Number) } f, err := os.Open(p.path) if err != nil { return nil, err } defer func() { _ = f.Close() }() img, err := png.Decode(f) if err != nil { return nil, fmt.Errorf("seite %d dekodieren: %w", p.Number, err) } p.img, p.loaded = img, true return img, nil } type Classifier interface { NeedsImage() bool Classify(p *Page) (Class, error) } // DetectorClassifier fuehrt einen inhaltsbasierten Detector als Classifier. type DetectorClassifier struct{ Detector } func (DetectorClassifier) NeedsImage() bool { return false } func (d DetectorClassifier) Classify(p *Page) (Class, error) { return Class{Separator: d.IsSeparator(p.Content)}, nil } // Chain fragt die Classifier der Reihe nach; die erste Trennseite gewinnt, // den Wert liefert der erste nicht leere. type Chain []Classifier func (c Chain) NeedsImage() bool { for _, x := range c { if x.NeedsImage() { return true } } return false } func (c Chain) Classify(p *Page) (Class, error) { for _, x := range c { cls, err := x.Classify(p) if err != nil { return Class{}, err } if cls.Separator { return cls, nil } } return Class{}, nil } const ( DefaultBlankInk = 0.0005 inkThreshold = 160 inkMarginRatio = 0.05 ) // BlankClassifier erkennt eine Leerseite am Tintenanteil des gerasterten // Bildes; ein Randstreifen bleibt unberuecksichtigt (Scannerschatten). type BlankClassifier struct{ MaxInk float64 } func (BlankClassifier) NeedsImage() bool { return true } func (b BlankClassifier) Classify(p *Page) (Class, error) { img, err := p.Image() if err != nil { return Class{}, err } max := b.MaxInk if max <= 0 { max = DefaultBlankInk } return Class{Separator: inkRatio(img) <= max}, nil } func inkRatio(img image.Image) float64 { bounds := img.Bounds() mx := int(float64(bounds.Dx()) * inkMarginRatio) my := int(float64(bounds.Dy()) * inkMarginRatio) x0, x1 := bounds.Min.X+mx, bounds.Max.X-mx y0, y1 := bounds.Min.Y+my, bounds.Max.Y-my total := (x1 - x0) * (y1 - y0) if total <= 0 { return 0 } ink := 0 if g, ok := img.(*image.Gray); ok { for y := y0; y < y1; y++ { row := g.Pix[(y-g.Rect.Min.Y)*g.Stride+(x0-g.Rect.Min.X) : (y-g.Rect.Min.Y)*g.Stride+(x1-g.Rect.Min.X)] for _, v := range row { if v < inkThreshold { ink++ } } } return float64(ink) / float64(total) } for y := y0; y < y1; y++ { for x := x0; x < x1; x++ { r, g, b, _ := img.At(x, y).RGBA() if (r+g+b)/3>>8 < inkThreshold { ink++ } } } return float64(ink) / float64(total) } // BarcodeClassifier erkennt eine Trennseite an einem Code-128- oder QR-Code, // dessen Text mit Prefix beginnt. Ohne Praefix wuerde jede Seite mit einem // Barcode (z. B. Rechnungsnummer) den Stapel teilen. type BarcodeClassifier struct{ Prefix string } func (BarcodeClassifier) NeedsImage() bool { return true } func (b BarcodeClassifier) Classify(p *Page) (Class, error) { if b.Prefix == "" { return Class{}, fmt.Errorf("barcode-praefix fehlt") } img, err := p.Image() if err != nil { return Class{}, err } for _, text := range decodeBarcodes(img) { if strings.HasPrefix(text, b.Prefix) { return Class{Separator: true, Value: strings.TrimSpace(strings.TrimPrefix(text, b.Prefix))}, nil } } return Class{}, nil } func decodeBarcodes(img image.Image) []string { bmp, err := gozxing.NewBinaryBitmapFromImage(img) if err != nil { return nil } hints := map[gozxing.DecodeHintType]interface{}{gozxing.DecodeHintType_TRY_HARDER: true} var out []string for _, r := range []gozxing.Reader{oned.NewCode128Reader(), qrcode.NewQRCodeReader()} { if res, err := r.Decode(bmp, hints); err == nil { out = append(out, res.GetText()) } } return out }