Initial commit

This commit is contained in:
2025-03-20 08:04:47 +01:00
commit f81480587b
14 changed files with 759 additions and 0 deletions

176
src/datamatrix.rs Normal file
View File

@@ -0,0 +1,176 @@
use datamatrix::{DataMatrixBuilder, EncodationType};
use datamatrix::data::DataEncodingError;
use datamatrix::placement::PathSegment;
use flagset::FlagSet;
use js_sys::{JsString, Uint8Array};
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const TS_ENCODINGS: &'static str = r#"
/// Ugh, I hate this so much. But this is what it says in the spec...
type EncodationName = "ascii" | "c40" | "text" | "x12" | "edifact" | "base256";
type Encodation = EncodationName | Array<EncodationName>;
interface CodingOptions {
size?: SymbolSizeSpec;
macros?: bool;
encodings?: Encodation;
}
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "CodingOptions")]
pub type CodingOptions;
type Encodation;
#[wasm_bindgen(method, structural, getter = size)]
fn co_size(this: &CodingOptions) -> Option<crate::size_spec::SizeSpec>;
#[wasm_bindgen(method, structural, getter = macros)]
fn co_macros(this: &CodingOptions) -> Option<bool>;
#[wasm_bindgen(method, structural, getter = encodings)]
fn co_encodings(this: &CodingOptions) -> Option<Encodation>;
#[wasm_bindgen(typescript_type = "Uint8Array | string")]
pub type Buffer;
}
enum DataSource {
String(String),
Bytes(Vec<u8>),
}
fn buffer_from_js(buf: Buffer) -> Result<DataSource, JsError> {
buf.dyn_into::<Uint8Array>()
.map(|array| DataSource::Bytes(array.to_vec()))
.or_else(|js| {
js.dyn_into::<js_sys::JsString>()
.and_then(|s| s.as_string().ok_or_else(|| JsValue::from(s).into()))
.map(DataSource::String)
})
.or_else(|_| Err(JsError::new("Buffer must be a string or Uint8Array")))
}
fn encodation_type_from_js_string(s: JsValue) -> EncodationType {
let s = s
.as_string()
.unwrap_or_else(|| wasm_bindgen::throw_str("Invalid encodation type"));
let string = s.as_str();
match string {
"ascii" => EncodationType::Ascii,
"c40" => EncodationType::C40,
"text" => EncodationType::Text,
"x12" => EncodationType::X12,
"edifact" => EncodationType::Edifact,
"base256" => EncodationType::Base256,
_ => wasm_bindgen::throw_str("Invalid encodation type"),
}
}
fn encodation_type_from_js(e: Encodation) -> FlagSet<EncodationType> {
e.dyn_into::<JsString>()
.map(|s| encodation_type_from_js_string(s.into()).into())
.or_else(|e| {
e.dyn_into::<js_sys::Array>().map(|a| {
a.iter()
.map(|js| encodation_type_from_js_string(js))
.fold(FlagSet::default(), |a, n| a | n)
})
})
.unwrap_or_else(|_| wasm_bindgen::throw_str("Invalid encodation type"))
}
#[wasm_bindgen]
pub struct DataMatrix{
dmtx: datamatrix::DataMatrix,
bitmap: datamatrix::placement::Bitmap<bool>,
}
#[wasm_bindgen]
impl DataMatrix {
#[wasm_bindgen(constructor)]
pub fn encode(data: Buffer, options: Option<CodingOptions>) -> Result<DataMatrix, JsError> {
let mut builder = DataMatrixBuilder::new();
if let Some(options) = options {
if let Some(size) = options.co_size() {
let size = crate::size_spec::symbolset_from_json(size);
builder = builder.with_symbol_list(size);
}
if let Some(encoding) = options.co_encodings() {
builder = builder.with_encodation_types(encodation_type_from_js(encoding));
}
if let Some(macros) = options.co_macros() {
builder = builder.with_macros(macros);
}
}
match buffer_from_js(data)? {
DataSource::Bytes(bytes) => builder.encode(bytes.as_slice()),
DataSource::String(str) => builder.encode_str(&str),
}
.map(|dmtx| {
let bitmap = dmtx.bitmap();
DataMatrix{dmtx, bitmap}
})
.map_err(|err| match err {
DataEncodingError::TooMuchOrIllegalData => "Too much data",
DataEncodingError::SymbolListEmpty => "No valid symbol size",
})
.map_err(|err| JsError::new(err))
.map_err(Into::into)
}
/// Get the full set of codewords encoded into this barcode
pub fn codewords(&self) -> Box<[u8]> {
self.dmtx.codewords().to_vec().into_boxed_slice()
}
/// Get the data part of the low-level encoding of the barcode
pub fn data_codewords(&self) -> Box<[u8]> {
self.dmtx.data_codewords().to_vec().into_boxed_slice()
}
/// Draw the barcode via an SVG path. Note: this path expects even/odd fill
/// i.e., embed this in `<path fill-rule="evenodd" d="..." />
pub fn svg_path(&self, x0: Option<isize>, y0: Option<isize>) -> String {
use std::fmt::Write;
let mut path: String = String::new();
let x0 = x0.unwrap_or(0);
let y0 = y0.unwrap_or(0);
write!(path, "M{x0},{y0}").unwrap();
for part in self.bitmap.path() {
match part {
PathSegment::Move(dx, dy) => write!(path, "m{dx},{dy}"),
PathSegment::Horizontal(h) => write!(path, "h{h}"),
PathSegment::Vertical(v) => write!(path, "v{v}"),
PathSegment::Close => write!(path, "z"),
}.unwrap();
}
path
}
/// Return an array of filled pixel coordinates.
/// The returned buffer is `[x0,y0,x1,y1,...]`
pub fn pixels(&self) -> Box<[u8]> {
let mut ret = Vec::new();
for (x,y) in self.bitmap.pixels() {
if x > 255 || y > 255 {
panic!("Returned too high of a pixel number");
}
ret.push(x as u8);
ret.push(y as u8);
}
ret.into_boxed_slice()
}
pub fn width(&self) -> usize {
self.bitmap.width()
}
pub fn height(&self) -> usize {
self.bitmap.height()
}
}

14
src/lib.rs Normal file
View File

@@ -0,0 +1,14 @@
mod utils;
macro_rules! hash_map {
($($($key:expr),+ => $value:expr),*$(,)?) => {{
let mut res = HashMap::new();
$($(
res.insert($key, $value);
)+)*
res
}};
}
mod size_spec;
mod datamatrix;

183
src/size_spec.rs Normal file
View File

@@ -0,0 +1,183 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::LazyLock;
use datamatrix::{SymbolList, SymbolSize};
use wasm_bindgen::throw_val;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const SIZE_SPEC_TS: &'static str = r#"
interface SymbolSizeFilter {
square?: bool;
rectangle?: bool;
extended?: bool;
min_width?: number;
max_width?: number;
min_height?: number;
max_height?: number;
}
type SymbolSize = "10" | "10x10" | "12" | "12x12" | "14" | "14x14" | "16" |
"16x16" | "18" | "18x18" | "20" | "20x20" | "22" | "22x22" | "24" |
"24x24" | "26" | "26x26" | "32" | "32x32" | "36" | "36x36" | "40" |
"40x40" | "44" | "44x44" | "48" | "48x48" | "52" | "52x52" | "64" |
"64x64" | "72" | "72x72" | "80" | "80x80" | "88" | "88x88" | "96" |
"96x96" | "104" | "104x104" | "120" | "120x120" | "132" | "132x132" |
"144" | "144x144" | "8x18" | "8x32" | "12x26" | "12x36" | "16x36" |
"16x48" | "8x48" | "8x64" | "8x80" | "8x96" | "8x120" | "8x144" |
"12x64" | "12x88" | "16x64" | "20x36" | "20x44" | "20x64" | "22x48" |
"24x48" | "24x64" | "26x40" | "26x48" | "26x64";
type SymbolSizeSpec = SymbolSize | SymbolSizeFilter | Array<SymbolSizeSpec>;
"#;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(typescript_type = "SymbolSizeSpec")]
pub type SizeSpec;
}
#[derive(Serialize, Deserialize)]
pub struct SizeFilter {
square: Option<bool>,
rectangle: Option<bool>,
extended: Option<bool>,
min_width: Option<usize>,
max_width: Option<usize>,
min_height: Option<usize>,
max_height: Option<usize>,
}
static SYMBOL_SIZE_STRINGS: LazyLock<HashMap<&'static str, SymbolSize>> = LazyLock::new(|| {
hash_map! {
"10" , "10x10" => SymbolSize::Square10,
"12" , "12x12" => SymbolSize::Square12,
"14" , "14x14" => SymbolSize::Square14,
"16" , "16x16" => SymbolSize::Square16,
"18" , "18x18" => SymbolSize::Square18,
"20" , "20x20" => SymbolSize::Square20,
"22" , "22x22" => SymbolSize::Square22,
"24" , "24x24" => SymbolSize::Square24,
"26" , "26x26" => SymbolSize::Square26,
"32", "32x32" => SymbolSize::Square32,
"36", "36x36" => SymbolSize::Square36,
"40", "40x40" => SymbolSize::Square40,
"44", "44x44" => SymbolSize::Square44,
"48", "48x48" => SymbolSize::Square48,
"52", "52x52" => SymbolSize::Square52,
"64", "64x64" => SymbolSize::Square64,
"72", "72x72" => SymbolSize::Square72,
"80", "80x80" => SymbolSize::Square80,
"88", "88x88" => SymbolSize::Square88,
"96", "96x96" => SymbolSize::Square96,
"104", "104x104" => SymbolSize::Square104,
"120", "120x120" => SymbolSize::Square120,
"132", "132x132" => SymbolSize::Square132,
"144", "144x144" => SymbolSize::Square144,
"8x18" => SymbolSize::Rect8x18,
"8x32" => SymbolSize::Rect8x32,
"12x26" => SymbolSize::Rect12x26,
"12x36" => SymbolSize::Rect12x36,
"16x36" => SymbolSize::Rect16x36,
"16x48" => SymbolSize::Rect16x48,
"8x48" => SymbolSize::Rect8x48,
"8x64" => SymbolSize::Rect8x64,
"8x80" => SymbolSize::Rect8x80,
"8x96" => SymbolSize::Rect8x96,
"8x120" => SymbolSize::Rect8x120,
"8x144" => SymbolSize::Rect8x144,
"12x64" => SymbolSize::Rect12x64,
"12x88" => SymbolSize::Rect12x88,
"16x64" => SymbolSize::Rect16x64,
"20x36" => SymbolSize::Rect20x36,
"20x44" => SymbolSize::Rect20x44,
"20x64" => SymbolSize::Rect20x64,
"22x48" => SymbolSize::Rect22x48,
"24x48" => SymbolSize::Rect24x48,
"24x64" => SymbolSize::Rect24x64,
"26x40" => SymbolSize::Rect26x40,
"26x48" => SymbolSize::Rect26x48,
"26x64" => SymbolSize::Rect26x64,
}
});
fn symbolsize_from_string(name: &str) -> Option<SymbolSize> {
SYMBOL_SIZE_STRINGS.get(name).copied()
}
fn symbolsize_from_int(name: u32) -> Option<SymbolSize> {
Some(match name {
10 => SymbolSize::Square10,
12 => SymbolSize::Square12,
14 => SymbolSize::Square14,
16 => SymbolSize::Square16,
18 => SymbolSize::Square18,
20 => SymbolSize::Square20,
22 => SymbolSize::Square22,
24 => SymbolSize::Square24,
26 => SymbolSize::Square26,
32 => SymbolSize::Square32,
36 => SymbolSize::Square36,
40 => SymbolSize::Square40,
44 => SymbolSize::Square44,
48 => SymbolSize::Square48,
52 => SymbolSize::Square52,
64 => SymbolSize::Square64,
72 => SymbolSize::Square72,
80 => SymbolSize::Square80,
88 => SymbolSize::Square88,
96 => SymbolSize::Square96,
104 => SymbolSize::Square104,
120 => SymbolSize::Square120,
132 => SymbolSize::Square132,
144 => SymbolSize::Square144,
_ => return None,
})
}
pub fn symbolset_from_json(json: SizeSpec) -> SymbolList {
if let Some(size) = json.as_string() {
if let Some(size) = symbolsize_from_string(&size) {
return size.into()
}
} else if let Some(size) = json.as_f64() {
if size as u32 as f64 == size {
if let Some(size) = symbolsize_from_int(size as u32) {
return size.into()
}
}
} else if let Some(arr) = json.dyn_ref::<js_sys::Array>() {
let mut symset = SymbolList::with_whitelist(std::iter::empty());
for item in arr.values() {
match item {
Err(err) => throw_val(err),
Ok(val) => symset.extend(symbolset_from_json(val.into())),
}
}
return symset;
} else if let Ok(value) = serde_wasm_bindgen::from_value::<SizeFilter>(json.into()) {
let mut symset = if value.extended.unwrap_or(false) {
SymbolList::with_extended_rectangles()
} else {
SymbolList::default()
};
if !value.square.unwrap_or(true) {
symset = symset.enforce_rectangular()
}
if !value.rectangle.unwrap_or(true) {
symset = symset.enforce_square()
}
if let Some(v) = value.min_height {
symset = symset.enforce_height_in(v..);
}
if let Some(v) = value.max_height {
symset = symset.enforce_height_in(..v + 1);
}
if let Some(v) = value.min_width {
symset = symset.enforce_width_in(v..);
}
if let Some(v) = value.max_width {
symset = symset.enforce_width_in(..v + 1);
}
return symset;
}
wasm_bindgen::throw_str("Invalid size");
}

11
src/utils.rs Normal file
View File

@@ -0,0 +1,11 @@
#[allow(unused)]
pub fn set_panic_hook() {
// When the `console_error_panic_hook` feature is enabled, we can call the
// `set_panic_hook` function at least once during initialization, and then
// we will get better error messages if our code ever panics.
//
// For more details see
// https://github.com/rustwasm/console_error_panic_hook#readme
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}