Files
nexarch/dms/internal/batchsplit/batchsplit.go
T
patrick 9e2664e2b2
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
FDN-19: pdfcpu ohne Konfigurationsverzeichnis, Download-URL mit Routenpraefix, Identitaet im Uebergangsmodus erzwingen
2026-09-21 10:38:15 +02:00

187 lines
4.8 KiB
Go

// Package batchsplit teilt einen Scan-Stapel (Mehrseiten-PDF) an Trennseiten
// in Einzeldokumente (INT-06). Die Erkennung der Trennseite steckt hinter
// Classifier (Textmarker, Leerseite, Barcode; INT-10).
package batchsplit
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"github.com/pdfcpu/pdfcpu/pkg/api"
"gitea.perlbach24.de/scripte/nexarch/dms/internal/pdfcfg"
)
const DefaultMarker = "NEXARCH-TRENNSEITE"
var (
ErrOnlySeparators = errors.New("batchsplit: stapel enthaelt nur trennseiten")
ErrNoRenderer = errors.New("batchsplit: seitenrasterung fuer die trennseitenerkennung nicht verfuegbar")
)
type Detector interface {
IsSeparator(pageContent []byte) bool
}
// MarkerDetector erkennt eine Trennseite an einem Textmarker im Seiteninhalt.
type MarkerDetector struct{ Marker string }
func (d MarkerDetector) IsSeparator(pageContent []byte) bool {
m := d.Marker
if m == "" {
m = DefaultMarker
}
return bytes.Contains(pageContent, []byte("("+m+")"))
}
var pagePattern = regexp.MustCompile(`_Content_page_(\d+)\.txt$`)
// Part ist ein Teildokument; Value ist der Wert der davorliegenden
// Barcode-Trennseite (leer, wenn keine vorhanden).
type Part struct {
Content []byte
Value string
}
// Split liefert die Einzeldokumente in Seitenreihenfolge; Trennseiten
// selbst entfallen. Ohne Trennseite kommt der Eingang unveraendert als
// einziges Element zurueck.
func Split(content []byte, d Detector) ([][]byte, error) {
parts, err := SplitParts(content, DetectorClassifier{d}, nil)
if err != nil {
return nil, err
}
out := make([][]byte, len(parts))
for i, p := range parts {
out[i] = p.Content
}
return out, nil
}
// SplitParts wie Split, mit seitenweiser Klassifikation; r wird nur benoetigt,
// wenn c gerasterte Seiten verlangt (Leerseiten, Barcodes).
func SplitParts(content []byte, c Classifier, r Renderer) ([]Part, error) {
dir, err := os.MkdirTemp("", "batchsplit-*")
if err != nil {
return nil, fmt.Errorf("batchsplit: temp-verzeichnis anlegen: %w", err)
}
defer func() { _ = os.RemoveAll(dir) }()
in := filepath.Join(dir, "in.pdf")
if err := os.WriteFile(in, content, 0o600); err != nil {
return nil, fmt.Errorf("batchsplit: quelldatei schreiben: %w", err)
}
conf := pdfcfg.Default()
pages, err := api.PageCountFile(in)
if err != nil {
return nil, fmt.Errorf("batchsplit: seitenzahl ermitteln: %w", err)
}
contentDir := filepath.Join(dir, "content")
if err := os.Mkdir(contentDir, 0o700); err != nil {
return nil, err
}
if err := api.ExtractContentFile(in, contentDir, nil, conf); err != nil {
return nil, fmt.Errorf("batchsplit: seiteninhalt lesen: %w", err)
}
files, err := filepath.Glob(filepath.Join(contentDir, "*_Content_page_*.txt"))
if err != nil {
return nil, err
}
streams := make([][]byte, pages+1)
for _, f := range files {
m := pagePattern.FindStringSubmatch(f)
if m == nil {
continue
}
n, _ := strconv.Atoi(m[1])
if n < 1 || n > pages {
continue
}
b, err := os.ReadFile(f)
if err != nil {
return nil, fmt.Errorf("batchsplit: seiteninhalt %d lesen: %w", n, err)
}
streams[n] = b
}
var rendered map[int]string
if c.NeedsImage() {
if r == nil {
return nil, ErrNoRenderer
}
rendered, err = r.Render(in, dir)
if err != nil {
return nil, fmt.Errorf("batchsplit: seiten rastern: %w", err)
}
}
separator := make([]bool, pages+1)
values := make([]string, pages+1)
anySeparator := false
for n := 1; n <= pages; n++ {
cls, err := c.Classify(newPage(n, streams[n], rendered[n]))
if err != nil {
return nil, fmt.Errorf("batchsplit: seite %d klassifizieren: %w", n, err)
}
separator[n], values[n] = cls.Separator, cls.Value
anySeparator = anySeparator || cls.Separator
}
ranges := runs(separator, pages)
if !anySeparator {
return []Part{{Content: content}}, nil
}
if len(ranges) == 0 {
return nil, ErrOnlySeparators
}
out := make([]Part, 0, len(ranges))
prevEnd := 0
for i, rg := range ranges {
dst := filepath.Join(dir, fmt.Sprintf("out-%d.pdf", i))
if err := api.TrimFile(in, dst, []string{fmt.Sprintf("%d-%d", rg[0], rg[1])}, conf); err != nil {
return nil, fmt.Errorf("batchsplit: teildokument %d-%d erzeugen: %w", rg[0], rg[1], err)
}
b, err := os.ReadFile(dst)
if err != nil {
return nil, err
}
value := ""
for p := rg[0] - 1; p > prevEnd; p-- {
if values[p] != "" {
value = values[p]
break
}
}
out = append(out, Part{Content: b, Value: value})
prevEnd = rg[1]
}
return out, nil
}
func runs(separator []bool, pages int) [][2]int {
var out [][2]int
start := 0
for p := 1; p <= pages; p++ {
if separator[p] {
if start != 0 {
out = append(out, [2]int{start, p - 1})
start = 0
}
continue
}
if start == 0 {
start = p
}
}
if start != 0 {
out = append(out, [2]int{start, pages})
}
return out
}