pconvert-rust 0.5.1

Rust version of P(NG)Convert, a simple PNG conversion tool.
Documentation
//! PNG decode/encode and load functions, console log macros,
//! argument parsing from javascript input to inner-crate rust types
//! and other utility functions.

use crate::blending::params::{BlendAlgorithmParams, Value};
use crate::blending::BlendAlgorithm;
use crate::errors::PConvertError;
use crate::utils::{decode_png, encode_png};
use crate::utils::{image_compression_from, image_filter_from};
use crate::wasm::conversions::JSONParams;
use image::codecs::png::{CompressionType, FilterType};
use image::{ImageBuffer, Rgba};
use js_sys::{Array, Uint8Array};
use serde_json::Value as JSONValue;
use std::collections::HashMap;
use std::str::FromStr;
use wasm_bindgen::prelude::*;
use wasm_bindgen::Clamped;
use wasm_bindgen_futures::JsFuture;
use web_sys::{File, ImageData};

#[wasm_bindgen]
extern "C" {
    #[derive(Clone, Debug)]
    pub type NodeFs;

    #[wasm_bindgen(js_namespace = console)]
    pub fn log(s: &str);

    #[wasm_bindgen(js_name = require)]
    pub fn node_require(s: &str) -> NodeFs;

    #[wasm_bindgen(method, js_name = readFileSync, structural)]
    fn readFileSync(fs: &NodeFs, path: &str) -> Vec<u8>;

    #[wasm_bindgen(method, js_name = readFile, structural)]
    fn readFile(fs: &NodeFs, path: &str, callback: js_sys::Function);

    #[wasm_bindgen(method, js_name = writeFileSync, structural)]
    fn writeFileSync(fs: &NodeFs, path: &str, data: &[u8]);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

/// Receives a `File` and returns the decoded PNG byte buffer.
pub async fn load_png(
    file: File,
    demultiply: bool,
) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, JsValue> {
    let array_buffer = JsFuture::from(file.array_buffer()).await?;
    let uint8_array = Uint8Array::new(&array_buffer);
    let png = decode_png(&uint8_array.to_vec()[..], demultiply)?;
    Ok(png)
}

/// Receives png buffer data and encodes it as a `File` with specified
/// `CompressionType` and `FilterType`.
pub fn encode_file(
    image_buffer: ImageBuffer<Rgba<u8>, Vec<u8>>,
    compression: CompressionType,
    filter: FilterType,
    target_file_name: String,
) -> Result<File, JsValue> {
    let mut encoded_data = Vec::<u8>::with_capacity(image_buffer.to_vec().capacity());
    encode_png(&mut encoded_data, &image_buffer, compression, filter)?;

    unsafe {
        let array_buffer = Uint8Array::view(&encoded_data);
        File::new_with_u8_array_sequence(&Array::of1(&array_buffer), &target_file_name)
    }
}

/// Receives png buffer data and encodes it as an `ImageData` object with
/// specified `CompressionType` and `FilterType`.
pub fn encode_image_data(
    image_buffer: ImageBuffer<Rgba<u8>, Vec<u8>>,
    compression: CompressionType,
    filter: FilterType,
) -> Result<ImageData, JsValue> {
    let (width, height) = image_buffer.dimensions();

    let mut encoded_data = Vec::<u8>::with_capacity(image_buffer.to_vec().capacity());
    encode_png(&mut encoded_data, &image_buffer, compression, filter)?;

    let bytes = &mut image_buffer.to_vec();
    let clamped_bytes: Clamped<&[u8]> = Clamped(bytes);

    ImageData::new_with_u8_clamped_array_and_sh(clamped_bytes, width, height)
}

/// Attempts to parse a `&String` to a `BlendAlgorithm`.
/// Returns the enum variant if it suceeds. Otherwise it returns a `PConvertError`.
pub fn build_algorithm(algorithm: &str) -> Result<BlendAlgorithm, PConvertError> {
    match BlendAlgorithm::from_str(algorithm) {
        Ok(algorithm) => Ok(algorithm),
        Err(algorithm) => Err(PConvertError::ArgumentError(format!(
            "Invalid algorithm '{}'",
            algorithm
        ))),
    }
}

/// Attempts to build a vector of blending operations and extra parameters.
/// One pair per blending operation. Returns a `PConvertError` if it fails parsing.
pub fn build_params(
    algorithms: &[JsValue],
) -> Result<Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)>, PConvertError> {
    let mut result = Vec::new();

    for algorithm in algorithms {
        if algorithm.is_string() {
            let algorithm = build_algorithm(
                &algorithm
                    .as_string()
                    .unwrap_or_else(|| "multiplicative".to_string()),
            )?;

            result.push((algorithm, None));
        } else if algorithm.is_object() {
            let params: JSONParams = serde_wasm_bindgen::from_value(algorithm.clone()).unwrap();
            let algorithm = build_algorithm(&params.algorithm)?;

            let mut blending_params = BlendAlgorithmParams::new();
            for (param_name, param_value) in params.params {
                let param_value: Value = param_value.into();
                blending_params.insert(param_name, param_value);
            }

            result.push((algorithm, Some(blending_params)));
        }
    }

    Ok(result)
}

/// Retrieves the `image::codecs::png::CompressionType` value from the
/// `HashMap<String, JSONValue>` map if it exists.
/// Otherwise it returns the default value: `CompressionType::Fast`.
pub fn get_compression_type(options: &Option<HashMap<String, JSONValue>>) -> CompressionType {
    options.as_ref().map_or(CompressionType::Fast, |options| {
        options
            .get("compression")
            .map_or(CompressionType::Fast, |compression| match compression {
                JSONValue::String(compression) => image_compression_from(compression.to_string()),
                _ => CompressionType::Fast,
            })
    })
}

/// Retrieves the `image::codecs::png::FilterType` value from the
/// `HashMap<String, JSONValue>` map if it exists.
/// Otherwise it returns the default value: `FilterType::NoFilter`.
pub fn get_filter_type(options: &Option<HashMap<String, JSONValue>>) -> FilterType {
    options.as_ref().map_or(FilterType::NoFilter, |options| {
        options
            .get("filter")
            .map_or(FilterType::NoFilter, |filter| match filter {
                JSONValue::String(filter) => image_filter_from(filter.to_string()),
                _ => FilterType::NoFilter,
            })
    })
}

/// Logs the header/column names of the benchmarks table to the browser
/// console (with `console.log`).
pub fn log_benchmark_header() {
    console_log!(
        "{:<20}{:<20}{:<20}{:<20}",
        "Algorithm",
        "Compression",
        "Filter",
        "Times"
    );
}

/// Logs one line (algorithm, compression, filter, blend time, read time, write time)
/// of the benchmarks table to the browser console (with `console.log`).
pub fn log_benchmark(
    algorithm: String,
    compression: CompressionType,
    filter: FilterType,
    blend_time: f64,
    read_time: f64,
    write_time: f64,
) {
    console_log!(
        "{:<20}{:<20}{:<20}{:<20}",
        algorithm,
        format!("{:#?}", compression),
        format!("{:#?}", filter),
        format!(
            "{}ms (blend {}ms, read {}ms, write {}ms)",
            read_time + blend_time + write_time,
            blend_time,
            read_time,
            write_time
        )
    );
}

/// Wrapper function for nodejs `fs.readFileSync`.
pub fn node_read_file_sync(fs: &NodeFs, path: &str) -> Vec<u8> {
    fs.readFileSync(path)
}

/// Rust Future from nodejs `fs.readFile` Promise (awaitable in node).
pub fn node_read_file_async(fs: &NodeFs, path: &str) -> wasm_bindgen_futures::JsFuture {
    let promise = js_sys::Promise::new(&mut |resolve, reject| {
        let callback = js_sys::Function::new_with_args(
            "resolve, reject, err, data",
            "err ? reject(err) : resolve(data);",
        )
        .bind2(&JsValue::NULL, &resolve, &reject);
        fs.readFile(path, callback)
    });

    wasm_bindgen_futures::JsFuture::from(promise)
}

/// Wrapper function for nodejs `fs.writeFileSync`.
pub fn node_write_file_sync(fs: &NodeFs, path: &str, data: &[u8]) {
    fs.writeFileSync(path, data);
}