Files
nexarch/dms/internal/batchsplit/batchsplit.go
T
patrickandClaude Sonnet 5 75fdf9fb83
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
INT-06: scanner-anbindung über hot-folder
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 19:47:41 +02:00

135 lines
3.5 KiB
Go

// Package batchsplit teilt einen Scan-Stapel (Mehrseiten-PDF) an Trennseiten
// in Einzeldokumente (INT-06). Die Erkennung der Trennseite steckt hinter
// Detector, damit Leerseiten- und Barcode-Erkennung (INT-10) ohne Umbau
// des Aufteilens angeschlossen werden koennen.
package batchsplit
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)
const DefaultMarker = "NEXARCH-TRENNSEITE"
var ErrOnlySeparators = errors.New("batchsplit: stapel enthaelt nur trennseiten")
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$`)
// 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) {
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 := model.NewDefaultConfiguration()
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
}
separator := make([]bool, 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)
}
separator[n] = d.IsSeparator(b)
}
ranges := runs(separator, pages)
anySeparator := false
for i := 1; i <= pages; i++ {
anySeparator = anySeparator || separator[i]
}
if !anySeparator {
return [][]byte{content}, nil
}
if len(ranges) == 0 {
return nil, ErrOnlySeparators
}
out := make([][]byte, 0, len(ranges))
for i, r := range ranges {
dst := filepath.Join(dir, fmt.Sprintf("out-%d.pdf", i))
if err := api.TrimFile(in, dst, []string{fmt.Sprintf("%d-%d", r[0], r[1])}, conf); err != nil {
return nil, fmt.Errorf("batchsplit: teildokument %d-%d erzeugen: %w", r[0], r[1], err)
}
b, err := os.ReadFile(dst)
if err != nil {
return nil, err
}
out = append(out, b)
}
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
}