Skip to content

Commit

Permalink
fix: improve error handling (#3)
Browse files Browse the repository at this point in the history
<!--
Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->
  • Loading branch information
EstebanBorai authored May 27, 2023
1 parent dd9309f commit 54cf5bb
Show file tree
Hide file tree
Showing 14 changed files with 256 additions and 43 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
tags:
- 'v*'

concurrency:
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true

jobs:
Publish:
runs-on: ubuntu-latest
Expand Down
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Test

on:
push:
branches:
- main
pull_request:
branches:
- main

concurrency:
group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}"
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v2

- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2

- name: Install Wasm Pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

- name: Tests
run: cargo test

test_wasm:
runs-on: ubuntu-latest
steps:
- name: Checkout Source Code
uses: actions/checkout@v2

- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2

- uses: browser-actions/setup-geckodriver@latest
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Install Just Command Runner
uses: extractions/setup-just@v1

- name: Install Wasm Pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

- name: Tests
run: just test_ci
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ bin/
pkg/
wasm-pack.log
http-server
.DS_Store
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mince"
version = "0.0.6"
version = "0.0.7"
authors = ["Esteban Borai <estebanborai@gmail.com>"]
edition = "2021"

Expand All @@ -11,9 +11,11 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"]

[dependencies]
anyhow = "1.0.71"
image = "0.24.6"
js-sys = "0.3.61"
wasm-bindgen = "0.2.84"
thiserror = "1.0.40"
wasm-bindgen = "0.2.86"
wasm-bindgen-futures = "0.4.34"
web-sys = { version = "0.3.61", features = ["Blob", "BlobPropertyBag", "Event", "File", "FileReader", "FilePropertyBag"] }

Expand All @@ -27,7 +29,6 @@ console_error_panic_hook = { version = "0.1.6", optional = true }
# compared to the default allocator's ~10K. It is slower than the default
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
anyhow = "1.0.71"

[dev-dependencies]
wasm-bindgen-test = "0.3.13"
Expand Down
8 changes: 7 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ dev: clean
cp -R ./pkg ./testing/pkg

serve:
./http-server --verbose --logger ./testing
./http-server --verbose --logger ./www

test:
wasm-pack test --headless --firefox --geckodriver ./drivers/geckodriver

test_ci:
wasm-pack test --headless --firefox
Binary file added assets/jpeg420exif.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added drivers/geckodriver
Binary file not shown.
26 changes: 26 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use thiserror::Error;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;

pub type Result<T> = std::result::Result<T, MinceError>;

#[wasm_bindgen]
#[derive(Debug, Error)]
pub enum MinceError {
#[error("IO error")]
Generic,
#[error("Failed to read file into bytes")]
FileRead,
#[error("Failed to detect image format")]
DetectImageFormat,
#[error("Failed to decode image")]
DecodeImage,
#[error("Failed to encode image")]
EncodeImage,
}

impl Into<JsValue> for MinceError {
fn into(self) -> JsValue {
JsValue::from_str(&self.to_string())
}
}
104 changes: 75 additions & 29 deletions src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ use image::{DynamicImage, ImageFormat, ImageOutputFormat};
use js_sys::{Array, Uint8Array};
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Blob, BlobPropertyBag, File, FilePropertyBag};
use web_sys::{File, FilePropertyBag};

use crate::console;
use crate::error::{MinceError, Result};

/// Supported image formats for `Mince`
#[wasm_bindgen]
Expand Down Expand Up @@ -69,15 +72,17 @@ pub struct Metadata {
pub width: u32,
pub height: u32,
pub format: Format,
pub size: u64,
}

#[wasm_bindgen]
impl Metadata {
pub fn new(width: u32, height: u32, format: Format) -> Self {
pub fn new(width: u32, height: u32, format: Format, size: u64) -> Self {
Self {
width,
height,
format,
size,
}
}
}
Expand All @@ -100,20 +105,27 @@ impl Mince {
}

/// Reads a browser file into a `Mince` instance
pub async fn from_file(file: File) -> Mince {
let array_buffer = JsFuture::from(file.array_buffer()).await.unwrap();
let uint8_array = Uint8Array::new(&array_buffer);
let inner = uint8_array.to_vec();
let cursor = Cursor::new(inner);
let reader = ImageReader::new(cursor).with_guessed_format().unwrap();
let format = reader.format().unwrap();
let image = reader.decode().unwrap();
let meta = Metadata::new(image.width(), image.height(), format.into());

Self {
pub async fn from_file(file: File) -> Result<Mince> {
let bytes = Self::file_bytes(file).await?;
let size = bytes.len() as u64;
let cursor = Cursor::new(bytes);
let reader = ImageReader::new(cursor)
.with_guessed_format()
.map_err(|err| {
console::error(&format!("Error reading file: {:?}", err));
MinceError::FileRead
})?;
let format = reader.format().ok_or(MinceError::DetectImageFormat)?;
let image = reader.decode().map_err(|err| {
console::error(&format!("Error decoding file: {:?}", err));
MinceError::DecodeImage
})?;
let meta = Metadata::new(image.width(), image.height(), format.into(), size);

Ok(Self {
inner: Box::new(image),
meta,
}
})
}

pub fn meta(&self) -> Metadata {
Expand All @@ -130,31 +142,65 @@ impl Mince {
Mince::new(dynamic_image, self.meta())
}

pub fn write_blob(&self) -> Blob {
let mut options = BlobPropertyBag::new();
options.type_(self.meta.format.mime());
pub fn to_file(&self) -> Result<File> {
let format = self.meta.format;
let mut bytes: Vec<u8> = Vec::with_capacity(self.meta.size as usize);
let mut buf = Cursor::new(&mut bytes);

let bytes = self.inner.as_bytes();
let uint8_array = Uint8Array::from(bytes);
let blob =
Blob::new_with_u8_array_sequence_and_options(&JsValue::from(uint8_array), &options)
.unwrap();
self.inner.write_to(&mut buf, format).map_err(|err| {
console::error(&format!("Error encoding image: {:?}", err));
MinceError::EncodeImage
})?;

blob
Ok(Self::write_file(&bytes, &self.filename(), format.mime()))
}

pub fn write_file(&self) -> File {
let sequence = Array::new();
sequence.set(0, self.write_blob().into());
/// Reads a browser file and returns a `Vec<u8>` containing the bytes
async fn file_bytes(file: File) -> Result<Vec<u8>> {
let array_buffer = JsFuture::from(file.array_buffer()).await.map_err(|err| {
console::error(&format!("Error reading file: {:?}", err));
MinceError::FileRead
})?;
let uint8_array = Uint8Array::new(&array_buffer);
let bytes = uint8_array.to_vec();

Ok(bytes)
}

let mut options = FilePropertyBag::new();
pub(crate) fn write_file(bytes: &[u8], filename: &str, mime: &str) -> File {
let uint8_array = Uint8Array::from(bytes);
let sequence = Array::new();
sequence.push(&uint8_array.buffer());

options.type_(self.meta.format.mime());
let mut file_options = FilePropertyBag::new();
file_options.type_(mime);

File::new_with_blob_sequence_and_options(&sequence, &self.filename(), &options).unwrap()
File::new_with_blob_sequence_and_options(&sequence, filename, &file_options)
.expect("Failed to create File from Blob")
}

fn filename(&self) -> String {
format!("mince_image.{}", self.meta.format.extension())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_format_mime() {
assert_eq!(Format::Jpeg.mime(), "image/jpeg");
assert_eq!(Format::Png.mime(), "image/png");
assert_eq!(Format::Gif.mime(), "image/gif");
assert_eq!(Format::Unsupported.mime(), "image/unsupported");
}

#[test]
fn test_format_extension() {
assert_eq!(Format::Jpeg.extension(), "jpeg");
assert_eq!(Format::Png.extension(), "png");
assert_eq!(Format::Gif.extension(), "gif");
assert_eq!(Format::Unsupported.extension(), "unsupported");
}
}
24 changes: 14 additions & 10 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
mod image;
mod utils;

use wasm_bindgen::prelude::*;
pub mod error;
pub mod image;
pub mod utils;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern "C" {
// Use `js_namespace` here to bind `console.log(..)` instead of just
// `log(..)`
#[wasm_bindgen(js_namespace = console)]
pub fn log(s: &str);
pub mod console {
use wasm_bindgen::prelude::*;

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

#[wasm_bindgen(js_namespace = console)]
pub fn error(s: &str);
}
}
29 changes: 29 additions & 0 deletions tests/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#![cfg(target_arch = "wasm32")]

use js_sys::{Array, Uint8Array};
use wasm_bindgen_test::*;
use web_sys::{File as JsFile, FilePropertyBag};

pub const JPEG_420_EFIX: &[u8; 768608] = include_bytes!("../assets/jpeg420exif.jpg");

pub fn read_file(bytes: &[u8], mime: &str) -> JsFile {
// Prepare a Blob from the file bytes
let uint8_array = Uint8Array::from(bytes);
let sequence = Array::new();
sequence.push(&uint8_array.buffer());

let mut file_options = FilePropertyBag::new();
file_options.type_(mime);

JsFile::new_with_blob_sequence_and_options(&sequence, "test", &file_options)
.expect("Failed to create File from Blob")
}

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn makes_file_from_bytes() {
let file = read_file(JPEG_420_EFIX, "image/jpeg");

assert_eq!(file.size() as usize, 768608);
}
36 changes: 36 additions & 0 deletions tests/web.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Test suite for the Web and headless browsers.
#![cfg(target_arch = "wasm32")]

mod utils;

use std::assert_eq;

use wasm_bindgen_test::*;

use mince::image::Mince;

use utils::{read_file, JPEG_420_EFIX};

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
async fn reads_file_metadata() {
let file = read_file(JPEG_420_EFIX, "image/jpeg");
let mince = Mince::from_file(file).await.unwrap();
let meta = mince.meta();

assert_eq!(meta.width, 2048);
assert_eq!(meta.height, 1536);
assert_eq!(meta.format, mince::image::Format::Jpeg);
}

#[wasm_bindgen_test]
async fn encodes_jpeg() {
let file = read_file(JPEG_420_EFIX, "image/jpeg");
let mince = Mince::from_file(file).await.unwrap();
let output_file = mince.to_file().expect("Failed to encode image");

assert_eq!(output_file.name(), "mince_image.jpeg");
assert_eq!(output_file.type_(), "image/jpeg");
assert_eq!(output_file.size(), 2068725.);
}
9 changes: 9 additions & 0 deletions webdriver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"moz:firefoxOptions": {
"prefs": {
"media.navigator.streams.fake": true,
"media.navigator.permission.disabled": true
},
"args": []
}
}
File renamed without changes.

0 comments on commit 54cf5bb

Please sign in to comment.