183 lines
6.2 KiB
Go
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
|
|
}
|