Files
mesh-catalog/modules/brother/cmd/brother/capture.go
T

183 lines
6.2 KiB
Go

package main
import (
"bytes"
"context"
"fmt"
"image/jpeg"
"math"
"time"
)
// The device process's scan (novox/hq ADR 0468 clause 4): the glass scanned over eSCL, the document cut out of
// the platen's white and built into a one-page PDF, or the feeder's sheets (both sides on request) into one multi-page PDF. Keeping it in the bucket and saying scan.added are the
// device process's (deviceproc.go); filing it in the scan folder is the tool's (scan.go).
func mm(pixels, dpi int) float64 {
return math.Round(float64(pixels)/float64(dpi)*25.4*10) / 10
}
// Capture is a scanned page or pages as a PDF, with what the event says of it. Width, height and resolution are
// the first page's.
type Capture struct {
PDF []byte
SHA256 string
WidthMM float64
HeightMM float64
Resolution int
Cropped bool
Pages int
At time.Time
}
// ScanRequest is what one scan asks beyond the configuration: the feeder instead of the glass, and both sides.
type ScanRequest struct {
Feeder bool
Duplex bool
// Apart cuts each of several documents lying apart on the glass out as its own PDF (issue 1528); without it
// the glass gives one document, as it always did.
Apart bool
}
// ParseSource reads the scanner.scan arguments source (glass, the default, or feeder) and duplex. Duplex is for the
// feeder: the glass holds one sheet.
func ParseSource(source string, duplex bool) (ScanRequest, error) {
return ParseScan(source, duplex, false)
}
// ParseScan is ParseSource with apart: several documents on the glass, each its own file; a feeder scan is of
// sheets and has none lying apart.
func ParseScan(source string, duplex, apart bool) (ScanRequest, error) {
req, err := parseSource(source, duplex)
if err == nil && apart && req.Feeder {
return ScanRequest{}, fmt.Errorf("apart is for the glass: the feeder takes each sheet as its own page already")
}
req.Apart = apart
return req, err
}
func parseSource(source string, duplex bool) (ScanRequest, error) {
switch source {
case "", "glass":
if duplex {
return ScanRequest{}, fmt.Errorf("duplex is for the feeder: the glass holds one page (set source to feeder)")
}
return ScanRequest{}, nil
case "feeder":
return ScanRequest{Feeder: true, Duplex: duplex}, nil
}
return ScanRequest{}, fmt.Errorf("source %q is not glass or feeder", source)
}
// Capture scans the glass or the feeder and builds the PDFs: one for a feeder job (every page of it), and for the
// glass one for each separate document lying on the platen when crop is set (issue 1528), in reading order. A glass
// page is cropped when the settings say so; a feeder page is a whole sheet and never cropped. Nothing is kept and
// nothing is said here; an error says why the scanner gave no page. The resolution is the image's own, never the
// one asked (1440/AC2).
func (s *Service) Capture(ctx context.Context, req ScanRequest) ([]Capture, error) {
at := s.Now()
scans, err := NewClient(s.Settings.Scanner).ScanPages(ctx, ScanOptions{Resolution: s.Settings.Resolution,
ColorMode: s.Settings.ColorMode, Feeder: req.Feeder, Duplex: req.Duplex})
if err != nil {
return nil, err
}
if !req.Feeder {
dpi, err := Density(scans[0])
if err != nil {
return nil, fmt.Errorf("page 1: %w", err)
}
cuts, err := Crop(scans[0], s.Settings.Crop)
one := []Cropped{cuts}
if req.Apart {
one, err = CropAll(scans[0], true, dpi)
}
if err != nil {
return nil, err
}
out := make([]Capture, len(one))
for i, cut := range one {
if out[i], err = captureOf([]PDFPage{{JPEG: cut.JPEG, DPI: dpi}}, cut, dpi, at); err != nil {
return nil, err
}
}
return out, nil
}
pages := make([]PDFPage, len(scans))
var first Cropped
var firstDPI int
for i, scan := range scans {
dpi, err := Density(scan)
if err != nil {
return nil, fmt.Errorf("page %d: %w", i+1, err)
}
cfg, err := jpeg.DecodeConfig(bytes.NewReader(scan))
if err != nil {
return nil, fmt.Errorf("page %d: the scanned image cannot be read: %w", i+1, err)
}
pages[i] = PDFPage{JPEG: scan, DPI: dpi}
if i == 0 {
first, firstDPI = Cropped{JPEG: scan, Width: cfg.Width, Height: cfg.Height}, dpi
}
}
c, err := captureOf(pages, first, firstDPI, at)
if err != nil {
return nil, err
}
return []Capture{c}, nil
}
// captureOf is the PDF of the pages, with what the event says of it from the first page.
func captureOf(pages []PDFPage, first Cropped, firstDPI int, at time.Time) (Capture, error) {
pdf, err := PDFPages(pages)
if err != nil {
return Capture{}, err
}
return Capture{PDF: pdf, SHA256: sha256Hex(pdf), WidthMM: mm(first.Width, firstDPI), HeightMM: mm(first.Height, firstDPI),
Resolution: firstDPI, Cropped: first.Cropped, Pages: len(pages), At: at}, nil
}
// Body is the event scan.added's body (ADR 0464 §5 with the object key of ADR 0468 clause 2): never the image,
// and no path, since filing in a folder is the tool's, after the event.
func (c Capture) Body(node, colorMode string) map[string]any {
pages := max(c.Pages, 1)
return map[string]any{
"object": c.SHA256 + ".pdf",
"node": node,
"bytes": len(c.PDF),
"sha256": c.SHA256,
"width-mm": c.WidthMM,
"height-mm": c.HeightMM,
"resolution": c.Resolution,
"color-mode": colorMode,
"cropped": c.Cropped,
"pages": pages,
"scanned-at": c.At.UTC().Format(time.RFC3339),
}
}
// GlassImage is a page scanned from the glass.
type GlassImage struct {
JPEG []byte
DPI, Width, Height int
Cropped bool
}
// GlassPage scans one page from the glass for a document of several pages: cropped to its document when the settings
// say so (the first document, never split in several), with the resolution the scanner gave.
func (s *Service) GlassPage(ctx context.Context) (page GlassImage, err error) {
scans, err := NewClient(s.Settings.Scanner).ScanPages(ctx, ScanOptions{Resolution: s.Settings.Resolution, ColorMode: s.Settings.ColorMode})
if err != nil {
return page, err
}
dpi, err := Density(scans[0])
if err != nil {
return page, err
}
cut, err := Crop(scans[0], s.Settings.Crop)
if err != nil {
return page, err
}
page.JPEG, page.DPI, page.Width, page.Height, page.Cropped = cut.JPEG, dpi, cut.Width, cut.Height, cut.Cropped
return page, nil
}