///|
pub(all) struct MediaSize {
x : Int
y : Int
} derive(Eq, Debug)
///|
pub extend MediaSize with Eq::{equal, not_equal}
///|
pub extend MediaSize with @debug.Debug::{to_repr}
///|
/// Dimensions are expressed in hundredths of a millimetre, as specified by PWG.
pub fn MediaSize::new(x : Int, y : Int) -> MediaSize raise IppError {
if x < 1 || y < 1 {
raise Invalid("media dimensions must be positive")
}
{ x, y, }
}
///|
pub fn MediaSize::a4() -> MediaSize {
{ x: 21000, y: 29700, }
}
///|
pub fn MediaSize::letter() -> MediaSize {
{ x: 21590, y: 27940, }
}
///|
pub fn MediaSize::collection(self : MediaSize) -> IppValue {
Collection([
Attribute::new("x-dimension", Integer(self.x)),
Attribute::new("y-dimension", Integer(self.y)),
])
}
///|
pub(all) struct MediaMargins {
top : Int
bottom : Int
left : Int
right : Int
} derive(Eq, Debug)
///|
pub extend MediaMargins with Eq::{equal, not_equal}
///|
pub extend MediaMargins with @debug.Debug::{to_repr}
///|
pub fn MediaMargins::new(
top : Int,
bottom : Int,
left : Int,
right : Int,
) -> MediaMargins raise IppError {
if top < 0 || bottom < 0 || left < 0 || right < 0 {
raise Invalid("media margins cannot be negative")
}
{ top, bottom, left, right, }
}
///|
pub(all) struct MediaSpec {
size : MediaSize
margins : MediaMargins?
source : String?
kind : String?
name : String?
} derive(Eq, Debug)
///|
pub extend MediaSpec with Eq::{equal, not_equal}
///|
pub extend MediaSpec with @debug.Debug::{to_repr}
///|
pub fn MediaSpec::new(size : MediaSize) -> MediaSpec {
{ size, margins: None, source: None, kind: None, name: None, }
}
///|
pub fn MediaSpec::validate(self : MediaSpec) -> Unit raise IppError {
let _ = MediaSize::new(self.size.x, self.size.y)
match self.margins {
Some(margins) => {
let _ = MediaMargins::new(
margins.top,
margins.bottom,
margins.left,
margins.right,
)
if margins.left >= self.size.x ||
margins.right >= self.size.x - margins.left ||
margins.top >= self.size.y ||
margins.bottom >= self.size.y - margins.top {
raise Invalid("media margins leave no printable area")
}
}
None => ()
}
for keyword in [self.source, self.kind, self.name] {
match keyword {
Some(value) =>
if !valid_attribute_name(value) {
raise Invalid("invalid media collection keyword")
}
None => ()
}
}
}
///|
pub fn MediaSpec::collection(self : MediaSpec) -> IppValue raise IppError {
self.validate()
let fields = [Attribute::new("media-size", self.size.collection())]
match self.margins {
Some(margins) => {
fields.push(Attribute::new("media-top-margin", Integer(margins.top)))
fields.push(
Attribute::new("media-bottom-margin", Integer(margins.bottom)),
)
fields.push(Attribute::new("media-left-margin", Integer(margins.left)))
fields.push(Attribute::new("media-right-margin", Integer(margins.right)))
}
None => ()
}
match self.source {
Some(value) => fields.push(Attribute::new("media-source", Keyword(value)))
None => ()
}
match self.kind {
Some(value) => fields.push(Attribute::new("media-type", Keyword(value)))
None => ()
}
match self.name {
Some(value) =>
fields.push(Attribute::new("media-size-name", Keyword(value)))
None => ()
}
Collection(fields)
}
///|
fn collection_fields(value : IppValue) -> Group raise IppError {
match value {
Collection(fields) => Group::printer(fields)
_ => raise Invalid("media collection expected")
}
}
///|
fn required_collection_value(
group : Group,
name : String,
) -> IppValue raise IppError {
match group.find(name) {
Some(attribute) =>
match attribute.first() {
Some(value) => value
None => raise Invalid("empty media collection field")
}
None => raise Invalid("missing media collection field: \{name}")
}
}
///|
fn required_collection_int(group : Group, name : String) -> Int raise IppError {
match required_collection_value(group, name) {
Integer(value) => value
_ => raise Invalid("media dimension or margin requires integer")
}
}
///|
pub fn media_spec_from_collection(value : IppValue) -> MediaSpec raise IppError {
let fields = collection_fields(value)
let size_fields = collection_fields(
required_collection_value(fields, "media-size"),
)
let size = MediaSize::new(
required_collection_int(size_fields, "x-dimension"),
required_collection_int(size_fields, "y-dimension"),
)
let margin_names = [
"media-top-margin", "media-bottom-margin", "media-left-margin", "media-right-margin",
]
let present = margin_names
.filter(fn(name) { fields.find(name) is Some(_) })
.length()
let margins = if present == 0 {
None
} else {
if present != 4 {
raise Invalid("media margins must specify all four edges")
}
Some(
MediaMargins::new(
required_collection_int(fields, margin_names[0]),
required_collection_int(fields, margin_names[1]),
required_collection_int(fields, margin_names[2]),
required_collection_int(fields, margin_names[3]),
),
)
}
let spec = MediaSpec::{
size,
margins,
source: fields.get_string("media-source"),
kind: fields.get_string("media-type"),
name: fields.get_string("media-size-name"),
}
spec.validate()
spec
}
///|
pub fn Capabilities::media_database(
self : Capabilities,
) -> Array[MediaSpec] raise IppError {
match self.printer.find("media-col-database") {
None => []
Some(attribute) =>
attribute.values.map(value => media_spec_from_collection(value))
}
}
///|
pub fn Capabilities::validate_media(
self : Capabilities,
spec : MediaSpec,
) -> Array[Diagnostic] raise IppError {
spec.validate()
let diagnostics : Array[Diagnostic] = []
let database = self.media_database()
if database.is_empty() {
diagnostics.push({
severity: Warning,
attribute: "media-col",
message: "printer has not advertised its media database",
})
return diagnostics
}
let matching = database.filter(fn(candidate) { candidate.size == spec.size })
if matching.is_empty() {
diagnostics.push({
severity: Error,
attribute: "media-size",
message: "requested dimensions are not in media-col-database",
})
} else {
let compatible = matching.any(fn(candidate) {
(spec.source is None || candidate.source == spec.source) &&
(spec.kind is None || candidate.kind == spec.kind) &&
(spec.margins is None || candidate.margins == spec.margins)
})
if !compatible {
diagnostics.push({
severity: Error,
attribute: "media-col",
message: "requested tray, media type or margins do not match a media entry",
})
}
}
diagnostics
}
///|
/// Add media-col without losing the operation attributes or document bytes.
pub fn with_media_collection(
message : Message,
spec : MediaSpec,
) -> Message raise IppError {
if message.code != 2 && message.code != 4 && message.code != 5 {
raise Invalid(
"media-col is only supported for print, validate or create-job",
)
}
let value = spec.collection()
let groups = message.groups.map(fn(group) {
if group.tag != 2 {
return group
}
let attributes = group.attributes.filter(fn(attribute) {
attribute.name != "media" && attribute.name != "media-col"
})
attributes.push(Attribute::new("media-col", value))
Group::job(attributes)
})
if !groups.any(fn(group) { group.tag == 2 }) {
groups.push(Group::job([Attribute::new("media-col", value)]))
}
Message::{ ..message, groups, }
}