├── test_pics ├── default_signer_alice.png └── alice.svg ├── .gitignore ├── coloring_order ├── Cargo.toml ├── .github └── workflows │ ├── rust-fmt.yml │ ├── rust-clippy.yml │ └── rust-test.yml ├── src ├── identicon_example.svg ├── circles.rs ├── colors.rs └── lib.rs ├── README.md └── LICENSE /test_pics/default_signer_alice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paritytech/polkadot-identicon-rust/HEAD/test_pics/default_signer_alice.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # IDEs 13 | .vscode/ 14 | .idea/ 15 | -------------------------------------------------------------------------------- /coloring_order: -------------------------------------------------------------------------------- 1 | Order in which the circles are colored: 2 | 3 | 4 | 0 5 | 6 | 2 17 7 | 8 | 3 1 15 9 | 10 | 4 16 11 | 12 | 5 18 14 13 | 14 | 7 13 15 | 16 | 6 10 12 17 | 18 | 8 11 19 | 20 | 9 21 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | description = "rust port of identicon generator for polkadot from polkadot.js" 3 | license = "Apache-2.0" 4 | name = "plot_icon" 5 | version = "0.3.0" 6 | authors = ["Alexander Slesarev ", "Vera Abramova "] 7 | edition = "2021" 8 | repository = "https://github.com/paritytech/polkadot-identicon-rust" 9 | homepage = "https://github.com/paritytech/polkadot-identicon-rust" 10 | keywords = ["identicon", "icon", "parity", "polkadot", "substrate"] 11 | 12 | [dependencies] 13 | blake2-rfc = "0.2.18" 14 | hex = {version = "0.4.3", optional = true} 15 | image = {version = "0.24.0", default-features = false, features = ["ico"], optional = true} 16 | palette = {version = "0.6.0", default-features = false, features = ["std"]} 17 | png = {version = "0.17.3", optional = true} 18 | svg = {version = "0.13.0", optional = true} 19 | 20 | [features] 21 | default = ["pix", "vec"] 22 | 23 | pix = ["image", "png"] 24 | vec = ["hex", "svg"] 25 | 26 | [lib] 27 | name = "plot_icon" 28 | crate-type = ["lib"] 29 | -------------------------------------------------------------------------------- /.github/workflows/rust-fmt.yml: -------------------------------------------------------------------------------- 1 | name: Rustfmt 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - stable 9 | 10 | jobs: 11 | rustfmt: 12 | name: Cargo fmt 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Cancel Previous Runs 17 | uses: styfle/cancel-workflow-action@0.9.1 18 | with: 19 | access_token: ${{ github.token }} 20 | 21 | - name: Checkout sources 22 | uses: actions/checkout@v2.4.0 23 | with: 24 | fetch-depth: 50 25 | submodules: 'recursive' 26 | 27 | - name: Install Rust stable toolchain 28 | uses: actions-rs/toolchain@v1.0.7 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | override: true 33 | 34 | - name: cargo fmt 35 | run: cargo fmt --all -- --check 36 | -------------------------------------------------------------------------------- /.github/workflows/rust-clippy.yml: -------------------------------------------------------------------------------- 1 | name: Rust clippy 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - stable 9 | 10 | jobs: 11 | linter: 12 | name: Cargo clippy 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Cancel Previous Runs 17 | uses: styfle/cancel-workflow-action@0.9.1 18 | with: 19 | access_token: ${{ github.token }} 20 | 21 | - name: Checkout sources 22 | uses: actions/checkout@v2.4.0 23 | with: 24 | fetch-depth: 50 25 | submodules: 'recursive' 26 | 27 | - name: Install Rust stable toolchain 28 | uses: actions-rs/toolchain@v1.0.7 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | override: true 33 | 34 | - name: Rust Cache 35 | uses: Swatinem/rust-cache@v1.3.0 36 | with: 37 | working-directory: . 38 | 39 | - name: cargo clippy 40 | run: cargo clippy --all-targets --all-features -- -D warnings 41 | -------------------------------------------------------------------------------- /.github/workflows/rust-test.yml: -------------------------------------------------------------------------------- 1 | name: Rust test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - stable 9 | 10 | jobs: 11 | check: 12 | name: Cargo nextest 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Cancel Previous Runs 17 | uses: styfle/cancel-workflow-action@0.9.1 18 | with: 19 | access_token: ${{ github.token }} 20 | 21 | - name: Checkout sources 22 | uses: actions/checkout@v2.4.0 23 | with: 24 | fetch-depth: 50 25 | submodules: 'recursive' 26 | 27 | - name: Install Rust stable toolchain 28 | uses: actions-rs/toolchain@v1.0.7 29 | with: 30 | profile: minimal 31 | toolchain: stable 32 | override: true 33 | 34 | - name: Install cargo-nextest 35 | uses: baptiste0928/cargo-install@v1 36 | with: 37 | crate: cargo-nextest 38 | version: 0.9 39 | 40 | - name: Rust Cache 41 | uses: Swatinem/rust-cache@v1.3.0 42 | with: 43 | working-directory: . 44 | 45 | - name: cargo nextest 46 | run: cargo nextest run --retries 2 47 | -------------------------------------------------------------------------------- /test_pics/alice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/identicon_example.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Crate `plot_icon` 3 | 4 | ## Overview 5 | 6 | This is a lib crate for generating standard 19-circle icons in `png` and in `svg` format. 7 | ![identicon](./src/identicon_example.svg) 8 | 9 | Output is `Vec` `png` data, or `svg::Document` with `svg` data, both could be easily printed into files. 10 | 11 | The identicon color scheme and elements arrangement follow the published javascript [code](https://github.com/paritytech/oo7/blob/master/packages/polkadot-identicon/src/index.jsx) for polkadot identicon generation. This crate is intended mainly for use by [Signer](https://github.com/paritytech/parity-signer). 12 | 13 | 14 | ## Input 15 | 16 | Identicon is generated for `&[u8]` input slice. During identicon generation, this input slice gets hashed, therefore, any length would be acceptable. 17 | 18 | Typical input slice is a public key. Public key is often encountered as a hexadecimal string (`d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d`) or as a base58 network-specific string (`5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY`), both could be easily transformed into `&[u8]` input. 19 | 20 | Crate also supports generation of identicon-like images with user-provided colors in RGBA format. 21 | 22 | 23 | ## PNG 24 | 25 | Signer uses images in `png` format, since `svg` format is not sufficiently supported on devices side and might be non-deterministic. Therefore, this crate sticks mostly to `png` generation. Feature `"pix"` (enabled by default) enables generation of `png` images. 26 | 27 | Function `generate_png` produces `png` data for identicon, and requires: 28 | - `&[u8]` slice 29 | - target image size in pixels (`u16`) 30 | `png` images are generated pixel-by-pixel, and the quality of final image is determined by the image size. Each `png` pixel (with integer coordinates) falling within the identicon circle element (with float circle parameters) gets the color of the circle. Below certain image size (approximately 100 pix) the circles become too pixelated. Also, images with even number of pixels size are off-centered by a pixel. 31 | 32 | Signer needs small `png` identicon icons. Exact parameters are yet TBD (at the moment, identicons are 30 pix and device-independent), however, the straightforward approach with `generate_png` does not produce acceptable results. 33 | 34 | Possible solution is to generate larger identicon and then scale it down in Signer frontend, but it was noticed that the scaling results (pixelation, color distribution) are device-dependent and although a minor thing, it should definitely be avoided in *identicon*. 35 | 36 | To generate reproducible small identicons, the rescaling is performed within the crate. A larger `png` is generated, and then scaled down to originally desired size. This procedure results in both less pixelated circles and compensated off-centering. 37 | 38 | Function `generate_png_scaled_custom` performs the scaling with custom parameters, and requires: 39 | - `&[u8]` slice 40 | - **target** identicon size in pixels (`u8` - it is for small identicons, after all) 41 | - scaling factor (`u8`), how much larger the larger `png` actually is 42 | - filter ([`FilterType`](https://docs.rs/image/latest/image/imageops/enum.FilterType.html)) used for image resize 43 | 44 | The scaling factor reasonable values are in range `[4..=8]`, below it the pixelation persists, above it the images are not visibly improving anymore, and may even seem blurry. 45 | 46 | All filters produce reasonable results, except `FilterType::Nearest` that yields visibly distorted images and therefore is not recommended. 47 | 48 | Function `generate_png_scaled_default` performs the scaling with default scaling parameters (scaling factor `5` and filter `FilterType::Lanczos3`) for image with default Signer identicon size (30 pix), and requires only: 49 | - `&[u8]` slice 50 | If somehow the generation of the identicon fails, function outputs default-sized (30x30) transparent `png` image, i.e. it never produces an error. 51 | 52 | Function `generate_png_with_colors` is similar to `generate_png`, but accepts identicon colors directly, and does not generate color set itself. This is intended mainly for tests. Function `generate_png_with_colors` requires: 53 | - `[[u8; 4]; 19]` 19-element set of colors in RGBA format 54 | - target image size in pixels (`u16`) 55 | 56 | Function `generate_png_scaled_custom_with_colors` is similar to `generate_png_scaled_custom`, but accepts identicon colors directly, and does not generate color set itself. This is intended mainly for tests. Function `generate_png_scaled_custom_with_colors` requires: 57 | - `[[u8; 4]; 19]` 19-element set of colors in RGBA format 58 | - target identicon size in pixels (`u8`) 59 | - scaling factor (`u8`) 60 | - filter ([`FilterType`](https://docs.rs/image/latest/image/imageops/enum.FilterType.html)) used for image resize 61 | 62 | 63 | ## SVG 64 | 65 | Feature `"vec"` (enabled by default) enables infallible generation of identicon pictures in `svg` format. Since `svg` is a vector format, no image size parameters are needed. 66 | 67 | Function `generate_svg` reqires only `&[u8]` input slice. 68 | 69 | Function `generate_svg_with_colors` uses pre-set colors and is intended mainly for tests. It requires only the color set (`[[u8; 4]; 19]` 19-element set of colors in RGBA format). 70 | 71 | 72 | ## Tests and Examples 73 | 74 | Tests in `colors.rs` module check if the color sets calculated for Alice and Bob are identical to the colors in the corresponding well-known icons. 75 | 76 | Doc tests in `lib.rs` produce various test pics, both png (through different functions and parameters) and `svg`. 77 | 78 | 79 | ## Notes 80 | 81 | There are several uncertainties about how the original published code was designed to work, those should be clarified, eventually. 82 | 83 | For example, calculated HSL color saturation could range 30..109, and is processed as percents. Crate `palette` (currently used here) processes saturation values over 100 as percents over 100, and gives some results (slightly different from results for 100% saturation), but it is necessary to check if the calculations in js and here are matching. 84 | 85 | See details in code comments. 86 | 87 | -------------------------------------------------------------------------------- /src/circles.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "vec")] 2 | use svg::node::element; 3 | 4 | #[cfg(any(feature = "vec", feature = "pix"))] 5 | use crate::colors::Color; 6 | 7 | /// Information about the circle 8 | #[cfg(feature = "pix")] 9 | #[derive(Clone, Copy, Debug, PartialEq)] 10 | struct Circle { 11 | x_center: f32, 12 | y_center: f32, 13 | radius: f32, 14 | rgba_color: Color, 15 | } 16 | 17 | /// Function to determine if the point (x, y) is within the circle 18 | #[cfg(feature = "pix")] 19 | fn in_circle(x: i32, y: i32, circle: &Circle) -> bool { 20 | (x as f32 - circle.x_center).powi(2) + (y as f32 - circle.y_center).powi(2) 21 | < circle.radius.powi(2) 22 | } 23 | 24 | /// Information about circle center position 25 | /// 26 | /// `position_circle_set` sets default positions for small circles in 19-circles icon 27 | #[derive(Clone, Copy, Debug, PartialEq)] 28 | pub struct CirclePosition { 29 | pub x_center: f32, 30 | pub y_center: f32, 31 | } 32 | 33 | /// Set default positions of small circles in 19-circles icon 34 | /// 35 | /// Input is `f32` center-to-center distance between small circles 36 | pub fn position_circle_set(center_to_center: f32) -> [CirclePosition; 19] { 37 | let a = center_to_center; 38 | let b = center_to_center * 3f32.sqrt() / 2f32; 39 | [ 40 | CirclePosition { 41 | x_center: 0f32, 42 | y_center: -2f32 * a, 43 | }, 44 | CirclePosition { 45 | x_center: 0f32, 46 | y_center: -a, 47 | }, 48 | CirclePosition { 49 | x_center: -b, 50 | y_center: -3f32 * a / 2f32, 51 | }, 52 | CirclePosition { 53 | x_center: -2f32 * b, 54 | y_center: -a, 55 | }, 56 | CirclePosition { 57 | x_center: -b, 58 | y_center: -a / 2f32, 59 | }, 60 | CirclePosition { 61 | x_center: -2f32 * b, 62 | y_center: 0f32, 63 | }, 64 | CirclePosition { 65 | x_center: -2f32 * b, 66 | y_center: a, 67 | }, 68 | CirclePosition { 69 | x_center: -b, 70 | y_center: a / 2f32, 71 | }, 72 | CirclePosition { 73 | x_center: -b, 74 | y_center: 3f32 * a / 2f32, 75 | }, 76 | CirclePosition { 77 | x_center: 0f32, 78 | y_center: 2f32 * a, 79 | }, 80 | CirclePosition { 81 | x_center: 0f32, 82 | y_center: a, 83 | }, 84 | CirclePosition { 85 | x_center: b, 86 | y_center: 3f32 * a / 2f32, 87 | }, 88 | CirclePosition { 89 | x_center: 2f32 * b, 90 | y_center: a, 91 | }, 92 | CirclePosition { 93 | x_center: b, 94 | y_center: a / 2f32, 95 | }, 96 | CirclePosition { 97 | x_center: 2f32 * b, 98 | y_center: 0f32, 99 | }, 100 | CirclePosition { 101 | x_center: 2f32 * b, 102 | y_center: -a, 103 | }, 104 | CirclePosition { 105 | x_center: b, 106 | y_center: -a / 2f32, 107 | }, 108 | CirclePosition { 109 | x_center: b, 110 | y_center: -3f32 * a / 2f32, 111 | }, 112 | CirclePosition { 113 | x_center: 0f32, 114 | y_center: 0f32, 115 | }, 116 | ] 117 | } 118 | 119 | /// Function to finalize 19 circles with properly corresponding colors and radius 120 | #[cfg(feature = "pix")] 121 | fn get_colored_circles( 122 | center_to_center: f32, 123 | small_radius: f32, 124 | colors: [Color; 19], 125 | ) -> [Circle; 19] { 126 | let positions = position_circle_set(center_to_center); 127 | let mut out: Vec = Vec::with_capacity(19); 128 | for (i, position) in positions.iter().enumerate() { 129 | let new = Circle { 130 | x_center: position.x_center, 131 | y_center: position.y_center, 132 | radius: small_radius, 133 | rgba_color: colors[i], 134 | }; 135 | out.push(new); 136 | } 137 | out.try_into().expect("always generate 19-element set") 138 | } 139 | 140 | /// Calculate `png` image pixel data (only pixel colors) 141 | /// 142 | /// Iterates over all `png` image pixels and sets the color. 143 | /// 144 | /// Requires image size in pixels (equal to diameter of largest, outer circle), 145 | /// and identicon colors 146 | #[cfg(feature = "pix")] 147 | pub fn calculate_png_data(size_in_pixels: u16, colors: [Color; 19]) -> Vec { 148 | let mut data: Vec = Vec::new(); 149 | let big_radius = size_in_pixels as f32 / 2f32; 150 | let small_radius = big_radius / 32f32 * 5f32; 151 | let center_to_center = big_radius / 8f32 * 3f32; 152 | 153 | let big_circle = Circle { 154 | x_center: 0f32, 155 | y_center: 0f32, 156 | radius: big_radius, 157 | rgba_color: Color::foreground(), 158 | }; 159 | 160 | let small_circles_set = get_colored_circles(center_to_center, small_radius, colors); 161 | 162 | let iter_start = -(size_in_pixels as i32) / 2; 163 | let iter_end = { (size_in_pixels >> 1) + (size_in_pixels & 0x01) } as i32; 164 | 165 | // calculating color for each pixel 166 | for y in iter_start..iter_end { 167 | for x in iter_start..iter_end { 168 | if in_circle(x, y, &big_circle) { 169 | let mut some_small_circle = None; 170 | for cir in small_circles_set.iter() { 171 | if in_circle(x, y, cir) { 172 | some_small_circle = Some(cir.rgba_color); 173 | break; 174 | } 175 | } 176 | match some_small_circle { 177 | Some(color) => data.extend_from_slice(&color.to_array()), 178 | None => data.extend_from_slice(&big_circle.rgba_color.to_array()), 179 | } 180 | } else { 181 | data.extend_from_slice(&Color::background().to_array()) 182 | } 183 | } 184 | } 185 | data 186 | } 187 | 188 | /// Calculate `svg` file contents 189 | /// 190 | /// Inputs radius of outer circle (largest one) and identicon colors 191 | #[cfg(feature = "vec")] 192 | pub fn calculate_svg_data(big_radius: f32, colors: [Color; 19]) -> Vec { 193 | let mut out: Vec = Vec::with_capacity(20); 194 | out.push( 195 | element::Circle::new() 196 | .set("cx", 0f32) 197 | .set("cy", 0f32) 198 | .set("r", big_radius) 199 | .set("fill", Color::foreground().to_hex()) 200 | .set("stroke", "none"), 201 | ); 202 | let small_radius = big_radius / 32f32 * 5f32; 203 | let center_to_center = big_radius / 8f32 * 3f32; 204 | let positions = position_circle_set(center_to_center); 205 | for (i, position) in positions.iter().enumerate() { 206 | out.push( 207 | element::Circle::new() 208 | .set("cx", position.x_center) 209 | .set("cy", position.y_center) 210 | .set("r", small_radius) 211 | .set("fill", colors[i].to_hex()) 212 | .set("stroke", "none"), 213 | ); 214 | } 215 | out 216 | } 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/colors.rs: -------------------------------------------------------------------------------- 1 | use blake2_rfc::blake2b::blake2b; 2 | use palette::{FromColor, FromComponent, Hsl, RgbHue, Srgb}; 3 | 4 | /// Struct to store default coloring schemes 5 | struct SchemeElement { 6 | freq: u8, 7 | colors: [usize; 19], 8 | } 9 | 10 | /// Function to set default coloring schemes, taken as is from js code 11 | #[rustfmt::skip] 12 | fn default_schemes() -> [SchemeElement; 7] { 13 | [ 14 | SchemeElement { 15 | // "target" 16 | freq: 1, 17 | colors: [0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 0, 28, 0, 1], 18 | }, 19 | SchemeElement { 20 | // "cube", 21 | freq: 20, 22 | colors: [0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 0, 1, 3, 2, 4, 3, 5], 23 | }, 24 | SchemeElement { 25 | // "quazar", 26 | freq: 16, 27 | colors: [1, 2, 3, 1, 2, 4, 5, 5, 4, 1, 2, 3, 1, 2, 4, 5, 5, 4, 0], 28 | }, 29 | SchemeElement { 30 | // "flower", 31 | freq: 32, 32 | colors: [0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 3], 33 | }, 34 | SchemeElement { 35 | // "cyclic", 36 | freq: 32, 37 | colors: [0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5, 6], 38 | }, 39 | SchemeElement { 40 | // "vmirror", 41 | freq: 128, 42 | colors: [0, 1, 2, 3, 4, 5, 3, 4, 2, 0, 1, 6, 7, 8, 9, 7, 8, 6, 10], 43 | }, 44 | SchemeElement { 45 | // "hmirror", 46 | freq: 128, 47 | colors: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 8, 6, 7, 5, 3, 4, 2, 11], 48 | }, 49 | ] 50 | } 51 | 52 | /// Circle color, in RGBA format 53 | #[derive(Clone, Copy, Debug, PartialEq)] 54 | pub struct Color { 55 | pub red: u8, 56 | pub green: u8, 57 | pub blue: u8, 58 | pub alpha: u8, 59 | } 60 | 61 | impl Color { 62 | /// convert `Color` into `[u8; 4]` array, for `png` image pixel-by-pixel generation 63 | #[cfg(feature = "pix")] 64 | pub fn to_array(&self) -> [u8; 4] { 65 | [self.red, self.green, self.blue, self.alpha] 66 | } 67 | 68 | /// convert `Color` into hex string, for `svg` image generation 69 | #[cfg(feature = "vec")] 70 | pub fn to_hex(&self) -> String { 71 | format!("#{}", hex::encode([self.red, self.green, self.blue])) 72 | } 73 | 74 | /// set `Color` to default background, needed only for `png` images 75 | #[cfg(feature = "pix")] 76 | pub fn background() -> Self { 77 | Self { 78 | red: 255, 79 | green: 255, 80 | blue: 255, 81 | alpha: 0, 82 | } 83 | } 84 | 85 | /// set `Color` to default color of the large circle enclosing the small ones 86 | pub fn foreground() -> Self { 87 | Self { 88 | red: 238, 89 | green: 238, 90 | blue: 238, 91 | alpha: 255, 92 | } 93 | } 94 | 95 | /// function to derive color from `u8` number and saturation component 96 | /// calculated elsewhere; 97 | /// is accessible and used only for `u8` numbers other than 0 and 255; 98 | /// no check here is done for b value; 99 | fn derive(b: u8, sat_component: f64) -> Self { 100 | // HSL color hue in degrees 101 | // calculated as integer, same as in js code 102 | // transformation to u16 is done to avoid overflow 103 | let h = (b as u16 % 64 * 360) / 64; 104 | // recalculated into `RgbHue`, to be used as HSL hue component 105 | let h_component = RgbHue::from_degrees(h as f64); 106 | 107 | // HSL lightness in percents 108 | let l: u8 = match b / 64 { 109 | 0 => 53, 110 | 1 => 15, 111 | 2 => 35, 112 | _ => 75, 113 | }; 114 | // recalculated in HSL lightness component (component range is 0.00 to 1.00) 115 | let l_component: f64 = (l as f64) / 100f64; 116 | 117 | // defining HSL color 118 | let color_hsl = Hsl::new(h_component, sat_component, l_component); 119 | 120 | // transforming HSL color into RGB color, possibly lossy, TODO check if too lossy 121 | let color_srgb = Srgb::from_color(color_hsl); 122 | 123 | // getting red, green, blue components, transforming them in 0..255 range of u8 124 | let red = u8::from_component(color_srgb.red); 125 | let green = u8::from_component(color_srgb.green); 126 | let blue = u8::from_component(color_srgb.blue); 127 | 128 | // finalize color, set alpha value to 255 129 | Self { 130 | red, 131 | green, 132 | blue, 133 | alpha: 255, 134 | } 135 | } 136 | } 137 | 138 | /// Function to calculate identicon colors from `&[u8]` input slice. 139 | /// Total 19 colors are always produced. 140 | pub fn get_colors(into_id: &[u8]) -> [Color; 19] { 141 | let into_zero = &[0u8; 32]; 142 | let zero = blake2b(64, &[], into_zero).as_bytes().to_vec(); 143 | 144 | let id_prep = blake2b(64, &[], into_id).as_bytes().to_vec(); 145 | 146 | let mut id: Vec = Vec::with_capacity(64); 147 | for (i, x) in id_prep.iter().enumerate() { 148 | let new = x.wrapping_sub(zero[i]); 149 | id.push(new); 150 | } 151 | 152 | // Since `id[29]` is u8, `sat` could range from 30 to 109, i.e. it always fits into u8. 153 | // Transformation of id[29] into u16 is to avoid overflow in multiplication 154 | // (wrapping could be used, but is more bulky). 155 | // TODO For color calculation `sat` is used as saturation in percents 156 | // (this is taken as is from js code). 157 | // However, this way `sat_component` could have values above 1.00. 158 | // Palette crate does not check at this moment that `sat_component` is not 159 | // overflowing 1.00, and produces some kind of resulting color. 160 | // Need to find out what should have happened if the sat values are above 100. 161 | let sat = (((id[29] as u16 * 70 / 256 + 26) % 80) + 30) as u8; 162 | let sat_component: f64 = (sat as f64) / 100f64; 163 | 164 | // calculating palette: set of 32 RGBA colors to be used in drawing 165 | // only id vector is used for this calculation 166 | let mut my_palette: Vec = Vec::with_capacity(64); 167 | for (i, x) in id.iter().enumerate() { 168 | let b = x.wrapping_add((i as u8 % 28).wrapping_mul(58)); 169 | let new = match b { 170 | 0 => Color { 171 | red: 4, 172 | green: 4, 173 | blue: 4, 174 | alpha: 255, 175 | }, 176 | 255 => Color::foreground(), // small circle is transparent, thus whatever is underneath it goes into `png` data, underneath is the foreground-colored large circle 177 | _ => Color::derive(b, sat_component), 178 | }; 179 | my_palette.push(new); 180 | } 181 | 182 | // loading default coloring schemes 183 | let schemes = default_schemes(); 184 | 185 | // `total` is the sum of frequencies for all scheme elements in coloring schemes, 186 | // in current setting is always 357 187 | let mut total = 0; 188 | for x in schemes.iter() { 189 | total += x.freq as u32; 190 | } 191 | 192 | // `d` is used to determine the coloring scheme to be used. 193 | // Transformation into u32 is used to avoid overflow. 194 | let d = (id[30] as u32 + (id[31] as u32) * 256) % total; 195 | 196 | // determining the coloring scheme to be used 197 | let my_scheme = choose_scheme(schemes, d); 198 | 199 | // calculating rotation for the coloring scheme 200 | let rot = (id[28] % 6) * 3; 201 | 202 | // picking colors from palette using coloring scheme with rotation applied 203 | let mut my_colors: Vec = Vec::with_capacity(19); 204 | for i in 0..19 { 205 | let num_color = { 206 | if i < 18 { 207 | (i + rot) % 18 208 | } else { 209 | 18 210 | } 211 | } as usize; 212 | let num_palette = my_scheme.colors[num_color]; 213 | let color = my_palette[num_palette]; 214 | my_colors.push(color); 215 | } 216 | 217 | my_colors 218 | .try_into() 219 | .expect("always generate 19-element set") 220 | } 221 | 222 | /// Function to choose the coloring scheme based on value d. 223 | /// Note that d is calculated as remainder of division by total sum of frequencies, 224 | /// so it can not exceed the total sum of frequencies 225 | fn choose_scheme(schemes: [SchemeElement; 7], d: u32) -> SchemeElement { 226 | let mut sum = 0; 227 | let mut found_scheme = None; 228 | for x in schemes { 229 | sum += x.freq as u32; 230 | if d < sum { 231 | found_scheme = Some(x); 232 | break; 233 | } 234 | } 235 | found_scheme.expect("should always be determined: d is calculated as remainder of division by total sum of frequencies, so it can not exceed the total sum of frequencies") 236 | } 237 | 238 | #[cfg(test)] 239 | mod tests { 240 | use super::*; 241 | 242 | const ALICE: &[u8] = &[ 243 | 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 244 | 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 245 | ]; // Alice public key; corresponds to hexadecimal "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" and base58 "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" in westend network 246 | const BOB: &[u8] = &[ 247 | 142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 248 | 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72, 249 | ]; // Bob public key; corresponds to hexadecimal "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48" and base58 "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" in westend network 250 | 251 | /// made with a color picking GIMP tool using the icon from polkadot website 252 | /// alpha set to 255 for all 253 | #[rustfmt::skip] 254 | fn alice_website() -> [Color; 19] { 255 | [ 256 | Color{red: 165, green: 227, blue: 156, alpha: 255}, 257 | Color{red: 60, green: 40, blue: 17, alpha: 255}, 258 | Color{red: 184, green: 68, blue: 202, alpha: 255}, 259 | Color{red: 139, green: 39, blue: 88, alpha: 255}, 260 | Color{red: 135, green: 68, blue: 202, alpha: 255}, 261 | Color{red: 225, green: 156, blue: 227, alpha: 255}, 262 | Color{red: 139, green: 39, blue: 88, alpha: 255}, 263 | Color{red: 135, green: 68, blue: 202, alpha: 255}, 264 | Color{red: 184, green: 68, blue: 202, alpha: 255}, 265 | Color{red: 165, green: 227, blue: 156, alpha: 255}, 266 | Color{red: 60, green: 40, blue: 17, alpha: 255}, 267 | Color{red: 162, green: 202, blue: 68, alpha: 255}, 268 | Color{red: 39, green: 139, blue: 139, alpha: 255}, 269 | Color{red: 187, green: 202, blue: 68, alpha: 255}, 270 | Color{red: 38, green: 60, blue: 17, alpha: 255}, 271 | Color{red: 39, green: 139, blue: 139, alpha: 255}, 272 | Color{red: 187, green: 202, blue: 68, alpha: 255}, 273 | Color{red: 162, green: 202, blue: 68, alpha: 255}, 274 | Color{red: 61, green: 39, blue: 139, alpha: 255}, 275 | ] 276 | } 277 | 278 | /// made with a color picking GIMP tool using the icon from polkadot website 279 | /// alpha set to 255 for all 280 | #[rustfmt::skip] 281 | fn bob_website() -> [Color; 19] { 282 | [ 283 | Color{red: 58, green: 120, blue: 61, alpha: 255}, 284 | Color{red: 200, green: 214, blue: 169, alpha: 255}, 285 | Color{red: 214, green: 169, blue: 182, alpha: 255}, 286 | Color{red: 36, green: 52, blue: 25, alpha: 255}, 287 | Color{red: 127, green: 93, blue: 177, alpha: 255}, 288 | Color{red: 214, green: 169, blue: 182, alpha: 255}, 289 | Color{red: 58, green: 120, blue: 61, alpha: 255}, 290 | Color{red: 200, green: 214, blue: 169, alpha: 255}, 291 | Color{red: 52, green: 25, blue: 30, alpha: 255}, 292 | Color{red: 113, green: 177, blue: 93, alpha: 255}, 293 | Color{red: 58, green: 120, blue: 114, alpha: 255}, 294 | Color{red: 58, green: 120, blue: 108, alpha: 255}, 295 | Color{red: 118, green: 93, blue: 177, alpha: 255}, 296 | Color{red: 25, green: 52, blue: 39, alpha: 255}, 297 | Color{red: 58, green: 120, blue: 108, alpha: 255}, 298 | Color{red: 113, green: 177, blue: 93, alpha: 255}, 299 | Color{red: 58, green: 120, blue: 114, alpha: 255}, 300 | Color{red: 52, green: 25, blue: 30, alpha: 255}, 301 | Color{red: 33, green: 25, blue: 52, alpha: 255}, 302 | ] 303 | } 304 | 305 | #[test] 306 | fn colors_alice() { 307 | let alice_calculated = get_colors(ALICE); 308 | assert!( 309 | alice_website() == alice_calculated, 310 | "Got different Alice colors:\n{:?}", 311 | alice_calculated 312 | ); 313 | } 314 | 315 | #[test] 316 | fn colors_bob() { 317 | let bob_calculated = get_colors(BOB); 318 | assert!( 319 | bob_website() == bob_calculated, 320 | "Got different Bob colors:\n{:?}", 321 | bob_calculated 322 | ); 323 | } 324 | 325 | #[test] 326 | fn colors_derive() { 327 | let b: u8 = 212u8; 328 | let saturation = 0.56; 329 | let color = Color::derive(b, saturation); 330 | assert_eq!( 331 | color, 332 | Color { 333 | red: 165, 334 | green: 227, 335 | blue: 156, 336 | alpha: 255, 337 | } 338 | ); 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Generates 19-circle identicons in `png` and in `svg` format from `&[u8]` input slice. Slice length could be arbitrary, as identicon generation starts with input hashing. Typical input is a public key. 2 | //! 3 | //! Identicon example: 4 | #![doc=include_str!("identicon_example.svg")] 5 | //! 6 | //! The identicon color scheme and elements arrangement follow the published javascript [code](https://github.com/paritytech/oo7/blob/master/packages/polkadot-identicon/src/index.jsx) for polkadot identicon generation. This crate is intended mainly for use by [Signer](https://github.com/paritytech/parity-signer). 7 | //! 8 | //! Crate also supports generation of identicon-like images with pre-set colors in RGBA format, mainly for test purposes. 9 | //! 10 | //! Feature `"pix"` supports generation of `png` images, feature `"vec"` - generation of `svg` images. Both are made available by default. 11 | 12 | #![deny(unused_crate_dependencies)] 13 | 14 | #[cfg(feature = "pix")] 15 | use image::{ 16 | imageops::{resize, FilterType}, 17 | load_from_memory, 18 | }; 19 | 20 | #[cfg(feature = "vec")] 21 | use svg::Document; 22 | 23 | pub mod circles; 24 | pub mod colors; 25 | pub use colors::Color; 26 | 27 | #[cfg(feature = "pix")] 28 | const SIZE_IN_PIXELS: u8 = 30; 29 | #[cfg(feature = "pix")] 30 | const SCALING_FACTOR: u8 = 5; 31 | #[cfg(feature = "pix")] 32 | const FILTER_TYPE: FilterType = FilterType::Lanczos3; 33 | #[cfg(feature = "pix")] 34 | /// Static 30x30 transparent `png`, for rare cases when the identicon 35 | /// generation fails or when the data to generate identicon is unavailable 36 | pub const EMPTY_PNG: &[u8] = &[ 37 | 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 30, 0, 0, 0, 30, 8, 6, 38 | 0, 0, 0, 59, 48, 174, 162, 0, 0, 0, 46, 73, 68, 65, 84, 120, 156, 237, 205, 65, 1, 0, 32, 12, 39 | 0, 33, 237, 31, 122, 182, 56, 31, 131, 2, 220, 153, 57, 63, 136, 51, 226, 140, 56, 35, 206, 40 | 136, 51, 226, 140, 56, 35, 206, 136, 51, 251, 226, 7, 36, 207, 89, 197, 10, 134, 29, 92, 0, 0, 41 | 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, 42 | ]; 43 | 44 | /// Polkadot identicon `png` data in `u8` vector format, from `&[u8]` input slice 45 | /// 46 | /// Input slice could be of any length, as it gets hashed anyways; 47 | /// typical input is a public key. 48 | /// 49 | /// Resulting image quality depends on image size, 50 | /// images start looking acceptable for sizes apporimately 100 pix and above. 51 | /// 52 | /// ## Example 53 | /// 54 | /// Let's generate a set of identicons of varying size. 55 | /// 56 | /// ``` 57 | /// use image::load_from_memory; 58 | /// use plot_icon::generate_png; 59 | /// 60 | /// let alice: &[u8] = &[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125]; 61 | /// let size_set = [16, 32, 45, 64, 128, 256, 512]; 62 | /// for size_in_pixels in size_set.into_iter() { 63 | /// let content = generate_png(alice, size_in_pixels).unwrap(); 64 | /// let image = load_from_memory(&content).unwrap(); 65 | /// assert!(image.width() == size_in_pixels as u32); 66 | /// assert!(image.height() == size_in_pixels as u32); 67 | /// } 68 | /// ``` 69 | #[cfg(feature = "pix")] 70 | pub fn generate_png(into_id: &[u8], size_in_pixels: u16) -> Result, png::EncodingError> { 71 | let colors = colors::get_colors(into_id); 72 | generate_png_with_colors(colors, size_in_pixels) 73 | } 74 | 75 | /// Polkadot identicon `png` data in `u8` vector format, with given colors 76 | /// 77 | /// Makes regular identicons if properly generated color set is plugged in. 78 | /// Also could be used to generate test identicon-like pictures. 79 | /// 80 | /// Input [`Color`] set is in RGBA format. 81 | /// 82 | /// ## Example 83 | /// 84 | /// Make identicon with gray circles. 85 | /// Such identicon-like image could be used as constant for rare cases when real `png` identicon failed to generate. 86 | /// 87 | /// ``` 88 | /// use plot_icon::{generate_png_with_colors, colors::Color}; 89 | /// 90 | /// let colors = [Color{red: 200, green: 200, blue: 200, alpha: 255}; 19]; 91 | /// let size_in_pixels = 250; 92 | /// let content = generate_png_with_colors(colors, size_in_pixels).unwrap(); 93 | /// ``` 94 | #[cfg(feature = "pix")] 95 | pub fn generate_png_with_colors( 96 | colors: [Color; 19], 97 | size_in_pixels: u16, 98 | ) -> Result, png::EncodingError> { 99 | let data = circles::calculate_png_data(size_in_pixels, colors); 100 | make_png_from_data(&data, size_in_pixels) 101 | } 102 | 103 | /// Data for small-sized identicon `png`, from `&[u8]` input slice, 104 | /// larger image is generated first and then scaled down to fit the required size 105 | /// 106 | /// Input slice could be of any length, as it gets hashed anyways; 107 | /// typical input is a public key. 108 | /// 109 | /// Function is expected to be used with image size values approximately 100 pix and below. 110 | /// 111 | /// # Scaling factor 112 | /// 113 | /// Scaling factor here is how much bigger the image gets generated, before it is scaled down. 114 | /// 115 | /// ## Example 116 | /// 117 | /// Let's generate a set of small identicons by scaling with different scaling factors. 118 | /// 119 | /// Set the filter type to CatmullRom, and vary the scaling factor. 120 | /// ``` 121 | /// use image::imageops::FilterType; 122 | /// use plot_icon::generate_png_scaled_custom; 123 | /// 124 | /// let alice: &[u8] = &[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125]; 125 | /// let size_in_pixels = 32; 126 | /// let filter = FilterType::CatmullRom; 127 | /// for scaling_factor in 1..=16 { 128 | /// let content = generate_png_scaled_custom (alice, size_in_pixels, scaling_factor, filter).unwrap(); 129 | /// } 130 | /// ``` 131 | /// For image size 32, the `scaling_factor = 1` results in strongly pixelated image, 132 | /// it is identical to `generate_png` result. 133 | /// With `scaling_factor = 2` image is already much less pixelated, off-centering still 134 | /// visible; off-centering virtually disappears for `scaling factor = 4` and above, 135 | /// after `scaling factor = 6` it is quite challenging to find any image differences at all. 136 | /// 137 | /// # Filter type 138 | /// 139 | /// [`FilterType`](https://docs.rs/image/latest/image/imageops/enum.FilterType.html) is the filter used during the scaling. 140 | /// 141 | /// ## Example 142 | /// 143 | /// Let's generate another set of small identicons by scaling with different filters. 144 | /// 145 | /// Set the scaling factor to 8, and vary the filter. 146 | /// ``` 147 | /// use image::imageops::FilterType; 148 | /// use plot_icon::generate_png_scaled_custom; 149 | /// 150 | /// let bob: &[u8] = &[142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72]; 151 | /// let filter_set = [ 152 | /// FilterType::Nearest, 153 | /// FilterType::Triangle, 154 | /// FilterType::CatmullRom, 155 | /// FilterType::Gaussian, 156 | /// FilterType::Lanczos3, 157 | /// ]; 158 | /// let scaling_factor = 8; 159 | /// let size_in_pixels = 32; 160 | /// for filter in filter_set.into_iter() { 161 | /// let content = generate_png_scaled_custom (bob, size_in_pixels, scaling_factor, filter).unwrap(); 162 | /// } 163 | /// ``` 164 | /// All identicons are readable, 165 | /// the one made with `FilterType::Nearest` seems too pixelated, 166 | /// the one made with `FilterType::Gaussian` seems a bit blurry. 167 | /// 168 | /// # Selecting the defaults for Signer 169 | /// 170 | /// To select the default values properly, a set of images with different scaling factors 171 | /// was generated for each of the filters. 172 | /// Signer at the moment uses 30 pix images. 173 | /// 174 | /// ## Making image set to choose from 175 | /// 176 | /// ``` 177 | /// use image::imageops::FilterType; 178 | /// use plot_icon::generate_png_scaled_custom; 179 | /// 180 | /// let alice: &[u8] = &[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125]; 181 | /// let bob: &[u8] = &[142, 175, 4, 21, 22, 135, 115, 99, 38, 201, 254, 161, 126, 37, 252, 82, 135, 97, 54, 147, 201, 18, 144, 156, 178, 38, 170, 71, 148, 242, 106, 72]; 182 | /// let filter_set = [ 183 | /// FilterType::Nearest, 184 | /// FilterType::Triangle, 185 | /// FilterType::CatmullRom, 186 | /// FilterType::Gaussian, 187 | /// FilterType::Lanczos3, 188 | /// ]; 189 | /// let size_in_pixels = 30; 190 | /// for i in 2..=10 { 191 | /// for filter in filter_set.iter() { 192 | /// for id_slice in [alice, bob].into_iter() { 193 | /// let content = generate_png_scaled_custom (id_slice, size_in_pixels, i, *filter).unwrap(); 194 | /// } 195 | /// } 196 | /// } 197 | /// ``` 198 | /// For now the default scaling factor is selected to be `5`, 199 | /// the default filter is selected to be `FilterType::Lanczos3`. 200 | /// 201 | #[cfg(feature = "pix")] 202 | pub fn generate_png_scaled_custom( 203 | into_id: &[u8], 204 | size_in_pixels: u8, 205 | scaling_factor: u8, 206 | filter_type: FilterType, 207 | ) -> Result, IdenticonError> { 208 | let colors = colors::get_colors(into_id); 209 | generate_png_scaled_custom_with_colors(colors, size_in_pixels, scaling_factor, filter_type) 210 | } 211 | 212 | /// Data for small-sized identicon `png`, with given colors, 213 | /// larger image is generated first and then scaled down to fit the required size 214 | /// 215 | /// Makes regular identicons if properly generated color set is plugged in. 216 | /// Also could be used to generate test identicon-like pictures. 217 | /// 218 | /// Input [`Color`] set is in RGBA format. 219 | /// 220 | /// Function is expected to be used with image size values approximately 100 pix and below. 221 | /// 222 | /// ## Example 223 | /// 224 | /// Make identicon with gray circles. 225 | /// 226 | /// ``` 227 | /// use image::imageops::FilterType; 228 | /// use plot_icon::{generate_png_scaled_custom_with_colors, colors::Color}; 229 | /// 230 | /// let rgb_value = 150; 231 | /// let size_in_pixels = 30; 232 | /// let scaling_factor = 5; 233 | /// let filter_type = FilterType::Lanczos3; 234 | /// let colors = [Color{red: rgb_value, green: rgb_value, blue: rgb_value, alpha: 255}; 19]; 235 | /// let content = generate_png_scaled_custom_with_colors(colors, size_in_pixels, scaling_factor, filter_type).unwrap(); 236 | /// ``` 237 | #[cfg(feature = "pix")] 238 | pub fn generate_png_scaled_custom_with_colors( 239 | colors: [Color; 19], 240 | size_in_pixels: u8, 241 | scaling_factor: u8, 242 | filter_type: FilterType, 243 | ) -> Result, IdenticonError> { 244 | let data_large = 245 | generate_png_with_colors(colors, size_in_pixels as u16 * scaling_factor as u16) 246 | .map_err(IdenticonError::Png)?; 247 | let image_large = load_from_memory(&data_large).map_err(IdenticonError::Image)?; 248 | let image_small = resize( 249 | &image_large, 250 | size_in_pixels as u32, 251 | size_in_pixels as u32, 252 | filter_type, 253 | ); 254 | make_png_from_data(&image_small, size_in_pixels as u16).map_err(IdenticonError::Png) 255 | } 256 | 257 | /// Data for small-sized identicon `png`, from `&[u8]` input slice, 258 | /// with default settings used for Signer app 259 | /// 260 | /// Generates larger image and then scales it down to fit the required size, 261 | /// with default filter (`FilterType::Lanczos3`) and default scaling factor (`5`). 262 | /// Function is unfallible. If `png` generation itself fails, is falls back to transparent 30x30 `png`. 263 | /// 264 | /// ## Example 265 | /// 266 | /// ``` 267 | /// use plot_icon::generate_png_scaled_default; 268 | /// 269 | /// let alice: &[u8] = &[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125]; 270 | /// let filename = "test_pics/default_signer_alice.png"; 271 | /// let content = generate_png_scaled_default(alice); 272 | /// assert!(std::fs::read(&filename).unwrap() == content, "Generated different image for {}!", filename); 273 | /// ``` 274 | /// 275 | #[cfg(feature = "pix")] 276 | pub fn generate_png_scaled_default(into_id: &[u8]) -> Vec { 277 | match generate_png_scaled_custom(into_id, SIZE_IN_PIXELS, SCALING_FACTOR, FILTER_TYPE) { 278 | Ok(a) => a, 279 | Err(_) => EMPTY_PNG.to_vec(), 280 | } 281 | } 282 | 283 | /// Helper function to write calculated pixel-by-pixel `png` pixel data in `png` format, header and all 284 | #[cfg(feature = "pix")] 285 | fn make_png_from_data(data: &[u8], size_in_pixels: u16) -> Result, png::EncodingError> { 286 | let mut out: Vec = Vec::new(); 287 | let mut encoder = png::Encoder::new(&mut out, size_in_pixels as u32, size_in_pixels as u32); 288 | encoder.set_color(png::ColorType::Rgba); 289 | encoder.set_depth(png::BitDepth::Eight); 290 | let mut writer = encoder.write_header()?; 291 | writer.write_image_data(data)?; 292 | drop(writer); 293 | Ok(out) 294 | } 295 | 296 | /// Errors in `png` identicon generation 297 | #[derive(Debug)] 298 | #[cfg(feature = "pix")] 299 | pub enum IdenticonError { 300 | /// [`png::EncodingError`](https://docs.rs/png/latest/png/enum.EncodingError.html) 301 | /// 302 | /// From `png` crate, could appear on writing the pixel data into `png`, 303 | /// generally should not happen, since the `png` parameters are matching the pixel data generated 304 | Png(png::EncodingError), 305 | /// [`image::ImageError`](https://docs.rs/image/latest/image/error/enum.ImageError.html) 306 | /// 307 | /// From `image` crate, could appear on loading freshly made `png` into `DynamicImage`, 308 | /// very unlikely 309 | Image(image::ImageError), 310 | } 311 | 312 | #[cfg(feature = "pix")] 313 | impl IdenticonError { 314 | /// displaying error text 315 | pub fn show(&self) -> String { 316 | match &self { 317 | IdenticonError::Png(e) => format!("Error encoding data into png format: {}", e), 318 | IdenticonError::Image(e) => format!( 319 | "Error transforming png data into DynamicImage format: {}", 320 | e 321 | ), 322 | } 323 | } 324 | } 325 | 326 | #[cfg(feature = "pix")] 327 | impl std::fmt::Display for IdenticonError { 328 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 329 | write!(f, "{}", self.show()) 330 | } 331 | } 332 | 333 | /// Identicon [`svg::Document`](https://docs.rs/svg/latest/svg/type.Document.html) 334 | /// data, from `&[u8]` input slice 335 | /// 336 | /// Input slice could be of any length, as it gets hashed anyways; 337 | /// typical input is a public key. 338 | /// 339 | /// ## Example 340 | /// 341 | /// ``` 342 | /// use plot_icon::generate_svg; 343 | /// 344 | /// let alice: &[u8] = &[212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125]; 345 | /// let svg_document = generate_svg (&alice); 346 | /// let mut svg_expected_content = String::new(); 347 | /// svg::open("test_pics/alice.svg", &mut svg_expected_content).unwrap(); 348 | /// assert!(svg_document.to_string() == svg_expected_content, "Expected different svg content, got: {}", svg_document); 349 | /// ``` 350 | #[cfg(feature = "vec")] 351 | pub fn generate_svg(into_id: &[u8]) -> Document { 352 | let colors = colors::get_colors(into_id); 353 | generate_svg_with_colors(colors) 354 | } 355 | 356 | /// Identicon [`svg::Document`](https://docs.rs/svg/latest/svg/type.Document.html) 357 | /// data, with given colors 358 | /// 359 | /// Makes regular identicons if properly generated color set is plugged in. 360 | /// Also could be used to generate test identicon-like pictures. 361 | /// 362 | /// Input [`Color`] set is in RGBA format. 363 | /// 364 | /// ## Example 365 | /// 366 | /// ``` 367 | /// use plot_icon::{generate_svg_with_colors, colors::Color}; 368 | /// 369 | /// let colors = [Color{red: 200, green: 200, blue: 200, alpha: 255}; 19]; 370 | /// let svg_document = generate_svg_with_colors(colors); 371 | /// ``` 372 | #[cfg(feature = "vec")] 373 | pub fn generate_svg_with_colors(colors: [Color; 19]) -> Document { 374 | let unit = 10; // svg is vector format, the unit size is arbitrary, and does not influence the outcome 375 | let mut document = Document::new().set("viewBox", (-unit, -unit, 2 * unit, 2 * unit)); 376 | let data = circles::calculate_svg_data(unit as f32, colors); 377 | for x in data.into_iter() { 378 | document = document.add(x); 379 | } 380 | document 381 | } 382 | --------------------------------------------------------------------------------