diff --git a/proto/wsscan/attributedelement.go b/proto/wsscan/attributedelement.go new file mode 100644 index 00000000..21ecbddd --- /dev/null +++ b/proto/wsscan/attributedelement.go @@ -0,0 +1,115 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// AttributedElement: reusable type for elements with +// text value and optional wscn:MustHonor, wscn:Override, wscn:UsedDefault attributes + +package wsscan + +import ( + "fmt" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// AttributedElement holds a value and optional wscn:MustHonor, wscn:Override, +// and wscn:UsedDefault attributes. +// +// The attributes are xs:string but must be boolean values: "0", "1", "false", or "true" +// (case-insensitive, whitespace ignored). +// +// This type is generic and can be used for elements like +// that have these attributes along with text content. +type AttributedElement[T any] struct { + Value T + MustHonor optional.Val[BooleanElement] + Override optional.Val[BooleanElement] + UsedDefault optional.Val[BooleanElement] +} + +// decodeAttributedElement fills the struct from an XML element. +// +// decodeValue is a function that decodes the value type T from a string. +func decodeAttributedElement[T any]( + root xmldoc.Element, + decodeValue func(string) (T, error), +) (AttributedElement[T], error) { + var elem AttributedElement[T] + + // Decode the value from text content + var err error + elem.Value, err = decodeValue(root.Text) + if err != nil { + return elem, err + } + + // Decode optional attributes with validation + if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found { + mustHonor := BooleanElement(attr.Value) + if err := mustHonor.Validate(); err != nil { + return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err)) + } + elem.MustHonor = optional.New(mustHonor) + } + if attr, found := root.AttrByName(NsWSCN + ":Override"); found { + override := BooleanElement(attr.Value) + if err := override.Validate(); err != nil { + return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("override: %w", err)) + } + elem.Override = optional.New(override) + } + if attr, found := root.AttrByName(NsWSCN + ":UsedDefault"); found { + usedDefault := BooleanElement(attr.Value) + if err := usedDefault.Validate(); err != nil { + return elem, xmldoc.XMLErrWrap(root, fmt.Errorf("usedDefault: %w", err)) + } + elem.UsedDefault = optional.New(usedDefault) + } + + return elem, nil +} + +// toXML creates an XML element from the struct. +// +// name is the XML element name (e.g., "wscn:Rotation"). +// valueToString converts the value type T to its string representation. +func (a AttributedElement[T]) toXML( + name string, + valueToString func(T) string, +) xmldoc.Element { + elm := xmldoc.Element{ + Name: name, + Text: valueToString(a.Value), + } + + // Add optional attributes if present + attrs := make([]xmldoc.Attr, 0, 3) + if mustHonor := optional.Get(a.MustHonor); mustHonor != "" { + attrs = append(attrs, xmldoc.Attr{ + Name: NsWSCN + ":MustHonor", + Value: string(mustHonor), + }) + } + if override := optional.Get(a.Override); override != "" { + attrs = append(attrs, xmldoc.Attr{ + Name: NsWSCN + ":Override", + Value: string(override), + }) + } + if usedDefault := optional.Get(a.UsedDefault); usedDefault != "" { + attrs = append(attrs, xmldoc.Attr{ + Name: NsWSCN + ":UsedDefault", + Value: string(usedDefault), + }) + } + + if len(attrs) > 0 { + elm.Attrs = attrs + } + + return elm +} diff --git a/proto/wsscan/attributedelement_test.go b/proto/wsscan/attributedelement_test.go new file mode 100644 index 00000000..6fe7a859 --- /dev/null +++ b/proto/wsscan/attributedelement_test.go @@ -0,0 +1,251 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for AttributedElement + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestAttributedElement_RoundTrip(t *testing.T) { + orig := AttributedElement[RotationValue]{ + Value: Rotation90, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("true")), + } + + elm := orig.toXML(NsWSCN+":Rotation", func(rv RotationValue) string { + return rv.String() + }) + + if elm.Name != NsWSCN+":Rotation" { + t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Rotation", elm.Name) + } + if elm.Text != "90" { + t.Errorf("expected text '90', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "true" { + t.Errorf("expected UsedDefault='true', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeAttributedElement(elm, func(s string) (RotationValue, error) { + return DecodeRotationValue(s), nil + }) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestAttributedElement_NoAttributes(t *testing.T) { + orig := AttributedElement[RotationValue]{ + Value: Rotation180, + } + + elm := orig.toXML(NsWSCN+":Rotation", func(rv RotationValue) string { + return rv.String() + }) + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + + decoded, err := decodeAttributedElement(elm, func(s string) (RotationValue, error) { + return DecodeRotationValue(s), nil + }) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestAttributedElement_StringValue(t *testing.T) { + orig := AttributedElement[string]{ + Value: "some-value", + MustHonor: optional.New(BooleanElement("1")), + } + + elm := orig.toXML(NsWSCN+":SomeElement", func(s string) string { + return s + }) + + if elm.Text != "some-value" { + t.Errorf("expected text 'some-value', got '%s'", elm.Text) + } + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + + decoded, err := decodeAttributedElement(elm, func(s string) (string, error) { + return s, nil + }) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestAttributedElement_FromXML(t *testing.T) { + // Create XML element manually + root := xmldoc.Element{ + Name: NsWSCN + ":Rotation", + Text: "270", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "false"}, + {Name: NsWSCN + ":Override", Value: "true"}, + }, + } + + decoded, err := decodeAttributedElement(root, func(s string) (RotationValue, error) { + val := DecodeRotationValue(s) + if val == UnknownRotationValue { + return val, xmldoc.XMLErrWrap(root, nil) + } + return val, nil + }) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != Rotation270 { + t.Errorf("expected Rotation270, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "false" { + t.Errorf("expected MustHonor='false', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "true" { + t.Errorf("expected Override='true', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); usedDefault != "" { + t.Errorf("expected empty UsedDefault, got '%s'", usedDefault) + } +} + +func TestAttributedElement_InvalidBooleanAttributes(t *testing.T) { + tests := []struct { + name string + attr string + value string + wantErr bool + }{ + { + name: "valid true", + attr: "MustHonor", + value: "true", + wantErr: false, + }, + { + name: "valid false", + attr: "MustHonor", + value: "false", + wantErr: false, + }, + { + name: "valid 1", + attr: "MustHonor", + value: "1", + wantErr: false, + }, + { + name: "valid 0", + attr: "MustHonor", + value: "0", + wantErr: false, + }, + { + name: "invalid value", + attr: "MustHonor", + value: "invalid", + wantErr: true, + }, + { + name: "invalid empty", + attr: "MustHonor", + value: "", + wantErr: true, + }, + { + name: "invalid Override", + attr: "Override", + value: "yes", + wantErr: true, + }, + { + name: "invalid UsedDefault", + attr: "UsedDefault", + value: "maybe", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Rotation", + Text: "90", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":" + tt.attr, Value: tt.value}, + }, + } + + _, err := decodeAttributedElement(root, func(s string) (RotationValue, error) { + val := DecodeRotationValue(s) + if val == UnknownRotationValue { + return val, xmldoc.XMLErrWrap(root, nil) + } + return val, nil + }) + + if tt.wantErr { + if err == nil { + t.Errorf("expected error for %s='%s', got nil", tt.attr, tt.value) + } + } else { + if err != nil { + t.Errorf("unexpected error for %s='%s': %v", tt.attr, tt.value, err) + } + } + }) + } +} diff --git a/proto/wsscan/colorprocessing.go b/proto/wsscan/colorprocessing.go new file mode 100644 index 00000000..7c7c3093 --- /dev/null +++ b/proto/wsscan/colorprocessing.go @@ -0,0 +1,36 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// ColorProcessing element + +package wsscan + +import ( + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// ColorProcessing represents the optional element +// that specifies the color-processing mode of the input source on the scanner. +// +// Standard values are: "BlackAndWhite1", "Grayscale4", "Grayscale8", "Grayscale16", +// "RGB24", "RGB48", "RGBa32", "RGBa64" (see ColorEntry for details). +// Vendor-defined values are also allowed and will decode as UnknownColorEntry. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +type ColorProcessing = AttributedElement[ColorEntry] + +// decodeColorProcessing decodes [ColorProcessing] from the XML tree. +func decodeColorProcessing(root xmldoc.Element) (ColorProcessing, error) { + return decodeAttributedElement(root, func(s string) (ColorEntry, error) { + return DecodeColorEntry(s), nil + }) +} + +// toXMLColorProcessing generates XML tree for the [ColorProcessing]. +func toXMLColorProcessing(cp ColorProcessing, name string) xmldoc.Element { + return cp.toXML(name, ColorEntry.String) +} diff --git a/proto/wsscan/colorprocessing_test.go b/proto/wsscan/colorprocessing_test.go new file mode 100644 index 00000000..d7a4a91c --- /dev/null +++ b/proto/wsscan/colorprocessing_test.go @@ -0,0 +1,245 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for ColorProcessing + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestColorProcessing_RoundTrip(t *testing.T) { + orig := ColorProcessing{ + Value: RGB24, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLColorProcessing(orig, NsWSCN+":ColorProcessing") + + if elm.Name != NsWSCN+":ColorProcessing" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":ColorProcessing", elm.Name) + } + if elm.Text != "RGB24" { + t.Errorf("expected text 'RGB24', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeColorProcessing(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestColorProcessing_NoAttributes(t *testing.T) { + orig := ColorProcessing{ + Value: Grayscale8, + } + + elm := toXMLColorProcessing(orig, NsWSCN+":ColorProcessing") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "Grayscale8" { + t.Errorf("expected text 'Grayscale8', got '%s'", elm.Text) + } + + decoded, err := decodeColorProcessing(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestColorProcessing_StandardValues(t *testing.T) { + standardValues := []struct { + enumValue ColorEntry + textValue string + }{ + {BlackAndWhite1, "BlackAndWhite1"}, + {Grayscale4, "Grayscale4"}, + {Grayscale8, "Grayscale8"}, + {Grayscale16, "Grayscale16"}, + {RGB24, "RGB24"}, + {RGB48, "RGB48"}, + {RGBA32, "RGBa32"}, + {RGBA64, "RGBa64"}, + } + + for _, tc := range standardValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := ColorProcessing{ + Value: tc.enumValue, + } + + elm := toXMLColorProcessing(orig, NsWSCN+":ColorProcessing") + if elm.Text != tc.textValue { + t.Errorf("expected text '%s', got '%s'", tc.textValue, elm.Text) + } + + decoded, err := decodeColorProcessing(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + }) + } +} + +func TestColorProcessing_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":ColorProcessing", + Text: "RGBa32", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeColorProcessing(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != RGBA32 { + t.Errorf("expected value RGBA32, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestColorProcessing_InvalidBooleanAttributes(t *testing.T) { + // Test that invalid boolean values in attributes are rejected + root := xmldoc.Element{ + Name: NsWSCN + ":ColorProcessing", + Text: "RGB24", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeColorProcessing(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestColorProcessing_AllStandardValuesWithAttributes(t *testing.T) { + standardValues := []struct { + enumValue ColorEntry + textValue string + }{ + {BlackAndWhite1, "BlackAndWhite1"}, + {Grayscale4, "Grayscale4"}, + {Grayscale8, "Grayscale8"}, + {Grayscale16, "Grayscale16"}, + {RGB24, "RGB24"}, + {RGB48, "RGB48"}, + {RGBA32, "RGBa32"}, + {RGBA64, "RGBa64"}, + } + + for _, tc := range standardValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := ColorProcessing{ + Value: tc.enumValue, + MustHonor: optional.New(BooleanElement("1")), + Override: optional.New(BooleanElement("0")), + UsedDefault: optional.New(BooleanElement("true")), + } + + elm := toXMLColorProcessing(orig, NsWSCN+":ColorProcessing") + decoded, err := decodeColorProcessing(elm) + if err != nil { + t.Fatalf("decode returned error for value '%s': %v", tc.textValue, err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes for value '%s', got %d", tc.textValue, len(elm.Attrs)) + } + }) + } +} + +func TestColorProcessing_VendorDefinedValues(t *testing.T) { + // Test that vendor-defined values decode to UnknownColorEntry + vendorValues := []string{"vendor-color-1", "custom-color", "extended-color-value"} + + for _, val := range vendorValues { + t.Run(val, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":ColorProcessing", + Text: val, + } + + decoded, err := decodeColorProcessing(root) + if err != nil { + t.Fatalf("decode returned error for vendor-defined value '%s': %v", val, err) + } + if decoded.Value != UnknownColorEntry { + t.Errorf("expected UnknownColorEntry, got %v", decoded.Value) + } + // When encoding UnknownColorEntry, it will return "Unknown" + elm := toXMLColorProcessing(decoded, NsWSCN+":ColorProcessing") + if elm.Text != "Unknown" { + t.Errorf("expected text 'Unknown' for UnknownColorEntry, got '%s'", elm.Text) + } + }) + } +} diff --git a/proto/wsscan/compressionqualityfactor.go b/proto/wsscan/compressionqualityfactor.go new file mode 100644 index 00000000..a43bd5b8 --- /dev/null +++ b/proto/wsscan/compressionqualityfactor.go @@ -0,0 +1,44 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// CompressionQualityFactor element + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// CompressionQualityFactor represents the optional +// element that specifies an idealized integer amount of image quality, +// on a scale from 0 through 100. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +type CompressionQualityFactor = AttributedElement[int] + +// decodeCompressionQualityFactor decodes [CompressionQualityFactor] from the XML tree. +func decodeCompressionQualityFactor(root xmldoc.Element) ( + CompressionQualityFactor, error) { + return decodeAttributedElement(root, func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %q", s) + } + if val < 0 || val > 100 { + return 0, fmt.Errorf("value out of range [0-100]: %d", val) + } + return val, nil + }) +} + +// toXMLCompressionQualityFactor generates XML tree for the [CompressionQualityFactor]. +func toXMLCompressionQualityFactor(cqf CompressionQualityFactor, name string) xmldoc.Element { + return cqf.toXML(name, strconv.Itoa) +} diff --git a/proto/wsscan/compressionqualityfactor_test.go b/proto/wsscan/compressionqualityfactor_test.go new file mode 100644 index 00000000..45fb8243 --- /dev/null +++ b/proto/wsscan/compressionqualityfactor_test.go @@ -0,0 +1,197 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for CompressionQualityFactor + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestCompressionQualityFactor_RoundTrip(t *testing.T) { + orig := CompressionQualityFactor{ + Value: 85, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLCompressionQualityFactor(orig, NsWSCN+":CompressionQualityFactor") + + if elm.Name != NsWSCN+":CompressionQualityFactor" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":CompressionQualityFactor", elm.Name) + } + if elm.Text != "85" { + t.Errorf("expected text '85', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeCompressionQualityFactor(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestCompressionQualityFactor_NoAttributes(t *testing.T) { + orig := CompressionQualityFactor{ + Value: 50, + } + + elm := toXMLCompressionQualityFactor(orig, NsWSCN+":CompressionQualityFactor") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "50" { + t.Errorf("expected text '50', got '%s'", elm.Text) + } + + decoded, err := decodeCompressionQualityFactor(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestCompressionQualityFactor_Validation(t *testing.T) { + tests := []struct { + name string + text string + wantErr bool + wantVal int + }{ + { + name: "valid minimum", + text: "0", + wantErr: false, + wantVal: 0, + }, + { + name: "valid maximum", + text: "100", + wantErr: false, + wantVal: 100, + }, + { + name: "valid middle", + text: "50", + wantErr: false, + wantVal: 50, + }, + { + name: "invalid negative", + text: "-1", + wantErr: true, + }, + { + name: "invalid too large", + text: "101", + wantErr: true, + }, + { + name: "invalid not a number", + text: "abc", + wantErr: true, + }, + { + name: "invalid empty", + text: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":CompressionQualityFactor", + Text: tt.text, + } + + decoded, err := decodeCompressionQualityFactor(root) + if tt.wantErr { + if err == nil { + t.Errorf("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if decoded.Value != tt.wantVal { + t.Errorf("expected value %d, got %d", tt.wantVal, decoded.Value) + } + } + }) + } +} + +func TestCompressionQualityFactor_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":CompressionQualityFactor", + Text: "75", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeCompressionQualityFactor(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != 75 { + t.Errorf("expected value 75, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} diff --git a/proto/wsscan/contenttype.go b/proto/wsscan/contenttype.go new file mode 100644 index 00000000..c70bdde0 --- /dev/null +++ b/proto/wsscan/contenttype.go @@ -0,0 +1,42 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// ContentType element + +package wsscan + +import ( + "fmt" + + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// ContentType represents the optional element +// that specifies the document content type. +// +// Supported values are defined by [ContentTypeValue]. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +type ContentType = AttributedElement[ContentTypeValue] + +// decodeContentType decodes [ContentType] from the XML tree. +func decodeContentType(root xmldoc.Element) (ContentType, error) { + return decodeAttributedElement(root, func(s string) (ContentTypeValue, error) { + val := DecodeContentTypeValue(s) + if val == UnknownContentTypeValue { + return val, xmldoc.XMLErrWrap(root, fmt.Errorf("invalid ContentTypeValue: %q", s)) + } + return val, nil + }) +} + +// toXMLContentType generates XML tree for the [ContentType]. +func toXMLContentType(ct ContentType, name string) xmldoc.Element { + return ct.toXML(name, func(v ContentTypeValue) string { + return v.String() + }) +} diff --git a/proto/wsscan/contenttype_test.go b/proto/wsscan/contenttype_test.go new file mode 100644 index 00000000..6059d3a3 --- /dev/null +++ b/proto/wsscan/contenttype_test.go @@ -0,0 +1,167 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for ContentType + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestContentType_RoundTrip(t *testing.T) { + orig := ContentType{ + Value: Auto, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLContentType(orig, NsWSCN+":ContentType") + + if elm.Name != NsWSCN+":ContentType" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":ContentType", elm.Name) + } + if elm.Text != "Auto" { + t.Errorf("expected text 'Auto', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeContentType(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestContentType_NoAttributes(t *testing.T) { + orig := ContentType{ + Value: Text, + } + + elm := toXMLContentType(orig, NsWSCN+":ContentType") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "Text" { + t.Errorf("expected text 'Text', got '%s'", elm.Text) + } + + decoded, err := decodeContentType(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestContentType_StandardValues(t *testing.T) { + standardValues := []ContentTypeValue{Auto, Text, Photo, Halftone, Mixed} + + for _, val := range standardValues { + t.Run(val.String(), func(t *testing.T) { + orig := ContentType{ + Value: val, + } + + elm := toXMLContentType(orig, NsWSCN+":ContentType") + if elm.Text != val.String() { + t.Errorf("expected text '%s', got '%s'", val.String(), elm.Text) + } + + decoded, err := decodeContentType(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != val { + t.Errorf("expected value %s, got %s", val, decoded.Value) + } + }) + } +} + +func TestContentType_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":ContentType", + Text: "Photo", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeContentType(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != Photo { + t.Errorf("expected value 'Photo', got '%s'", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestContentType_InvalidBooleanAttributes(t *testing.T) { + // Test that invalid boolean values in attributes are rejected + root := xmldoc.Element{ + Name: NsWSCN + ":ContentType", + Text: "Text", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeContentType(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} diff --git a/proto/wsscan/exposure.go b/proto/wsscan/exposure.go new file mode 100644 index 00000000..3e38f574 --- /dev/null +++ b/proto/wsscan/exposure.go @@ -0,0 +1,103 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Exposure element + +package wsscan + +import ( + "fmt" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// Exposure represents the optional element +// that specifies the exposure settings of the document. +// +// It includes an optional wscn:MustHonor attribute (xs:string, +// but should be a boolean value: 0, false, 1, or true). +// +// The element contains child elements: AutoExposure (required) +// and ExposureSettings (required). +type Exposure struct { + MustHonor optional.Val[BooleanElement] + AutoExposure BooleanElement + ExposureSettings ExposureSettings +} + +// toXML generates XML tree for the [Exposure]. +func (exp Exposure) toXML(name string) xmldoc.Element { + children := []xmldoc.Element{ + exp.AutoExposure.toXML(NsWSCN + ":AutoExposure"), + exp.ExposureSettings.toXML(NsWSCN + ":ExposureSettings"), + } + + elm := xmldoc.Element{ + Name: name, + Children: children, + } + + // Add optional MustHonor attribute if present + if mustHonor := optional.Get(exp.MustHonor); mustHonor != "" { + elm.Attrs = []xmldoc.Attr{ + { + Name: NsWSCN + ":MustHonor", + Value: string(mustHonor), + }, + } + } + + return elm +} + +// decodeExposure decodes [Exposure] from the XML tree. +func decodeExposure(root xmldoc.Element) (Exposure, error) { + var exp Exposure + + // Decode optional MustHonor attribute with validation + if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found { + mustHonor := BooleanElement(attr.Value) + if err := mustHonor.Validate(); err != nil { + return exp, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err)) + } + exp.MustHonor = optional.New(mustHonor) + } + + // Decode required child elements + var autoExposureFound, exposureSettingsFound bool + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":AutoExposure": + autoExp, err := decodeBooleanElement(child) + if err != nil { + return exp, fmt.Errorf("autoExposure: %w", + xmldoc.XMLErrWrap(child, err)) + } + exp.AutoExposure = autoExp + autoExposureFound = true + case NsWSCN + ":ExposureSettings": + expSettings, err := decodeExposureSettings(child) + if err != nil { + return exp, fmt.Errorf("exposureSettings: %w", + xmldoc.XMLErrWrap(child, err)) + } + exp.ExposureSettings = expSettings + exposureSettingsFound = true + } + } + + if !autoExposureFound { + return exp, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:AutoExposure", NsWSCN)) + } + if !exposureSettingsFound { + return exp, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:ExposureSettings", NsWSCN)) + } + + return exp, nil +} diff --git a/proto/wsscan/exposure_test.go b/proto/wsscan/exposure_test.go new file mode 100644 index 00000000..ee78ae40 --- /dev/null +++ b/proto/wsscan/exposure_test.go @@ -0,0 +1,171 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for Exposure + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestExposure_RoundTrip(t *testing.T) { + orig := Exposure{ + MustHonor: optional.New(BooleanElement("true")), + AutoExposure: BooleanElement("1"), + ExposureSettings: ExposureSettings{ + Brightness: optional.New(AttributedElement[int]{ + Value: 50, + }), + }, + } + + elm := orig.toXML(NsWSCN + ":Exposure") + + if elm.Name != NsWSCN+":Exposure" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":Exposure", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + + // Check MustHonor attribute + if elm.Attrs[0].Name != NsWSCN+":MustHonor" { + t.Errorf("expected attribute name '%s', got '%s'", + NsWSCN+":MustHonor", elm.Attrs[0].Name) + } + if elm.Attrs[0].Value != "true" { + t.Errorf("expected MustHonor='true', got '%s'", elm.Attrs[0].Value) + } + + // Decode back + decoded, err := decodeExposure(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if orig.AutoExposure != decoded.AutoExposure { + t.Errorf("expected AutoExposure %+v, got %+v", orig.AutoExposure, decoded.AutoExposure) + } + if !reflect.DeepEqual(orig.ExposureSettings, decoded.ExposureSettings) { + t.Errorf("expected ExposureSettings %+v, got %+v", orig.ExposureSettings, decoded.ExposureSettings) + } +} + +func TestExposure_MissingAutoExposure(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Exposure", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ExposureSettings", + Children: []xmldoc.Element{}, + }, + }, + } + + _, err := decodeExposure(root) + if err == nil { + t.Errorf("expected error for missing AutoExposure, got nil") + } +} + +func TestExposure_MissingExposureSettings(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Exposure", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":AutoExposure", + Text: "true", + }, + }, + } + + _, err := decodeExposure(root) + if err == nil { + t.Errorf("expected error for missing ExposureSettings, got nil") + } +} + +func TestExposure_FromXML(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Exposure", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "1"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":AutoExposure", + Text: "true", + }, + { + Name: NsWSCN + ":ExposureSettings", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Brightness", + Text: "50", + }, + }, + }, + }, + } + + decoded, err := decodeExposure(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "1" { + t.Errorf("expected MustHonor='1', got '%s'", mustHonor) + } + if decoded.AutoExposure != BooleanElement("true") { + t.Errorf("expected AutoExposure='true', got '%v'", decoded.AutoExposure) + } + expSettings := decoded.ExposureSettings + if brightness := optional.Get(expSettings.Brightness); brightness.Value != 50 { + t.Errorf("expected Brightness=50, got %d", brightness.Value) + } +} + +func TestExposure_InvalidBooleanAttribute(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Exposure", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeExposure(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestExposure_InvalidAutoExposure(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Exposure", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":AutoExposure", + Text: "invalid", + }, + }, + } + + _, err := decodeExposure(root) + if err == nil { + t.Errorf("expected error for invalid AutoExposure value 'invalid', got nil") + } +} diff --git a/proto/wsscan/exposuresettings.go b/proto/wsscan/exposuresettings.go new file mode 100644 index 00000000..a3a9e273 --- /dev/null +++ b/proto/wsscan/exposuresettings.go @@ -0,0 +1,103 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// ExposureSettings element +// +// The required element contains individual +// adjustment values that the WSD Scan Service should apply to the +// image data after acquisition. + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// ExposureSettings represents the element. +// +// It has no attributes and the following optional child elements: +// - +// - +// - +// +// Each child element is modeled as [AttributedElement] with +// int value and optional Override / UsedDefault attributes. +type ExposureSettings struct { + Brightness optional.Val[AttributedElement[int]] + Contrast optional.Val[AttributedElement[int]] + Sharpness optional.Val[AttributedElement[int]] +} + +// toXML generates XML tree for the [ExposureSettings]. +func (es ExposureSettings) toXML(name string) xmldoc.Element { + children := make([]xmldoc.Element, 0, 3) + + if es.Brightness != nil { + b := optional.Get(es.Brightness) + children = append(children, b.toXML(NsWSCN+":Brightness", + strconv.Itoa)) + } + if es.Contrast != nil { + c := optional.Get(es.Contrast) + children = append(children, c.toXML(NsWSCN+":Contrast", + strconv.Itoa)) + } + if es.Sharpness != nil { + s := optional.Get(es.Sharpness) + children = append(children, s.toXML(NsWSCN+":Sharpness", + strconv.Itoa)) + } + + return xmldoc.Element{ + Name: name, + Children: children, + } +} + +// decodeExposureSettings decodes [ExposureSettings] from the XML tree. +func decodeExposureSettings(root xmldoc.Element) (ExposureSettings, error) { + var es ExposureSettings + + decodeValue := func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %w", err) + } + return val, nil + } + + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":Brightness": + val, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return es, fmt.Errorf("brightness: %w", + xmldoc.XMLErrWrap(child, err)) + } + es.Brightness = optional.New(val) + case NsWSCN + ":Contrast": + val, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return es, fmt.Errorf("contrast: %w", + xmldoc.XMLErrWrap(child, err)) + } + es.Contrast = optional.New(val) + case NsWSCN + ":Sharpness": + val, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return es, fmt.Errorf("sharpness: %w", + xmldoc.XMLErrWrap(child, err)) + } + es.Sharpness = optional.New(val) + } + } + + return es, nil +} diff --git a/proto/wsscan/exposuresettings_test.go b/proto/wsscan/exposuresettings_test.go new file mode 100644 index 00000000..21b75085 --- /dev/null +++ b/proto/wsscan/exposuresettings_test.go @@ -0,0 +1,106 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for ExposureSettings + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestExposureSettings_RoundTrip(t *testing.T) { + orig := ExposureSettings{ + Brightness: optional.New(AttributedElement[int]{ + Value: 50, + Override: optional.New(BooleanElement("1")), + }), + Contrast: optional.New(AttributedElement[int]{ + Value: 75, + }), + } + + elm := orig.toXML(NsWSCN + ":ExposureSettings") + + if elm.Name != NsWSCN+":ExposureSettings" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":ExposureSettings", elm.Name) + } + + decoded, err := decodeExposureSettings(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if !reflect.DeepEqual(optional.Get(orig.Brightness), optional.Get(decoded.Brightness)) { + t.Errorf("Brightness mismatch: expected %+v, got %+v", + optional.Get(orig.Brightness), optional.Get(decoded.Brightness)) + } + if !reflect.DeepEqual(optional.Get(orig.Contrast), optional.Get(decoded.Contrast)) { + t.Errorf("Contrast mismatch: expected %+v, got %+v", + optional.Get(orig.Contrast), optional.Get(decoded.Contrast)) + } + if decoded.Sharpness != nil { + t.Errorf("expected Sharpness to be nil, got %+v", optional.Get(decoded.Sharpness)) + } +} + +func TestExposureSettings_FromXML(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":ExposureSettings", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Brightness", + Text: "50", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "0"}, + }, + }, + { + Name: NsWSCN + ":Sharpness", + Text: "25", + }, + }, + } + + decoded, err := decodeExposureSettings(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if optional.Get(decoded.Brightness).Value != 50 { + t.Errorf("expected Brightness value 50, got '%v'", + optional.Get(decoded.Brightness).Value) + } + if override := optional.Get(optional.Get(decoded.Brightness).Override); override != "0" { + t.Errorf("expected Brightness Override='0', got '%s'", override) + } + if optional.Get(decoded.Sharpness).Value != 25 { + t.Errorf("expected Sharpness value 25, got '%v'", + optional.Get(decoded.Sharpness).Value) + } +} + +func TestExposureSettings_InvalidIntegerValue(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":ExposureSettings", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Brightness", + Text: "invalid", + }, + }, + } + + _, err := decodeExposureSettings(root) + if err == nil { + t.Errorf("expected error for invalid Brightness value, got nil") + } +} diff --git a/proto/wsscan/filmscanmodeelement.go b/proto/wsscan/filmscanmodeelement.go new file mode 100644 index 00000000..11141e3b --- /dev/null +++ b/proto/wsscan/filmscanmodeelement.go @@ -0,0 +1,42 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// FilmScanMode element (not to be confused with FilmScanMode enum type) + +package wsscan + +import ( + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// FilmScanModeElement represents the optional element +// that specifies the exposure type of the film to be scanned. +// +// Standard values include: "NotApplicable", "ColorSlideFilm", +// "ColorNegativeFilm", "BlackandWhiteNegativeFilm". +// Values can be extended or subset, so any string value is accepted. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +// +// Note: This is different from the [FilmScanMode] enum type which is used +// in FilmScanModesSupported lists. +type FilmScanModeElement = AttributedElement[string] + +// decodeFilmScanModeElement decodes [FilmScanModeElement] from the XML tree. +func decodeFilmScanModeElement(root xmldoc.Element) (FilmScanModeElement, error) { + return decodeAttributedElement(root, func(s string) (string, error) { + // Accept any string value as values can be extended/subset + return s, nil + }) +} + +// toXMLFilmScanModeElement generates XML tree for the [FilmScanModeElement]. +func toXMLFilmScanModeElement(fsm FilmScanModeElement, name string) xmldoc.Element { + return fsm.toXML(name, func(s string) string { + return s + }) +} diff --git a/proto/wsscan/filmscanmodeelement_test.go b/proto/wsscan/filmscanmodeelement_test.go new file mode 100644 index 00000000..947f4d21 --- /dev/null +++ b/proto/wsscan/filmscanmodeelement_test.go @@ -0,0 +1,194 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for FilmScanModeElement + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestFilmScanModeElement_RoundTrip(t *testing.T) { + orig := FilmScanModeElement{ + Value: "ColorSlideFilm", + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLFilmScanModeElement(orig, NsWSCN+":FilmScanMode") + + if elm.Name != NsWSCN+":FilmScanMode" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":FilmScanMode", elm.Name) + } + if elm.Text != "ColorSlideFilm" { + t.Errorf("expected text 'ColorSlideFilm', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeFilmScanModeElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestFilmScanModeElement_NoAttributes(t *testing.T) { + orig := FilmScanModeElement{ + Value: "NotApplicable", + } + + elm := toXMLFilmScanModeElement(orig, NsWSCN+":FilmScanMode") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "NotApplicable" { + t.Errorf("expected text 'NotApplicable', got '%s'", elm.Text) + } + + decoded, err := decodeFilmScanModeElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestFilmScanModeElement_StandardValues(t *testing.T) { + standardValues := []string{ + "NotApplicable", + "ColorSlideFilm", + "ColorNegativeFilm", + "BlackandWhiteNegativeFilm", + } + + for _, val := range standardValues { + t.Run(val, func(t *testing.T) { + orig := FilmScanModeElement{ + Value: val, + } + + elm := toXMLFilmScanModeElement(orig, NsWSCN+":FilmScanMode") + if elm.Text != val { + t.Errorf("expected text '%s', got '%s'", val, elm.Text) + } + + decoded, err := decodeFilmScanModeElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != val { + t.Errorf("expected value %s, got %s", val, decoded.Value) + } + }) + } +} + +func TestFilmScanModeElement_ExtendedValues(t *testing.T) { + // Test that extended values are accepted (as per spec: "You can both extend and subset values") + extendedValues := []string{"CustomFilmType", "AnotherType", "ExtendedValue"} + + for _, val := range extendedValues { + t.Run(val, func(t *testing.T) { + orig := FilmScanModeElement{ + Value: val, + } + + elm := toXMLFilmScanModeElement(orig, NsWSCN+":FilmScanMode") + decoded, err := decodeFilmScanModeElement(elm) + if err != nil { + t.Fatalf("decode returned error for extended value '%s': %v", val, err) + } + if decoded.Value != val { + t.Errorf("expected value %s, got %s", val, decoded.Value) + } + }) + } +} + +func TestFilmScanModeElement_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":FilmScanMode", + Text: "ColorNegativeFilm", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeFilmScanModeElement(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != "ColorNegativeFilm" { + t.Errorf("expected value 'ColorNegativeFilm', got '%s'", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestFilmScanModeElement_InvalidBooleanAttributes(t *testing.T) { + // Test that invalid boolean values in attributes are rejected + root := xmldoc.Element{ + Name: NsWSCN + ":FilmScanMode", + Text: "ColorSlideFilm", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeFilmScanModeElement(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} diff --git a/proto/wsscan/formatelement.go b/proto/wsscan/formatelement.go new file mode 100644 index 00000000..9e17a624 --- /dev/null +++ b/proto/wsscan/formatelement.go @@ -0,0 +1,39 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Format element (not to be confused with FormatValue enum type) + +package wsscan + +import ( + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// FormatElement represents the optional element +// that indicates a single file format and compression type supported by the scanner. +// +// Standard values include: "dib", "exif", "jbig", "jfif", "jpeg2k", "pdf-a", "png", +// "tiff-single-uncompressed", "tiff-single-g4", "tiff-single-g3mh", +// "tiff-single-jpeg-tn2", "tiff-multi-uncompressed", "tiff-multi-g4", +// "tiff-multi-g3mh", "tiff-multi-jpeg-tn2", "xps". +// Vendor-defined values are also allowed and will decode as UnknownFormatValue. +// +// It includes optional wscn:Override and wscn:UsedDefault attributes +// (all xs:string, but should be boolean values: 0, false, 1, or true). +// Note: This element does NOT have a MustHonor attribute. +type FormatElement = AttributedElement[FormatValue] + +// decodeFormatElement decodes [FormatElement] from the XML tree. +func decodeFormatElement(root xmldoc.Element) (FormatElement, error) { + return decodeAttributedElement(root, func(s string) (FormatValue, error) { + return DecodeFormatValue(s), nil + }) +} + +// toXMLFormatElement generates XML tree for the [FormatElement]. +func toXMLFormatElement(f FormatElement, name string) xmldoc.Element { + return f.toXML(name, FormatValue.String) +} diff --git a/proto/wsscan/formatelement_test.go b/proto/wsscan/formatelement_test.go new file mode 100644 index 00000000..2f3ff61a --- /dev/null +++ b/proto/wsscan/formatelement_test.go @@ -0,0 +1,261 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for FormatElement + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestFormatElement_RoundTrip(t *testing.T) { + orig := FormatElement{ + Value: PNG, + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLFormatElement(orig, NsWSCN+":Format") + + if elm.Name != NsWSCN+":Format" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":Format", elm.Name) + } + if elm.Text != "png" { + t.Errorf("expected text 'png', got '%s'", elm.Text) + } + if len(elm.Attrs) != 2 { + t.Errorf("expected 2 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + // MustHonor should not be present + if _, found := attrsMap[NsWSCN+":MustHonor"]; found { + t.Errorf("expected MustHonor to not be present, but it was found") + } + + // Decode back + decoded, err := decodeFormatElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestFormatElement_NoAttributes(t *testing.T) { + orig := FormatElement{ + Value: JPEG2K, + } + + elm := toXMLFormatElement(orig, NsWSCN+":Format") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "jpeg2k" { + t.Errorf("expected text 'jpeg2k', got '%s'", elm.Text) + } + + decoded, err := decodeFormatElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestFormatElement_StandardValues(t *testing.T) { + standardValues := []struct { + formatValue FormatValue + textValue string + }{ + {DIB, "dib"}, + {EXIF, "exif"}, + {JBIG, "jbig"}, + {JFIF, "jfif"}, + {JPEG2K, "jpeg2k"}, + {PDFA, "pdf-a"}, + {PNG, "png"}, + {TIFFSingleUncompressed, "tiff-single-uncompressed"}, + {TIFFSingleG4, "tiff-single-g4"}, + {TIFFSingleG3MH, "tiff-single-g3mh"}, + {TIFFSingleJPEGTN2, "tiff-single-jpeg-tn2"}, + {TIFFMultiUncompressed, "tiff-multi-uncompressed"}, + {TIFFMultiG4, "tiff-multi-g4"}, + {TIFFMultiG3MH, "tiff-multi-g3mh"}, + {TIFFMultiJPEGTN2, "tiff-multi-jpeg-tn2"}, + {XPS, "xps"}, + } + + for _, tc := range standardValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := FormatElement{ + Value: tc.formatValue, + } + + elm := toXMLFormatElement(orig, NsWSCN+":Format") + if elm.Text != tc.textValue { + t.Errorf("expected text '%s', got '%s'", tc.textValue, elm.Text) + } + + decoded, err := decodeFormatElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != tc.formatValue { + t.Errorf("expected value %v, got %v", tc.formatValue, decoded.Value) + } + }) + } +} + +func TestFormatElement_VendorDefinedValues(t *testing.T) { + // Test that vendor-defined values decode to UnknownFormatValue + vendorValues := []string{"vendor-format-1", "custom-format", "extended-value"} + + for _, val := range vendorValues { + t.Run(val, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Format", + Text: val, + } + + decoded, err := decodeFormatElement(root) + if err != nil { + t.Fatalf("decode returned error for vendor-defined value '%s': %v", val, err) + } + if decoded.Value != UnknownFormatValue { + t.Errorf("expected UnknownFormatValue, got %v", decoded.Value) + } + // When encoding UnknownFormatValue, it will return "Unknown" + elm := toXMLFormatElement(decoded, NsWSCN+":Format") + if elm.Text != "Unknown" { + t.Errorf("expected text 'Unknown' for UnknownFormatValue, got '%s'", elm.Text) + } + }) + } +} + +func TestFormatElement_FromXML(t *testing.T) { + // Create XML element manually with Override and UsedDefault (no MustHonor) + root := xmldoc.Element{ + Name: NsWSCN + ":Format", + Text: "tiff-multi-g4", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "0"}, + {Name: NsWSCN + ":UsedDefault", Value: "true"}, + }, + } + + decoded, err := decodeFormatElement(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != TIFFMultiG4 { + t.Errorf("expected value TIFFMultiG4, got %v", decoded.Value) + } + if override := optional.Get(decoded.Override); string(override) != "0" { + t.Errorf("expected Override='0', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "true" { + t.Errorf("expected UsedDefault='true', got '%s'", usedDefault) + } + // MustHonor should not be set + if decoded.MustHonor != nil { + t.Errorf("expected MustHonor to be nil, got %+v", decoded.MustHonor) + } +} + +func TestFormatElement_InvalidBooleanAttributes(t *testing.T) { + // Test that invalid boolean values in attributes are rejected + root := xmldoc.Element{ + Name: NsWSCN + ":Format", + Text: "png", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "invalid"}, + }, + } + + _, err := decodeFormatElement(root) + if err == nil { + t.Errorf("expected error for invalid Override value 'invalid', got nil") + } +} + +func TestFormatElement_OnlyOverride(t *testing.T) { + // Test with only Override attribute + orig := FormatElement{ + Value: PDFA, + Override: optional.New(BooleanElement("1")), + } + + elm := toXMLFormatElement(orig, NsWSCN+":Format") + + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + if elm.Attrs[0].Name != NsWSCN+":Override" { + t.Errorf("expected Override attribute, got %s", elm.Attrs[0].Name) + } + + decoded, err := decodeFormatElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != PDFA { + t.Errorf("expected value PDFA, got %v", decoded.Value) + } +} + +func TestFormatElement_OnlyUsedDefault(t *testing.T) { + // Test with only UsedDefault attribute + orig := FormatElement{ + Value: EXIF, + UsedDefault: optional.New(BooleanElement("false")), + } + + elm := toXMLFormatElement(orig, NsWSCN+":Format") + + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + if elm.Attrs[0].Name != NsWSCN+":UsedDefault" { + t.Errorf("expected UsedDefault attribute, got %s", elm.Attrs[0].Name) + } + + decoded, err := decodeFormatElement(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != EXIF { + t.Errorf("expected value EXIF, got %v", decoded.Value) + } +} diff --git a/proto/wsscan/imagestotransfer.go b/proto/wsscan/imagestotransfer.go new file mode 100644 index 00000000..e0af974b --- /dev/null +++ b/proto/wsscan/imagestotransfer.go @@ -0,0 +1,45 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// ImagesToTransfer element + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// ImagesToTransfer represents the optional element +// that specifies the number of images to scan for the current job. +// +// The value must be an integer in the range from 0 through 2147483648. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +type ImagesToTransfer = AttributedElement[int] + +// decodeImagesToTransfer decodes [ImagesToTransfer] from the XML tree. +func decodeImagesToTransfer(root xmldoc.Element) ( + ImagesToTransfer, error) { + return decodeAttributedElement(root, func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %q", s) + } + if val < 0 || val > 2147483648 { + return 0, fmt.Errorf("value out of range [0-2147483648]: %d", val) + } + return val, nil + }) +} + +// toXMLImagesToTransfer generates XML tree for the [ImagesToTransfer]. +func toXMLImagesToTransfer(itt ImagesToTransfer, name string) xmldoc.Element { + return itt.toXML(name, strconv.Itoa) +} diff --git a/proto/wsscan/imagestotransfer_test.go b/proto/wsscan/imagestotransfer_test.go new file mode 100644 index 00000000..62178903 --- /dev/null +++ b/proto/wsscan/imagestotransfer_test.go @@ -0,0 +1,249 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for ImagesToTransfer + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestImagesToTransfer_RoundTrip(t *testing.T) { + orig := ImagesToTransfer{ + Value: 10, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLImagesToTransfer(orig, NsWSCN+":ImagesToTransfer") + + if elm.Name != NsWSCN+":ImagesToTransfer" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":ImagesToTransfer", elm.Name) + } + if elm.Text != "10" { + t.Errorf("expected text '10', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeImagesToTransfer(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestImagesToTransfer_NoAttributes(t *testing.T) { + orig := ImagesToTransfer{ + Value: 5, + } + + elm := toXMLImagesToTransfer(orig, NsWSCN+":ImagesToTransfer") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "5" { + t.Errorf("expected text '5', got '%s'", elm.Text) + } + + decoded, err := decodeImagesToTransfer(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestImagesToTransfer_Validation(t *testing.T) { + tests := []struct { + name string + text string + wantErr bool + wantVal int + }{ + { + name: "valid minimum", + text: "0", + wantErr: false, + wantVal: 0, + }, + { + name: "valid maximum", + text: "2147483648", + wantErr: false, + wantVal: 2147483648, + }, + { + name: "valid middle", + text: "100", + wantErr: false, + wantVal: 100, + }, + { + name: "valid large value", + text: "1000000", + wantErr: false, + wantVal: 1000000, + }, + { + name: "invalid negative", + text: "-1", + wantErr: true, + }, + { + name: "invalid too large", + text: "2147483649", + wantErr: true, + }, + { + name: "invalid not a number", + text: "abc", + wantErr: true, + }, + { + name: "invalid empty", + text: "", + wantErr: true, + }, + { + name: "invalid float", + text: "10.5", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":ImagesToTransfer", + Text: tt.text, + } + + decoded, err := decodeImagesToTransfer(root) + if tt.wantErr { + if err == nil { + t.Errorf("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if decoded.Value != tt.wantVal { + t.Errorf("expected value %d, got %d", tt.wantVal, decoded.Value) + } + } + }) + } +} + +func TestImagesToTransfer_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":ImagesToTransfer", + Text: "25", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeImagesToTransfer(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != 25 { + t.Errorf("expected value 25, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestImagesToTransfer_EdgeCases(t *testing.T) { + tests := []struct { + name string + value int + }{ + { + name: "zero", + value: 0, + }, + { + name: "one", + value: 1, + }, + { + name: "maximum", + value: 2147483648, + }, + { + name: "near maximum", + value: 2147483647, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + orig := ImagesToTransfer{ + Value: tt.value, + } + + elm := toXMLImagesToTransfer(orig, NsWSCN+":ImagesToTransfer") + decoded, err := decodeImagesToTransfer(elm) + if err != nil { + t.Fatalf("decode returned error for value %d: %v", tt.value, err) + } + if decoded.Value != tt.value { + t.Errorf("expected value %d, got %d", tt.value, decoded.Value) + } + }) + } +} diff --git a/proto/wsscan/inputmediasize.go b/proto/wsscan/inputmediasize.go new file mode 100644 index 00000000..aa14b2b3 --- /dev/null +++ b/proto/wsscan/inputmediasize.go @@ -0,0 +1,89 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// InputMediaSize element + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// InputMediaSize represents the required element +// that specifies the size of the media to be scanned for the current job. +// +// It has no attributes and the following required child elements: +// - +// - +// +// Both Width and Height are modeled as [AttributedElement] with +// int value and optional Override / UsedDefault attributes. +// The values must be in the range from 1 through 2147483648 and are +// in units of one-thousandths (1/1000) of an inch. +type InputMediaSize struct { + Width AttributedElement[int] + Height AttributedElement[int] +} + +// toXML generates XML tree for the [InputMediaSize]. +func (ims InputMediaSize) toXML(name string) xmldoc.Element { + return xmldoc.Element{ + Name: name, + Children: []xmldoc.Element{ + ims.Width.toXML(NsWSCN+":Width", strconv.Itoa), + ims.Height.toXML(NsWSCN+":Height", strconv.Itoa), + }, + } +} + +// decodeInputMediaSize decodes [InputMediaSize] from the XML tree. +func decodeInputMediaSize(root xmldoc.Element) (InputMediaSize, error) { + var ims InputMediaSize + + decodeValue := func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %w", err) + } + return val, nil + } + + var widthFound, heightFound bool + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":Width": + width, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return ims, fmt.Errorf("width: %w", + xmldoc.XMLErrWrap(child, err)) + } + ims.Width = width + widthFound = true + case NsWSCN + ":Height": + height, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return ims, fmt.Errorf("height: %w", + xmldoc.XMLErrWrap(child, err)) + } + ims.Height = height + heightFound = true + } + } + + if !widthFound { + return ims, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:Width", NsWSCN)) + } + if !heightFound { + return ims, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:Height", NsWSCN)) + } + + return ims, nil +} diff --git a/proto/wsscan/inputmediasize_test.go b/proto/wsscan/inputmediasize_test.go new file mode 100644 index 00000000..a6b213a3 --- /dev/null +++ b/proto/wsscan/inputmediasize_test.go @@ -0,0 +1,204 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for InputMediaSize + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestInputMediaSize_RoundTrip(t *testing.T) { + orig := InputMediaSize{ + Width: AttributedElement[int]{ + Value: 8500, + Override: optional.New(BooleanElement("1")), + }, + Height: AttributedElement[int]{ + Value: 11000, + UsedDefault: optional.New(BooleanElement("true")), + }, + } + + elm := orig.toXML(NsWSCN + ":InputMediaSize") + + if elm.Name != NsWSCN+":InputMediaSize" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":InputMediaSize", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + + decoded, err := decodeInputMediaSize(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if !reflect.DeepEqual(orig.Width, decoded.Width) { + t.Errorf("Width mismatch: expected %+v, got %+v", orig.Width, decoded.Width) + } + if !reflect.DeepEqual(orig.Height, decoded.Height) { + t.Errorf("Height mismatch: expected %+v, got %+v", orig.Height, decoded.Height) + } +} + +func TestInputMediaSize_NoAttributes(t *testing.T) { + orig := InputMediaSize{ + Width: AttributedElement[int]{ + Value: 8500, + }, + Height: AttributedElement[int]{ + Value: 11000, + }, + } + + elm := orig.toXML(NsWSCN + ":InputMediaSize") + + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if elm.Children[0].Text != "8500" { + t.Errorf("expected Width text '8500', got '%s'", elm.Children[0].Text) + } + if elm.Children[1].Text != "11000" { + t.Errorf("expected Height text '11000', got '%s'", elm.Children[1].Text) + } + + decoded, err := decodeInputMediaSize(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Width.Value != 8500 { + t.Errorf("expected Width=8500, got %d", decoded.Width.Value) + } + if decoded.Height.Value != 11000 { + t.Errorf("expected Height=11000, got %d", decoded.Height.Value) + } +} + +func TestInputMediaSize_FromXML(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "8500", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "0"}, + }, + }, + { + Name: NsWSCN + ":Height", + Text: "11000", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + }, + }, + } + + decoded, err := decodeInputMediaSize(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Width.Value != 8500 { + t.Errorf("expected Width=8500, got %d", decoded.Width.Value) + } + if override := optional.Get(decoded.Width.Override); override != "0" { + t.Errorf("expected Width Override='0', got '%s'", override) + } + if decoded.Height.Value != 11000 { + t.Errorf("expected Height=11000, got %d", decoded.Height.Value) + } + if usedDefault := optional.Get(decoded.Height.UsedDefault); usedDefault != "false" { + t.Errorf("expected Height UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestInputMediaSize_MissingWidth(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Height", + Text: "11000", + }, + }, + } + + _, err := decodeInputMediaSize(root) + if err == nil { + t.Errorf("expected error for missing Width, got nil") + } +} + +func TestInputMediaSize_MissingHeight(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "8500", + }, + }, + } + + _, err := decodeInputMediaSize(root) + if err == nil { + t.Errorf("expected error for missing Height, got nil") + } +} + +func TestInputMediaSize_InvalidIntegerValue(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "invalid", + }, + { + Name: NsWSCN + ":Height", + Text: "11000", + }, + }, + } + + _, err := decodeInputMediaSize(root) + if err == nil { + t.Errorf("expected error for invalid Width value, got nil") + } +} + +func TestInputMediaSize_InvalidBooleanAttributes(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "8500", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "invalid"}, + }, + }, + { + Name: NsWSCN + ":Height", + Text: "11000", + }, + }, + } + + _, err := decodeInputMediaSize(root) + if err == nil { + t.Errorf("expected error for invalid Override value, got nil") + } +} diff --git a/proto/wsscan/inputsize.go b/proto/wsscan/inputsize.go new file mode 100644 index 00000000..0709d6e8 --- /dev/null +++ b/proto/wsscan/inputsize.go @@ -0,0 +1,106 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// InputSize element + +package wsscan + +import ( + "fmt" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// InputSize represents the optional element +// that specifies the size of the original scan media. +// +// It includes an optional wscn:MustHonor attribute (xs:string, +// but should be a boolean value: 0, false, 1, or true). +// +// The element contains child elements: +// - DocumentSizeAutoDetect (optional BooleanElement) +// - InputMediaSize (required InputMediaSize) +type InputSize struct { + MustHonor optional.Val[BooleanElement] + DocumentSizeAutoDetect optional.Val[BooleanElement] + InputMediaSize InputMediaSize +} + +// toXML generates XML tree for the [InputSize]. +func (is InputSize) toXML(name string) xmldoc.Element { + children := make([]xmldoc.Element, 0, 2) + + // Add DocumentSizeAutoDetect if present + if is.DocumentSizeAutoDetect != nil { + autoDetect := optional.Get(is.DocumentSizeAutoDetect) + children = append(children, + autoDetect.toXML(NsWSCN+":DocumentSizeAutoDetect")) + } + + // Add InputMediaSize (required) + children = append(children, is.InputMediaSize.toXML(NsWSCN+":InputMediaSize")) + + elm := xmldoc.Element{ + Name: name, + Children: children, + } + + // Add optional MustHonor attribute if present + if mustHonor := optional.Get(is.MustHonor); mustHonor != "" { + elm.Attrs = []xmldoc.Attr{ + { + Name: NsWSCN + ":MustHonor", + Value: string(mustHonor), + }, + } + } + + return elm +} + +// decodeInputSize decodes [InputSize] from the XML tree. +func decodeInputSize(root xmldoc.Element) (InputSize, error) { + var is InputSize + + // Decode optional MustHonor attribute with validation + if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found { + mustHonor := BooleanElement(attr.Value) + if err := mustHonor.Validate(); err != nil { + return is, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err)) + } + is.MustHonor = optional.New(mustHonor) + } + + // Decode child elements + var inputMediaSizeFound bool + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":DocumentSizeAutoDetect": + autoDetect, err := decodeBooleanElement(child) + if err != nil { + return is, fmt.Errorf("documentSizeAutoDetect: %w", + xmldoc.XMLErrWrap(child, err)) + } + is.DocumentSizeAutoDetect = optional.New(autoDetect) + case NsWSCN + ":InputMediaSize": + mediaSize, err := decodeInputMediaSize(child) + if err != nil { + return is, fmt.Errorf("inputMediaSize: %w", + xmldoc.XMLErrWrap(child, err)) + } + is.InputMediaSize = mediaSize + inputMediaSizeFound = true + } + } + + if !inputMediaSizeFound { + return is, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:InputMediaSize", NsWSCN)) + } + + return is, nil +} diff --git a/proto/wsscan/inputsize_test.go b/proto/wsscan/inputsize_test.go new file mode 100644 index 00000000..8bf5a7ff --- /dev/null +++ b/proto/wsscan/inputsize_test.go @@ -0,0 +1,249 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for InputSize + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestInputSize_RoundTrip(t *testing.T) { + orig := InputSize{ + MustHonor: optional.New(BooleanElement("true")), + DocumentSizeAutoDetect: optional.New(BooleanElement("1")), + InputMediaSize: InputMediaSize{ + Width: AttributedElement[int]{ + Value: 8500, + }, + Height: AttributedElement[int]{ + Value: 11000, + }, + }, + } + + elm := orig.toXML(NsWSCN + ":InputSize") + + if elm.Name != NsWSCN+":InputSize" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":InputSize", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + + // Check MustHonor attribute + if elm.Attrs[0].Name != NsWSCN+":MustHonor" { + t.Errorf("expected attribute name '%s', got '%s'", + NsWSCN+":MustHonor", elm.Attrs[0].Name) + } + if elm.Attrs[0].Value != "true" { + t.Errorf("expected MustHonor='true', got '%s'", elm.Attrs[0].Value) + } + + // Decode back + decoded, err := decodeInputSize(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.DocumentSizeAutoDetect, decoded.DocumentSizeAutoDetect) { + t.Errorf("expected DocumentSizeAutoDetect %+v, got %+v", + orig.DocumentSizeAutoDetect, decoded.DocumentSizeAutoDetect) + } + if !reflect.DeepEqual(orig.InputMediaSize, decoded.InputMediaSize) { + t.Errorf("expected InputMediaSize %+v, got %+v", + orig.InputMediaSize, decoded.InputMediaSize) + } + if decoded.InputMediaSize.Width.Value != 8500 { + t.Errorf("expected InputMediaSize.Width=8500, got %d", decoded.InputMediaSize.Width.Value) + } + if decoded.InputMediaSize.Height.Value != 11000 { + t.Errorf("expected InputMediaSize.Height=11000, got %d", decoded.InputMediaSize.Height.Value) + } +} + +func TestInputSize_DocumentSizeAutoDetectOnly(t *testing.T) { + orig := InputSize{ + DocumentSizeAutoDetect: optional.New(BooleanElement("true")), + } + + elm := orig.toXML(NsWSCN + ":InputSize") + + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if elm.Children[0].Name != NsWSCN+":DocumentSizeAutoDetect" { + t.Errorf("expected child name '%s', got '%s'", + NsWSCN+":DocumentSizeAutoDetect", elm.Children[0].Name) + } + if elm.Children[0].Text != "true" { + t.Errorf("expected DocumentSizeAutoDetect text 'true', got '%s'", elm.Children[0].Text) + } + + decoded, err := decodeInputSize(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.DocumentSizeAutoDetect == nil { + t.Errorf("expected DocumentSizeAutoDetect to be set") + } + if optional.Get(decoded.DocumentSizeAutoDetect) != BooleanElement("true") { + t.Errorf("expected DocumentSizeAutoDetect='true', got '%v'", + optional.Get(decoded.DocumentSizeAutoDetect)) + } +} + +func TestInputSize_InputMediaSizeOnly(t *testing.T) { + orig := InputSize{ + InputMediaSize: InputMediaSize{ + Width: AttributedElement[int]{ + Value: 8500, + }, + Height: AttributedElement[int]{ + Value: 11000, + }, + }, + } + + elm := orig.toXML(NsWSCN + ":InputSize") + + if len(elm.Children) != 1 { + t.Errorf("expected 1 child, got %d", len(elm.Children)) + } + if elm.Children[0].Name != NsWSCN+":InputMediaSize" { + t.Errorf("expected child name '%s', got '%s'", + NsWSCN+":InputMediaSize", elm.Children[0].Name) + } + + decoded, err := decodeInputSize(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.InputMediaSize.Width.Value != 8500 { + t.Errorf("expected Width=8500, got %d", decoded.InputMediaSize.Width.Value) + } + if decoded.InputMediaSize.Height.Value != 11000 { + t.Errorf("expected Height=11000, got %d", decoded.InputMediaSize.Height.Value) + } +} + +func TestInputSize_FromXML(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputSize", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "1"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":DocumentSizeAutoDetect", + Text: "true", + }, + { + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "8500", + }, + { + Name: NsWSCN + ":Height", + Text: "11000", + }, + }, + }, + }, + } + + decoded, err := decodeInputSize(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "1" { + t.Errorf("expected MustHonor='1', got '%s'", mustHonor) + } + if autoDetect := optional.Get(decoded.DocumentSizeAutoDetect); autoDetect != BooleanElement("true") { + t.Errorf("expected DocumentSizeAutoDetect='true', got '%v'", autoDetect) + } + if decoded.InputMediaSize.Width.Value != 8500 { + t.Errorf("expected Width=8500, got %d", decoded.InputMediaSize.Width.Value) + } + if decoded.InputMediaSize.Height.Value != 11000 { + t.Errorf("expected Height=11000, got %d", decoded.InputMediaSize.Height.Value) + } +} + +func TestInputSize_InvalidBooleanAttribute(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputSize", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeInputSize(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestInputSize_InvalidDocumentSizeAutoDetect(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":DocumentSizeAutoDetect", + Text: "invalid", + }, + { + Name: NsWSCN + ":InputMediaSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "8500", + }, + { + Name: NsWSCN + ":Height", + Text: "11000", + }, + }, + }, + }, + } + + _, err := decodeInputSize(root) + if err == nil { + t.Errorf("expected error for invalid DocumentSizeAutoDetect value 'invalid', got nil") + } +} + +func TestInputSize_MissingInputMediaSize(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputSize", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":DocumentSizeAutoDetect", + Text: "true", + }, + }, + } + + _, err := decodeInputSize(root) + if err == nil { + t.Errorf("expected error for missing InputMediaSize, got nil") + } +} diff --git a/proto/wsscan/inputsource.go b/proto/wsscan/inputsource.go new file mode 100644 index 00000000..0bb850df --- /dev/null +++ b/proto/wsscan/inputsource.go @@ -0,0 +1,35 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// InputSource element + +package wsscan + +import ( + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// InputSource represents the optional element +// that specifies the source of the original document on the scanning device. +// +// Standard values are: "ADF", "ADFDuplex", "Film", "Platen". +// Vendor-defined values are also allowed and will decode as UnknownInputSourceValue. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (all xs:string, but should be boolean values: 0, false, 1, or true). +type InputSource = AttributedElement[InputSourceValue] + +// decodeInputSource decodes [InputSource] from the XML tree. +func decodeInputSource(root xmldoc.Element) (InputSource, error) { + return decodeAttributedElement(root, func(s string) (InputSourceValue, error) { + return DecodeInputSourceValue(s), nil + }) +} + +// toXMLInputSource generates XML tree for the [InputSource]. +func toXMLInputSource(is InputSource, name string) xmldoc.Element { + return is.toXML(name, InputSourceValue.String) +} diff --git a/proto/wsscan/inputsource_test.go b/proto/wsscan/inputsource_test.go new file mode 100644 index 00000000..1baf52db --- /dev/null +++ b/proto/wsscan/inputsource_test.go @@ -0,0 +1,237 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for InputSource + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestInputSource_RoundTrip(t *testing.T) { + orig := InputSource{ + Value: InputSourceADF, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLInputSource(orig, NsWSCN+":InputSource") + + if elm.Name != NsWSCN+":InputSource" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":InputSource", elm.Name) + } + if elm.Text != "ADF" { + t.Errorf("expected text 'ADF', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeInputSource(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestInputSource_NoAttributes(t *testing.T) { + orig := InputSource{ + Value: InputSourcePlaten, + } + + elm := toXMLInputSource(orig, NsWSCN+":InputSource") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "Platen" { + t.Errorf("expected text 'Platen', got '%s'", elm.Text) + } + + decoded, err := decodeInputSource(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestInputSource_StandardValues(t *testing.T) { + standardValues := []struct { + enumValue InputSourceValue + textValue string + }{ + {InputSourceADF, "ADF"}, + {InputSourceADFDuplex, "ADFDuplex"}, + {InputSourceFilm, "Film"}, + {InputSourcePlaten, "Platen"}, + } + + for _, tc := range standardValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := InputSource{ + Value: tc.enumValue, + } + + elm := toXMLInputSource(orig, NsWSCN+":InputSource") + if elm.Text != tc.textValue { + t.Errorf("expected text '%s', got '%s'", tc.textValue, elm.Text) + } + + decoded, err := decodeInputSource(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + }) + } +} + +func TestInputSource_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":InputSource", + Text: "ADFDuplex", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeInputSource(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != InputSourceADFDuplex { + t.Errorf("expected value InputSourceADFDuplex, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestInputSource_InvalidBooleanAttributes(t *testing.T) { + // Test that invalid boolean values in attributes are rejected + root := xmldoc.Element{ + Name: NsWSCN + ":InputSource", + Text: "ADF", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + } + + _, err := decodeInputSource(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestInputSource_AllStandardValuesWithAttributes(t *testing.T) { + standardValues := []struct { + enumValue InputSourceValue + textValue string + }{ + {InputSourceADF, "ADF"}, + {InputSourceADFDuplex, "ADFDuplex"}, + {InputSourceFilm, "Film"}, + {InputSourcePlaten, "Platen"}, + } + + for _, tc := range standardValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := InputSource{ + Value: tc.enumValue, + MustHonor: optional.New(BooleanElement("1")), + Override: optional.New(BooleanElement("0")), + UsedDefault: optional.New(BooleanElement("true")), + } + + elm := toXMLInputSource(orig, NsWSCN+":InputSource") + decoded, err := decodeInputSource(elm) + if err != nil { + t.Fatalf("decode returned error for value '%s': %v", tc.textValue, err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes for value '%s', got %d", tc.textValue, len(elm.Attrs)) + } + }) + } +} + +func TestInputSource_VendorDefinedValues(t *testing.T) { + // Test that vendor-defined values decode to UnknownInputSourceValue + vendorValues := []string{"vendor-source-1", "custom-source", "extended-value"} + + for _, val := range vendorValues { + t.Run(val, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":InputSource", + Text: val, + } + + decoded, err := decodeInputSource(root) + if err != nil { + t.Fatalf("decode returned error for vendor-defined value '%s': %v", val, err) + } + if decoded.Value != UnknownInputSourceValue { + t.Errorf("expected UnknownInputSourceValue, got %v", decoded.Value) + } + // When encoding UnknownInputSourceValue, it will return "Unknown" + elm := toXMLInputSource(decoded, NsWSCN+":InputSource") + if elm.Text != "Unknown" { + t.Errorf("expected text 'Unknown' for UnknownInputSourceValue, got '%s'", elm.Text) + } + }) + } +} diff --git a/proto/wsscan/inputsourcevalue.go b/proto/wsscan/inputsourcevalue.go new file mode 100644 index 00000000..a9696617 --- /dev/null +++ b/proto/wsscan/inputsourcevalue.go @@ -0,0 +1,51 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// InputSource value + +package wsscan + +// InputSourceValue defines the source of the original document on the scanning device. +type InputSourceValue int + +// Known input source values +const ( + UnknownInputSourceValue InputSourceValue = iota // Unknown input source + InputSourceADF // Document delivered by ADF, front side only + InputSourceADFDuplex // Document delivered by ADF, both sides + InputSourceFilm // Document scanned using film scanning option + InputSourcePlaten // Document scanned from scanner platen +) + +// String returns a string representation of the [InputSourceValue]. +func (isv InputSourceValue) String() string { + switch isv { + case InputSourceADF: + return "ADF" + case InputSourceADFDuplex: + return "ADFDuplex" + case InputSourceFilm: + return "Film" + case InputSourcePlaten: + return "Platen" + } + return "Unknown" +} + +// DecodeInputSourceValue decodes [InputSourceValue] out of its XML string representation. +func DecodeInputSourceValue(s string) InputSourceValue { + switch s { + case "ADF": + return InputSourceADF + case "ADFDuplex": + return InputSourceADFDuplex + case "Film": + return InputSourceFilm + case "Platen": + return InputSourcePlaten + } + return UnknownInputSourceValue +} diff --git a/proto/wsscan/inputsourcevalue_test.go b/proto/wsscan/inputsourcevalue_test.go new file mode 100644 index 00000000..a5093ad2 --- /dev/null +++ b/proto/wsscan/inputsourcevalue_test.go @@ -0,0 +1,51 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for InputSourceValue + +package wsscan + +import "testing" + +func TestInputSourceValue_String(t *testing.T) { + tests := []struct { + value InputSourceValue + expected string + }{ + {InputSourceADF, "ADF"}, + {InputSourceADFDuplex, "ADFDuplex"}, + {InputSourceFilm, "Film"}, + {InputSourcePlaten, "Platen"}, + {UnknownInputSourceValue, "Unknown"}, + } + + for _, tt := range tests { + if got := tt.value.String(); got != tt.expected { + t.Errorf("InputSourceValue(%d).String() = %q, want %q", tt.value, got, tt.expected) + } + } +} + +func TestDecodeInputSourceValue(t *testing.T) { + tests := []struct { + input string + expected InputSourceValue + }{ + {"ADF", InputSourceADF}, + {"ADFDuplex", InputSourceADFDuplex}, + {"Film", InputSourceFilm}, + {"Platen", InputSourcePlaten}, + {"Unknown", UnknownInputSourceValue}, + {"invalid", UnknownInputSourceValue}, + {"", UnknownInputSourceValue}, + } + + for _, tt := range tests { + if got := DecodeInputSourceValue(tt.input); got != tt.expected { + t.Errorf("DecodeInputSourceValue(%q) = %v, want %v", tt.input, got, tt.expected) + } + } +} diff --git a/proto/wsscan/resolution.go b/proto/wsscan/resolution.go new file mode 100644 index 00000000..fcbd5b8b --- /dev/null +++ b/proto/wsscan/resolution.go @@ -0,0 +1,115 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Resolution element + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// Resolution represents the optional element +// that specifies the resolution of the scanned image. +// +// It includes an optional wscn:MustHonor attribute (xs:string, +// but should be a boolean value: 0, false, 1, or true). +// +// The element contains child elements: +// - Width (required AttributedElement[int]) - resolution width in pixels per inch +// - Height (optional AttributedElement[int]) - resolution height in pixels per inch +// If Height is missing, the Width value should be used, yielding a square resolution +// (for example, 300 x 300). +type Resolution struct { + MustHonor optional.Val[BooleanElement] + Width AttributedElement[int] + Height optional.Val[AttributedElement[int]] +} + +// toXML generates XML tree for the [Resolution]. +func (res Resolution) toXML(name string) xmldoc.Element { + children := []xmldoc.Element{ + res.Width.toXML(NsWSCN+":Width", strconv.Itoa), + } + + // Add Height if present + if res.Height != nil { + height := optional.Get(res.Height) + children = append(children, height.toXML(NsWSCN+":Height", strconv.Itoa)) + } + + elm := xmldoc.Element{ + Name: name, + Children: children, + } + + // Add optional MustHonor attribute if present + if mustHonor := optional.Get(res.MustHonor); mustHonor != "" { + elm.Attrs = []xmldoc.Attr{ + { + Name: NsWSCN + ":MustHonor", + Value: string(mustHonor), + }, + } + } + + return elm +} + +// decodeResolution decodes [Resolution] from the XML tree. +func decodeResolution(root xmldoc.Element) (Resolution, error) { + var res Resolution + + // Decode optional MustHonor attribute with validation + if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found { + mustHonor := BooleanElement(attr.Value) + if err := mustHonor.Validate(); err != nil { + return res, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err)) + } + res.MustHonor = optional.New(mustHonor) + } + + decodeValue := func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %w", err) + } + return val, nil + } + + // Decode child elements + var widthFound bool + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":Width": + width, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return res, fmt.Errorf("width: %w", + xmldoc.XMLErrWrap(child, err)) + } + res.Width = width + widthFound = true + case NsWSCN + ":Height": + height, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return res, fmt.Errorf("height: %w", + xmldoc.XMLErrWrap(child, err)) + } + res.Height = optional.New(height) + } + } + + if !widthFound { + return res, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:Width", NsWSCN)) + } + + return res, nil +} diff --git a/proto/wsscan/resolution_test.go b/proto/wsscan/resolution_test.go new file mode 100644 index 00000000..f8070396 --- /dev/null +++ b/proto/wsscan/resolution_test.go @@ -0,0 +1,299 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for Resolution + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestResolution_RoundTrip_WithHeight(t *testing.T) { + orig := Resolution{ + MustHonor: optional.New(BooleanElement("true")), + Width: AttributedElement[int]{ + Value: 300, + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("true")), + }, + Height: optional.New(AttributedElement[int]{ + Value: 600, + Override: optional.New(BooleanElement("1")), + }), + } + + elm := orig.toXML(NsWSCN + ":Resolution") + + if elm.Name != NsWSCN+":Resolution" { + t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Resolution", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + + // Check MustHonor attribute + if elm.Attrs[0].Name != NsWSCN+":MustHonor" || elm.Attrs[0].Value != "true" { + t.Errorf("expected MustHonor='true', got %+v", elm.Attrs[0]) + } + + // Decode back + decoded, err := decodeResolution(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Width.Value != orig.Width.Value { + t.Errorf("expected Width.Value %d, got %d", orig.Width.Value, decoded.Width.Value) + } + if decoded.Height == nil { + t.Errorf("expected Height to be present, got nil") + } else { + decodedHeight := optional.Get(decoded.Height) + origHeight := optional.Get(orig.Height) + if decodedHeight.Value != origHeight.Value { + t.Errorf("expected Height.Value %d, got %d", origHeight.Value, decodedHeight.Value) + } + } +} + +func TestResolution_RoundTrip_WithoutHeight(t *testing.T) { + orig := Resolution{ + Width: AttributedElement[int]{ + Value: 300, + }, + // Height is nil (optional) + } + + elm := orig.toXML(NsWSCN + ":Resolution") + + if elm.Name != NsWSCN+":Resolution" { + t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Resolution", elm.Name) + } + if len(elm.Children) != 1 { + t.Errorf("expected 1 child (only Width), got %d", len(elm.Children)) + } + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %d", len(elm.Attrs)) + } + + // Check that only Width is present + if elm.Children[0].Name != NsWSCN+":Width" { + t.Errorf("expected first child to be Width, got %s", elm.Children[0].Name) + } + if elm.Children[0].Text != "300" { + t.Errorf("expected Width text '300', got '%s'", elm.Children[0].Text) + } + + // Decode back + decoded, err := decodeResolution(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Width.Value != orig.Width.Value { + t.Errorf("expected Width.Value %d, got %d", orig.Width.Value, decoded.Width.Value) + } + if decoded.Height != nil { + t.Errorf("expected Height to be nil, got %+v", decoded.Height) + } +} + +func TestResolution_FromXML_WithHeight(t *testing.T) { + // Create XML element manually with Width and Height + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "300", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "1"}, + }, + }, + { + Name: NsWSCN + ":Height", + Text: "600", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":UsedDefault", Value: "true"}, + }, + }, + }, + } + + decoded, err := decodeResolution(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Width.Value != 300 { + t.Errorf("expected Width.Value 300, got %d", decoded.Width.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Width.Override); string(override) != "1" { + t.Errorf("expected Width.Override='1', got '%s'", override) + } + if decoded.Height == nil { + t.Fatalf("expected Height to be present, got nil") + } + height := optional.Get(decoded.Height) + if height.Value != 600 { + t.Errorf("expected Height.Value 600, got %d", height.Value) + } + if usedDefault := optional.Get(height.UsedDefault); string(usedDefault) != "true" { + t.Errorf("expected Height.UsedDefault='true', got '%s'", usedDefault) + } +} + +func TestResolution_FromXML_WithoutHeight(t *testing.T) { + // Create XML element manually with only Width (no Height) + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "300", + }, + }, + } + + decoded, err := decodeResolution(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Width.Value != 300 { + t.Errorf("expected Width.Value 300, got %d", decoded.Width.Value) + } + if decoded.Height != nil { + t.Errorf("expected Height to be nil when not present in XML, got %+v", decoded.Height) + } +} + +func TestResolution_MissingWidth(t *testing.T) { + // Create XML element without required Width + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Height", + Text: "600", + }, + }, + } + + _, err := decodeResolution(root) + if err == nil { + t.Errorf("expected error for missing required Width element, got nil") + } +} + +func TestResolution_InvalidWidthValue(t *testing.T) { + // Create XML element with invalid Width value + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "invalid", + }, + }, + } + + _, err := decodeResolution(root) + if err == nil { + t.Errorf("expected error for invalid Width value 'invalid', got nil") + } +} + +func TestResolution_InvalidHeightValue(t *testing.T) { + // Create XML element with invalid Height value + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "300", + }, + { + Name: NsWSCN + ":Height", + Text: "invalid", + }, + }, + } + + _, err := decodeResolution(root) + if err == nil { + t.Errorf("expected error for invalid Height value 'invalid', got nil") + } +} + +func TestResolution_InvalidMustHonor(t *testing.T) { + // Create XML element with invalid MustHonor attribute + root := xmldoc.Element{ + Name: NsWSCN + ":Resolution", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":Width", + Text: "300", + }, + }, + } + + _, err := decodeResolution(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestResolution_WidthAttributes(t *testing.T) { + // Test Width with all attributes + orig := Resolution{ + Width: AttributedElement[int]{ + Value: 300, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + }, + } + + elm := orig.toXML(NsWSCN + ":Resolution") + if len(elm.Children) != 1 { + t.Fatalf("expected 1 child, got %d", len(elm.Children)) + } + + widthElm := elm.Children[0] + if widthElm.Name != NsWSCN+":Width" { + t.Fatalf("expected Width element, got %s", widthElm.Name) + } + if len(widthElm.Attrs) != 3 { + t.Errorf("expected 3 attributes on Width, got %d", len(widthElm.Attrs)) + } + + decoded, err := decodeResolution(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if !reflect.DeepEqual(orig.Width, decoded.Width) { + t.Errorf("expected Width %+v, got %+v", orig.Width, decoded.Width) + } +} diff --git a/proto/wsscan/rotation.go b/proto/wsscan/rotation.go new file mode 100644 index 00000000..ef89b8a3 --- /dev/null +++ b/proto/wsscan/rotation.go @@ -0,0 +1,41 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Rotation element + +package wsscan + +import ( + "fmt" + + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// Rotation represents the optional element +// that specifies the amount to rotate each image of the scanned document. +// +// It includes optional wscn:MustHonor, wscn:Override, and wscn:UsedDefault +// attributes (xs:string, but should be boolean values: 0, false, 1, or true). +// +// The element contains a required text value that must be one of: 0, 90, 180, or 270. +type Rotation = AttributedElement[RotationValue] + +// decodeRotation decodes [Rotation] from the XML tree. +func decodeRotation(root xmldoc.Element) (Rotation, error) { + return decodeAttributedElement(root, func(s string) (RotationValue, error) { + val := DecodeRotationValue(s) + if val == UnknownRotationValue { + return val, xmldoc.XMLErrWrap(root, + fmt.Errorf("rotation value must be one of 0, 90, 180, or 270, got %q", s)) + } + return val, nil + }) +} + +// toXMLRotation generates XML tree for the [Rotation]. +func toXMLRotation(r Rotation, name string) xmldoc.Element { + return r.toXML(name, RotationValue.String) +} diff --git a/proto/wsscan/rotation_test.go b/proto/wsscan/rotation_test.go new file mode 100644 index 00000000..1b0251f7 --- /dev/null +++ b/proto/wsscan/rotation_test.go @@ -0,0 +1,386 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for Rotation + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestRotation_RoundTrip(t *testing.T) { + orig := Rotation{ + Value: Rotation90, + MustHonor: optional.New(BooleanElement("true")), + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + + if elm.Name != NsWSCN+":Rotation" { + t.Errorf("expected element name '%s', got '%s'", + NsWSCN+":Rotation", elm.Name) + } + if elm.Text != "90" { + t.Errorf("expected text '90', got '%s'", elm.Text) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes, got %d", len(elm.Attrs)) + } + + // Check attributes + attrsMap := make(map[string]string) + for _, attr := range elm.Attrs { + attrsMap[attr.Name] = attr.Value + } + if attrsMap[NsWSCN+":MustHonor"] != "true" { + t.Errorf("expected MustHonor='true', got '%s'", attrsMap[NsWSCN+":MustHonor"]) + } + if attrsMap[NsWSCN+":Override"] != "false" { + t.Errorf("expected Override='false', got '%s'", attrsMap[NsWSCN+":Override"]) + } + if attrsMap[NsWSCN+":UsedDefault"] != "1" { + t.Errorf("expected UsedDefault='1', got '%s'", attrsMap[NsWSCN+":UsedDefault"]) + } + + // Decode back + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } +} + +func TestRotation_NoAttributes(t *testing.T) { + orig := Rotation{ + Value: Rotation180, + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %+v", elm.Attrs) + } + if elm.Text != "180" { + t.Errorf("expected text '180', got '%s'", elm.Text) + } + + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != orig.Value { + t.Errorf("expected value %v, got %v", orig.Value, decoded.Value) + } +} + +func TestRotation_AllValidValues(t *testing.T) { + validValues := []struct { + enumValue RotationValue + textValue string + }{ + {Rotation0, "0"}, + {Rotation90, "90"}, + {Rotation180, "180"}, + {Rotation270, "270"}, + } + + for _, tc := range validValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := Rotation{ + Value: tc.enumValue, + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + if elm.Text != tc.textValue { + t.Errorf("expected text '%s', got '%s'", tc.textValue, elm.Text) + } + + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + }) + } +} + +func TestRotation_FromXML(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":Rotation", + Text: "270", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + } + + decoded, err := decodeRotation(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != Rotation270 { + t.Errorf("expected value Rotation270, got %v", decoded.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.Override); string(override) != "1" { + t.Errorf("expected Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestRotation_InvalidValues(t *testing.T) { + invalidValues := []string{ + "45", + "91", + "179", + "271", + "360", + "-90", + "invalid", + "", + " 90 ", + } + + for _, val := range invalidValues { + t.Run(val, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Rotation", + Text: val, + } + + _, err := decodeRotation(root) + if err == nil { + t.Errorf("expected error for invalid rotation value '%s', got nil", val) + } + }) + } +} + +func TestRotation_InvalidBooleanAttributes(t *testing.T) { + tests := []struct { + name string + attr string + value string + wantErr bool + }{ + { + name: "valid true", + attr: "MustHonor", + value: "true", + wantErr: false, + }, + { + name: "valid false", + attr: "MustHonor", + value: "false", + wantErr: false, + }, + { + name: "valid 1", + attr: "MustHonor", + value: "1", + wantErr: false, + }, + { + name: "valid 0", + attr: "MustHonor", + value: "0", + wantErr: false, + }, + { + name: "invalid value", + attr: "MustHonor", + value: "invalid", + wantErr: true, + }, + { + name: "invalid empty", + attr: "MustHonor", + value: "", + wantErr: true, + }, + { + name: "invalid Override", + attr: "Override", + value: "yes", + wantErr: true, + }, + { + name: "invalid UsedDefault", + attr: "UsedDefault", + value: "maybe", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := xmldoc.Element{ + Name: NsWSCN + ":Rotation", + Text: "90", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":" + tt.attr, Value: tt.value}, + }, + } + + _, err := decodeRotation(root) + if tt.wantErr { + if err == nil { + t.Errorf("expected error for %s='%s', got nil", tt.attr, tt.value) + } + } else { + if err != nil { + t.Errorf("unexpected error for %s='%s': %v", tt.attr, tt.value, err) + } + } + }) + } +} + +func TestRotation_AllValidValuesWithAttributes(t *testing.T) { + validValues := []struct { + enumValue RotationValue + textValue string + }{ + {Rotation0, "0"}, + {Rotation90, "90"}, + {Rotation180, "180"}, + {Rotation270, "270"}, + } + + for _, tc := range validValues { + t.Run(tc.textValue, func(t *testing.T) { + orig := Rotation{ + Value: tc.enumValue, + MustHonor: optional.New(BooleanElement("1")), + Override: optional.New(BooleanElement("0")), + UsedDefault: optional.New(BooleanElement("true")), + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode returned error for value '%s': %v", tc.textValue, err) + } + if decoded.Value != tc.enumValue { + t.Errorf("expected value %v, got %v", tc.enumValue, decoded.Value) + } + if len(elm.Attrs) != 3 { + t.Errorf("expected 3 attributes for value '%s', got %d", tc.textValue, len(elm.Attrs)) + } + }) + } +} + +func TestRotation_BoundaryValues(t *testing.T) { + // Test all valid rotation values + boundaryTests := []struct { + name string + enumValue RotationValue + textValue string + }{ + {"minimum", Rotation0, "0"}, + {"90 degrees", Rotation90, "90"}, + {"180 degrees", Rotation180, "180"}, + {"maximum", Rotation270, "270"}, + } + + for _, tt := range boundaryTests { + t.Run(tt.name, func(t *testing.T) { + orig := Rotation{ + Value: tt.enumValue, + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode with value %s returned error: %v", tt.textValue, err) + } + if decoded.Value != tt.enumValue { + t.Errorf("expected value %v, got %v", tt.enumValue, decoded.Value) + } + if elm.Text != tt.textValue { + t.Errorf("expected text '%s', got '%s'", tt.textValue, elm.Text) + } + }) + } +} + +func TestRotation_AttributesOnAllValues(t *testing.T) { + // Test that all rotation values work correctly with all attribute combinations + rotationValues := []RotationValue{Rotation0, Rotation90, Rotation180, Rotation270} + attrCombinations := []struct { + name string + mustHonor optional.Val[BooleanElement] + override optional.Val[BooleanElement] + usedDefault optional.Val[BooleanElement] + }{ + {"no attributes", nil, nil, nil}, + {"only MustHonor", optional.New(BooleanElement("true")), nil, nil}, + {"only Override", nil, optional.New(BooleanElement("false")), nil}, + {"only UsedDefault", nil, nil, optional.New(BooleanElement("1"))}, + {"all attributes", optional.New(BooleanElement("0")), optional.New(BooleanElement("1")), optional.New(BooleanElement("true"))}, + } + + for _, rotVal := range rotationValues { + for _, attrCombo := range attrCombinations { + t.Run(rotVal.String()+"_"+attrCombo.name, func(t *testing.T) { + orig := Rotation{ + Value: rotVal, + MustHonor: attrCombo.mustHonor, + Override: attrCombo.override, + UsedDefault: attrCombo.usedDefault, + } + + elm := toXMLRotation(orig, NsWSCN+":Rotation") + decoded, err := decodeRotation(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.Value != rotVal { + t.Errorf("expected value %v, got %v", rotVal, decoded.Value) + } + if !reflect.DeepEqual(orig.MustHonor, decoded.MustHonor) { + t.Errorf("expected MustHonor %+v, got %+v", orig.MustHonor, decoded.MustHonor) + } + if !reflect.DeepEqual(orig.Override, decoded.Override) { + t.Errorf("expected Override %+v, got %+v", orig.Override, decoded.Override) + } + if !reflect.DeepEqual(orig.UsedDefault, decoded.UsedDefault) { + t.Errorf("expected UsedDefault %+v, got %+v", orig.UsedDefault, decoded.UsedDefault) + } + }) + } + } +} diff --git a/proto/wsscan/scaling.go b/proto/wsscan/scaling.go new file mode 100644 index 00000000..9f30e68e --- /dev/null +++ b/proto/wsscan/scaling.go @@ -0,0 +1,114 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Scaling element + +package wsscan + +import ( + "fmt" + "strconv" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +// Scaling represents the optional element +// that specifies the scaling of both the width and height of the scanned document. +// +// It includes an optional wscn:MustHonor attribute (xs:string, +// but should be a boolean value: 0, false, 1, or true). +// +// The element contains child elements: +// - ScalingWidth (required AttributedElement[int]) - scaling width value in range 1-1000 +// Note: ScalingWidth should only use Override and UsedDefault attributes, not MustHonor +// - ScalingHeight (required AttributedElement[int]) - scaling height value in range 1-1000 +// Note: ScalingHeight should only use Override and UsedDefault attributes, not MustHonor +type Scaling struct { + MustHonor optional.Val[BooleanElement] + ScalingWidth AttributedElement[int] + ScalingHeight AttributedElement[int] +} + +// toXML generates XML tree for the [Scaling]. +func (sc Scaling) toXML(name string) xmldoc.Element { + children := []xmldoc.Element{ + sc.ScalingWidth.toXML(NsWSCN+":ScalingWidth", strconv.Itoa), + sc.ScalingHeight.toXML(NsWSCN+":ScalingHeight", strconv.Itoa), + } + + elm := xmldoc.Element{ + Name: name, + Children: children, + } + + if mustHonor := optional.Get(sc.MustHonor); mustHonor != "" { + elm.Attrs = []xmldoc.Attr{ + { + Name: NsWSCN + ":MustHonor", + Value: string(mustHonor), + }, + } + } + + return elm +} + +// decodeScaling decodes [Scaling] from the XML tree. +func decodeScaling(root xmldoc.Element) (Scaling, error) { + var sc Scaling + + // Decode optional MustHonor attribute with validation + if attr, found := root.AttrByName(NsWSCN + ":MustHonor"); found { + mustHonor := BooleanElement(attr.Value) + if err := mustHonor.Validate(); err != nil { + return sc, xmldoc.XMLErrWrap(root, fmt.Errorf("mustHonor: %w", err)) + } + sc.MustHonor = optional.New(mustHonor) + } + + decodeValue := func(s string) (int, error) { + val, err := strconv.Atoi(s) + if err != nil { + return 0, fmt.Errorf("invalid integer: %w", err) + } + return val, nil + } + + // Decode child elements + var widthFound, heightFound bool + for _, child := range root.Children { + switch child.Name { + case NsWSCN + ":ScalingWidth": + width, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return sc, fmt.Errorf("scalingWidth: %w", + xmldoc.XMLErrWrap(child, err)) + } + sc.ScalingWidth = width + widthFound = true + case NsWSCN + ":ScalingHeight": + height, err := decodeAttributedElement(child, decodeValue) + if err != nil { + return sc, fmt.Errorf("scalingHeight: %w", + xmldoc.XMLErrWrap(child, err)) + } + sc.ScalingHeight = height + heightFound = true + } + } + + if !widthFound { + return sc, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:ScalingWidth", NsWSCN)) + } + if !heightFound { + return sc, xmldoc.XMLErrWrap(root, + fmt.Errorf("missing required element: %s:ScalingHeight", NsWSCN)) + } + + return sc, nil +} diff --git a/proto/wsscan/scaling_test.go b/proto/wsscan/scaling_test.go new file mode 100644 index 00000000..32eb5c69 --- /dev/null +++ b/proto/wsscan/scaling_test.go @@ -0,0 +1,409 @@ +// MFP - Multi-Function Printers and scanners toolkit +// WS-Scan core protocol +// +// Copyright (C) 2024 and up by Yogesh Singla (yogeshsingla481@gmail.com) +// See LICENSE for license terms and conditions +// +// Test for Scaling + +package wsscan + +import ( + "reflect" + "testing" + + "github.com/OpenPrinting/go-mfp/util/optional" + "github.com/OpenPrinting/go-mfp/util/xmldoc" +) + +func TestScaling_RoundTrip_Complete(t *testing.T) { + orig := Scaling{ + MustHonor: optional.New(BooleanElement("true")), + ScalingWidth: AttributedElement[int]{ + Value: 500, + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("true")), + }, + ScalingHeight: AttributedElement[int]{ + Value: 600, + Override: optional.New(BooleanElement("1")), + }, + } + + elm := orig.toXML(NsWSCN + ":Scaling") + + if elm.Name != NsWSCN+":Scaling" { + t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Scaling", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if len(elm.Attrs) != 1 { + t.Errorf("expected 1 attribute, got %d", len(elm.Attrs)) + } + + // Check MustHonor attribute + if elm.Attrs[0].Name != NsWSCN+":MustHonor" || elm.Attrs[0].Value != "true" { + t.Errorf("expected MustHonor='true', got %+v", elm.Attrs[0]) + } + + // Decode back + decoded, err := decodeScaling(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if !reflect.DeepEqual(orig.ScalingWidth, decoded.ScalingWidth) { + t.Errorf("expected ScalingWidth %+v, got %+v", orig.ScalingWidth, decoded.ScalingWidth) + } + if !reflect.DeepEqual(orig.ScalingHeight, decoded.ScalingHeight) { + t.Errorf("expected ScalingHeight %+v, got %+v", orig.ScalingHeight, decoded.ScalingHeight) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "true" { + t.Errorf("expected MustHonor='true', got '%s'", mustHonor) + } +} + +func TestScaling_RoundTrip_NoAttributes(t *testing.T) { + orig := Scaling{ + ScalingWidth: AttributedElement[int]{ + Value: 100, + }, + ScalingHeight: AttributedElement[int]{ + Value: 200, + }, + } + + elm := orig.toXML(NsWSCN + ":Scaling") + + if elm.Name != NsWSCN+":Scaling" { + t.Errorf("expected element name '%s', got '%s'", NsWSCN+":Scaling", elm.Name) + } + if len(elm.Children) != 2 { + t.Errorf("expected 2 children, got %d", len(elm.Children)) + } + if len(elm.Attrs) != 0 { + t.Errorf("expected no attributes, got %d", len(elm.Attrs)) + } + + // Decode back + decoded, err := decodeScaling(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.ScalingWidth.Value != orig.ScalingWidth.Value { + t.Errorf("expected ScalingWidth.Value %d, got %d", + orig.ScalingWidth.Value, decoded.ScalingWidth.Value) + } + if decoded.ScalingHeight.Value != orig.ScalingHeight.Value { + t.Errorf("expected ScalingHeight.Value %d, got %d", + orig.ScalingHeight.Value, decoded.ScalingHeight.Value) + } +} + +func TestScaling_FromXML_Complete(t *testing.T) { + // Create XML element manually with all attributes + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "0"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "300", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":Override", Value: "1"}, + {Name: NsWSCN + ":UsedDefault", Value: "false"}, + }, + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "400", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":UsedDefault", Value: "true"}, + }, + }, + }, + } + + decoded, err := decodeScaling(root) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if decoded.ScalingWidth.Value != 300 { + t.Errorf("expected ScalingWidth.Value 300, got %d", decoded.ScalingWidth.Value) + } + if decoded.ScalingHeight.Value != 400 { + t.Errorf("expected ScalingHeight.Value 400, got %d", decoded.ScalingHeight.Value) + } + if mustHonor := optional.Get(decoded.MustHonor); string(mustHonor) != "0" { + t.Errorf("expected MustHonor='0', got '%s'", mustHonor) + } + if override := optional.Get(decoded.ScalingWidth.Override); string(override) != "1" { + t.Errorf("expected ScalingWidth.Override='1', got '%s'", override) + } + if usedDefault := optional.Get(decoded.ScalingWidth.UsedDefault); string(usedDefault) != "false" { + t.Errorf("expected ScalingWidth.UsedDefault='false', got '%s'", usedDefault) + } +} + +func TestScaling_MissingScalingWidth(t *testing.T) { + // Create XML element without required ScalingWidth + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingHeight", + Text: "500", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for missing required ScalingWidth element, got nil") + } +} + +func TestScaling_MissingScalingHeight(t *testing.T) { + // Create XML element without required ScalingHeight + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "500", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for missing required ScalingHeight element, got nil") + } +} + +func TestScaling_InvalidScalingWidthValue_TooLow(t *testing.T) { + // Create XML element with ScalingWidth value below minimum (0) + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "0", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "500", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for ScalingWidth value 0 (below minimum 1), got nil") + } +} + +func TestScaling_InvalidScalingWidthValue_TooHigh(t *testing.T) { + // Create XML element with ScalingWidth value above maximum (1001) + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "1001", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "500", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for ScalingWidth value 1001 (above maximum 1000), got nil") + } +} + +func TestScaling_InvalidScalingHeightValue_TooLow(t *testing.T) { + // Create XML element with ScalingHeight value below minimum (0) + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "500", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "0", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for ScalingHeight value 0 (below minimum 1), got nil") + } +} + +func TestScaling_InvalidScalingHeightValue_TooHigh(t *testing.T) { + // Create XML element with ScalingHeight value above maximum (1001) + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "500", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "1001", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for ScalingHeight value 1001 (above maximum 1000), got nil") + } +} + +func TestScaling_InvalidScalingWidthValue_NotInteger(t *testing.T) { + // Create XML element with invalid ScalingWidth value + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "invalid", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "500", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for invalid ScalingWidth value 'invalid', got nil") + } +} + +func TestScaling_InvalidScalingHeightValue_NotInteger(t *testing.T) { + // Create XML element with invalid ScalingHeight value + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "500", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "invalid", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for invalid ScalingHeight value 'invalid', got nil") + } +} + +func TestScaling_InvalidMustHonor(t *testing.T) { + // Create XML element with invalid MustHonor attribute + root := xmldoc.Element{ + Name: NsWSCN + ":Scaling", + Attrs: []xmldoc.Attr{ + {Name: NsWSCN + ":MustHonor", Value: "invalid"}, + }, + Children: []xmldoc.Element{ + { + Name: NsWSCN + ":ScalingWidth", + Text: "500", + }, + { + Name: NsWSCN + ":ScalingHeight", + Text: "600", + }, + }, + } + + _, err := decodeScaling(root) + if err == nil { + t.Errorf("expected error for invalid MustHonor value 'invalid', got nil") + } +} + +func TestScaling_BoundaryValues(t *testing.T) { + // Test minimum value (1) + orig1 := Scaling{ + ScalingWidth: AttributedElement[int]{Value: 1}, + ScalingHeight: AttributedElement[int]{Value: 1}, + } + elm1 := orig1.toXML(NsWSCN + ":Scaling") + decoded1, err := decodeScaling(elm1) + if err != nil { + t.Fatalf("decode with value 1 returned error: %v", err) + } + if decoded1.ScalingWidth.Value != 1 || decoded1.ScalingHeight.Value != 1 { + t.Errorf("expected values 1, got %d, %d", decoded1.ScalingWidth.Value, decoded1.ScalingHeight.Value) + } + + // Test maximum value (1000) + orig2 := Scaling{ + ScalingWidth: AttributedElement[int]{Value: 1000}, + ScalingHeight: AttributedElement[int]{Value: 1000}, + } + elm2 := orig2.toXML(NsWSCN + ":Scaling") + decoded2, err := decodeScaling(elm2) + if err != nil { + t.Fatalf("decode with value 1000 returned error: %v", err) + } + if decoded2.ScalingWidth.Value != 1000 || decoded2.ScalingHeight.Value != 1000 { + t.Errorf("expected values 1000, got %d, %d", decoded2.ScalingWidth.Value, decoded2.ScalingHeight.Value) + } +} + +func TestScaling_AttributesOnChildElements(t *testing.T) { + // Test ScalingWidth and ScalingHeight with Override and UsedDefault attributes + // Note: Per spec, ScalingWidth and ScalingHeight should NOT have MustHonor attribute + // (only the parent Scaling element has MustHonor) + orig := Scaling{ + ScalingWidth: AttributedElement[int]{ + Value: 300, + Override: optional.New(BooleanElement("false")), + UsedDefault: optional.New(BooleanElement("1")), + }, + ScalingHeight: AttributedElement[int]{ + Value: 400, + Override: optional.New(BooleanElement("true")), + UsedDefault: optional.New(BooleanElement("false")), + }, + } + + elm := orig.toXML(NsWSCN + ":Scaling") + if len(elm.Children) != 2 { + t.Fatalf("expected 2 children, got %d", len(elm.Children)) + } + + decoded, err := decodeScaling(elm) + if err != nil { + t.Fatalf("decode returned error: %v", err) + } + + if !reflect.DeepEqual(orig.ScalingWidth, decoded.ScalingWidth) { + t.Errorf("expected ScalingWidth %+v, got %+v", orig.ScalingWidth, decoded.ScalingWidth) + } + if !reflect.DeepEqual(orig.ScalingHeight, decoded.ScalingHeight) { + t.Errorf("expected ScalingHeight %+v, got %+v", orig.ScalingHeight, decoded.ScalingHeight) + } +}