{"id":24837210,"url":"https://github.com/davidpeicho/swizzler","last_synced_at":"2025-09-08T03:41:31.207Z","repository":{"id":101200063,"uuid":"218852691","full_name":"DavidPeicho/swizzler","owner":"DavidPeicho","description":"🦀 CLI packing multiple texture channels into a single output texture","archived":false,"fork":false,"pushed_at":"2020-04-04T15:39:22.000Z","size":2339,"stargazers_count":22,"open_issues_count":1,"forks_count":1,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-08-28T22:58:43.328Z","etag":null,"topics":["assets","channels","cli","texture"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DavidPeicho.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-10-31T20:11:42.000Z","updated_at":"2025-01-19T11:27:14.000Z","dependencies_parsed_at":null,"dependency_job_id":"74a7d379-da4c-486c-b32f-7316bc5495eb","html_url":"https://github.com/DavidPeicho/swizzler","commit_stats":null,"previous_names":["davidpeicho/swizzler","albedo-engine/swizzler"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/DavidPeicho/swizzler","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidPeicho%2Fswizzler","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidPeicho%2Fswizzler/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidPeicho%2Fswizzler/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidPeicho%2Fswizzler/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DavidPeicho","download_url":"https://codeload.github.com/DavidPeicho/swizzler/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DavidPeicho%2Fswizzler/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274129887,"owners_count":25227267,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-08T02:00:09.813Z","response_time":121,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["assets","channels","cli","texture"],"created_at":"2025-01-31T05:53:34.038Z","updated_at":"2025-09-08T03:41:31.186Z","avatar_url":"https://github.com/DavidPeicho.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eSwizzler!\u003c/h1\u003e\n\n\u003cdiv align=\"center\"\u003e\n  Fast CLI to pack multiple images into a single output\n\u003c/div\u003e\n\n![Swizzler Demo](/images/cli.gif)\n\n\u003cp style=\"text-align: center\"\u003eThanks to the \u003ca href=\"https://freepbr.com/about-free-pbr/\"\u003eFree PBR\u003c/a\u003e website for the textures used in this demo\u003c/p\u003e\n\nIn simple terms, **Swizzler!** can pack your textures channels together into a\nsingle output image.\n\nTake as an example an asset containing an _albedo_ map and an _ambient occlusion_ map.\nThe _albedo_ is encoded on the **RGB** channels, and the _ambient_ is encoded only\non the **R** channel. It's thus possible to combine them into a single **RGBA** texture containing the albedo (in the **RGB** channels) and the ambient occlusion (in the **A** channel).\n\n**Swizzler!** provides two modes:\n\n* [Manual](#manual) ⟶ Generates a single output from multiple sources\n* [Session](#session) ⟶ Traverses a directory and automatically generates\ntextures based on a predefined configuration\n\n## Installation\n\n### 1. Download CLI\n\nIf you just need the **CLI**, you can download it directly in the [Release Tab](https://github.com/albedo-engine/swizzler/releases).\n\n#### 2. Install CLI from sources\n\nAlternatively, you can download, build, and install the _CLI_ locally using:\n\n```sh\n$ cargo install --git https://github.com/albedo-engine/swizzler.git\n```\n\nCheck that the installation was successful:\n\n```sh\n$ swizzler --version\nswizzler-cli 0.1.0\n```\n\n#### 3. Install library as a dependency\n\n**Swizzler!** can also be used programmatically. Simply add a dependency to **Swizzler!** in your `Cargo.toml`:\n\n```toml\n[dependencies]\nswizzler = { git = \"https://github.com/albedo-engine/swizzler.git\" }\n```\n## CLI Usage\n\n### Manual\n\nYou can manually generate a new texture by providing the channel to extract for each source:\n\n```sh\n$ swizzler manual -i ./source_1.png:0 -i ./source_2.png:0 ...\n```\n\nEach `-i` argument takes a source image followed by the delimiting  character `:` and the channel to read.\n\nThe position of each `-i` argument is used to select the destination channel.\n\nFor instance, if you have an _RGB_ source image (`source.png`), and you want to shuffle the channels as _BGR_, you simply need to run:\n\n```sh\n$ swizzler manual -i ./source.png:2 -i ./source.png:1 -i ./source.png:0\n```\n\nThe number of arguments defines the number of channels of the output image. For instance, generating a _grayscale_ image is done by using:\n\n```sh\n$ swizzler manual -i ./source.png:0\n```\n\nYou can leave some channels empty by specifying the `none` keyword for an input:\n\n```sh\n$ swizzler manual -i red.png:0 -i none -i none -i alpha.png:3\n```\n\n### Session\n\nYou may want to process a folder containing several textures. The [Manual Command](#manual)\nis handy but can be difficult to use when you need to find what files should be grouped together.\n\nLet's see how you could process an entire folder of images. In this example, we\nare going to generate a texture mixing the metalness in the `red` channel, and\nthe roughness in the `alpha` channel.\n\nLet's assume we have some textures in a folder `./textures`:\n\n```sh\n$ ls ./textures\nenemy_albedo.png    enemy_metalness.png enemy_roughness.png\nhero_albedo.png     hero_metalness.png  hero_roughness.png\n```\n\nWe can define a configuration file to process those textures:\n\n```sh\n$ cat ./config.json\n{\n  \"base\": \"(.*)_.*\",\n  \"matchers\": [\n      { \"id\": \"metalness\", \"matcher\": \"(?i)metal(ness)?\" },\n      { \"id\": \"roughness\", \"matcher\": \"(?i)rough(ness)?\" },\n      { \"id\": \"albedo\", \"matcher\": \"(?i)albedo\" }\n  ],\n  \"targets\": [\n    {\n      \"name\": \"-metalness-roughness.png\",\n      \"output_format\": \"png\",\n      \"inputs\": [\n          [ \"metalness\", 0 ],\n          null,\n          null,\n          [ \"roughness\", 0 ]\n      ]\n    }\n  ]\n}\n```\n\n*  `base` attribute is used to extract the name of the asset (here `\"hero\"` or `\"enemy\"`)\n* `matchers` attribute is used to identify the type of textures. Each entry will\nlook for a particular match\n* `targets` attributes is used to generate new textures, using the files\nresolved by the `matchers`.\n\nTo learn more about each attribute, please take a look at the\n[Configuration File section](#configuration-file).\n\nWe can now run the CLI on our `textures` folder:\n\n```sh\n$ swizzler session --folder ./textures --config ./config.json\n```\n\nAlternatively, you can provide the `config.json` file on STDIN:\n\n```sh\n$ cat ./config.json | swizzler session --folder ./textures\n```\n\nThe results will be generated in the folder `__swizzler_build`:\n\n```sh\n$ ls ./__swizzler_build\nenemy-metalness-roughness.png hero-metalness-roughness.png\n```\n\nAs you can see, the CLI extracted two kind of assets (`hero` and `enemy`), and\ngenerated two textures. Each generated texture contains the metalness and the\nroughness swizzled together.\n\n### Configuration File\n\n```\n{\n\n  \"base\": String,\n\n  \"matchers\": [\n\n      { \"id\": String, \"matcher\": String },\n      ...\n\n  ],\n\n  \"targets\": [\n\n      {\n          \"name\": String,\n\n          \"output_format\": String,\n\n          \"inputs\": [\n\n              [ \"metalness\", 0 ],\n              ...\n\n          ]\n      }\n\n  ]\n}\n```\n\n#### `base` attribute\n\nThe `base` attribute describes how to extract the name of the asset from a path.\nThis **has to be** a [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression) with **one** capturing group.\n\nExample:\n\n```json\n\"base\": \"(.*)_.*\"\n```\n\nCaptures everything before the last `_` occurence.\n\n#### `matchers` attribute\n\nThe `matchers` attribute provide a list of files to match under the same asset.\n\n* `id` is used to identify mathched files\n* `matcher` provides a regular expression checking input files for a match.\n\nExample:\n\n```json\n\"matchers\": [\n    { \"id\": \"metalness\", \"matcher\": \"(?i)metal(ness)?\" },\n    { \"id\": \"roughness\", \"matcher\": \"(?i)rough(ness)?\" }\n]\n```\n\nIn this example, file containing _\"metalness\"_ will be assigned the **id** `'metalness'`,\nand files containing _\"roughness\"_ will be assigned the **id** `'roughness'`.\n\n#### `targets` attributes\n\nThe `targets` attribute makes use of the `matchers` list to generate a new texture.\n\n* `name` gets appended to the `base` name of the asset\n* `output_format` chooses the encoding format of the generated texture. Take a look\nat the [encoding formats](#encoding-formats) for all available options.\n\nExample:\n\n```json\n\"targets\": [\n    {\n      \"name\": \"-metalness-roughness.png\",\n      \"output_format\": \"png\",\n      \"inputs\": [\n          [ \"metalness\", 0 ],\n          null,\n          null,\n          [ \"roughness\", 0 ]\n      ]\n    }\n]\n```\n\nHere, this target configuration will create a texture with the name `'{base}-metalness-roughness.png'`, for each asset containing a match for a\n`metalness` and `roughness` source.\n\n### Arguments\n\n#### Manual command\n\nUsage:\n\n```sh\n$ swizzler manual [-i PATH] ... [-i PATH]\n```\n\n|Argument|Value|Description|\n|:--:|:--:|:--------------------|\n|**-o, --output**|_Path_|Relative path to which output the texture|\n|**-i, --input**|_Path_|Relative path to the texture source to use|\n|**-f, --format**|_String_|Format to use for saving. Default to the extension format if not provided|\n\n#### Session command\n\nUsage:\n\n```sh\n$ swizzler session --folder PATH [--config PATH_TO_CONFIG]\n```\n\n|Argument|Value|Description|\n|:--:|:--:|:--------------------|\n|**-f, --folder**|_Path_|Relative path to the folder to process|\n|**-o, --output**|_[Path]_|Relative path to the folder in which to output files|\n|**-c, --config**|_[Path]_|Relative path to the config to use|\n|**-n, --num_threads**|_[Number]_|Number of threads to use. Default to the number of logical core of the machine|\n\n#### Encoding formats\n\n* `png`\n* `jpg`\n* `tga`\n* `tif`\n* `pnm`\n* `ico`\n* `bmp`\n\nThose formats can be used directly on the CLI using the `manual` command, or via\na configuration file (for `session` run).\n\n## Library usage\n\n### Swizzle\n\nYou can generate a new texture from those descriptors using:\n\n* `to_luma()` ⟶ swizzle inputs into a _Grayscale_ image\n* `to_luma_a()` ⟶ swizzle inputs into a _Grayscale-Alpha_ image\n* `to_rgb()` ⟶ swizzle inputs into a _RGB_ image\n* `to_rgba()` ⟶ swizzle inputs into a _RGBA_ image\n\nThose functions use descriptors (`ChannelDescriptor`) to generate the final\ntexture.\n\nThere are several ways to create descriptors:\n\n```rust\nuse swizzler::{ChannelDescriptor};\n\n// From a string.\nlet descriptor = ChannelDescriptor::from_description(\"./my_input.png:0\").unwrap();\n\n// From path + channel\nlet path = std::Path::PathBuf::from(\"./my_input.png\");\nlet descriptor = ChannelDescriptor::from_path(path, 0).unwrap();\n\n// From an image + channel\nlet descriptor = ChannelDescriptor::from_path(my_image, 0).unwrap();\n```\n\nExample generating a _RGBA_ texture:\n\n```rust\nuse swizzler::{to_rgba};\n\nlet r_channel = ChannelDescriptor::from_path(..., ...).unwrap();\nlet a_channel = ChannelDescriptor::from_path(..., ...).unwrap();\n\n// Generates a RGBA image with two descriptors. The output image `green`\n// and `blue` channels are left empty.\nlet result = to_rgba(Some(r_channel), None, None, Some(a_channel)).unwrap();\n```\n\n\u003e NOTE: you can use `None` to leave a channel empty.\n\nThe result image is an `ImageBuffer` from the [image crate](https://docs.rs/image/0.23.2/image/struct.ImageBuffer.html), that you can manipulate like any other image:\n\n```rust\nresult.save(\"./output.png\").unwrap();\n```\n\n### Running a session\n\nYou can run a session programmatically by creating an `AssetReader` (A.K.A a \"resolver\"),\nand a `Session`.\n\n```rust\nuse regex::Regex;\nuse swizzler::session::{\n    GenericAssetReader\n    GenericTarget,\n    RegexMatcher,\n    Session,\n};\n\n// Creates a resolver and add matcher to it. Remember that matchers\n// are used to group files together under a common asset.\nlet resolver = GenericAssetReader::new()\n  .set_base(Regex::new(\"(.*)_.*\").unwrap())\n  .add_matcher(\n    Box::new(RegexMatcher::new(\"metalness\", Regex::new(r\"(?i)metal(ness)?\").unwrap()))\n  )\n  .add_matcher(\n    Box::new(RegexMatcher::new(\"roughness\", Regex::new(r\"(?i)rough(ness)?\").unwrap()))\n  )\n\n// Creates a target. Each target describes a texture to generate.\nlet metal_roughness_target = GenericTarget::new(vec![\n  (\"metalness\", 0),\n  None,\n  None,\n  (\"roughness\", 0),\n])\n\n// The `Session` will generate images using multiple threads, and save them\n// to disk.\nlet session = Session::new()\n  .set_output_folder(...)\n  .set_max_threads_nb(...)\n  .add_target(metal_roughness_target);\n\n// Reads all assets on the main thread, using our assets reader.\nlet assets = match resolve_assets_dir(\u0026command.folder, \u0026resolver) {\n  Some(list) =\u003e list,\n  Err(error) =\u003e eprintln!(\"Error reading folder: {:?}\", error),\n};\n\n// Goes through all assets, load all sources, swizzle the textures and save them\n// to disk.\nlet errors = session.run(\u0026assets);\nfor e in \u0026errors {\n    eprintln!(\"Error processing file: {:?}\", e);\n}\n```\n\n## Contributing\n\nContributions are welcome and appreciated!\n\nThis CLI has been written as a project to learn Rust. It's the first piece of\nRust code I've ever written, and it's likely that I made wrong design decisions.\n\nIf you have any ideas about how to improve the architecture or the performance, please\nfeel to contribute by raising an issue or creating a pull request.\n\nWhen contributing to the library, please ensure that all the tests pass using:\n\n```sh\n$ cargo test\n```\n\nThe library is formatted using [rustfmt](https://github.com/rust-lang/rustfmt).\nYou can run the formatter by using:\n\n```sh\ncargo fmt\n```\n\n## Notes\n\n**Swizzler!** being my first Rust project, I needed a template source code for inspiration on best practices.\n\nThis CLI is heavily inspired by [Texture Synthesis](https://github.com/EmbarkStudios/texture-synthesis) from the EmbarkStudios team. Thanks for their open source\ncontributions!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidpeicho%2Fswizzler","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdavidpeicho%2Fswizzler","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdavidpeicho%2Fswizzler/lists"}