Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,15 +112,17 @@ Arguments:
<OUTPUT> Name of the file in which to save the spritesheet

Options:
-r, --ratio <RATIO> Set the output pixel ratio [default: 1]
--retina Set the pixel ratio to 2 (equivalent to `--ratio=2`)
--unique Store only unique images in the spritesheet, and map them to multiple names
--recursive Include images in sub-directories
--spacing <SPACING> Add pixel spacing between sprites [default: 0]
-m, --minify-index-file Remove whitespace from the JSON index file
--sdf Output a spritesheet using a signed distance field for each sprite
-h, --help Print help
-V, --version Print version
-r, --ratio <RATIO> Set the output pixel ratio [default: 1]
--retina Set the pixel ratio to 2 (equivalent to `--ratio=2`)
--unique Store only unique images in the spritesheet, and map them to multiple names
--recursive Include images in sub-directories
--spacing <SPACING> Add pixel spacing between sprites [default: 0]
--oxipng <LEVEL> Specify the PNG optimization level (0–6, default: 2)
--zopfli <ITERATIONS> Optimize the output PNG with zopfli (1–255, very slow)
-m, --minify-index-file Remove whitespace from the JSON index file
--sdf Output a spritesheet using a signed distance field for each sprite
-h, --help Print help
-V, --version Print version
```

## Using Spreet as a Rust library
Expand Down
16 changes: 16 additions & 0 deletions src/bin/spreet/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ pub struct Cli {
/// Add pixel spacing between sprites
#[arg(long, default_value_t = 0, value_parser = is_non_negative)]
pub spacing: u8,
/// Specify the PNG optimization level (0–6, default: 2)
#[arg(long, group = "optlevel", value_name = "LEVEL", value_parser = is_max_6)]
pub oxipng: Option<u8>,
/// Optimize the output PNG with zopfli (1–255, very slow)
#[arg(long, group = "optlevel", value_name = "ITERATIONS", value_parser = is_positive)]
pub zopfli: Option<u8>,
/// Remove whitespace from the JSON index file
#[arg(short, long)]
pub minify_index_file: bool,
Expand Down Expand Up @@ -64,3 +70,13 @@ fn is_non_negative(s: &str) -> Result<u8, String> {
Ok(result)
})
}

/// Clap validator to ensure that an unsigned integer parsed from a string is no more than 6.
fn is_max_6(s: &str) -> Result<u8, String> {
u8::from_str(s)
.map_err(|e| e.to_string())
.and_then(|result| match result {
i if i <= 6 => Ok(result),
_ => Err(String::from("must be a number no more than 6")),
})
}
14 changes: 12 additions & 2 deletions src/bin/spreet/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use std::num::NonZero;

use clap::Parser;
use spreet::{get_svg_input_paths, load_svg, sprite_name, Sprite, Spritesheet};
use spreet::{get_svg_input_paths, load_svg, sprite_name, Optlevel, Sprite, Spritesheet};

mod cli;

Expand Down Expand Up @@ -64,10 +65,19 @@ fn main() {
std::process::exit(exitcode::DATAERR);
};

let optlevel = match (args.oxipng, args.zopfli) {
(None, None) => Optlevel::default(),
(Some(level), None) => Optlevel::Oxipng { level },
(None, Some(iterations)) => Optlevel::Zopfli {
iterations: NonZero::new(iterations).unwrap(),
},
(Some(_), Some(_)) => unreachable!(),
};

// Save the bitmapped spritesheet to a local PNG.
let file_prefix = args.output;
let spritesheet_path = format!("{file_prefix}.png");
if let Err(e) = spritesheet.save_spritesheet(&spritesheet_path) {
if let Err(e) = spritesheet.save_spritesheet_at(&spritesheet_path, optlevel) {
eprintln!("Error: could not save spritesheet to {spritesheet_path} ({e})");
std::process::exit(exitcode::IOERR);
};
Expand Down
58 changes: 55 additions & 3 deletions src/sprite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::fs::File;
use std::io::Write;
use std::num::NonZero;
use std::path::Path;

use crunch::{Item, PackedItem, PackedItems, Rotation};
Expand Down Expand Up @@ -399,6 +400,12 @@ struct PixmapItem {
sprite: Sprite,
}

/// Optimization level for PNG image output.
pub enum Optlevel {
Oxipng { level: u8 },
Zopfli { iterations: NonZero<u8> },
}

impl Spritesheet {
pub fn new(
sprites: BTreeMap<String, Sprite>,
Expand Down Expand Up @@ -500,23 +507,44 @@ impl Spritesheet {
/// Encode the spritesheet to the in-memory PNG image.
///
/// The `spritesheet` `Pixmap` is converted to an in-memory PNG, optimised using the [`oxipng`]
/// library.
/// library with the default optimization level.
///
/// The spritesheet will match an index that can be retrieved with [`Self::get_index`].
///
/// [`oxipng`]: https://github.com/shssoichiro/oxipng
pub fn encode_png(&self) -> SpreetResult<Vec<u8>> {
self.encode_png_at(Optlevel::default())
}

/// Encode the spritesheet to the in-memory PNG image with the specified optimization level.
///
/// The `spritesheet` `Pixmap` is converted to an in-memory PNG, optimised using the [`oxipng`]
/// library.
///
/// The spritesheet will match an index that can be retrieved with [`Self::get_index`].
///
/// [`oxipng`]: https://github.com/shssoichiro/oxipng
pub fn encode_png_at(&self, optlevel: Optlevel) -> SpreetResult<Vec<u8>> {
let options = match optlevel {
Optlevel::Oxipng { level } => oxipng::Options::from_preset(level.min(6)),
Optlevel::Zopfli { iterations } => oxipng::Options {
deflate: oxipng::Deflaters::Zopfli { iterations },
..oxipng::Options::max_compression()
},
};

Ok(optimize_from_memory(
self.sheet.encode_png()?.as_slice(),
&oxipng::Options::default(),
&options,
)?)
}

/// Saves the spritesheet to a local file named `path`.
///
/// A spritesheet, called an [image file] in the Mapbox Style Specification, is a PNG image
/// containing all the individual sprite images. The `spritesheet` `Pixmap` is converted to an
/// in-memory PNG, optimised using the [`oxipng`] library, and saved to a local file.
/// in-memory PNG, optimised using the [`oxipng`] library with the default optimization level,
/// and saved to a local file.
///
/// The spritesheet will match an index file that can be saved with [`Self::save_index`].
///
Expand All @@ -526,6 +554,24 @@ impl Spritesheet {
Ok(std::fs::write(path, self.encode_png()?)?)
}

/// Saves the spritesheet to a local file named `path` with the specified optimization level.
///
/// A spritesheet, called an [image file] in the Mapbox Style Specification, is a PNG image
/// containing all the individual sprite images. The `spritesheet` `Pixmap` is converted to an
/// in-memory PNG, optimised using the [`oxipng`] library, and saved to a local file.
///
/// The spritesheet will match an index file that can be saved with [`Self::save_index`].
///
/// [image file]: https://docs.mapbox.com/mapbox-gl-js/style-spec/sprite/#image-file
/// [`oxipng`]: https://github.com/shssoichiro/oxipng
pub fn save_spritesheet_at<P: AsRef<Path>>(
&self,
path: P,
optlevel: Optlevel,
) -> SpreetResult<()> {
Ok(std::fs::write(path, self.encode_png_at(optlevel)?)?)
}

/// Get the `sprite_index` that can be serialized to JSON.
///
/// An [index file] is defined in the Mapbox Style Specification as a JSON document containing a
Expand Down Expand Up @@ -560,6 +606,12 @@ impl Spritesheet {
}
}

impl Default for Optlevel {
fn default() -> Self {
Optlevel::Oxipng { level: 2 }
}
}

/// Returns the name (unique id within a spritesheet) taken from a file.
///
/// The unique sprite name is the relative path from `path` to `base_path`
Expand Down