// 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 }