issue 1440: the scan module's eSCL client, settings and image density
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// eSCL (the AirScan protocol) over HTTP: POST <base>/ScanJobs answers 201 and the job's Location, GET
|
||||
// <Location>/NextDocument answers the image, and a second NextDocument answers 404 once the job is done.
|
||||
|
||||
// maxImage bounds what a scanner may send: a 600 dpi A4 JPEG is a few tens of megabytes.
|
||||
const maxImage = 256 << 20
|
||||
|
||||
// Client speaks eSCL to one scanner.
|
||||
type Client struct {
|
||||
Base string
|
||||
HTTP *http.Client
|
||||
}
|
||||
|
||||
// NewClient is a client for the eSCL base URL, with a timeout that outlasts a slow scan.
|
||||
func NewClient(base string) *Client {
|
||||
return &Client{Base: strings.TrimRight(base, "/"), HTTP: &http.Client{Timeout: 3 * time.Minute}}
|
||||
}
|
||||
|
||||
// Capabilities are the platen's largest scan region, in the 300ths of an inch the region is asked in.
|
||||
type Capabilities struct {
|
||||
MaxWidth, MaxHeight int
|
||||
}
|
||||
|
||||
type capabilitiesXML struct {
|
||||
MaxWidth int `xml:"Platen>PlatenInputCaps>MaxWidth"`
|
||||
MaxHeight int `xml:"Platen>PlatenInputCaps>MaxHeight"`
|
||||
}
|
||||
|
||||
// Status is what /ScannerStatus says.
|
||||
type Status struct {
|
||||
State string `xml:"State" json:"state"`
|
||||
AdfState string `xml:"AdfState" json:"adf-state"`
|
||||
}
|
||||
|
||||
func (c *Client) get(ctx context.Context, rawURL string) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.HTTP.Do(req)
|
||||
}
|
||||
|
||||
func readBody(resp *http.Response, limit int64) ([]byte, error) {
|
||||
defer resp.Body.Close()
|
||||
return io.ReadAll(io.LimitReader(resp.Body, limit))
|
||||
}
|
||||
|
||||
// Capabilities asks the scanner how large its glass is.
|
||||
func (c *Client) Capabilities(ctx context.Context) (Capabilities, error) {
|
||||
resp, err := c.get(ctx, c.Base+"/ScannerCapabilities")
|
||||
if err != nil {
|
||||
return Capabilities{}, fmt.Errorf("the scanner does not answer: %w", err)
|
||||
}
|
||||
body, err := readBody(resp, 1<<20)
|
||||
if err != nil {
|
||||
return Capabilities{}, fmt.Errorf("the scanner's capabilities cannot be read: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return Capabilities{}, fmt.Errorf("the scanner refused its capabilities: %s", resp.Status)
|
||||
}
|
||||
var x capabilitiesXML
|
||||
if err := xml.Unmarshal(body, &x); err != nil {
|
||||
return Capabilities{}, fmt.Errorf("the scanner's capabilities are not XML: %w", err)
|
||||
}
|
||||
if x.MaxWidth <= 0 || x.MaxHeight <= 0 {
|
||||
return Capabilities{}, fmt.Errorf("the scanner's capabilities name no platen size")
|
||||
}
|
||||
return Capabilities{MaxWidth: x.MaxWidth, MaxHeight: x.MaxHeight}, nil
|
||||
}
|
||||
|
||||
// Status asks what the scanner is doing.
|
||||
func (c *Client) Status(ctx context.Context) (Status, error) {
|
||||
resp, err := c.get(ctx, c.Base+"/ScannerStatus")
|
||||
if err != nil {
|
||||
return Status{}, fmt.Errorf("the scanner does not answer: %w", err)
|
||||
}
|
||||
body, err := readBody(resp, 1<<20)
|
||||
if err != nil {
|
||||
return Status{}, fmt.Errorf("the scanner's status cannot be read: %w", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return Status{}, fmt.Errorf("the scanner refused its status: %s", resp.Status)
|
||||
}
|
||||
var s Status
|
||||
if err := xml.Unmarshal(body, &s); err != nil {
|
||||
return Status{}, fmt.Errorf("the scanner's status is not XML: %w", err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// colorModes maps the config's words to eSCL's.
|
||||
var colorModes = map[string]string{"color": "RGB24", "gray": "Grayscale8"}
|
||||
|
||||
// ScanSettings is the request body: the Platen, the whole glass, the colour mode and resolution, as a JPEG.
|
||||
func ScanSettings(c Capabilities, resolution int, colorMode string) (string, error) {
|
||||
escl, ok := colorModes[colorMode]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("color-mode %q is not color or gray", colorMode)
|
||||
}
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scan:ScanSettings xmlns:scan="http://schemas.hp.com/imaging/escl/2011/05/03" xmlns:pwg="http://www.pwg.org/schemas/2010/12/sm">
|
||||
<pwg:Version>2.63</pwg:Version>
|
||||
<pwg:ScanRegions><pwg:ScanRegion><pwg:ContentRegionUnits>escl:ThreeHundredthsOfInches</pwg:ContentRegionUnits><pwg:XOffset>0</pwg:XOffset><pwg:YOffset>0</pwg:YOffset><pwg:Width>%d</pwg:Width><pwg:Height>%d</pwg:Height></pwg:ScanRegion></pwg:ScanRegions>
|
||||
<pwg:InputSource>Platen</pwg:InputSource>
|
||||
<scan:ColorMode>%s</scan:ColorMode>
|
||||
<scan:XResolution>%d</scan:XResolution><scan:YResolution>%d</scan:YResolution>
|
||||
<pwg:DocumentFormat>image/jpeg</pwg:DocumentFormat>
|
||||
</scan:ScanSettings>
|
||||
`, c.MaxWidth, c.MaxHeight, escl, resolution, resolution), nil
|
||||
}
|
||||
|
||||
// Scan puts one job to the scanner and returns the JPEG it sends. The resolution asked is only a request: the
|
||||
// real one is the image's own density (Density), for the real scanner answered 300 with 200.
|
||||
func (c *Client) Scan(ctx context.Context, resolution int, colorMode string) ([]byte, error) {
|
||||
caps, err := c.Capabilities(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
settings, err := ScanSettings(caps, resolution, colorMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.Base+"/ScanJobs", bytes.NewReader([]byte(settings)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "text/xml")
|
||||
resp, err := c.HTTP.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the scanner does not answer: %w", err)
|
||||
}
|
||||
reply, _ := readBody(resp, 1<<20)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return nil, fmt.Errorf("the scanner refused the scan job: %s %s", resp.Status, strings.TrimSpace(string(reply)))
|
||||
}
|
||||
loc := resp.Header.Get("Location")
|
||||
if loc == "" {
|
||||
return nil, fmt.Errorf("the scanner took the scan job but named no Location")
|
||||
}
|
||||
base, _ := url.Parse(c.Base + "/")
|
||||
ref, err := url.Parse(loc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the scan job's Location %q is not a URL: %w", loc, err)
|
||||
}
|
||||
doc := strings.TrimRight(base.ResolveReference(ref).String(), "/") + "/NextDocument"
|
||||
got, err := c.get(ctx, doc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the scanned page cannot be fetched: %w", err)
|
||||
}
|
||||
image, err := readBody(got, maxImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the scanned page cannot be read: %w", err)
|
||||
}
|
||||
if got.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("the scanner gave no page: %s", got.Status)
|
||||
}
|
||||
if ct := got.Header.Get("Content-Type"); !strings.HasPrefix(ct, "image/jpeg") {
|
||||
return nil, fmt.Errorf("the scanner sent %q, not image/jpeg", ct)
|
||||
}
|
||||
return image, nil
|
||||
}
|
||||
|
||||
// Density is the image's own resolution in dpi, from its JFIF header (units dpi, or dots per cm).
|
||||
func Density(jpegBytes []byte) (int, error) {
|
||||
b := jpegBytes
|
||||
if len(b) < 4 || b[0] != 0xFF || b[1] != 0xD8 {
|
||||
return 0, fmt.Errorf("the image is not a JPEG")
|
||||
}
|
||||
for i := 2; i+4 <= len(b) && b[i] == 0xFF; {
|
||||
marker, n := b[i+1], int(b[i+2])<<8|int(b[i+3])
|
||||
if marker == 0xE0 && n >= 16 && i+4+12 <= len(b) && string(b[i+4:i+9]) == "JFIF\x00" {
|
||||
units := b[i+11]
|
||||
x := int(b[i+12])<<8 | int(b[i+13])
|
||||
switch {
|
||||
case units == 1 && x > 0:
|
||||
return x, nil
|
||||
case units == 2 && x > 0:
|
||||
return int(float64(x)*2.54 + 0.5), nil
|
||||
}
|
||||
return 0, fmt.Errorf("the image's JFIF header gives no resolution")
|
||||
}
|
||||
if marker == 0xDA || n < 2 {
|
||||
break
|
||||
}
|
||||
i += 2 + n
|
||||
}
|
||||
return 0, fmt.Errorf("the image has no JFIF header, so its resolution is unknown")
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func squeeze(s string) string { return strings.ReplaceAll(s, "\n", "") }
|
||||
|
||||
// 1440/AC1: the captured capabilities, job creation, document and status replies, served by a test server.
|
||||
func TestScanPostsOnePlatenJobOverTheWholeGlassAndFetchesTheDocumentOnce(t *testing.T) {
|
||||
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
||||
image, err := NewClient(f.base()).Scan(context.Background(), 300, "color")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.posts != 1 {
|
||||
t.Fatalf("%d ScanJobs posts, want 1", f.posts)
|
||||
}
|
||||
// The body is the request the real scanner accepted (testdata/escl/settings.xml): Platen, 2550x3507
|
||||
// 300ths of an inch from the capabilities, RGB24, 300 dpi, image/jpeg.
|
||||
if got, want := squeeze(f.postBody), squeeze(string(fixture(t, "settings.xml"))); got != want {
|
||||
t.Fatalf("the job is not the captured request\n got: %s\nwant: %s", got, want)
|
||||
}
|
||||
if f.documents != 1 {
|
||||
t.Fatalf("%d NextDocument requests, want exactly 1", f.documents)
|
||||
}
|
||||
if want := fixture(t, "scan.jpg"); string(image) != string(want) {
|
||||
t.Fatalf("the returned image is %d bytes, want the scanner's %d", len(image), len(want))
|
||||
}
|
||||
}
|
||||
|
||||
// 1440/AC1: the colour mode and resolution asked are the configured ones.
|
||||
func TestScanAsksForTheConfiguredResolutionAndColourMode(t *testing.T) {
|
||||
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
||||
if _, err := NewClient(f.base()).Scan(context.Background(), 600, "gray"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range []string{"<scan:ColorMode>Grayscale8</scan:ColorMode>", "<scan:XResolution>600</scan:XResolution>", "<scan:YResolution>600</scan:YResolution>"} {
|
||||
if !strings.Contains(f.postBody, want) {
|
||||
t.Errorf("the job lacks %s:\n%s", want, f.postBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1440/AC1: a job the scanner refuses is an error that says so, and no page is fetched.
|
||||
func TestScanSaysWhyAScannerRefusal(t *testing.T) {
|
||||
f := newFakeScanner(t, fixture(t, "scan.jpg"))
|
||||
f.refuse = 503
|
||||
_, err := NewClient(f.base()).Scan(context.Background(), 300, "color")
|
||||
if err == nil || !strings.Contains(err.Error(), "503") {
|
||||
t.Fatalf("error = %v, want the refusal's 503", err)
|
||||
}
|
||||
if f.documents != 0 {
|
||||
t.Fatalf("%d documents fetched after a refusal", f.documents)
|
||||
}
|
||||
}
|
||||
|
||||
// 1440/AC2: the image's own density, never the request's.
|
||||
func TestDensityIsTheImagesOwn(t *testing.T) {
|
||||
dpi, err := Density(fixture(t, "scan.jpg"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dpi != 200 {
|
||||
t.Fatalf("density = %d dpi, want the captured scan's 200 (the request said 300)", dpi)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDensityOfAnImageWithNoneIsAnError(t *testing.T) {
|
||||
if _, err := Density([]byte{0xFF, 0xD8, 0xFF, 0xDA, 0, 2}); err == nil {
|
||||
t.Fatal("an image with no JFIF header has a resolution")
|
||||
}
|
||||
}
|
||||
|
||||
// 1440/AC8: the captured status replies.
|
||||
func TestStatusReadsStateAndAdfState(t *testing.T) {
|
||||
f := newFakeScanner(t, nil)
|
||||
s, err := NewClient(f.base()).Status(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s.State != "Idle" || s.AdfState != "ScannerAdfEmpty" {
|
||||
t.Fatalf("status = %+v, want Idle / ScannerAdfEmpty", s)
|
||||
}
|
||||
}
|
||||
|
||||
// 1440/AC7: which value is unset is said by name.
|
||||
func TestMissingNamesWhatIsUnset(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
s Settings
|
||||
want string
|
||||
}{
|
||||
{Settings{Folder: "/x"}, "scanner"},
|
||||
{Settings{Scanner: "http://192.0.2.10/eSCL"}, "scan-folder"},
|
||||
} {
|
||||
err := c.s.Missing()
|
||||
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("Missing(%+v) = %v, want it to name %s", c.s, err, c.want)
|
||||
}
|
||||
}
|
||||
if err := (Settings{Scanner: "a", Folder: "/x"}).Missing(); err != nil {
|
||||
t.Errorf("both set: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fixture reads a capture of the real scanner's answers (testdata/escl, 2026-10-20). Tests never reach a real scanner.
|
||||
func fixture(t *testing.T, name string) []byte {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(filepath.Join("testdata", "escl", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// fakeScanner serves the captured eSCL answers and records what it was asked.
|
||||
type fakeScanner struct {
|
||||
*httptest.Server
|
||||
mu sync.Mutex
|
||||
image []byte
|
||||
postBody string
|
||||
posts int
|
||||
documents int // NextDocument requests
|
||||
refuse int // when set, the status ScanJobs answers instead of 201
|
||||
noPage bool
|
||||
}
|
||||
|
||||
func newFakeScanner(t *testing.T, image []byte) *fakeScanner {
|
||||
t.Helper()
|
||||
f := &fakeScanner{image: image}
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/eSCL/ScannerCapabilities", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
w.Write(fixture(t, "capabilities.xml"))
|
||||
})
|
||||
mux.HandleFunc("/eSCL/ScannerStatus", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/xml")
|
||||
w.Write(fixture(t, "status-idle.xml"))
|
||||
})
|
||||
mux.HandleFunc("/eSCL/ScanJobs", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "post only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
b := make([]byte, 1<<16)
|
||||
n, _ := r.Body.Read(b)
|
||||
f.postBody = string(b[:n])
|
||||
f.posts++
|
||||
if f.refuse != 0 {
|
||||
http.Error(w, "busy", f.refuse)
|
||||
return
|
||||
}
|
||||
// The captured Location names the real scanner's host; the test's own server stands in for it.
|
||||
w.Header().Set("Location", f.URL+"/eSCL/ScanJobs/7320ead5")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
})
|
||||
mux.HandleFunc("/eSCL/ScanJobs/7320ead5/NextDocument", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.documents++
|
||||
if f.documents > 1 || f.noPage {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
w.Write(f.image)
|
||||
})
|
||||
f.Server = httptest.NewServer(mux)
|
||||
t.Cleanup(f.Close)
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *fakeScanner) base() string { return f.URL + "/eSCL" }
|
||||
@@ -0,0 +1,76 @@
|
||||
// Package main is the scan module's Go tools bundle (novox/hq ADR 0464, issue 1440): it scans the glass of a
|
||||
// network scanner over eSCL, crops the document, files it as a one-page PDF in a folder and says so on the bus.
|
||||
// A scan happens when a tool is called; the module runs no process of its own.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Settings are the module's five config values, as the mesh writes them into settings.json (ADR 0348).
|
||||
type Settings struct {
|
||||
// Scanner is the eSCL base URL, such as http://192.0.2.10/eSCL.
|
||||
Scanner string
|
||||
// Folder is the absolute path the PDFs are filed in. It is not a manifest resource: a directory's path
|
||||
// cannot take a config value (issue 1441), so the code creates it.
|
||||
Folder string
|
||||
// Resolution is the dpi asked of the scanner: 100, 200, 300 or 600.
|
||||
Resolution int
|
||||
// ColorMode is "color" or "gray".
|
||||
ColorMode string
|
||||
// Crop says whether the document is cut out of the platen's white.
|
||||
Crop bool
|
||||
}
|
||||
|
||||
// LoadSettings reads settings.json (the file named by SCAN_SETTINGS) and lays the defaults under it. A missing
|
||||
// or unreadable file is an error that names it; an unset scanner or folder is Missing's to say.
|
||||
func LoadSettings(path string) (Settings, error) {
|
||||
s := Settings{Resolution: 300, ColorMode: "color", Crop: true}
|
||||
if path == "" {
|
||||
return s, fmt.Errorf("SCAN_SETTINGS names no settings file: the module's config does not reach the code")
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return s, fmt.Errorf("settings cannot be read: %w", err)
|
||||
}
|
||||
var in map[string]any
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
return s, fmt.Errorf("settings are not JSON (%s): %w", path, err)
|
||||
}
|
||||
if v, ok := in["scanner"].(string); ok {
|
||||
s.Scanner = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := in["scan-folder"].(string); ok {
|
||||
s.Folder = strings.TrimSpace(v)
|
||||
}
|
||||
if v, ok := in["resolution"].(float64); ok && v > 0 {
|
||||
s.Resolution = int(v)
|
||||
}
|
||||
if v, ok := in["color-mode"].(string); ok && v != "" {
|
||||
s.ColorMode = v
|
||||
}
|
||||
if v, ok := in["crop"].(bool); ok {
|
||||
s.Crop = v
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// Missing is nil when both required values are set, and otherwise says which is not: no tool writes or
|
||||
// asks anything before that (1440/AC7).
|
||||
func (s Settings) Missing() error {
|
||||
var unset []string
|
||||
if s.Scanner == "" {
|
||||
unset = append(unset, "scanner")
|
||||
}
|
||||
if s.Folder == "" {
|
||||
unset = append(unset, "scan-folder")
|
||||
}
|
||||
if len(unset) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("config value %s is not set: the operator gives it with the config verb for the scan module on this node",
|
||||
strings.Join(unset, " and "))
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scan:ScannerCapabilities
|
||||
xmlns:scan="http://schemas.hp.com/imaging/escl/2011/05/03"
|
||||
xmlns:pwg="http://www.pwg.org/schemas/2010/12/sm"
|
||||
xmlns:dest="http://schemas.hp.com/imaging/destination/2011/06/06"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://schemas.hp.com/imaging/escl/2011/05/03 ../../schemas/eSCL-1_92.xsd">
|
||||
<pwg:Version>2.93</pwg:Version>
|
||||
<pwg:MakeAndModel>Brother MFC-L8390CDW series</pwg:MakeAndModel>
|
||||
<pwg:SerialNumber>E82414G4N581626</pwg:SerialNumber>
|
||||
<scan:Manufacturer>Brother</scan:Manufacturer>
|
||||
<scan:UUID>e3248000-80ce-11db-8000-94ddf83ec78a</scan:UUID>
|
||||
<scan:AdminURI>http://BRW58CDC9DFC80B.local./net/net/airprint.html</scan:AdminURI>
|
||||
<scan:IconURI>http://BRW58CDC9DFC80B.local./icons/device-icons-128.png</scan:IconURI>
|
||||
<scan:Certifications>
|
||||
<scan:Certification>
|
||||
<scan:Name>mopria-certified-scan</scan:Name>
|
||||
<scan:Version>1.4</scan:Version>
|
||||
</scan:Certification>
|
||||
</scan:Certifications>
|
||||
<scan:Platen>
|
||||
<scan:PlatenInputCaps>
|
||||
<scan:MinWidth>64</scan:MinWidth>
|
||||
<scan:MaxWidth>2550</scan:MaxWidth>
|
||||
<scan:MinHeight>64</scan:MinHeight>
|
||||
<scan:MaxHeight>3507</scan:MaxHeight>
|
||||
<scan:MaxXOffset>2486</scan:MaxXOffset>
|
||||
<scan:MaxYOffset>3443</scan:MaxYOffset>
|
||||
<scan:MaxScanRegions>1</scan:MaxScanRegions>
|
||||
<scan:SettingProfiles>
|
||||
<scan:SettingProfile>
|
||||
<scan:ColorModes>
|
||||
<scan:ColorMode>BlackAndWhite1</scan:ColorMode>
|
||||
<scan:ColorMode>Grayscale8</scan:ColorMode>
|
||||
<scan:ColorMode>RGB24</scan:ColorMode>
|
||||
</scan:ColorModes>
|
||||
<scan:DocumentFormats>
|
||||
<pwg:DocumentFormat>application/pdf</pwg:DocumentFormat>
|
||||
<pwg:DocumentFormat>image/jpeg</pwg:DocumentFormat>
|
||||
<scan:DocumentFormatExt>application/pdf</scan:DocumentFormatExt>
|
||||
<scan:DocumentFormatExt>image/jpeg</scan:DocumentFormatExt>
|
||||
</scan:DocumentFormats>
|
||||
<scan:SupportedResolutions>
|
||||
<scan:DiscreteResolutions>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>100</scan:XResolution>
|
||||
<scan:YResolution>100</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>200</scan:XResolution>
|
||||
<scan:YResolution>200</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>300</scan:XResolution>
|
||||
<scan:YResolution>300</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>600</scan:XResolution>
|
||||
<scan:YResolution>600</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
</scan:DiscreteResolutions>
|
||||
</scan:SupportedResolutions>
|
||||
<scan:ColorSpaces>
|
||||
<scan:ColorSpace>CMYK</scan:ColorSpace>
|
||||
<scan:ColorSpace>YCC</scan:ColorSpace>
|
||||
<scan:ColorSpace>sRGB</scan:ColorSpace>
|
||||
</scan:ColorSpaces>
|
||||
<scan:CcdChannels>
|
||||
<scan:CcdChannel>Red</scan:CcdChannel>
|
||||
<scan:CcdChannel>Green</scan:CcdChannel>
|
||||
<scan:CcdChannel>Blue</scan:CcdChannel>
|
||||
<scan:CcdChannel>NTSC</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcd</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcdEmulated</scan:CcdChannel>
|
||||
</scan:CcdChannels>
|
||||
<scan:BinaryRenderings>
|
||||
<scan:BinaryRendering>Halftone</scan:BinaryRendering>
|
||||
<scan:BinaryRendering>Threshold</scan:BinaryRendering>
|
||||
</scan:BinaryRenderings>
|
||||
</scan:SettingProfile>
|
||||
</scan:SettingProfiles>
|
||||
<scan:SupportedIntents>
|
||||
<scan:Intent>Document</scan:Intent>
|
||||
<scan:Intent>TextAndGraphic</scan:Intent>
|
||||
<scan:Intent>Photo</scan:Intent>
|
||||
<scan:Intent>Preview</scan:Intent>
|
||||
</scan:SupportedIntents>
|
||||
<scan:MaxOpticalXResolution>1200</scan:MaxOpticalXResolution>
|
||||
<scan:MaxOpticalYResolution>2400</scan:MaxOpticalYResolution>
|
||||
<scan:RiskyLeftMargin>0</scan:RiskyLeftMargin>
|
||||
<scan:RiskyRightMargin>0</scan:RiskyRightMargin>
|
||||
<scan:RiskyTopMargin>0</scan:RiskyTopMargin>
|
||||
<scan:RiskyBottomMargin>0</scan:RiskyBottomMargin>
|
||||
<scan:MaxPhysicalWidth>2550</scan:MaxPhysicalWidth>
|
||||
<scan:MaxPhysicalHeight>3507</scan:MaxPhysicalHeight>
|
||||
</scan:PlatenInputCaps>
|
||||
</scan:Platen>
|
||||
<scan:Adf>
|
||||
<scan:AdfSimplexInputCaps>
|
||||
<scan:MinWidth>64</scan:MinWidth>
|
||||
<scan:MaxWidth>2550</scan:MaxWidth>
|
||||
<scan:MinHeight>1740</scan:MinHeight>
|
||||
<scan:MaxHeight>4200</scan:MaxHeight>
|
||||
<scan:MaxXOffset>2486</scan:MaxXOffset>
|
||||
<scan:MaxYOffset>2460</scan:MaxYOffset>
|
||||
<scan:MaxScanRegions>1</scan:MaxScanRegions>
|
||||
<scan:SettingProfiles>
|
||||
<scan:SettingProfile>
|
||||
<scan:ColorModes>
|
||||
<scan:ColorMode>BlackAndWhite1</scan:ColorMode>
|
||||
<scan:ColorMode>Grayscale8</scan:ColorMode>
|
||||
<scan:ColorMode>RGB24</scan:ColorMode>
|
||||
</scan:ColorModes>
|
||||
<scan:DocumentFormats>
|
||||
<pwg:DocumentFormat>application/pdf</pwg:DocumentFormat>
|
||||
<pwg:DocumentFormat>image/jpeg</pwg:DocumentFormat>
|
||||
<scan:DocumentFormatExt>application/pdf</scan:DocumentFormatExt>
|
||||
<scan:DocumentFormatExt>image/jpeg</scan:DocumentFormatExt>
|
||||
</scan:DocumentFormats>
|
||||
<scan:SupportedResolutions>
|
||||
<scan:DiscreteResolutions>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>100</scan:XResolution>
|
||||
<scan:YResolution>100</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>200</scan:XResolution>
|
||||
<scan:YResolution>200</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>300</scan:XResolution>
|
||||
<scan:YResolution>300</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>600</scan:XResolution>
|
||||
<scan:YResolution>600</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
</scan:DiscreteResolutions>
|
||||
</scan:SupportedResolutions>
|
||||
<scan:ColorSpaces>
|
||||
<scan:ColorSpace>CMYK</scan:ColorSpace>
|
||||
<scan:ColorSpace>YCC</scan:ColorSpace>
|
||||
<scan:ColorSpace>sRGB</scan:ColorSpace>
|
||||
</scan:ColorSpaces>
|
||||
<scan:CcdChannels>
|
||||
<scan:CcdChannel>Red</scan:CcdChannel>
|
||||
<scan:CcdChannel>Green</scan:CcdChannel>
|
||||
<scan:CcdChannel>Blue</scan:CcdChannel>
|
||||
<scan:CcdChannel>NTSC</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcd</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcdEmulated</scan:CcdChannel>
|
||||
</scan:CcdChannels>
|
||||
<scan:BinaryRenderings>
|
||||
<scan:BinaryRendering>Halftone</scan:BinaryRendering>
|
||||
<scan:BinaryRendering>Threshold</scan:BinaryRendering>
|
||||
</scan:BinaryRenderings>
|
||||
</scan:SettingProfile>
|
||||
</scan:SettingProfiles>
|
||||
<scan:SupportedIntents>
|
||||
<scan:Intent>Document</scan:Intent>
|
||||
<scan:Intent>TextAndGraphic</scan:Intent>
|
||||
<scan:Intent>Photo</scan:Intent>
|
||||
<scan:Intent>Preview</scan:Intent>
|
||||
</scan:SupportedIntents>
|
||||
<scan:MaxOpticalXResolution>1200</scan:MaxOpticalXResolution>
|
||||
<scan:MaxOpticalYResolution>600</scan:MaxOpticalYResolution>
|
||||
<scan:RiskyLeftMargin>0</scan:RiskyLeftMargin>
|
||||
<scan:RiskyRightMargin>0</scan:RiskyRightMargin>
|
||||
<scan:RiskyTopMargin>0</scan:RiskyTopMargin>
|
||||
<scan:RiskyBottomMargin>0</scan:RiskyBottomMargin>
|
||||
<scan:MaxPhysicalWidth>2550</scan:MaxPhysicalWidth>
|
||||
<scan:MaxPhysicalHeight>4200</scan:MaxPhysicalHeight>
|
||||
</scan:AdfSimplexInputCaps>
|
||||
<scan:AdfDuplexInputCaps>
|
||||
<scan:MinWidth>64</scan:MinWidth>
|
||||
<scan:MaxWidth>2550</scan:MaxWidth>
|
||||
<scan:MinHeight>1740</scan:MinHeight>
|
||||
<scan:MaxHeight>4200</scan:MaxHeight>
|
||||
<scan:MaxXOffset>2486</scan:MaxXOffset>
|
||||
<scan:MaxYOffset>2460</scan:MaxYOffset>
|
||||
<scan:MaxScanRegions>1</scan:MaxScanRegions>
|
||||
<scan:SettingProfiles>
|
||||
<scan:SettingProfile>
|
||||
<scan:ColorModes>
|
||||
<scan:ColorMode>BlackAndWhite1</scan:ColorMode>
|
||||
<scan:ColorMode>Grayscale8</scan:ColorMode>
|
||||
<scan:ColorMode>RGB24</scan:ColorMode>
|
||||
</scan:ColorModes>
|
||||
<scan:DocumentFormats>
|
||||
<pwg:DocumentFormat>application/pdf</pwg:DocumentFormat>
|
||||
<pwg:DocumentFormat>image/jpeg</pwg:DocumentFormat>
|
||||
<scan:DocumentFormatExt>application/pdf</scan:DocumentFormatExt>
|
||||
<scan:DocumentFormatExt>image/jpeg</scan:DocumentFormatExt>
|
||||
</scan:DocumentFormats>
|
||||
<scan:SupportedResolutions>
|
||||
<scan:DiscreteResolutions>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>100</scan:XResolution>
|
||||
<scan:YResolution>100</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>200</scan:XResolution>
|
||||
<scan:YResolution>200</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>300</scan:XResolution>
|
||||
<scan:YResolution>300</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
<scan:DiscreteResolution>
|
||||
<scan:XResolution>600</scan:XResolution>
|
||||
<scan:YResolution>600</scan:YResolution>
|
||||
</scan:DiscreteResolution>
|
||||
</scan:DiscreteResolutions>
|
||||
</scan:SupportedResolutions>
|
||||
<scan:ColorSpaces>
|
||||
<scan:ColorSpace>CMYK</scan:ColorSpace>
|
||||
<scan:ColorSpace>YCC</scan:ColorSpace>
|
||||
<scan:ColorSpace>sRGB</scan:ColorSpace>
|
||||
</scan:ColorSpaces>
|
||||
<scan:CcdChannels>
|
||||
<scan:CcdChannel>Red</scan:CcdChannel>
|
||||
<scan:CcdChannel>Green</scan:CcdChannel>
|
||||
<scan:CcdChannel>Blue</scan:CcdChannel>
|
||||
<scan:CcdChannel>NTSC</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcd</scan:CcdChannel>
|
||||
<scan:CcdChannel>GrayCcdEmulated</scan:CcdChannel>
|
||||
</scan:CcdChannels>
|
||||
<scan:BinaryRenderings>
|
||||
<scan:BinaryRendering>Halftone</scan:BinaryRendering>
|
||||
<scan:BinaryRendering>Threshold</scan:BinaryRendering>
|
||||
</scan:BinaryRenderings>
|
||||
</scan:SettingProfile>
|
||||
</scan:SettingProfiles>
|
||||
<scan:SupportedIntents>
|
||||
<scan:Intent>Document</scan:Intent>
|
||||
<scan:Intent>TextAndGraphic</scan:Intent>
|
||||
<scan:Intent>Photo</scan:Intent>
|
||||
<scan:Intent>Preview</scan:Intent>
|
||||
</scan:SupportedIntents>
|
||||
<scan:MaxOpticalXResolution>600</scan:MaxOpticalXResolution>
|
||||
<scan:MaxOpticalYResolution>600</scan:MaxOpticalYResolution>
|
||||
<scan:RiskyLeftMargin>0</scan:RiskyLeftMargin>
|
||||
<scan:RiskyRightMargin>0</scan:RiskyRightMargin>
|
||||
<scan:RiskyTopMargin>0</scan:RiskyTopMargin>
|
||||
<scan:RiskyBottomMargin>0</scan:RiskyBottomMargin>
|
||||
<scan:MaxPhysicalWidth>2550</scan:MaxPhysicalWidth>
|
||||
<scan:MaxPhysicalHeight>4200</scan:MaxPhysicalHeight>
|
||||
</scan:AdfDuplexInputCaps>
|
||||
<scan:FeederCapacity>50</scan:FeederCapacity>
|
||||
<scan:AdfOptions>
|
||||
<scan:AdfOption>DetectPaperLoaded</scan:AdfOption>
|
||||
<scan:AdfOption>Duplex</scan:AdfOption>
|
||||
</scan:AdfOptions>
|
||||
</scan:Adf>
|
||||
<scan:CompressionFactorSupport>
|
||||
<scan:Min>1</scan:Min>
|
||||
<scan:Max>3</scan:Max>
|
||||
<scan:Normal>2</scan:Normal>
|
||||
<scan:Step>1</scan:Step>
|
||||
</scan:CompressionFactorSupport>
|
||||
</scan:ScannerCapabilities>
|
||||
@@ -0,0 +1,6 @@
|
||||
HTTP/1.1 200 OK
|
||||
Content-Location: /eSCL/ScanJobs/7320ead5-ccb0-11f1-8ff9-94ddf83ec78a/doc-1
|
||||
Accept-Encoding: identity
|
||||
Content-Type: image/jpeg
|
||||
Transfer-Encoding: chunked
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
HTTP/1.1 201 Created
|
||||
Cache-Control: no-store
|
||||
Accept-Encoding: identity
|
||||
X-Frame-Options: DENY
|
||||
Content-Length: 0
|
||||
Content-Type: text/xml
|
||||
Connection: close
|
||||
Pragma: no-cache
|
||||
Location: http://192.168.1.171/eSCL/ScanJobs/7320ead5-ccb0-11f1-8ff9-94ddf83ec78a
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<scan:ScanSettings xmlns:scan="http://schemas.hp.com/imaging/escl/2011/05/03" xmlns:pwg="http://www.pwg.org/schemas/2010/12/sm">
|
||||
<pwg:Version>2.63</pwg:Version>
|
||||
<pwg:ScanRegions><pwg:ScanRegion><pwg:ContentRegionUnits>escl:ThreeHundredthsOfInches</pwg:ContentRegionUnits><pwg:XOffset>0</pwg:XOffset><pwg:YOffset>0</pwg:YOffset><pwg:Width>2550</pwg:Width><pwg:Height>3507</pwg:Height></pwg:ScanRegion></pwg:ScanRegions>
|
||||
<pwg:InputSource>Platen</pwg:InputSource>
|
||||
<scan:ColorMode>RGB24</scan:ColorMode>
|
||||
<scan:XResolution>300</scan:XResolution><scan:YResolution>300</scan:YResolution>
|
||||
<pwg:DocumentFormat>image/jpeg</pwg:DocumentFormat>
|
||||
</scan:ScanSettings>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><scan:ScannerStatus xmlns:pwg="http://www.pwg.org/schemas/2010/12/sm" xmlns:scan="http://schemas.hp.com/imaging/escl/2011/05/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.hp.com/imaging/escl/2011/05/03 ../../schemas/eSCL-1_92.xsd"><pwg:Version>2.93</pwg:Version><pwg:State>Idle</pwg:State><scan:AdfState>ScannerAdfEmpty</scan:AdfState><scan:Jobs><scan:JobInfo><pwg:JobUri>/eSCL/ScanJobs/7320ead5-ccb0-11f1-8ff9-94ddf83ec78a</pwg:JobUri><pwg:JobUuid>urn:uuid:7320ead5-ccb0-11f1-8ff9-94ddf83ec78a</pwg:JobUuid><scan:Age>9291071</scan:Age><pwg:ImagesCompleted>1</pwg:ImagesCompleted><pwg:ImagesToTransfer>1</pwg:ImagesToTransfer><pwg:JobState>Completed</pwg:JobState><pwg:JobStateReasons><pwg:JobStateReason>JobCompletedSuccessfully</pwg:JobStateReason></pwg:JobStateReasons></scan:JobInfo></scan:Jobs></scan:ScannerStatus>
|
||||
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><scan:ScannerStatus xmlns:pwg="http://www.pwg.org/schemas/2010/12/sm" xmlns:scan="http://schemas.hp.com/imaging/escl/2011/05/03" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://schemas.hp.com/imaging/escl/2011/05/03 ../../schemas/eSCL-1_92.xsd"><pwg:Version>2.93</pwg:Version><pwg:State>Idle</pwg:State><scan:AdfState>ScannerAdfEmpty</scan:AdfState><scan:Jobs><scan:JobInfo><pwg:JobUri>/eSCL/ScanJobs/7320ead5-ccb0-11f1-8ff9-94ddf83ec78a</pwg:JobUri><pwg:JobUuid>urn:uuid:7320ead5-ccb0-11f1-8ff9-94ddf83ec78a</pwg:JobUuid><scan:Age>9292192</scan:Age><pwg:ImagesCompleted>1</pwg:ImagesCompleted><pwg:ImagesToTransfer>1</pwg:ImagesToTransfer><pwg:JobState>Completed</pwg:JobState><pwg:JobStateReasons><pwg:JobStateReason>JobCompletedSuccessfully</pwg:JobStateReason></pwg:JobStateReasons></scan:JobInfo></scan:Jobs></scan:ScannerStatus>
|
||||
@@ -0,0 +1,3 @@
|
||||
module scan
|
||||
|
||||
go 1.26.0
|
||||
Reference in New Issue
Block a user