jippigy 0.4.0

Compress images while preserving exif data. Provides multi-threaded method for compressing in bulk.
Documentation
# jippigy
A multi-threaded image compression crate, powered by [turbojpeg](https://github.com/honzasp/rust-turbojpeg).

This crate provides methods of compressing JPEG images in a single-threaded  or multi-threaded way. Both methods preserves [EXIF](https://en.wikipedia.org/wiki/Exif) data of the original JPEG through [img_parts](https://docs.rs/img-parts/latest/img_parts/) crate.

# Error building `turbojpeg`?
The problem is typically related to `turbojpeg-sys` (see this [question](https://github.com/rfdzan/smoljpg/issues/4#issuecomment-2036065574) and my [attempt](https://github.com/rfdzan/jippigy/actions/runs/8552014019/job/23432251063#step:3:327) at setting up CI for this crate).

To successfully build `turbojpeg-sys`, you need to install `cmake`, a C compiler (gcc, clang, etc.), and NASM in your system (See: [`turbojpeg`](https://github.com/honzasp/rust-turbojpeg)'s [requirements](https://github.com/honzasp/rust-turbojpeg?tab=readme-ov-file#requirements)). For more details, see [`turbojpeg-sys`](https://github.com/honzasp/rust-turbojpeg/tree/master/turbojpeg-sys)'s [`Building`](https://github.com/honzasp/rust-turbojpeg/tree/master/turbojpeg-sys#building) section.

# Examples
Both `Single` and `Parallel` require you to use both of their respective `output_dir` methods. `with_` methods are optional.

## Single image compressions with `Single`
```rust
const IMAGE_DIR: &str = "/your/image/dir/";
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let image_dir_path = PathBuf::from(IMAGE_DIR);
    let _single = Single::builder(image_dir_path.join("DSC_0227.JPG"))
        .output_dir(image_dir_path.clone())? // This method is required.
        .with_prefix("jippigy_".to_string())
        .with_quality(95)
        .build()
        .compress()?;
    Ok(())
}
```
## Multi-threaded bulk compressions with `Parallel`
In this example, `Parallel` will attempt to create a separate directory `your/image/dir/compressed/` if it doesn't exist and save compressed images here.
```rust
const IMAGE_DIR: &str = "/your/image/dir/";
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let _parallel = Parallel::builder(image_dir_path.clone())
        .output_dir(image_dir_path.join("compressed"))? // This method is required.
        .with_prefix("jippigy_".to_string())
        .with_quality(50)
        .with_device(6) // Use 6 threads for this job.
        .build()
        .compress()?;
    Ok(())
}
```