pconvert_rust/wasm/
mod.rs

1//! Web Assembly (WASM) extension, exported functions and type conversions.
2
3#[macro_use]
4pub mod utils;
5
6pub mod benchmark;
7pub mod conversions;
8
9use crate::blending::params::BlendAlgorithmParams;
10use crate::blending::{
11    blend_images, demultiply_image, get_blending_algorithm, is_algorithm_multiplied, BlendAlgorithm,
12};
13use crate::constants;
14use crate::errors::PConvertError;
15use crate::utils::{decode_png, encode_png};
16use image::{ImageBuffer, Rgba, RgbaImage};
17use js_sys::try_iter;
18use serde::Serialize;
19use serde_json::json;
20use serde_wasm_bindgen::Serializer;
21use utils::{
22    build_algorithm, build_params, encode_file, encode_image_data, get_compression_type,
23    get_filter_type, load_png, node_read_file_async, node_read_file_sync, node_require,
24    node_write_file_sync,
25};
26use wasm_bindgen::prelude::*;
27use web_sys::{File, ImageData};
28
29/// Blends two `File`s into one, named `target_file_name`, using `algorithm` and the extra
30/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
31#[wasm_bindgen(js_name = blendImages)]
32pub async fn blend_images_js(
33    bot: File,
34    top: File,
35    target_file_name: String,
36    algorithm: Option<String>,
37    is_inline: Option<bool>,
38    options: JsValue,
39) -> Result<File, JsValue> {
40    let options = match options.is_object() {
41        true => serde_wasm_bindgen::from_value(options).ok(),
42        false => None,
43    };
44
45    let mut bot = load_png(bot, false).await?;
46    let mut top = load_png(top, false).await?;
47
48    blend_image_buffers(&mut bot, &mut top, algorithm, is_inline)?;
49
50    encode_file(
51        bot,
52        get_compression_type(&options),
53        get_filter_type(&options),
54        target_file_name,
55    )
56}
57
58/// Blends two `ImageData` objects into one using `algorithm` and the extra
59/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
60#[wasm_bindgen(js_name = blendImagesData)]
61pub fn blend_images_data_js(
62    bot: ImageData,
63    top: ImageData,
64    algorithm: Option<String>,
65    is_inline: Option<bool>,
66    options: JsValue,
67) -> Result<ImageData, JsValue> {
68    let options = match options.is_object() {
69        true => serde_wasm_bindgen::from_value(options).ok(),
70        false => None,
71    };
72
73    let (width, height) = (bot.width(), bot.height());
74    let mut bot = ImageBuffer::from_vec(width, height, bot.data().to_vec())
75        .ok_or_else(|| PConvertError::ArgumentError("Could not parse \"bot\"".to_string()))?;
76    let mut top = ImageBuffer::from_vec(width, height, top.data().to_vec())
77        .ok_or_else(|| PConvertError::ArgumentError("Could not parse \"top\"".to_string()))?;
78
79    blend_image_buffers(&mut bot, &mut top, algorithm, is_inline)?;
80
81    encode_image_data(
82        bot,
83        get_compression_type(&options),
84        get_filter_type(&options),
85    )
86}
87
88/// Blends two image buffers using `algorithm` and the extra
89/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
90pub fn blend_image_buffers(
91    bot: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
92    top: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
93    algorithm: Option<String>,
94    is_inline: Option<bool>,
95) -> Result<(), PConvertError> {
96    let algorithm = algorithm.unwrap_or_else(|| String::from("multiplicative"));
97    let algorithm = build_algorithm(&algorithm)?;
98    let algorithm_fn = get_blending_algorithm(&algorithm);
99    let demultiply = is_algorithm_multiplied(&algorithm);
100    let _is_inline = is_inline.unwrap_or(false);
101
102    if demultiply {
103        demultiply_image(bot);
104        demultiply_image(top);
105    }
106
107    blend_images(bot, top, &algorithm_fn, &None);
108    Ok(())
109}
110
111/// Blends multiple `File`s into one, named `target_file_name`, using `algorithm` and the extra
112/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
113#[wasm_bindgen(js_name = blendMultiple)]
114pub async fn blend_multiple_js(
115    image_files: JsValue,
116    target_file_name: String,
117    algorithm: Option<String>,
118    algorithms: Option<Vec<JsValue>>,
119    is_inline: Option<bool>,
120    options: JsValue,
121) -> Result<File, JsValue> {
122    let options = match options.is_object() {
123        true => serde_wasm_bindgen::from_value(options).ok(),
124        false => None,
125    };
126
127    let mut image_buffers = Vec::new();
128    let image_files = try_iter(&image_files).unwrap().unwrap();
129    for file in image_files {
130        let file = file?;
131        let img = load_png(file.into(), false).await?;
132
133        image_buffers.push(img);
134    }
135
136    let composition = blend_multiple_buffers(image_buffers, algorithm, algorithms, is_inline)?;
137    encode_file(
138        composition,
139        get_compression_type(&options),
140        get_filter_type(&options),
141        target_file_name,
142    )
143}
144
145/// Blends multiple `ImageData` objects into one using `algorithm` and the extra
146/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
147#[wasm_bindgen(js_name = blendMultipleData)]
148pub fn blend_multiple_data_js(
149    images: &JsValue,
150    algorithm: Option<String>,
151    algorithms: Option<Vec<JsValue>>,
152    is_inline: Option<bool>,
153    options: JsValue,
154) -> Result<ImageData, JsValue> {
155    let options = match options.is_object() {
156        true => serde_wasm_bindgen::from_value(options).ok(),
157        false => None,
158    };
159
160    let mut image_buffers: Vec<RgbaImage> = Vec::new();
161    let mut images = try_iter(images).unwrap().unwrap();
162    while let Some(Ok(img_data)) = images.next() {
163        let img_data: ImageData = img_data.into();
164        let img_buffer: RgbaImage = ImageBuffer::from_vec(
165            img_data.width(),
166            img_data.height(),
167            img_data.data().to_vec(),
168        )
169        .ok_or_else(|| PConvertError::ArgumentError("Could not parse \"bot\"".to_string()))?;
170
171        image_buffers.push(img_buffer);
172    }
173
174    let composition = blend_multiple_buffers(image_buffers, algorithm, algorithms, is_inline)?;
175    encode_image_data(
176        composition,
177        get_compression_type(&options),
178        get_filter_type(&options),
179    )
180}
181
182/// Returns a JSON object with the module constants (e.g. ALGORITHMS, COMPILER, COMPILER_VERSION, ...).
183#[wasm_bindgen(js_name = getModuleConstants)]
184pub fn get_module_constants_js() -> JsValue {
185    let filters: Vec<String> = constants::FILTER_TYPES
186        .to_vec()
187        .iter()
188        .map(|x| format!("{:?}", x))
189        .collect();
190
191    let compressions: Vec<String> = constants::COMPRESSION_TYPES
192        .to_vec()
193        .iter()
194        .map(|x| format!("{:?}", x))
195        .collect();
196
197    json!({
198        "COMPILATION_DATE": constants::COMPILATION_DATE,
199        "COMPILATION_TIME": constants::COMPILATION_TIME,
200        "VERSION": constants::VERSION,
201        "ALGORITHMS": constants::ALGORITHMS,
202        "COMPILER": constants::COMPILER,
203        "COMPILER_VERSION": constants::COMPILER_VERSION,
204        "LIBPNG_VERSION": constants::LIBPNG_VERSION,
205        "FEATURES": constants::FEATURES,
206        "PLATFORM_CPU_BITS": constants::PLATFORM_CPU_BITS,
207        "FILTER_TYPES": filters,
208        "COMPRESSION_TYPES": compressions
209    })
210    .serialize(&Serializer::json_compatible())
211    .unwrap()
212}
213
214/// [NodeJS only]
215/// Blends multiple images read from local file system into one using `algorithm` or `algorithms` and the extra
216/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
217#[wasm_bindgen(js_name = blendMultipleFs)]
218pub fn blend_multiple_fs(
219    image_paths: Vec<JsValue>,
220    out_path: String,
221    algorithm: Option<String>,
222    algorithms: Option<Vec<JsValue>>,
223    is_inline: Option<bool>,
224    options: JsValue,
225) -> Result<(), JsValue> {
226    let num_images = image_paths.len();
227
228    if num_images < 1 {
229        return Err(PConvertError::ArgumentError(
230            "ArgumentError: 'img_paths' must contain at least one path".to_string(),
231        )
232        .into());
233    }
234
235    let options = match options.is_object() {
236        true => serde_wasm_bindgen::from_value(options).ok(),
237        false => None,
238    };
239
240    let algorithms_to_apply: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)> =
241        if let Some(algorithms) = algorithms {
242            build_params(&algorithms)?
243        } else if let Some(algorithm) = algorithm {
244            let algorithm = build_algorithm(&algorithm)?;
245            vec![(algorithm, None); num_images - 1]
246        } else {
247            vec![(BlendAlgorithm::Multiplicative, None); num_images - 1]
248        };
249
250    if algorithms_to_apply.len() != num_images - 1 {
251        return Err(PConvertError::ArgumentError(format!(
252            "ArgumentError: 'algorithms' must be of size {} (one per blending operation)",
253            num_images - 1
254        ))
255        .into());
256    };
257
258    let _is_inline = is_inline.unwrap_or(false);
259
260    // loops through the algorithms to apply and blends the
261    // current composition with the next layer
262    let mut img_paths_iter = image_paths.iter();
263    let first_path = img_paths_iter
264        .next()
265        .unwrap()
266        .as_string()
267        .expect("path must be a string");
268
269    let node_fs = node_require("fs");
270
271    let first_demultiply = if !algorithms_to_apply.is_empty() {
272        is_algorithm_multiplied(&algorithms_to_apply[0].0)
273    } else {
274        false
275    };
276    let composition = node_read_file_sync(&node_fs, &first_path);
277    let mut composition = decode_png(&composition[..], first_demultiply)?;
278
279    let zip_iter = img_paths_iter.zip(algorithms_to_apply.iter());
280    for pair in zip_iter {
281        let path = pair.0.as_string().expect("path must be a string");
282        let (algorithm, algorithm_params) = pair.1;
283        let demultiply = is_algorithm_multiplied(algorithm);
284        let algorithm_fn = get_blending_algorithm(algorithm);
285        let current_layer = node_read_file_sync(&node_fs, &path);
286        let current_layer = decode_png(&current_layer[..], demultiply)?;
287        blend_images(
288            &mut composition,
289            &current_layer,
290            &algorithm_fn,
291            algorithm_params,
292        );
293    }
294
295    let compression_type = get_compression_type(&options);
296    let filter_type = get_filter_type(&options);
297
298    let mut encoded_data = Vec::<u8>::with_capacity(composition.to_vec().capacity());
299    encode_png(
300        &mut encoded_data,
301        &composition,
302        compression_type,
303        filter_type,
304    )?;
305
306    node_write_file_sync(&node_fs, &out_path, &encoded_data);
307
308    Ok(())
309}
310
311/// [NodeJS only]
312/// Asynchronously blends multiple images read from local file system into one using `algorithm` or `algorithms` and the extra
313/// `options` given. Algorithm defaults to `BlendAlgorithm::Multiplicative`.
314#[wasm_bindgen(js_name = blendMultipleFsAsync)]
315pub async fn blend_multiple_fs_async(
316    image_paths: Vec<JsValue>,
317    out_path: String,
318    algorithm: Option<String>,
319    algorithms: Option<Vec<JsValue>>,
320    is_inline: Option<bool>,
321    options: JsValue,
322) -> Result<(), JsValue> {
323    let num_images = image_paths.len();
324
325    if num_images < 1 {
326        return Err(PConvertError::ArgumentError(
327            "ArgumentError: 'img_paths' must contain at least one path".to_string(),
328        )
329        .into());
330    }
331
332    let options = match options.is_object() {
333        true => serde_wasm_bindgen::from_value(options).ok(),
334        false => None,
335    };
336
337    let algorithms_to_apply: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)> =
338        if let Some(algorithms) = algorithms {
339            build_params(&algorithms)?
340        } else if let Some(algorithm) = algorithm {
341            let algorithm = build_algorithm(&algorithm)?;
342            vec![(algorithm, None); num_images - 1]
343        } else {
344            vec![(BlendAlgorithm::Multiplicative, None); num_images - 1]
345        };
346
347    if algorithms_to_apply.len() != num_images - 1 {
348        return Err(PConvertError::ArgumentError(format!(
349            "ArgumentError: 'algorithms' must be of size {} (one per blending operation)",
350            num_images - 1
351        ))
352        .into());
353    };
354
355    let _is_inline = is_inline.unwrap_or(false);
356
357    let node_fs = node_require("fs");
358
359    let mut png_futures: Vec<Option<wasm_bindgen_futures::JsFuture>> =
360        Vec::with_capacity(num_images);
361    for path in image_paths.iter() {
362        let path = path.as_string().expect("path must be a string");
363        let png_future = node_read_file_async(&node_fs, &path);
364        png_futures.push(Some(png_future));
365    }
366
367    let first_demultiply = if !algorithms_to_apply.is_empty() {
368        is_algorithm_multiplied(&algorithms_to_apply[0].0)
369    } else {
370        false
371    };
372    let composition = png_futures[0].take().unwrap().await?;
373    let composition = js_sys::Uint8Array::from(composition).to_vec();
374    let mut composition = decode_png(&composition[..], first_demultiply)?;
375
376    // loops through the algorithms to apply and blends the
377    // current composition with the next layer
378    // retrieves the images from the result channels
379    for i in 1..png_futures.len() {
380        let (algorithm, algorithm_params) = &algorithms_to_apply[i - 1];
381        let demultiply = is_algorithm_multiplied(algorithm);
382        let algorithm_fn = get_blending_algorithm(algorithm);
383        let current_layer = png_futures[i].take().unwrap().await?;
384        let current_layer = js_sys::Uint8Array::from(current_layer).to_vec();
385        let current_layer = decode_png(&current_layer[..], demultiply)?;
386
387        blend_images(
388            &mut composition,
389            &current_layer,
390            &algorithm_fn,
391            algorithm_params,
392        );
393    }
394
395    let compression_type = get_compression_type(&options);
396    let filter_type = get_filter_type(&options);
397
398    let mut encoded_data = Vec::<u8>::with_capacity(composition.to_vec().capacity());
399    encode_png(
400        &mut encoded_data,
401        &composition,
402        compression_type,
403        filter_type,
404    )?;
405
406    node_write_file_sync(&node_fs, &out_path, &encoded_data);
407
408    Ok(())
409}
410
411fn blend_multiple_buffers(
412    image_buffers: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>,
413    algorithm: Option<String>,
414    algorithms: Option<Vec<JsValue>>,
415    is_inline: Option<bool>,
416) -> Result<ImageBuffer<Rgba<u8>, Vec<u8>>, PConvertError> {
417    let num_images = image_buffers.len();
418    if num_images < 1 {
419        return Err(PConvertError::ArgumentError(
420            "'images' must contain at least one path".to_string(),
421        ));
422    }
423
424    if algorithms.is_some() && algorithms.as_ref().unwrap().len() != num_images - 1 {
425        return Err(PConvertError::ArgumentError(format!(
426            "'algorithms' must be of size {} (one per blending operation)",
427            num_images - 1
428        )));
429    };
430
431    let _is_inline = is_inline.unwrap_or(false);
432
433    let algorithms_to_apply: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)> =
434        if let Some(algorithms) = algorithms {
435            build_params(&algorithms)?
436        } else if let Some(algorithm) = algorithm {
437            let algorithm = build_algorithm(&algorithm)?;
438            vec![(algorithm, None); num_images - 1]
439        } else {
440            vec![(BlendAlgorithm::Multiplicative, None); num_images - 1]
441        };
442
443    let mut image_buffers_iter = image_buffers.iter();
444    let first_demultiply = if !algorithms_to_apply.is_empty() {
445        is_algorithm_multiplied(&algorithms_to_apply[0].0)
446    } else {
447        false
448    };
449    let mut composition = image_buffers_iter.next().unwrap().to_owned();
450    if first_demultiply {
451        demultiply_image(&mut composition);
452    }
453    let zip_iter = image_buffers_iter.zip(algorithms_to_apply.iter());
454    for pair in zip_iter {
455        let mut current_layer = pair.0.to_owned();
456        let (algorithm, algorithm_params) = pair.1;
457        let demultiply = is_algorithm_multiplied(algorithm);
458        let algorithm_fn = get_blending_algorithm(algorithm);
459
460        if demultiply {
461            demultiply_image(&mut current_layer);
462        }
463
464        blend_images(
465            &mut composition,
466            &current_layer,
467            &algorithm_fn,
468            algorithm_params,
469        );
470    }
471
472    Ok(composition)
473}