Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
okaneco committed May 13, 2021
0 parents commit ff31e04
Show file tree
Hide file tree
Showing 8 changed files with 373 additions and 0 deletions.
105 changes: 105 additions & 0 deletions .github/workflows/rust-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Rust CD

on:
push:
tags:
- "*.*.*"

jobs:
publish-binary:
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
rust: [ stable ]
platform:
- os: macos-latest
os-name: macos
target: x86_64-apple-darwin
architecture: x86_64
binary-postfix: ""
use-cross: false
- os: ubuntu-latest
os-name: linux
target: x86_64-unknown-linux-gnu
architecture: x86_64
binary-postfix: ""
use-cross: false
- os: windows-latest
os-name: windows
target: x86_64-pc-windows-msvc
architecture: x86_64
binary-postfix: ".exe"
use-cross: false
- os: ubuntu-latest
os-name: linux
target: aarch64-unknown-linux-gnu
architecture: arm64
binary-postfix: ""
use-cross: true
- os: ubuntu-latest
os-name: linux
target: i686-unknown-linux-gnu
architecture: i686
binary-postfix: ""
use-cross: true

steps:
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
profile: minimal
override: true

- name: Checkout repository
uses: actions/checkout@v2

- name: Cargo build
uses: actions-rs/cargo@v1
with:
command: build
use-cross: ${{ matrix.platform.use-cross }}
toolchain: ${{ matrix.rust }}
args: --release --target ${{ matrix.platform.target }}

- name: Install strip command
if: ${{ matrix.platform.target == 'aarch64-unknown-linux-gnu' }}
shell: bash
run: |
sudo apt update
sudo apt-get install -y binutils-aarch64-linux-gnu
- name: Package final binary
shell: bash
run: |
cd target/${{ matrix.platform.target }}/release
####### reduce binary size by removing debug symbols #######
BINARY_NAME=jpegscans${{ matrix.platform.binary-postfix }}
if [[ ${{ matrix.platform.target }} == aarch64-unknown-linux-gnu ]]; then
GCC_PREFIX="aarch64-linux-gnu-"
else
GCC_PREFIX=""
fi
if [[ ${{ matrix.platform.target }} != x86_64-pc-windows-msvc ]]; then
"$GCC_PREFIX"strip $BINARY_NAME
fi
########## create tar.gz ##########
RELEASE_NAME=jpegscans-${GITHUB_REF/refs\/tags\//}-${{ matrix.platform.os-name }}-${{ matrix.platform.architecture }}
tar czvf $RELEASE_NAME.tar.gz $BINARY_NAME
########## create sha256 ##########
if [[ ${{ runner.os }} == 'Windows' ]]; then
certutil -hashfile $RELEASE_NAME.tar.gz sha256 | grep -E [A-Fa-f0-9]{64} > $RELEASE_NAME.sha256
else
shasum -a 256 $RELEASE_NAME.tar.gz > $RELEASE_NAME.sha256
fi
- name: Releasing assets
uses: softprops/action-gh-release@v1
with:
files: |
target/${{ matrix.platform.target }}/release/jpegscans-*.tar.gz
target/${{ matrix.platform.target }}/release/jpegscans-*.sha256
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 changes: 56 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Rust CI

on:
push:
branches: master
pull_request:
branches: master
schedule:
- cron: "0 0 1 * *" # monthly
workflow_dispatch: # allow manual triggering of the action

env:
RUSTFLAGS: "-Dwarnings"

jobs:
build-crate:
name: Build and test crate/docs
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
toolchain: [nightly, beta, stable]
include:
- os: macos-latest
toolchain: stable
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
components: rust-docs
override: true
- name: Build library
run: cargo build -v --lib --no-default-features
- name: Build binary
run: cargo build -v --bins

clippy-rustfmt:
name: Clippy and rustfmt
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
components: clippy, rustfmt
override: true
- name: clippy
run: cargo clippy
continue-on-error: true
- name: rustfmt
run: cargo fmt -- --check
continue-on-error: true
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/target
/*.jpg
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "jpegscans"
version = "0.1.0"
authors = ["okaneco <47607823+okaneco@users.noreply.github.com>"]
edition = "2018"
7 changes: 7 additions & 0 deletions LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Copyright 2021 Collyn O'Kane

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# jpegscans

Reads a progressive JPEG image file and produces the successive scan images from
that file.

## Usage

```bash
jpegscans [input] [output filename prefix]
```

The first argument is the `input` filename. The second argument is an optional
prefix for the scan files produced from the `input`. The default output prefix
is `scan`.
64 changes: 64 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! Crate for producing images of the progressive scan passes found in JPEG
//! image files.
use std::io::{Read, Seek};

/// Jpeg header magic bytes.
pub const JPEG_MAGIC_BYTES: [u8; 3] = [0xFF, 0xD8, 0xFF];
/// Byte padding.
pub const PADDING: u8 = 0x00;
/// Temporary marker.
pub const TEM: u8 = 0x01;
/// End of image marker.
pub const EOI: u8 = 0xD9;
/// Start of scan marker.
pub const SOS: u8 = 0xDA;
/// Fill bytes (markers may be preceded by any number of these).
pub const FILL: u8 = 0xFF;

/// Consume all bytes in the current marker section.
pub fn consume_marker_section<R: Read>(r: &mut R) -> Result<(), std::io::Error> {
let mut buf = [0, 0];
r.read_exact(&mut buf)?;

// Length of marker section includes length bytes, so we subtract 2 bytes
let marker_length = u16::from_be_bytes(buf).saturating_sub(2);

// Consume the section's bytes
for _ in 0..marker_length {
r.read_exact(&mut buf[..1])?;
}

Ok(())
}

/// Consume the bytes in an [SOS] marker section.
pub fn consume_sos_section<R: Read + Seek>(r: &mut R) -> Result<(), std::io::Error> {
loop {
match find_next_marker(r)? {
// Ignore Restart markers, temporary markers, and padding bytes
PADDING | TEM | 0xD0..=0xD7 => {}
// Rewind the cursor to process the next marker
_ => {
r.seek(std::io::SeekFrom::Current(-2))?;
return Ok(());
}
}
}
}

/// Consume bytes until the next marker is found.
pub fn find_next_marker<R: Read>(r: &mut R) -> Result<u8, std::io::Error> {
let mut buf = [0];

// Find next fill byte
while buf != [FILL] {
r.read_exact(&mut buf)?;
}

// Consume all of the fill bytes that precede the marker byte
while buf == [FILL] {
r.read_exact(&mut buf)?;
}

Ok(buf[0])
}
120 changes: 120 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
use jpegscans::{
consume_marker_section, consume_sos_section, find_next_marker, EOI, FILL, JPEG_MAGIC_BYTES, SOS,
};

const HELP_MESSAGE: &str = "jpegscans
Produce scan images from a progressive JPEG file.
USAGE:
jpegscans [input] [output filename prefix]";

fn main() {
if let Err(e) = try_main() {
eprintln!("{}", e);
std::process::exit(1);
}
}

fn try_main() -> Result<(), ScanError> {
let mut args = std::env::args().skip(1);

let input = match args.next() {
Some(s) => {
if matches!(s.as_str(), "-h" | "--help") {
println!("{}", HELP_MESSAGE);
return Ok(());
}
s
}
None => {
println!("{}", HELP_MESSAGE);
return Ok(());
}
};
let output = args.next().map_or_else(|| "scan".into(), |s| s);

let file = std::fs::read(input)?;
let mut cursor = std::io::Cursor::new(&file);

let mut buf = [0; 3];

// Return an error if the file header doesn't match JPEG bytes
std::io::Read::read_exact(&mut cursor, &mut buf)?;
if buf != JPEG_MAGIC_BYTES {
return Err(ScanError::InvalidFile);
}

let mut scan_count = 0u16;
for _ in 0..u32::MAX {
match find_next_marker(&mut cursor)? {
SOS => {
consume_sos_section(&mut cursor)?;

let scan = std::fs::File::create(format!("{}_{:05}.jpg", output, scan_count))?;
let mut w = std::io::BufWriter::new(scan);

let stream_position =
std::convert::TryFrom::try_from(std::io::Seek::stream_position(&mut cursor)?)?;

// Write all data up to the current stream position, followed by an EOI marker
std::io::Write::write_all(&mut w, &file[..stream_position])?;
std::io::Write::write_all(&mut w, &[FILL, EOI])?;

scan_count = scan_count
.checked_add(1)
.ok_or(ScanError::ScanCounterOverflow)?;
}
EOI => break,
_ => consume_marker_section(&mut cursor)?,
}
}

Ok(())
}

/// Error for processing progressive scan images.
#[derive(Debug)]
pub enum ScanError {
/// An error occurred during conversion of the stream position to a `usize`.
Convert(core::num::TryFromIntError),
/// The supplied file isn't a JPEG.
InvalidFile,
/// An error occurred while opening a file, writing to a file, or reading
/// from a cursor/file.
Io(std::io::Error),
/// The scan number counter overflowed.
ScanCounterOverflow,
}

impl std::fmt::Display for ScanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Convert(err) => write!(f, "{}", err),
Self::InvalidFile => write!(f, "Input file must be a JPEG"),
Self::Io(err) => write!(f, "{}", err),
Self::ScanCounterOverflow => write!(f, "Scan counter overflow"),
}
}
}

impl std::error::Error for ScanError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Convert(e) => Some(e),
Self::Io(e) => Some(e),
Self::InvalidFile | Self::ScanCounterOverflow => None,
}
}
}

impl std::convert::From<std::io::Error> for ScanError {
fn from(error: std::io::Error) -> Self {
Self::Io(error)
}
}

impl std::convert::From<std::num::TryFromIntError> for ScanError {
fn from(error: std::num::TryFromIntError) -> Self {
Self::Convert(error)
}
}

0 comments on commit ff31e04

Please sign in to comment.