use crate::error::Error;
use crate::formats::{SourceFormat, TargetFormat};
use serde::Serialize;
use serde_json::{Map, Value};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum ColorMode {
#[serde(rename = "BW")]
Bw,
#[serde(rename = "GRAYSCALE")]
Grayscale,
#[serde(rename = "COLOR")]
Color,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum PdfConversionMode {
#[serde(rename = "IMAGE")]
Image,
#[serde(rename = "NATIVE")]
Native,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
#[non_exhaustive]
pub enum ZplImageCompression {
#[serde(rename = "Z64")]
Z64,
#[serde(rename = "COMPRESSED_HEX")]
CompressedHex,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
pub struct Position {
pub x: i64,
pub y: i64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
pub struct LabelSize {
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<f64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub conversion_mode: Option<PdfConversionMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_number: Option<i64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ZplOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub commands_to_ignore: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_compression: Option<ZplImageCompression>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConversionOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub dpi: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rotation: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scaling: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub color_mode: Option<ColorMode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub darkness: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<Position>,
#[serde(skip_serializing_if = "Option::is_none")]
pub watermark: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dialect: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Vec<Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<LabelSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pdf: Option<PdfOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub zpl: Option<ZplOptions>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl ConversionOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn dpi(mut self, dpi: i64) -> Self {
self.dpi = Some(dpi);
self
}
#[must_use]
pub fn rotation(mut self, degrees: i64) -> Self {
self.rotation = Some(degrees);
self
}
#[must_use]
pub fn scaling(mut self, percent: f64) -> Self {
self.scaling = Some(percent);
self
}
#[must_use]
pub fn color_mode(mut self, mode: ColorMode) -> Self {
self.color_mode = Some(mode);
self
}
#[must_use]
pub fn darkness(mut self, darkness: i64) -> Self {
self.darkness = Some(darkness);
self
}
#[must_use]
pub fn position(mut self, x: i64, y: i64) -> Self {
self.position = Some(Position { x, y });
self
}
#[must_use]
pub fn watermark(mut self, watermark: bool) -> Self {
self.watermark = Some(watermark);
self
}
#[must_use]
pub fn dialect(mut self, dialect: impl Into<String>) -> Self {
self.dialect = Some(dialect.into());
self
}
#[must_use]
pub fn label_size(mut self, width_inches: f64, height_inches: f64) -> Self {
self.label = Some(LabelSize {
width: Some(width_inches),
height: Some(height_inches),
});
self
}
#[must_use]
pub fn pdf_conversion_mode(mut self, mode: PdfConversionMode) -> Self {
self.pdf
.get_or_insert_with(PdfOptions::default)
.conversion_mode = Some(mode);
self
}
#[must_use]
pub fn pdf_page(mut self, zero_based_page_number: i64) -> Self {
self.pdf.get_or_insert_with(PdfOptions::default).page_number = Some(zero_based_page_number);
self
}
#[must_use]
pub fn zpl_commands_to_ignore<I, S>(mut self, commands: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.zpl
.get_or_insert_with(ZplOptions::default)
.commands_to_ignore = Some(commands.into_iter().map(Into::into).collect());
self
}
#[must_use]
pub fn zpl_image_compression(mut self, compression: ZplImageCompression) -> Self {
self.zpl
.get_or_insert_with(ZplOptions::default)
.image_compression = Some(compression);
self
}
#[must_use]
pub fn with_data<I>(mut self, records: I) -> Self
where
I: IntoIterator<Item = Map<String, Value>>,
{
self.data = Some(records.into_iter().map(Value::Object).collect());
self
}
#[must_use]
pub fn extra(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
pub(crate) fn serialize(&self) -> Result<Option<String>, Error> {
if let Some(rotation) = self.rotation {
if rotation % 90 != 0 {
return Err(Error::validation(
"rotation",
format!("Rotation must be a multiple of 90 degrees, but was {rotation}."),
));
}
}
if let Some(darkness) = self.darkness {
if !(0..=100).contains(&darkness) {
return Err(Error::validation(
"darkness",
format!("Darkness must be between 0 and 100, but was {darkness}."),
));
}
}
if let Some(records) = &self.data {
for (index, record) in records.iter().enumerate() {
if !record.is_object() {
return Err(Error::validation(
"data",
format!(
"data[{index}] is {}; every entry must be an object whose keys are \
the label's variable field names.",
describe(record)
),
));
}
}
}
let rendered = serde_json::to_value(self).map_err(|error| {
Error::validation(
"options",
format!("could not serialize the options: {error}"),
)
})?;
match rendered {
Value::Object(map) if map.is_empty() => Ok(None),
other => Ok(Some(other.to_string())),
}
}
}
fn describe(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
#[derive(Clone, Debug)]
pub struct ConvertRequest {
pub source: SourceFormat,
pub target: TargetFormat,
pub body: Vec<u8>,
pub base64_text: bool,
pub options: ConversionOptions,
}
impl ConvertRequest {
#[must_use]
pub fn new(source: SourceFormat, target: TargetFormat, body: impl Into<Vec<u8>>) -> Self {
Self {
source,
target,
body: body.into(),
base64_text: false,
options: ConversionOptions::new(),
}
}
#[must_use]
pub fn options(mut self, options: ConversionOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn base64_text(mut self, base64_text: bool) -> Self {
self.base64_text = base64_text;
self
}
}