pconvert_rust/wasm/
utils.rs

1//! PNG decode/encode and load functions, console log macros,
2//! argument parsing from javascript input to inner-crate rust types
3//! and other utility functions.
4
5use crate::blending::params::{BlendAlgorithmParams, Value};
6use crate::blending::BlendAlgorithm;
7use crate::errors::PConvertError;
8use crate::utils::{decode_png, encode_png};
9use crate::utils::{image_compression_from, image_filter_from};
10use crate::wasm::conversions::JSONParams;
11use image::codecs::png::{CompressionType, FilterType};
12use image::{ImageBuffer, Rgba};
13use js_sys::{Array, Uint8Array};
14use serde_json::Value as JSONValue;
15use std::collections::HashMap;
16use std::str::FromStr;
17use wasm_bindgen::prelude::*;
18use wasm_bindgen::Clamped;
19use wasm_bindgen_futures::JsFuture;
20use web_sys::{File, ImageData};
21
22#[wasm_bindgen]
23extern "C" {
24    #[derive(Clone, Debug)]
25    pub type NodeFs;
26
27    #[wasm_bindgen(js_namespace = console)]
28    pub fn log(s: &str);
29
30    #[wasm_bindgen(js_name = require)]
31    pub fn node_require(s: &str) -> NodeFs;
32
33    #[wasm_bindgen(method, js_name = readFileSync, structural)]
34    fn readFileSync(fs: &NodeFs, path: &str) -> Vec<u8>;
35
36    #[wasm_bindgen(method, js_name = readFile, structural)]
37    fn readFile(fs: &NodeFs, path: &str, callback: js_sys::Function);
38
39    #[wasm_bindgen(method, js_name = writeFileSync, structural)]
40    fn writeFileSync(fs: &NodeFs, path: &str, data: &[u8]);
41}
42
43macro_rules! console_log {
44    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
45}
46
47/// Receives a `File` and returns the decoded PNG byte buffer.
48pub async fn load_png(
49    file: File,
50    demultiply: bool,
51) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, JsValue> {
52    let array_buffer = JsFuture::from(file.array_buffer()).await?;
53    let uint8_array = Uint8Array::new(&array_buffer);
54    let png = decode_png(&uint8_array.to_vec()[..], demultiply)?;
55    Ok(png)
56}
57
58/// Receives png buffer data and encodes it as a `File` with specified
59/// `CompressionType` and `FilterType`.
60pub fn encode_file(
61    image_buffer: ImageBuffer<Rgba<u8>, Vec<u8>>,
62    compression: CompressionType,
63    filter: FilterType,
64    target_file_name: String,
65) -> Result<File, JsValue> {
66    let mut encoded_data = Vec::<u8>::with_capacity(image_buffer.to_vec().capacity());
67    encode_png(&mut encoded_data, &image_buffer, compression, filter)?;
68
69    unsafe {
70        let array_buffer = Uint8Array::view(&encoded_data);
71        File::new_with_u8_array_sequence(&Array::of1(&array_buffer), &target_file_name)
72    }
73}
74
75/// Receives png buffer data and encodes it as an `ImageData` object with
76/// specified `CompressionType` and `FilterType`.
77pub fn encode_image_data(
78    image_buffer: ImageBuffer<Rgba<u8>, Vec<u8>>,
79    compression: CompressionType,
80    filter: FilterType,
81) -> Result<ImageData, JsValue> {
82    let (width, height) = image_buffer.dimensions();
83
84    let mut encoded_data = Vec::<u8>::with_capacity(image_buffer.to_vec().capacity());
85    encode_png(&mut encoded_data, &image_buffer, compression, filter)?;
86
87    let bytes = &mut image_buffer.to_vec();
88    let clamped_bytes: Clamped<&[u8]> = Clamped(bytes);
89
90    ImageData::new_with_u8_clamped_array_and_sh(clamped_bytes, width, height)
91}
92
93/// Attempts to parse a `&String` to a `BlendAlgorithm`.
94/// Returns the enum variant if it suceeds. Otherwise it returns a `PConvertError`.
95pub fn build_algorithm(algorithm: &str) -> Result<BlendAlgorithm, PConvertError> {
96    match BlendAlgorithm::from_str(algorithm) {
97        Ok(algorithm) => Ok(algorithm),
98        Err(algorithm) => Err(PConvertError::ArgumentError(format!(
99            "Invalid algorithm '{}'",
100            algorithm
101        ))),
102    }
103}
104
105/// Attempts to build a vector of blending operations and extra parameters.
106/// One pair per blending operation. Returns a `PConvertError` if it fails parsing.
107pub fn build_params(
108    algorithms: &[JsValue],
109) -> Result<Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)>, PConvertError> {
110    let mut result = Vec::new();
111
112    for algorithm in algorithms {
113        if algorithm.is_string() {
114            let algorithm = build_algorithm(
115                &algorithm
116                    .as_string()
117                    .unwrap_or_else(|| "multiplicative".to_string()),
118            )?;
119
120            result.push((algorithm, None));
121        } else if algorithm.is_object() {
122            let params: JSONParams = serde_wasm_bindgen::from_value(algorithm.clone()).unwrap();
123            let algorithm = build_algorithm(&params.algorithm)?;
124
125            let mut blending_params = BlendAlgorithmParams::new();
126            for (param_name, param_value) in params.params {
127                let param_value: Value = param_value.into();
128                blending_params.insert(param_name, param_value);
129            }
130
131            result.push((algorithm, Some(blending_params)));
132        }
133    }
134
135    Ok(result)
136}
137
138/// Retrieves the `image::codecs::png::CompressionType` value from the
139/// `HashMap<String, JSONValue>` map if it exists.
140/// Otherwise it returns the default value: `CompressionType::Fast`.
141pub fn get_compression_type(options: &Option<HashMap<String, JSONValue>>) -> CompressionType {
142    options.as_ref().map_or(CompressionType::Fast, |options| {
143        options
144            .get("compression")
145            .map_or(CompressionType::Fast, |compression| match compression {
146                JSONValue::String(compression) => image_compression_from(compression.to_string()),
147                _ => CompressionType::Fast,
148            })
149    })
150}
151
152/// Retrieves the `image::codecs::png::FilterType` value from the
153/// `HashMap<String, JSONValue>` map if it exists.
154/// Otherwise it returns the default value: `FilterType::NoFilter`.
155pub fn get_filter_type(options: &Option<HashMap<String, JSONValue>>) -> FilterType {
156    options.as_ref().map_or(FilterType::NoFilter, |options| {
157        options
158            .get("filter")
159            .map_or(FilterType::NoFilter, |filter| match filter {
160                JSONValue::String(filter) => image_filter_from(filter.to_string()),
161                _ => FilterType::NoFilter,
162            })
163    })
164}
165
166/// Logs the header/column names of the benchmarks table to the browser
167/// console (with `console.log`).
168pub fn log_benchmark_header() {
169    console_log!(
170        "{:<20}{:<20}{:<20}{:<20}",
171        "Algorithm",
172        "Compression",
173        "Filter",
174        "Times"
175    );
176}
177
178/// Logs one line (algorithm, compression, filter, blend time, read time, write time)
179/// of the benchmarks table to the browser console (with `console.log`).
180pub fn log_benchmark(
181    algorithm: String,
182    compression: CompressionType,
183    filter: FilterType,
184    blend_time: f64,
185    read_time: f64,
186    write_time: f64,
187) {
188    console_log!(
189        "{:<20}{:<20}{:<20}{:<20}",
190        algorithm,
191        format!("{:#?}", compression),
192        format!("{:#?}", filter),
193        format!(
194            "{}ms (blend {}ms, read {}ms, write {}ms)",
195            read_time + blend_time + write_time,
196            blend_time,
197            read_time,
198            write_time
199        )
200    );
201}
202
203/// Wrapper function for nodejs `fs.readFileSync`.
204pub fn node_read_file_sync(fs: &NodeFs, path: &str) -> Vec<u8> {
205    fs.readFileSync(path)
206}
207
208/// Rust Future from nodejs `fs.readFile` Promise (awaitable in node).
209pub fn node_read_file_async(fs: &NodeFs, path: &str) -> wasm_bindgen_futures::JsFuture {
210    let promise = js_sys::Promise::new(&mut |resolve, reject| {
211        let callback = js_sys::Function::new_with_args(
212            "resolve, reject, err, data",
213            "err ? reject(err) : resolve(data);",
214        )
215        .bind2(&JsValue::NULL, &resolve, &reject);
216        fs.readFile(path, callback)
217    });
218
219    wasm_bindgen_futures::JsFuture::from(promise)
220}
221
222/// Wrapper function for nodejs `fs.writeFileSync`.
223pub fn node_write_file_sync(fs: &NodeFs, path: &str, data: &[u8]) {
224    fs.writeFileSync(path, data);
225}