pconvert_rust/pymodule/
mod.rs

1//! Python extension, exported functions and type conversions.
2#![allow(clippy::borrow_deref_ref)]
3
4pub mod conversions;
5pub mod utils;
6
7use crate::blending::params::{BlendAlgorithmParams, Options};
8use crate::blending::{
9    blend_images, demultiply_image, get_blending_algorithm, is_algorithm_multiplied, BlendAlgorithm,
10};
11use crate::constants;
12use crate::errors::PConvertError;
13use crate::parallelism::{ResultMessage, ThreadPool};
14use crate::utils::{read_png_from_file, write_png_parallel, write_png_to_file};
15use pyo3::exceptions::PyException;
16use pyo3::prelude::*;
17use pyo3::types::{IntoPyDict, PyDict, PySequence};
18use std::sync::mpsc;
19use utils::{
20    build_algorithm, build_params, get_compression_type, get_filter_type, get_num_threads,
21};
22
23static mut THREAD_POOL: Option<ThreadPool> = None;
24
25#[pymodule]
26fn pconvert_rust(_py: Python, module: &PyModule) -> PyResult<()> {
27    unsafe {
28        let mut thread_pool = ThreadPool::new(constants::DEFAULT_THREAD_POOL_SIZE).unwrap();
29        thread_pool.start();
30        THREAD_POOL = Some(thread_pool);
31    }
32
33    module.add("COMPILATION_DATE", constants::COMPILATION_DATE)?;
34    module.add("COMPILATION_TIME", constants::COMPILATION_TIME)?;
35    module.add("VERSION", constants::VERSION)?;
36    module.add("ALGORITHMS", constants::ALGORITHMS.to_vec())?;
37    module.add("COMPILER", constants::COMPILER)?;
38    module.add("COMPILER_VERSION", constants::COMPILER_VERSION)?;
39    module.add("LIBPNG_VERSION", constants::LIBPNG_VERSION)?;
40    module.add("FEATURES", constants::FEATURES.to_vec())?;
41    module.add("PLATFORM_CPU_BITS", constants::PLATFORM_CPU_BITS)?;
42
43    let filters: Vec<String> = constants::FILTER_TYPES
44        .to_vec()
45        .iter()
46        .map(|x| format!("{:?}", x))
47        .collect();
48    module.add("FILTER_TYPES", filters)?;
49
50    let compressions: Vec<String> = constants::COMPRESSION_TYPES
51        .to_vec()
52        .iter()
53        .map(|x| format!("{:?}", x))
54        .collect();
55    module.add("COMPRESSION_TYPES", compressions)?;
56
57    #[pyfunction]
58    #[pyo3(name = "blend_images")]
59    fn blend_images_py(
60        py: Python,
61        bot_path: String,
62        top_path: String,
63        target_path: String,
64        algorithm: Option<String>,
65        is_inline: Option<bool>,
66        options: Option<Options>,
67    ) -> PyResult<()> {
68        // blends two images using either the single-threaded or the multiple-threaded version
69        // taking into consideration the requested number of thread in options
70        py.allow_threads(|| -> PyResult<()> {
71            let num_threads = get_num_threads(&options);
72            if num_threads == 0 {
73                blend_images_single_thread(
74                    bot_path,
75                    top_path,
76                    target_path,
77                    algorithm,
78                    is_inline,
79                    options,
80                )
81            } else {
82                unsafe {
83                    blend_images_multi_thread(
84                        bot_path,
85                        top_path,
86                        target_path,
87                        algorithm,
88                        is_inline,
89                        options,
90                        num_threads,
91                    )
92                }
93            }
94        })
95    }
96
97    #[pyfunction]
98    #[pyo3(name = "blend_multiple")]
99    fn blend_multiple_py(
100        py: Python,
101        img_paths: &PySequence,
102        out_path: String,
103        algorithm: Option<String>,
104        algorithms: Option<&PySequence>,
105        is_inline: Option<bool>,
106        options: Option<Options>,
107    ) -> PyResult<()> {
108        // parses python types to rust owned values so that they are safely shared between threads
109        let img_paths: Vec<String> = img_paths.extract()?;
110        let num_images = img_paths.len();
111
112        let algorithms_to_apply: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)> =
113            match (algorithms, algorithm) {
114                (Some(algorithms), _) if algorithms.len().unwrap() > 0 => build_params(algorithms)?,
115                (_, Some(algorithm)) => vec![(build_algorithm(&algorithm)?, None); num_images - 1],
116                _ => vec![(BlendAlgorithm::Multiplicative, None); num_images - 1],
117            };
118
119        // blends multiple images using either the single-threaded or the multiple-threaded version
120        // taking into consideration the requested number of thread in options
121        py.allow_threads(|| -> PyResult<()> {
122            let num_threads = get_num_threads(&options);
123            if num_threads == 0 {
124                blend_multiple_single_thread(
125                    img_paths,
126                    out_path,
127                    algorithms_to_apply,
128                    is_inline,
129                    options,
130                )
131            } else {
132                unsafe {
133                    blend_multiple_multi_thread(
134                        img_paths,
135                        out_path,
136                        algorithms_to_apply,
137                        is_inline,
138                        options,
139                        num_threads,
140                    )
141                }
142            }
143        })
144    }
145
146    #[pyfunction]
147    #[pyo3(name = "get_thread_pool_status")]
148    fn get_thread_pool_status(py: Python) -> PyResult<&PyDict> {
149        unsafe {
150            match &mut THREAD_POOL {
151                Some(thread_pool) => {
152                    let status_dict = thread_pool.get_status().into_py_dict(py);
153                    Ok(status_dict)
154                }
155                None => Err(PyException::new_err(
156                    "Accessing global thread pool".to_string(),
157                )),
158            }
159        }
160    }
161
162    module.add_function(wrap_pyfunction!(blend_images_py, module)?)?;
163    module.add_function(wrap_pyfunction!(blend_multiple_py, module)?)?;
164    module.add_function(wrap_pyfunction!(get_thread_pool_status, module)?)?;
165
166    Ok(())
167}
168
169fn blend_images_single_thread(
170    bot_path: String,
171    top_path: String,
172    target_path: String,
173    algorithm: Option<String>,
174    is_inline: Option<bool>,
175    options: Option<Options>,
176) -> PyResult<()> {
177    let algorithm = algorithm.unwrap_or_else(|| String::from("multiplicative"));
178    let algorithm = build_algorithm(&algorithm)?;
179
180    let _is_inline = is_inline.unwrap_or(false);
181
182    let demultiply = is_algorithm_multiplied(&algorithm);
183    let algorithm_fn = get_blending_algorithm(&algorithm);
184
185    let mut bot = read_png_from_file(bot_path, demultiply)?;
186    let top = read_png_from_file(top_path, demultiply)?;
187
188    blend_images(&mut bot, &top, &algorithm_fn, &None);
189
190    let compression_type = get_compression_type(&options);
191    let filter_type = get_filter_type(&options);
192    write_png_to_file(target_path, &bot, compression_type, filter_type)?;
193
194    Ok(())
195}
196
197unsafe fn blend_images_multi_thread(
198    bot_path: String,
199    top_path: String,
200    target_path: String,
201    algorithm: Option<String>,
202    is_inline: Option<bool>,
203    options: Option<Options>,
204    num_threads: usize,
205) -> PyResult<()> {
206    let algorithm = algorithm.unwrap_or_else(|| String::from("multiplicative"));
207    let algorithm = build_algorithm(&algorithm)?;
208    let _is_inline = is_inline.unwrap_or(false);
209    let demultiply = is_algorithm_multiplied(&algorithm);
210    let algorithm_fn = get_blending_algorithm(&algorithm);
211
212    let thread_pool = match &mut THREAD_POOL {
213        Some(thread_pool) => thread_pool,
214        None => panic!("Unable to access global pconvert thread pool"),
215    };
216
217    // expands thread pool to the desired number of threads/parallelism (if necessary and possible)
218    thread_pool.expand_to(num_threads);
219
220    let bot_result_channel = thread_pool
221        .execute(move || ResultMessage::ImageResult(read_png_from_file(bot_path, demultiply)));
222    let top_result_channel = thread_pool
223        .execute(move || ResultMessage::ImageResult(read_png_from_file(top_path, demultiply)));
224
225    let mut bot = match bot_result_channel.recv().unwrap() {
226        ResultMessage::ImageResult(result) => result,
227    }?;
228    let top = match top_result_channel.recv().unwrap() {
229        ResultMessage::ImageResult(result) => result,
230    }?;
231
232    blend_images(&mut bot, &top, &algorithm_fn, &None);
233
234    let compression_type = get_compression_type(&options);
235    let filter_type = get_filter_type(&options);
236    write_png_parallel(target_path, &bot, compression_type, filter_type)?;
237
238    Ok(())
239}
240
241fn blend_multiple_single_thread(
242    img_paths: Vec<String>,
243    out_path: String,
244    algorithms: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)>,
245    is_inline: Option<bool>,
246    options: Option<Options>,
247) -> PyResult<()> {
248    let num_images = img_paths.len();
249
250    if num_images < 1 {
251        return Err(PyErr::from(PConvertError::ArgumentError(
252            "ArgumentError: 'img_paths' must contain at least one path".to_string(),
253        )));
254    }
255
256    if algorithms.len() != num_images - 1 {
257        return Err(PyErr::from(PConvertError::ArgumentError(format!(
258            "ArgumentError: 'algorithms' must be of size {} (one per blending operation)",
259            num_images - 1
260        ))));
261    };
262
263    let _is_inline = is_inline.unwrap_or(false);
264
265    // loops through the algorithms to apply and blends the
266    // current composition with the next layer
267    let mut img_paths_iter = img_paths.iter();
268    let first_path = img_paths_iter.next().unwrap().to_string();
269    let first_demultiply = if !algorithms.is_empty() {
270        is_algorithm_multiplied(&algorithms[0].0)
271    } else {
272        false
273    };
274    let mut composition = read_png_from_file(first_path, first_demultiply)?;
275    let zip_iter = img_paths_iter.zip(algorithms.iter());
276    for pair in zip_iter {
277        let path = pair.0.to_string();
278        let (algorithm, algorithm_params) = pair.1;
279        let demultiply = is_algorithm_multiplied(algorithm);
280        let algorithm_fn = get_blending_algorithm(algorithm);
281        let current_layer = read_png_from_file(path, demultiply)?;
282        blend_images(
283            &mut composition,
284            &current_layer,
285            &algorithm_fn,
286            algorithm_params,
287        );
288    }
289
290    let compression_type = get_compression_type(&options);
291    let filter_type = get_filter_type(&options);
292    write_png_to_file(out_path, &composition, compression_type, filter_type)?;
293
294    Ok(())
295}
296
297unsafe fn blend_multiple_multi_thread(
298    img_paths: Vec<String>,
299    out_path: String,
300    algorithms: Vec<(BlendAlgorithm, Option<BlendAlgorithmParams>)>,
301    is_inline: Option<bool>,
302    options: Option<Options>,
303    num_threads: usize,
304) -> PyResult<()> {
305    let num_images = img_paths.len();
306
307    if num_images < 1 {
308        return Err(PyErr::from(PConvertError::ArgumentError(
309            "ArgumentError: 'img_paths' must contain at least one path".to_string(),
310        )));
311    }
312
313    if algorithms.len() != num_images - 1 {
314        return Err(PyErr::from(PConvertError::ArgumentError(format!(
315            "ArgumentError: 'algorithms' must be of size {} (one per blending operation)",
316            num_images - 1
317        ))));
318    };
319
320    let _is_inline = is_inline.unwrap_or(false);
321
322    let thread_pool = match &mut THREAD_POOL {
323        Some(thread_pool) => thread_pool,
324        None => panic!("Unable to access global pconvert thread pool"),
325    };
326
327    // expands thread pool to the desired number of threads/parallelism (if necessary and possible)
328    thread_pool.expand_to(num_threads);
329
330    let mut png_channels: Vec<mpsc::Receiver<ResultMessage>> = Vec::with_capacity(num_images);
331    for path in img_paths.into_iter() {
332        let result_channel = thread_pool.execute(move || -> ResultMessage {
333            ResultMessage::ImageResult(read_png_from_file(path, false))
334        });
335        png_channels.push(result_channel);
336    }
337
338    let first_demultiply = if !algorithms.is_empty() {
339        is_algorithm_multiplied(&algorithms[0].0)
340    } else {
341        false
342    };
343
344    let mut composition = match png_channels[0].recv().unwrap() {
345        ResultMessage::ImageResult(result) => result,
346    }?;
347    if first_demultiply {
348        demultiply_image(&mut composition)
349    }
350
351    // loops through the algorithms to apply and blends the
352    // current composition with the next layer
353    // retrieves the images from the result channels
354    for i in 1..png_channels.len() {
355        let (algorithm, algorithm_params) = &algorithms[i - 1];
356        let demultiply = is_algorithm_multiplied(algorithm);
357        let algorithm_fn = get_blending_algorithm(algorithm);
358        let mut current_layer = match png_channels[i].recv().unwrap() {
359            ResultMessage::ImageResult(result) => result,
360        }?;
361        if demultiply {
362            demultiply_image(&mut current_layer)
363        }
364
365        blend_images(
366            &mut composition,
367            &current_layer,
368            &algorithm_fn,
369            algorithm_params,
370        );
371    }
372
373    let compression_type = get_compression_type(&options);
374    let filter_type = get_filter_type(&options);
375    write_png_parallel(out_path, &composition, compression_type, filter_type)?;
376
377    Ok(())
378}