blob: 91d1a04d56c1c5eff277ec7832b17ac8e84deb3e [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Utilities related to a scanner's capabilities.
package utils
import (
"crypto/tls"
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"github.com/google/go-cmp/cmp"
)
// ScannableArea represents the maximum size a scanner source can scan.
type ScannableArea struct {
Height float32 `json:"Height"`
Width float32 `json:"Width"`
}
// LorgnetteSource represents a scanner source's capabilities as read from
// Lorgnette.
type LorgnetteSource struct {
ColorModes []string `json:"ColorModes"`
Resolutions []int `json:"Resolutions"`
ScannableArea ScannableArea `json:"ScannableArea"`
}
// LorgnetteCapabilities represents a scanner's capabilities as read from
// Lorgnette.
type LorgnetteCapabilities struct {
PlatenCaps LorgnetteSource `json:"SOURCE_PLATEN"`
AdfSimplexCaps LorgnetteSource `json:"SOURCE_ADF_SIMPLEX"`
AdfDuplexCaps LorgnetteSource `json:"SOURCE_ADF_DUPLEX"`
}
// DiscreteResolution represents a pair of X and Y resolution values supported
// by a scanner.
type DiscreteResolution struct {
XResolution int `xml:"XResolution"`
YResolution int `xml:"YResolution"`
}
// ResolutionRange represents a range of resolutions supported by a scanner, for
// one of the X or Y resolutions.
type ResolutionRange struct {
Min int `xml:"Min"`
Max int `xml:"Max"`
Normal int `xml:"Normal"`
Step int `xml:"Step"`
}
// SupportedResolutions represents all of the resolutions supported by a
// scanner.
type SupportedResolutions struct {
DiscreteResolutions []DiscreteResolution `xml:"DiscreteResolutions>DiscreteResolution"`
XResolutionRange ResolutionRange `xml:"ResolutionRange>XResolutionRange"`
YResolutionRange ResolutionRange `xml:"ResolutionRange>YResolutionRange"`
}
// SettingProfile represents a group of settings common to one or more
// SourceCapabilities.
type SettingProfile struct {
Name string `xml:"name,attr"`
Ref string `xml:"ref,attr"`
ColorModes []string `xml:"ColorModes>ColorMode"`
DocumentFormats []string `xml:"DocumentFormats>DocumentFormat"`
DocumentFormatsExt []string `xml:"DocumentFormats>DocumentFormatExt"`
SupportedResolutions SupportedResolutions `xml:"SupportedResolutions"`
}
// SourceCapabilities represents the capabilities of a single scanner source:
// Platen, ADF simplex, ADF duplex or camera.
type SourceCapabilities struct {
MaxWidth int `xml:"MaxWidth"`
MinWidth int `xml:"MinWidth"`
MaxHeight int `xml:"MaxHeight"`
MinHeight int `xml:"MinHeight"`
MaxScanRegions int `xml:"MaxScanRegions"`
SettingProfile SettingProfile `xml:"SettingProfiles>SettingProfile"`
MaxOpticalXResolution int `xml:"MaxOpticalXResolution"`
MaxOpticalYResolution int `xml:"MaxOpticalYResolution"`
MaxPhysicalWidth int `xml:"MaxPhysicalWidth"`
MaxPhysicalHeight int `xml:"MaxPhysicalHeight"`
}
// AdfCapabilities represents all of a scanner's ADF capabilities.
type AdfCapabilities struct {
AdfSimplexInputCaps SourceCapabilities `xml:"AdfSimplexInputCaps"`
AdfDuplexInputCaps SourceCapabilities `xml:"AdfDuplexInputCaps"`
AdfOptions []string `xml:"AdfOptions>AdfOption"`
}
// StoredJobRequestSupport represents a scanner's support for stored job
// requests.
type StoredJobRequestSupport struct {
MaxStoredJobRequests int `xml:"MaxStoredJobRequests"`
TimeoutInSeconds int `xml:"TimeoutInSeconds"`
PINLength int `xml:"PINLength"`
MaxJobNameLength int `xml:"MaxJobNameLength"`
}
// ScannerCapabilities represents all of a scanner's capabilities.
type ScannerCapabilities struct {
Version string `xml:"Version"`
MakeAndModel string `xml:"MakeAndModel"`
Manufacturer string `xml:"Manufacturer"`
SettingProfiles []SettingProfile `xml:"SettingProfiles>SettingProfile"`
PlatenInputCaps SourceCapabilities `xml:"Platen>PlatenInputCaps"`
AdfCapabilities AdfCapabilities `xml:"Adf"`
CameraInputCaps SourceCapabilities `xml:"Camera>CameraInputCaps"`
StoredJobRequestSupport StoredJobRequestSupport `xml:"StoredJobRequestSupport"`
}
// constructScannableAreaFromESCL constructs a ScannableArea object from eSCL
// units.
func constructScannableAreaFromESCL(maxHeight int, maxWidth int) (area ScannableArea) {
inchesToMM := 25.4
eSCLToInches := 300
area.Height = float32(maxHeight) * float32(inchesToMM) / float32(eSCLToInches)
area.Width = float32(maxWidth) * float32(inchesToMM) / float32(eSCLToInches)
return
}
// eSCLToLorgnetteColorMode converts `eSCLColorMode` to the corresponding
// Lorgnette color mode.
func eSCLToLorgnetteColorMode(eSCLColorMode string) string {
switch eSCLColorMode {
case "BlackAndWhite1":
return "MODE_LINEART"
case "Grayscale8":
return "MODE_GRAYSCALE"
case "RGB24":
return "MODE_COLOR"
default:
return "MODE_UNSPECIFIED"
}
}
// setReferencedProfileIfNecessary checks to see if `outProfile` references
// another SettingProfile, and if so, finds that profile in `referencedProfiles`
// and copies its information into `outProfile`.
func setReferencedProfileIfNecessary(
outProfile *SettingProfile, referencedProfiles []SettingProfile) error {
if outProfile.Ref == "" {
return nil
}
for _, profile := range referencedProfiles {
if profile.Name == outProfile.Ref {
*outProfile = profile
return nil
}
}
return fmt.Errorf("No profile found for reference: %s", outProfile.Ref)
}
// GetScannerCapabilities uses the HTTP address of the scanner to get its
// capabilities. `addr` should have a trailing slash. The returned
// ScannerCapabilities object is invalid when the returned error is non-nil. Any
// fields in ScannerCapabilities which were missing from the scanner's response
// will be left at their zero values.
func GetScannerCapabilities(addr string) (caps ScannerCapabilities, err error) {
// Deliberately ignore certificate errors because printers normally
// have self-signed certificates.
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true,
}
tr := &http.Transport{
TLSClientConfig: tlsConfig,
}
client := &http.Client{Transport: tr}
resp, err := client.Get(addr + "ScannerCapabilities")
if err != nil {
return
}
defer resp.Body.Close()
if resp.Status != "200 OK" {
err = fmt.Errorf("Unexpected HTTP response status: %s", resp.Status)
return
}
respbytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
err = xml.Unmarshal(respbytes, &caps)
if err != nil {
return
}
// Replace any references to SettingProfiles with the referenced
// SettingProfile.
err = setReferencedProfileIfNecessary(&caps.PlatenInputCaps.SettingProfile, caps.SettingProfiles)
if err != nil {
return
}
err = setReferencedProfileIfNecessary(&caps.AdfCapabilities.AdfSimplexInputCaps.SettingProfile, caps.SettingProfiles)
if err != nil {
return
}
err = setReferencedProfileIfNecessary(&caps.AdfCapabilities.AdfDuplexInputCaps.SettingProfile, caps.SettingProfiles)
if err != nil {
return
}
err = setReferencedProfileIfNecessary(&caps.CameraInputCaps.SettingProfile, caps.SettingProfiles)
if err != nil {
return
}
return
}
// ParseLorgnetteCapabilities parses `rawData` into a structured format. It
// expects `rawData` to be JSON output from the command
// `lorgnette_cli get_json_caps --scanner=$SCANNER`. Any fields in
// LorgnetteCapabilities which were missing from `rawData` will be left at their
// zero values. If `err` is non-nill, `caps` is invalid.
func ParseLorgnetteCapabilities(rawData string) (caps LorgnetteCapabilities, err error) {
err = json.Unmarshal([]byte(rawData), &caps)
return
}
// IsPopulated returns returns true iff `caps` is non-empty.
func (caps SourceCapabilities) IsPopulated() bool {
return !cmp.Equal(caps, SourceCapabilities{})
}
// ToLorgnetteResolutions converts `resolutions` to a format returned by
// lorgnette. All resolutions unsupported by lorgnette are dropped.
func (resolutions SupportedResolutions) ToLorgnetteResolutions() (lorgnetteResolutions []int) {
supportedResolutions := []int{75, 100, 150, 200, 300, 600}
for _, discreteResolution := range resolutions.DiscreteResolutions {
if discreteResolution.XResolution != discreteResolution.YResolution {
continue
}
for _, supportedResolution := range supportedResolutions {
if discreteResolution.XResolution == supportedResolution {
lorgnetteResolutions = append(lorgnetteResolutions, supportedResolution)
}
}
}
for _, supportedResolution := range supportedResolutions {
if supportedResolution < resolutions.XResolutionRange.Min || supportedResolution > resolutions.XResolutionRange.Max {
continue
}
if supportedResolution < resolutions.YResolutionRange.Min || supportedResolution > resolutions.YResolutionRange.Max {
continue
}
if (supportedResolution-resolutions.XResolutionRange.Min)%resolutions.XResolutionRange.Step == 0 && (supportedResolution-resolutions.YResolutionRange.Min)%resolutions.YResolutionRange.Step == 0 {
lorgnetteResolutions = append(lorgnetteResolutions, supportedResolution)
}
}
return
}
// ToLorgnetteSource converts `sourceCaps` to LorgnetteSource.
func (caps SourceCapabilities) ToLorgnetteSource() (lorgnetteSource LorgnetteSource) {
for _, colorMode := range caps.SettingProfile.ColorModes {
lorgnetteColorMode := eSCLToLorgnetteColorMode(colorMode)
if lorgnetteColorMode == "MODE_LINEART" {
// Skip black and white because sane-airscan doesn't support it.
continue
}
lorgnetteSource.ColorModes = append(lorgnetteSource.ColorModes, lorgnetteColorMode)
}
lorgnetteSource.Resolutions = caps.SettingProfile.SupportedResolutions.ToLorgnetteResolutions()
lorgnetteSource.ScannableArea = constructScannableAreaFromESCL(caps.MaxHeight, caps.MaxWidth)
return
}
// ToLorgnetteCaps converts `scannerCaps` to LorgnetteCapabilities.
func (scannerCaps ScannerCapabilities) ToLorgnetteCaps() (lorgnetteCaps LorgnetteCapabilities) {
lorgnetteCaps.PlatenCaps = scannerCaps.PlatenInputCaps.ToLorgnetteSource()
lorgnetteCaps.AdfSimplexCaps = scannerCaps.AdfCapabilities.AdfSimplexInputCaps.ToLorgnetteSource()
lorgnetteCaps.AdfDuplexCaps = scannerCaps.AdfCapabilities.AdfDuplexInputCaps.ToLorgnetteSource()
return
}