├── .gitignore ├── Cargo.toml ├── README.md ├── docs ├── 01.webp ├── 02.webp └── 03.webp ├── gray.html ├── gray.jpeg ├── index.html ├── pkg ├── .gitignore ├── README.md ├── package.json ├── rust_wasm_image_ascii.d.ts ├── rust_wasm_image_ascii.js ├── rust_wasm_image_ascii_bg.wasm └── rust_wasm_image_ascii_bg.wasm.d.ts ├── src ├── lib.rs ├── tai │ ├── README.md │ ├── arguments │ │ ├── argument_parsing.rs │ │ ├── config.rs │ │ └── mod.rs │ ├── mod.rs │ ├── operations │ │ ├── ascii.rs │ │ ├── braille.rs │ │ ├── dither.rs │ │ ├── mod.rs │ │ ├── onechar.rs │ │ └── otsu_threshold.rs │ └── utils.rs └── utils.rs └── test.html /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | .DS_Store -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-wasm-image-ascii" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | wasm-bindgen = { version = "0.2.81", features = ["serde-serialize"] } 9 | serde_json = "1.0.83" 10 | serde = { version = "1.0.138", features = ["derive"] } 11 | console_error_panic_hook = { version = "0.1.7", optional = true } 12 | wee_alloc = { version = "0.4.5", optional = true } 13 | image = "0.24.3" 14 | web-sys = { version = "0.3.58", features = ["console"] } 15 | 16 | [features] 17 | default = ["console_error_panic_hook", "wee_alloc"] 18 | 19 | [lib] 20 | crate-type = ["cdylib"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust wasm image to ascii 2 | 3 | ``` 4 | ██████████████████████████████████████████████████ 5 | ████████████████████ ██████████████████████████ 6 | ███████████████████ █ █████████████████████████ 7 | ██████████████████ █████ ███████████████████████ 8 | █████████████████ █████ █████████████████████ 9 | ████████████████ █████ █ ███████████████████ 10 | ██████████████ █████ █ ██ ████████████████ 11 | ████████████ ███ █ ██████ ███████████████ 12 | ███████████ █ ██ █ ██████ █ ██████████████ 13 | ██████████ ██ █ ██ █████ ███ █████████████ 14 | ██████████ ███ ████ ███████████ 15 | █████████ █ █████ ███ █ ███████████ 16 | █████████ █ █ ██ ███ █ ████████████ 17 | ██████████ ███ █ ███████ █ █████████████ 18 | ████████ █ █ █ ██ ██████████████ 19 | ███████ █ █ ███ ████ ███████████████ 20 | ██████ ██ ███ ███████████ █████████████████ 21 | ██████ █ █ ████ ████████████████ 22 | ██████ ██ ███████████████ 23 | ██████ █████ ██████████████ 24 | █████ ███████████████ 25 | ██████ ███ █ 心中有光 ████████████████ 26 | ███████ █ ██████████████████ 27 | ███████████ ██████████████████ 28 | ███████████████ ████████████████████ 29 | ███████████████ ███████████████████ 30 | ███████████ ██████████████████ 31 | █████████ ██ █████████████████ 32 | ████████ ███████████████ 33 | ████████ ███████ ██ ██████████████ 34 | ███████ ██████████ ████████████ 35 | ██████ █████████████ ████████████ 36 | ████ ██ ███████████████ ██ █████████ 37 | ███ ██████████████████████ █ ███████████ 38 | ██████████████████████████████████████████████████ 39 | ██████████████████████████████████████████████████ 40 | ``` 41 | 42 | ### 灰度算法 43 | 44 | 灰度算法对比: 45 | 46 | [https://lecepin.github.io/rust-wasm-image-ascii/gray.html](https://lecepin.github.io/rust-wasm-image-ascii/gray.html) 47 | 48 | ![image](https://user-images.githubusercontent.com/11046969/185297221-e4441e32-5802-418c-a3bf-e9f9900966e3.png) 49 | 50 | 51 | 这里直接使用的 `image` crate 的内置算法,上图中的第三种: 52 | 53 | ```rust 54 | // luminance formula credits: https://stackoverflow.com/a/596243 55 | // >>> Luminance = 0.2126*R + 0.7152*G + 0.0722*B <<< 56 | // calculate RGB values to get luminance of the pixel 57 | pub fn get_luminance(r: u8, g: u8, b: u8) -> f32 { 58 | let r = 0.2126 * (r as f32); 59 | let g = 0.7152 * (g as f32); 60 | let b = 0.0722 * (b as f32); 61 | r + g + b 62 | } 63 | ``` 64 | 65 | ### 简单版本 66 | 67 | 简单版本只做了一种效果,访问地址: [https://lecepin.github.io/rust-wasm-image-ascii/test.html](https://lecepin.github.io/rust-wasm-image-ascii/test.html) 68 | 69 | ![](./docs/02.webp) 70 | 71 | ### Tai 版本 72 | 73 | 看到一个支持 ASCII 种类挺多的 Rust 项目 https://github.com/MustafaSalih1993/tai ,于是将这个项目的 IO 部分进行了修改,适配 WASM 进行了编译处理。 74 | 75 | ![](./docs/03.webp) 76 | 77 | ## 安装&使用 78 | 79 | ```html 80 | 90 | ``` 91 | 92 | 可以直接使用仓库中 `pkg/` 目录中的文件,也可以使用 upkg 的资源 https://unpkg.com/browse/rust-wasm-image-ascii/ ,也可以 `npm install rust-wasm-image-ascii` 使用。 93 | 94 | 接口描述参考这里:[pkg/rust_wasm_image_ascii.d.ts](./pkg/rust_wasm_image_ascii.d.ts) 95 | 96 | -------------------------------------------------------------------------------- /docs/01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lecepin/rust-wasm-image-ascii/e09bf6faf09b0ae3d8bef3b427b729ec7bafe3de/docs/01.webp -------------------------------------------------------------------------------- /docs/02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lecepin/rust-wasm-image-ascii/e09bf6faf09b0ae3d8bef3b427b729ec7bafe3de/docs/02.webp -------------------------------------------------------------------------------- /docs/03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lecepin/rust-wasm-image-ascii/e09bf6faf09b0ae3d8bef3b427b729ec7bafe3de/docs/03.webp -------------------------------------------------------------------------------- /gray.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Gray Test 6 | 7 | 8 | 9 | 82 | 83 |
84 | 85 |
86 | 87 | 88 | 92 | 96 | 100 | 101 | 102 | 106 | 110 | 117 | 118 |
89 |
原图
90 | 91 |
93 |
最大值法
94 | 95 |
97 |
平均值法
98 | 99 |
103 |
加权平均值法:0.2126 * r + 0.7152 * g + 0.0722 * b
104 | 105 |
107 |
加权平均值法:0.299 * r + 0.587 * g + 0.114 * b
108 | 109 |
111 |
112 | 加权平均值法: Math.sqrt( (0.299 * r) ** 2 + (0.587 * g) ** 2 + 113 | (0.114 * b) ** 2 ) 114 |
115 | 116 |
119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
127 | 灰度算法对比 128 | 简单版本 129 | Tai 版本 130 |
131 | 132 | 133 | -------------------------------------------------------------------------------- /gray.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lecepin/rust-wasm-image-ascii/e09bf6faf09b0ae3d8bef3b427b729ec7bafe3de/gray.jpeg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rust-image-ascii 6 | 7 | 8 | 9 |
10 |
11 | WASM 文件加载中…
12 |
13 |

Rust-image-ascii

14 | 15 | 16 |
17 | 设置 18 | 27 | 37 | 38 |
39 | 40 |
41 | 42 | 43 |
44 | 45 |

 53 | 
 54 |     
102 |     
103 |     
104 | 灰度算法对比 105 | 简单版本 106 | Tai 版本 107 |
108 | 109 | 110 | -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | * -------------------------------------------------------------------------------- /pkg/README.md: -------------------------------------------------------------------------------- 1 | # Rust wasm image to ascii 2 | 3 | ``` 4 | ██████████████████████████████████████████████████ 5 | ████████████████████ ██████████████████████████ 6 | ███████████████████ █ █████████████████████████ 7 | ██████████████████ █████ ███████████████████████ 8 | █████████████████ █████ █████████████████████ 9 | ████████████████ █████ █ ███████████████████ 10 | ██████████████ █████ █ ██ ████████████████ 11 | ████████████ ███ █ ██████ ███████████████ 12 | ███████████ █ ██ █ ██████ █ ██████████████ 13 | ██████████ ██ █ ██ █████ ███ █████████████ 14 | ██████████ ███ ████ ███████████ 15 | █████████ █ █████ ███ █ ███████████ 16 | █████████ █ █ ██ ███ █ ████████████ 17 | ██████████ ███ █ ███████ █ █████████████ 18 | ████████ █ █ █ ██ ██████████████ 19 | ███████ █ █ ███ ████ ███████████████ 20 | ██████ ██ ███ ███████████ █████████████████ 21 | ██████ █ █ ████ ████████████████ 22 | ██████ ██ ███████████████ 23 | ██████ █████ ██████████████ 24 | █████ ███████████████ 25 | ██████ ███ █ 心中有光 ████████████████ 26 | ███████ █ ██████████████████ 27 | ███████████ ██████████████████ 28 | ███████████████ ████████████████████ 29 | ███████████████ ███████████████████ 30 | ███████████ ██████████████████ 31 | █████████ ██ █████████████████ 32 | ████████ ███████████████ 33 | ████████ ███████ ██ ██████████████ 34 | ███████ ██████████ ████████████ 35 | ██████ █████████████ ████████████ 36 | ████ ██ ███████████████ ██ █████████ 37 | ███ ██████████████████████ █ ███████████ 38 | ██████████████████████████████████████████████████ 39 | ██████████████████████████████████████████████████ 40 | ``` 41 | 42 | ### 灰度算法 43 | 44 | 灰度算法对比: 45 | 46 | [https://lecepin.github.io/rust-wasm-image-ascii/gray.html](https://lecepin.github.io/rust-wasm-image-ascii/gray.html) 47 | 48 | ![](./docs/01.webp) 49 | 50 | 这里直接使用的 `image` crate 的内置算法,上图中的第三种: 51 | 52 | ```rust 53 | // luminance formula credits: https://stackoverflow.com/a/596243 54 | // >>> Luminance = 0.2126*R + 0.7152*G + 0.0722*B <<< 55 | // calculate RGB values to get luminance of the pixel 56 | pub fn get_luminance(r: u8, g: u8, b: u8) -> f32 { 57 | let r = 0.2126 * (r as f32); 58 | let g = 0.7152 * (g as f32); 59 | let b = 0.0722 * (b as f32); 60 | r + g + b 61 | } 62 | ``` 63 | 64 | ### 简单版本 65 | 66 | 简单版本只做了一种效果,访问地址: [https://lecepin.github.io/rust-wasm-image-ascii/test.html](https://lecepin.github.io/rust-wasm-image-ascii/test.html) 67 | 68 | ![](./docs/02.webp) -------------------------------------------------------------------------------- /pkg/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rust-wasm-image-ascii", 3 | "version": "0.1.0", 4 | "files": [ 5 | "rust_wasm_image_ascii_bg.wasm", 6 | "rust_wasm_image_ascii.js", 7 | "rust_wasm_image_ascii.d.ts" 8 | ], 9 | "module": "rust_wasm_image_ascii.js", 10 | "types": "rust_wasm_image_ascii.d.ts", 11 | "sideEffects": false 12 | } -------------------------------------------------------------------------------- /pkg/rust_wasm_image_ascii.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * @param {Uint8Array} raw 5 | * @param {number} scale 6 | * @returns {Uint8Array} 7 | */ 8 | export function get_gray_image(raw: Uint8Array, scale: number): Uint8Array; 9 | /** 10 | * @param {Uint8Array} raw 11 | * @param {number} scale 12 | * @param {boolean} reverse 13 | * @returns {string} 14 | */ 15 | export function get_ascii_by_image(raw: Uint8Array, scale: number, reverse: boolean): string; 16 | /** 17 | * @param {Uint8Array} raw 18 | * @param {number} scale 19 | * @param {boolean} reverse 20 | * @param {string} style 21 | * @returns {string} 22 | */ 23 | export function get_ascii_by_image_tai(raw: Uint8Array, scale: number, reverse: boolean, style: string): string; 24 | /** 25 | */ 26 | export function run(): void; 27 | 28 | export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; 29 | 30 | export interface InitOutput { 31 | readonly memory: WebAssembly.Memory; 32 | readonly get_gray_image: (a: number, b: number, c: number, d: number) => void; 33 | readonly get_ascii_by_image: (a: number, b: number, c: number, d: number, e: number) => void; 34 | readonly get_ascii_by_image_tai: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void; 35 | readonly run: () => void; 36 | readonly __wbindgen_add_to_stack_pointer: (a: number) => number; 37 | readonly __wbindgen_malloc: (a: number) => number; 38 | readonly __wbindgen_free: (a: number, b: number) => void; 39 | readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; 40 | readonly __wbindgen_start: () => void; 41 | } 42 | 43 | /** 44 | * Synchronously compiles the given `bytes` and instantiates the WebAssembly module. 45 | * 46 | * @param {BufferSource} bytes 47 | * 48 | * @returns {InitOutput} 49 | */ 50 | export function initSync(bytes: BufferSource): InitOutput; 51 | 52 | /** 53 | * If `module_or_path` is {RequestInfo} or {URL}, makes a request and 54 | * for everything else, calls `WebAssembly.instantiate` directly. 55 | * 56 | * @param {InitInput | Promise} module_or_path 57 | * 58 | * @returns {Promise} 59 | */ 60 | export default function init (module_or_path?: InitInput | Promise): Promise; 61 | -------------------------------------------------------------------------------- /pkg/rust_wasm_image_ascii.js: -------------------------------------------------------------------------------- 1 | 2 | let wasm; 3 | 4 | const heap = new Array(32).fill(undefined); 5 | 6 | heap.push(undefined, null, true, false); 7 | 8 | function getObject(idx) { return heap[idx]; } 9 | 10 | let heap_next = heap.length; 11 | 12 | function dropObject(idx) { 13 | if (idx < 36) return; 14 | heap[idx] = heap_next; 15 | heap_next = idx; 16 | } 17 | 18 | function takeObject(idx) { 19 | const ret = getObject(idx); 20 | dropObject(idx); 21 | return ret; 22 | } 23 | 24 | const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); 25 | 26 | cachedTextDecoder.decode(); 27 | 28 | let cachedUint8Memory0 = new Uint8Array(); 29 | 30 | function getUint8Memory0() { 31 | if (cachedUint8Memory0.byteLength === 0) { 32 | cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer); 33 | } 34 | return cachedUint8Memory0; 35 | } 36 | 37 | function getStringFromWasm0(ptr, len) { 38 | return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); 39 | } 40 | 41 | let WASM_VECTOR_LEN = 0; 42 | 43 | function passArray8ToWasm0(arg, malloc) { 44 | const ptr = malloc(arg.length * 1); 45 | getUint8Memory0().set(arg, ptr / 1); 46 | WASM_VECTOR_LEN = arg.length; 47 | return ptr; 48 | } 49 | 50 | let cachedInt32Memory0 = new Int32Array(); 51 | 52 | function getInt32Memory0() { 53 | if (cachedInt32Memory0.byteLength === 0) { 54 | cachedInt32Memory0 = new Int32Array(wasm.memory.buffer); 55 | } 56 | return cachedInt32Memory0; 57 | } 58 | 59 | function getArrayU8FromWasm0(ptr, len) { 60 | return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len); 61 | } 62 | /** 63 | * @param {Uint8Array} raw 64 | * @param {number} scale 65 | * @returns {Uint8Array} 66 | */ 67 | export function get_gray_image(raw, scale) { 68 | try { 69 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 70 | const ptr0 = passArray8ToWasm0(raw, wasm.__wbindgen_malloc); 71 | const len0 = WASM_VECTOR_LEN; 72 | wasm.get_gray_image(retptr, ptr0, len0, scale); 73 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 74 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 75 | var v1 = getArrayU8FromWasm0(r0, r1).slice(); 76 | wasm.__wbindgen_free(r0, r1 * 1); 77 | return v1; 78 | } finally { 79 | wasm.__wbindgen_add_to_stack_pointer(16); 80 | } 81 | } 82 | 83 | /** 84 | * @param {Uint8Array} raw 85 | * @param {number} scale 86 | * @param {boolean} reverse 87 | * @returns {string} 88 | */ 89 | export function get_ascii_by_image(raw, scale, reverse) { 90 | try { 91 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 92 | const ptr0 = passArray8ToWasm0(raw, wasm.__wbindgen_malloc); 93 | const len0 = WASM_VECTOR_LEN; 94 | wasm.get_ascii_by_image(retptr, ptr0, len0, scale, reverse); 95 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 96 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 97 | return getStringFromWasm0(r0, r1); 98 | } finally { 99 | wasm.__wbindgen_add_to_stack_pointer(16); 100 | wasm.__wbindgen_free(r0, r1); 101 | } 102 | } 103 | 104 | const cachedTextEncoder = new TextEncoder('utf-8'); 105 | 106 | const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' 107 | ? function (arg, view) { 108 | return cachedTextEncoder.encodeInto(arg, view); 109 | } 110 | : function (arg, view) { 111 | const buf = cachedTextEncoder.encode(arg); 112 | view.set(buf); 113 | return { 114 | read: arg.length, 115 | written: buf.length 116 | }; 117 | }); 118 | 119 | function passStringToWasm0(arg, malloc, realloc) { 120 | 121 | if (realloc === undefined) { 122 | const buf = cachedTextEncoder.encode(arg); 123 | const ptr = malloc(buf.length); 124 | getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf); 125 | WASM_VECTOR_LEN = buf.length; 126 | return ptr; 127 | } 128 | 129 | let len = arg.length; 130 | let ptr = malloc(len); 131 | 132 | const mem = getUint8Memory0(); 133 | 134 | let offset = 0; 135 | 136 | for (; offset < len; offset++) { 137 | const code = arg.charCodeAt(offset); 138 | if (code > 0x7F) break; 139 | mem[ptr + offset] = code; 140 | } 141 | 142 | if (offset !== len) { 143 | if (offset !== 0) { 144 | arg = arg.slice(offset); 145 | } 146 | ptr = realloc(ptr, len, len = offset + arg.length * 3); 147 | const view = getUint8Memory0().subarray(ptr + offset, ptr + len); 148 | const ret = encodeString(arg, view); 149 | 150 | offset += ret.written; 151 | } 152 | 153 | WASM_VECTOR_LEN = offset; 154 | return ptr; 155 | } 156 | /** 157 | * @param {Uint8Array} raw 158 | * @param {number} scale 159 | * @param {boolean} reverse 160 | * @param {string} style 161 | * @returns {string} 162 | */ 163 | export function get_ascii_by_image_tai(raw, scale, reverse, style) { 164 | try { 165 | const retptr = wasm.__wbindgen_add_to_stack_pointer(-16); 166 | const ptr0 = passArray8ToWasm0(raw, wasm.__wbindgen_malloc); 167 | const len0 = WASM_VECTOR_LEN; 168 | const ptr1 = passStringToWasm0(style, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 169 | const len1 = WASM_VECTOR_LEN; 170 | wasm.get_ascii_by_image_tai(retptr, ptr0, len0, scale, reverse, ptr1, len1); 171 | var r0 = getInt32Memory0()[retptr / 4 + 0]; 172 | var r1 = getInt32Memory0()[retptr / 4 + 1]; 173 | return getStringFromWasm0(r0, r1); 174 | } finally { 175 | wasm.__wbindgen_add_to_stack_pointer(16); 176 | wasm.__wbindgen_free(r0, r1); 177 | } 178 | } 179 | 180 | /** 181 | */ 182 | export function run() { 183 | wasm.run(); 184 | } 185 | 186 | function addHeapObject(obj) { 187 | if (heap_next === heap.length) heap.push(heap.length + 1); 188 | const idx = heap_next; 189 | heap_next = heap[idx]; 190 | 191 | heap[idx] = obj; 192 | return idx; 193 | } 194 | 195 | async function load(module, imports) { 196 | if (typeof Response === 'function' && module instanceof Response) { 197 | if (typeof WebAssembly.instantiateStreaming === 'function') { 198 | try { 199 | return await WebAssembly.instantiateStreaming(module, imports); 200 | 201 | } catch (e) { 202 | if (module.headers.get('Content-Type') != 'application/wasm') { 203 | console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); 204 | 205 | } else { 206 | throw e; 207 | } 208 | } 209 | } 210 | 211 | const bytes = await module.arrayBuffer(); 212 | return await WebAssembly.instantiate(bytes, imports); 213 | 214 | } else { 215 | const instance = await WebAssembly.instantiate(module, imports); 216 | 217 | if (instance instanceof WebAssembly.Instance) { 218 | return { instance, module }; 219 | 220 | } else { 221 | return instance; 222 | } 223 | } 224 | } 225 | 226 | function getImports() { 227 | const imports = {}; 228 | imports.wbg = {}; 229 | imports.wbg.__wbg_new_693216e109162396 = function() { 230 | const ret = new Error(); 231 | return addHeapObject(ret); 232 | }; 233 | imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) { 234 | const ret = getObject(arg1).stack; 235 | const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); 236 | const len0 = WASM_VECTOR_LEN; 237 | getInt32Memory0()[arg0 / 4 + 1] = len0; 238 | getInt32Memory0()[arg0 / 4 + 0] = ptr0; 239 | }; 240 | imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) { 241 | try { 242 | console.error(getStringFromWasm0(arg0, arg1)); 243 | } finally { 244 | wasm.__wbindgen_free(arg0, arg1); 245 | } 246 | }; 247 | imports.wbg.__wbindgen_object_drop_ref = function(arg0) { 248 | takeObject(arg0); 249 | }; 250 | imports.wbg.__wbindgen_throw = function(arg0, arg1) { 251 | throw new Error(getStringFromWasm0(arg0, arg1)); 252 | }; 253 | 254 | return imports; 255 | } 256 | 257 | function initMemory(imports, maybe_memory) { 258 | 259 | } 260 | 261 | function finalizeInit(instance, module) { 262 | wasm = instance.exports; 263 | init.__wbindgen_wasm_module = module; 264 | cachedInt32Memory0 = new Int32Array(); 265 | cachedUint8Memory0 = new Uint8Array(); 266 | 267 | wasm.__wbindgen_start(); 268 | return wasm; 269 | } 270 | 271 | function initSync(bytes) { 272 | const imports = getImports(); 273 | 274 | initMemory(imports); 275 | 276 | const module = new WebAssembly.Module(bytes); 277 | const instance = new WebAssembly.Instance(module, imports); 278 | 279 | return finalizeInit(instance, module); 280 | } 281 | 282 | async function init(input) { 283 | if (typeof input === 'undefined') { 284 | input = new URL('rust_wasm_image_ascii_bg.wasm', import.meta.url); 285 | } 286 | const imports = getImports(); 287 | 288 | if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) { 289 | input = fetch(input); 290 | } 291 | 292 | initMemory(imports); 293 | 294 | const { instance, module } = await load(await input, imports); 295 | 296 | return finalizeInit(instance, module); 297 | } 298 | 299 | export { initSync } 300 | export default init; 301 | -------------------------------------------------------------------------------- /pkg/rust_wasm_image_ascii_bg.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lecepin/rust-wasm-image-ascii/e09bf6faf09b0ae3d8bef3b427b729ec7bafe3de/pkg/rust_wasm_image_ascii_bg.wasm -------------------------------------------------------------------------------- /pkg/rust_wasm_image_ascii_bg.wasm.d.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | export const memory: WebAssembly.Memory; 4 | export function get_gray_image(a: number, b: number, c: number, d: number): void; 5 | export function get_ascii_by_image(a: number, b: number, c: number, d: number, e: number): void; 6 | export function get_ascii_by_image_tai(a: number, b: number, c: number, d: number, e: number, f: number, g: number): void; 7 | export function run(): void; 8 | export function __wbindgen_add_to_stack_pointer(a: number): number; 9 | export function __wbindgen_malloc(a: number): number; 10 | export function __wbindgen_free(a: number, b: number): void; 11 | export function __wbindgen_realloc(a: number, b: number, c: number): number; 12 | export function __wbindgen_start(): void; 13 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod tai; 2 | mod utils; 3 | 4 | use crate::tai::{ 5 | arguments::config::Config, 6 | operations::{ascii::img_to_ascii, braille::img_to_braille, onechar::img_to_onechar}, 7 | }; 8 | use crate::utils::log; 9 | use image::{ 10 | codecs::jpeg::JpegEncoder, imageops::FilterType, load_from_memory, ColorType, GenericImageView, 11 | }; 12 | use wasm_bindgen::prelude::*; 13 | 14 | #[wasm_bindgen] 15 | pub fn get_gray_image(raw: Vec, scale: u32) -> Vec { 16 | let img = load_from_memory(&raw).unwrap(); 17 | let img = img 18 | .resize( 19 | (img.width() * scale / 100) as u32, 20 | (img.height() * scale / 100) as u32, 21 | FilterType::Nearest, 22 | ) 23 | .grayscale(); 24 | let (width, height) = (img.width(), img.height()); 25 | let img_raw = img.into_bytes(); 26 | 27 | // 给编码器一块内存空间,用来写入数据 28 | let mut output_buffer = vec![]; 29 | // 创建一个编码器,jpg 的可以指定质量 30 | let mut encoder = JpegEncoder::new_with_quality(&mut output_buffer, 100); 31 | 32 | // 编码输出 33 | encoder 34 | .encode(&img_raw, width, height, ColorType::L8) 35 | .unwrap(); 36 | // 直接把内存输出就行 37 | output_buffer 38 | } 39 | 40 | #[wasm_bindgen] 41 | pub fn get_ascii_by_image(raw: Vec, scale: u32, reverse: bool) -> String { 42 | let img = load_from_memory(&raw).unwrap(); 43 | let img = img 44 | .resize( 45 | (img.width() * scale / 100) as u32, 46 | (img.height() * scale / 100) as u32, 47 | FilterType::Nearest, 48 | ) 49 | .grayscale(); 50 | let mut pallete = [' ', '.', '\\', '*', '#', '$', '@']; 51 | let mut current_line = 0; 52 | let mut result = "".to_string(); 53 | 54 | if reverse { 55 | pallete.reverse(); 56 | } 57 | 58 | for (_, line, rgba) in img.pixels() { 59 | if current_line != line { 60 | result.push('\n'); 61 | current_line = line; 62 | } 63 | 64 | let r = 0.2126 * (rgba.0[0] as f32); 65 | let g = 0.7152 * (rgba.0[0] as f32); 66 | let b = 0.0722 * (rgba.0[0] as f32); 67 | let gray = r + g + b; 68 | let caracter = ((gray / 255.0) * (pallete.len() - 1) as f32).round() as usize; 69 | 70 | result.push(pallete[caracter]); 71 | 72 | // 填充一下,有些扁 73 | if caracter < (pallete.len() - 2) { 74 | result.push('.'); 75 | } else { 76 | result.push(' '); 77 | } 78 | } 79 | 80 | result 81 | } 82 | 83 | #[wasm_bindgen] 84 | pub fn get_ascii_by_image_tai(raw: Vec, scale: u32, reverse: bool, style: &str) -> String { 85 | let mut config = Config::default(); 86 | 87 | config.image_file_u8 = raw; 88 | config.scale = scale; 89 | config.reverse = reverse; 90 | 91 | match style { 92 | "OneChar" => img_to_onechar(config, reverse), 93 | "Braille" => img_to_braille(config), 94 | "Ascii" => { 95 | let mut table = vec![ 96 | ' ', ' ', ' ', ' ', '.', '.', '.', ',', ',', ',', '\'', ';', ':', '7', '3', 'l', 97 | 'o', 'b', 'd', 'x', 'k', 'O', '0', 'K', 'X', 'N', 'W', 'M', 98 | ]; 99 | 100 | if config.reverse { 101 | table.reverse(); 102 | } 103 | 104 | img_to_ascii(config, &table) 105 | } 106 | "Numbers" => { 107 | let mut table = vec![ 108 | ' ', ' ', ' ', ' ', '0', '1', '7', '6', '9', '4', '2', '3', '8', 109 | ]; 110 | 111 | if config.reverse { 112 | table.reverse(); 113 | } 114 | 115 | img_to_ascii(config, &table) 116 | } 117 | "Blocks" => { 118 | let mut table = vec![' ', ' ', ' ', ' ', '░', '▒', '▓', '█']; 119 | 120 | if config.reverse { 121 | table.reverse(); 122 | } 123 | 124 | img_to_ascii(config, &table) 125 | } 126 | _ => "".to_string(), 127 | } 128 | } 129 | 130 | #[wasm_bindgen(start)] 131 | pub fn run() { 132 | #[cfg(feature = "console_error_panic_hook")] 133 | console_error_panic_hook::set_once(); 134 | } 135 | -------------------------------------------------------------------------------- /src/tai/README.md: -------------------------------------------------------------------------------- 1 | 2 | Modified from https://github.com/MustafaSalih1993/tai -------------------------------------------------------------------------------- /src/tai/arguments/argument_parsing.rs: -------------------------------------------------------------------------------- 1 | // use crate::tai::{Config, Style}; 2 | // const VERSION: &str = "0.0.8"; // program version 3 | 4 | // pub fn parse(args: Vec) -> Option { 5 | // // defaults 6 | // let mut config = Config::default(); 7 | // let program = args[0].clone(); 8 | // let mut opts = Options::new(); 9 | 10 | // config.original_size = true; 11 | // config.style = check_style_arg(&style); 12 | // config.onechar = onechar; 13 | 14 | // config.scale = scale; 15 | 16 | // config.image_file = matches.free[0].to_string(); 17 | // } 18 | 19 | // fn check_style_arg(arg: &str) -> Style { 20 | // match arg { 21 | // "ascii" => Style::Ascii, 22 | // "blocks" => Style::Blocks, 23 | // "braille" => Style::Braille, 24 | // "numbers" => Style::Numbers, 25 | // "onechar" => Style::OneChar, 26 | // _ => Style::default(), 27 | // } 28 | // } 29 | -------------------------------------------------------------------------------- /src/tai/arguments/config.rs: -------------------------------------------------------------------------------- 1 | // use crate::tai::arguments::argument_parsing; 2 | 3 | #[derive(Debug)] 4 | pub enum Style { 5 | Ascii, 6 | Blocks, 7 | Braille, 8 | Numbers, 9 | OneChar, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct Config { 14 | pub background: u8, 15 | pub colored: bool, 16 | pub dither: bool, 17 | pub dither_scale: u8, 18 | pub image_file: String, 19 | pub onechar: char, 20 | pub original_size: bool, 21 | pub scale: u32, 22 | pub sleep: u64, 23 | pub style: Style, 24 | pub table: Vec, 25 | pub once: bool, 26 | pub image_file_u8: Vec, 27 | pub reverse: bool, 28 | } 29 | 30 | impl Default for Style { 31 | fn default() -> Self { 32 | Self::Braille 33 | } 34 | } 35 | 36 | impl Default for Config { 37 | fn default() -> Self { 38 | Self { 39 | background: 38, 40 | colored: false, 41 | dither: false, 42 | dither_scale: 16, 43 | image_file: String::new(), 44 | onechar: '█', 45 | original_size: false, 46 | scale: 2, 47 | sleep: 100, 48 | style: Style::default(), 49 | table: vec![], 50 | once: false, 51 | image_file_u8: vec![], 52 | reverse: false, 53 | } 54 | } 55 | } 56 | 57 | // impl Config { 58 | // // Parsing arguments and return a valid config 59 | // pub fn new(args: &mut std::env::Args) -> Option { 60 | // // converting from iterator to vector. 61 | // let args: Vec = args.collect(); 62 | // argument_parsing::parse(args) 63 | // } 64 | // } 65 | -------------------------------------------------------------------------------- /src/tai/arguments/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | -------------------------------------------------------------------------------- /src/tai/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arguments; 2 | pub mod operations; 3 | pub mod utils; 4 | 5 | use arguments::config::{Config, Style}; 6 | use operations::{ascii::img_to_ascii, braille::img_to_braille, onechar::img_to_onechar}; 7 | 8 | // TODO1: need better naming for functions and variables, it's sucks because 9 | // im not a native English speaker. 10 | 11 | // fn main() { 12 | // let mut args = env::args(); 13 | 14 | // // parse args and return a valid config with defaults 15 | // let config = match Config::new(&mut args) { 16 | // Some(val) => val, 17 | // None => return, 18 | // }; 19 | 20 | // // matching the style givin to decide which operation to apply. 21 | // match config.style { 22 | // Style::OneChar => { 23 | // img_to_onechar(config); 24 | // } 25 | // Style::Braille => { 26 | // img_to_braille(config); 27 | // } 28 | // Style::Ascii => { 29 | // let table = if config.table.is_empty() { 30 | // vec![ 31 | // ' ', ' ', ' ', ' ', '.', '.', '.', ',', ',', ',', '\'', ';', ':', '<', '>', 32 | // 'l', 'o', 'b', 'd', 'x', 'k', 'O', '0', 'K', 'X', 'N', 'W', 'M', 33 | // ] 34 | // } else { 35 | // config.table.clone() 36 | // }; 37 | // img_to_ascii(config, &table); 38 | // } 39 | // Style::Numbers => { 40 | // let table = vec![ 41 | // ' ', ' ', ' ', ' ', '0', '1', '7', '6', '9', '4', '2', '3', '8', 42 | // ]; 43 | // img_to_ascii(config, &table); 44 | // } 45 | // Style::Blocks => { 46 | // let table = vec![' ', ' ', ' ', ' ', '░', '▒', '▓', '█']; 47 | // img_to_ascii(config, &table); 48 | // } 49 | // }; 50 | // } 51 | -------------------------------------------------------------------------------- /src/tai/operations/ascii.rs: -------------------------------------------------------------------------------- 1 | use crate::tai::arguments::config::Config; 2 | use crate::tai::operations::dither::Dither; 3 | use crate::tai::utils::{colorize, get_luminance, open_and_resize, resize}; 4 | use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, RgbaImage}; 5 | use std::{fs::File, thread::sleep, time::Duration}; 6 | 7 | /* STATIC IMAGES 8 | 9 | algorithm for static images work this way: 10 | - open the image buffer 11 | - loop on the image buffer by 2x2 chuncks 12 | - calculate the luminance of the 2x2 chunck and get the average luminance 13 | - based on the luminance average select a character from the ascii table 14 | - print the selected character 15 | */ 16 | 17 | /* ANIMATED IMAGES 18 | 19 | algorithm for animated images work this way: 20 | - open the image frames 21 | - convert each frame to ascii(like static image) 22 | - return array of the processed frames 23 | - loop into the array of frames and print it to stdout 24 | */ 25 | 26 | // img_to_ascii converts to ascii,numbers,blocks 27 | pub fn img_to_ascii(config: Config, table: &[char]) -> String { 28 | if config.image_file.ends_with(".gif") { 29 | print_animated_image(&config, table); 30 | } else { 31 | return print_static_image(&config, table); 32 | } 33 | 34 | "".to_string() 35 | } 36 | 37 | // this function will loop into a small chunck of pixels (2*2) and return a string containing a character 38 | fn get_char(img: &RgbaImage, config: &Config, table: &[char], x: u32, y: u32) -> String { 39 | let mut sum = 0.0; 40 | let mut count = 0.0; 41 | for iy in y..y + 2 { 42 | for ix in x..x + 2 { 43 | let [red, green, blue, _] = img.get_pixel(ix, iy).0; 44 | let lumi = get_luminance(red, green, blue); 45 | sum += lumi; 46 | count += 1.0; 47 | } 48 | } 49 | let lumi_avg = sum / count; 50 | let cha = table[(lumi_avg / 255.0 * ((table.len() - 1) as f32)) as usize]; 51 | let cha = if config.colored { 52 | let [red, green, blue, _] = img.get_pixel(x, y).0; 53 | colorize(&[red, green, blue], cha, config.background) 54 | } else { 55 | format!("{}", cha) 56 | }; 57 | cha 58 | } 59 | // process a static image 60 | fn print_static_image(config: &Config, table: &[char]) -> String { 61 | let mut img = match open_and_resize(config) { 62 | Some(img) => img, 63 | None => return "".to_string(), 64 | }; 65 | let mut result = String::new(); 66 | 67 | if config.dither { 68 | img.dither(config.dither_scale); 69 | }; 70 | 71 | for y in (0..img.height() - 2).step_by(2) { 72 | for x in (0..img.width() - 2).step_by(2) { 73 | let ch = get_char(&img, config, table, x, y); 74 | // print!("{}", ch); 75 | result.push_str(&ch); 76 | } 77 | // println!(); 78 | result.push('\n'); 79 | } 80 | // println!(); 81 | result.push('\n'); 82 | 83 | result 84 | } 85 | 86 | fn loop_the_animation(config: &Config, frames: &[String]) { 87 | for frame in frames { 88 | print!("{}", frame); 89 | sleep(Duration::from_millis(config.sleep)) 90 | } 91 | } 92 | 93 | // this function will loop into frames converted to ascii 94 | // and sleep between each frame 95 | fn print_animated_image(config: &Config, table: &[char]) { 96 | let frames = get_animated_frames(config, table); 97 | if config.once { 98 | loop_the_animation(config, &frames); 99 | } else { 100 | loop { 101 | loop_the_animation(config, &frames); 102 | } 103 | } 104 | } 105 | 106 | // this function will open an animation file, decode it, and convert 107 | // it's frames pixels into ascii, will return a vector containing a 108 | // frames converted to ascii string 109 | fn get_animated_frames(config: &Config, table: &[char]) -> Vec { 110 | let mut out_frames = Vec::new(); // this is the return of this function 111 | let file_in = match File::open(&config.image_file) { 112 | Ok(file) => file, 113 | Err(_) => return out_frames, 114 | }; 115 | let decoder = GifDecoder::new(file_in).unwrap(); 116 | let frames = decoder 117 | .into_frames() 118 | .collect_frames() 119 | .expect("error decoding gif"); 120 | // pushing this ansi code to clear the screen in the start of the frames 121 | out_frames.push("\x1B[1J".to_string()); 122 | 123 | for frame in frames { 124 | // prolly this is not efficient, need to read image crate docs more! 125 | let img = DynamicImage::ImageRgba8(frame.buffer().clone()); 126 | let mut img = resize(img, config); 127 | if config.dither { 128 | img.dither(config.dither_scale); 129 | } 130 | 131 | let translated_frame = translate_frame(&img, config, table); 132 | // this code -> \x1B[r <- will seek/save the cursor position to the start of the art 133 | // read about control characters: https://en.wikipedia.org/wiki/Control_character 134 | // so for each frame will override the old one in stdout 135 | out_frames.push(format!("\x1B[r{}", translated_frame)); 136 | } 137 | out_frames 138 | } 139 | 140 | // this function will convert the pixels into ascii chars, put it in a string and return it 141 | fn translate_frame(img: &RgbaImage, config: &Config, table: &[char]) -> String { 142 | let mut out = String::new(); 143 | for y in (0..img.height() - 2).step_by(2) { 144 | for x in (0..img.width() - 2).step_by(2) { 145 | let cha = get_char(img, config, table, x, y); 146 | out.push_str(&cha); 147 | } 148 | out.push('\n'); 149 | } 150 | out 151 | } 152 | -------------------------------------------------------------------------------- /src/tai/operations/braille.rs: -------------------------------------------------------------------------------- 1 | use crate::tai::arguments::config::Config; 2 | use crate::tai::operations::dither::Dither; 3 | use crate::tai::utils::*; 4 | use image::{codecs::gif::GifDecoder, AnimationDecoder, DynamicImage, RgbaImage}; 5 | use std::{fs::File, thread::sleep, time::Duration}; 6 | 7 | use super::otsu_threshold::OtsuThreshold; 8 | 9 | /* Image to braille: 10 | source: https://en.wikipedia.org/wiki/Braille_Patterns 11 | 12 | - open the image 13 | - loop on the image buffer 14 | - collect a chunck of pixels (2*4) 15 | - calculate the chunck above and return a binary 16 | - parse the binary and turn it to a valid number 17 | - calculate the number and print a char based on it 18 | */ 19 | 20 | pub fn img_to_braille(config: Config) -> String { 21 | // checking if its animated 22 | if config.image_file.ends_with(".gif") { 23 | print_animated_image(&config); 24 | } else { 25 | // checking the image file is valid,if so opening the image. 26 | let img = image::load_from_memory(&config.image_file_u8).unwrap(); 27 | 28 | // resizing the image and converting it to "imagebuffer", 29 | let mut img = resize(img, &config); 30 | // checking if the user wants to dither the image. 31 | if config.dither { 32 | img.dither(config.dither_scale); 33 | }; 34 | 35 | return print_static(&img, &config); 36 | } 37 | "".to_string() 38 | } 39 | 40 | // taking a threshold value, image buffer, and origin pixel coordinates(x,y); 41 | // will calculate the pixels from the origin pixel(the x,y is the pixel coordinates) and 42 | // return a block of signals for everypixel. 43 | fn get_block_signals(threshold: u8, img: &RgbaImage, coord_x: u32, coord_y: u32) -> [[u8; 2]; 4] { 44 | let mut pixel_map = [[0u8; 2]; 4]; 45 | for iy in 0..4 { 46 | for ix in 0..2 { 47 | let [red, green, blue, _] = img.get_pixel(coord_x + ix, coord_y + iy).0; 48 | pixel_map[(iy) as usize][(ix) as usize] = 49 | if get_luminance(red, green, blue) > threshold as f32 { 50 | 1 51 | } else { 52 | continue; 53 | }; 54 | } 55 | } 56 | pixel_map 57 | } 58 | 59 | // this is the core parser function it will take a blocks of pixels converted to signals 60 | // (1 = raised pixel, 0 = unraised pixel), and then convert it to a binary and then to a valid char. 61 | fn translate(map: &mut [[u8; 2]; 4]) -> char { 62 | /* our pixel block(map) look like this: 63 | --------- 64 | | 0 | 1 | 65 | | 2 | 3 | 66 | | 4 | 5 | 67 | | 6 | 7 | 68 | --------- 69 | we want to convert it to this: 70 | --------- 71 | | 0 | 3 | 72 | | 1 | 4 | 73 | | 2 | 5 | 74 | | 6 | 7 | 75 | --------- 76 | */ 77 | // making a copy to to not mess up the indexes of the original pixel matrix. 78 | let cloned = *map; 79 | map[0][1] = cloned[1][1]; 80 | map[1][0] = cloned[0][1]; 81 | map[1][1] = cloned[2][0]; 82 | map[2][0] = cloned[1][0]; 83 | // converting to string 84 | let mut tmp = String::new(); 85 | for i in map { 86 | for j in i { 87 | tmp.push_str(&j.to_string()); 88 | } 89 | } 90 | // reverse the "raised dots" to get a valid binary. (read wikipedia link above) 91 | let tmp = tmp.chars().rev().collect::(); 92 | // converting from base2 to integer 93 | let c = (isize::from_str_radix(&tmp, 2).unwrap()) as u32; 94 | std::char::from_u32(c + 0x2800).unwrap() 95 | } 96 | 97 | // process a static image 98 | fn print_static(img: &RgbaImage, config: &Config) -> String { 99 | let best_threshold = DynamicImage::ImageRgba8(img.clone()) 100 | .into_luma8() 101 | .get_otsu_value(); 102 | let mut result = String::new(); 103 | 104 | for y in (0..img.height() - 4).step_by(4) { 105 | for x in (0..img.width() - 2).step_by(2) { 106 | let mut map = get_block_signals(best_threshold, img, x, y); 107 | let ch = translate(&mut map); 108 | if config.colored { 109 | let [r, g, b, _] = img.get_pixel(x, y).0; 110 | print!("{}", colorize(&[r, g, b], ch, config.background)); 111 | } else { 112 | // print!("{}", ch); 113 | result.push(ch); 114 | } 115 | } 116 | result.push('\n'); 117 | } 118 | 119 | result 120 | } 121 | 122 | fn loop_the_animation(config: &Config, frames: &[String]) { 123 | for frame in frames { 124 | print!("{}", frame); 125 | sleep(Duration::from_millis(config.sleep)) 126 | } 127 | } 128 | 129 | // process animated image 130 | fn print_animated_image(config: &Config) { 131 | let frames = get_animated_frames(config); 132 | if config.once { 133 | loop_the_animation(config, &frames); 134 | } else { 135 | loop { 136 | loop_the_animation(config, &frames); 137 | } 138 | } 139 | } 140 | 141 | fn get_animated_frames(config: &Config) -> Vec { 142 | let mut out_frames = Vec::new(); 143 | let file_in = match File::open(&config.image_file) { 144 | Ok(file) => file, 145 | Err(_) => return out_frames, 146 | }; 147 | let decoder = GifDecoder::new(file_in).unwrap(); 148 | let frames = decoder 149 | .into_frames() 150 | .collect_frames() 151 | .expect("error decoding gif"); 152 | // pushing this ansi code to clear the screen in the start of the frames 153 | out_frames.push("\x1B[1J".to_string()); 154 | 155 | for frame in frames { 156 | // prolly this is not efficient, need to read image crate docs more! 157 | let img = DynamicImage::ImageRgba8(frame.buffer().clone()); 158 | let mut img = resize(img, config); 159 | if config.dither { 160 | img.dither(config.dither_scale); 161 | } 162 | let translated_frame = translate_frame(&img, config); 163 | // this ansi code will seek/save the cursor position to the start of the art 164 | // so for each frame will override the old one in stdout 165 | out_frames.push(format!("\x1B[r{}", translated_frame)); 166 | } 167 | out_frames 168 | } 169 | 170 | fn translate_frame(img: &RgbaImage, config: &Config) -> String { 171 | let mut out = String::new(); 172 | let best_threshold = DynamicImage::ImageRgba8(img.clone()) 173 | .into_luma8() 174 | .get_otsu_value(); 175 | 176 | for y in (0..img.height() - 4).step_by(4) { 177 | for x in (0..img.width() - 2).step_by(2) { 178 | let mut map = get_block_signals(best_threshold, img, x, y); 179 | let ch = translate(&mut map); 180 | 181 | if config.colored { 182 | let [r, g, b, _] = img.get_pixel(x, y).0; 183 | out.push_str(&colorize(&[r, g, b], ch, config.background)); 184 | } else { 185 | out.push(ch); 186 | } 187 | } 188 | out.push('\n'); 189 | } 190 | out 191 | } 192 | -------------------------------------------------------------------------------- /src/tai/operations/dither.rs: -------------------------------------------------------------------------------- 1 | use image::RgbaImage; 2 | // This is error diff algorithm check the source below. 3 | // source : https://en.wikipedia.org/wiki/Floyd-Steinberg_dithering 4 | 5 | pub trait Dither { 6 | fn dither(&mut self, scale: u8); 7 | fn calculate_pixel(&mut self, pixel_coord: (u32, u32), err_pixel: [f32; 3], cal: f32); 8 | } 9 | 10 | impl Dither for RgbaImage { 11 | fn dither(&mut self, dither_scale: u8) { 12 | let scale = dither_scale as f32; 13 | 14 | for y in 0..self.height() - 1 { 15 | for x in 1..self.width() - 1 { 16 | let old_rgb: [u8; 4] = self.get_pixel(x, y).0; 17 | let new_rgb: [u8; 4] = find_closest_color(old_rgb, scale); 18 | 19 | self.get_pixel_mut(x, y).0[..3].clone_from_slice(&new_rgb[..3]); 20 | 21 | let mut pixel = self.get_pixel_mut(x, y).0; 22 | pixel[0] = new_rgb[0]; 23 | pixel[1] = new_rgb[1]; 24 | pixel[2] = new_rgb[2]; 25 | 26 | let err_r: f32 = old_rgb[0] as f32 - new_rgb[0] as f32; 27 | let err_g: f32 = old_rgb[1] as f32 - new_rgb[1] as f32; 28 | let err_b: f32 = old_rgb[2] as f32 - new_rgb[2] as f32; 29 | let err_pixel = [err_r, err_g, err_b]; 30 | 31 | self.calculate_pixel((x + 1, y), err_pixel, 7.0); 32 | self.calculate_pixel((x - 1, y + 1), err_pixel, 3.0); 33 | self.calculate_pixel((x, y + 1), err_pixel, 5.0); 34 | self.calculate_pixel((x + 1, y + 1), err_pixel, 1.0); 35 | } 36 | } 37 | } 38 | 39 | // this helper function will calculate the the neighbor pixel and add value from the error pixel as refrenced in wikipedia. 40 | fn calculate_pixel( 41 | &mut self, 42 | origin_pixel: (u32, u32), // coordinate (x,y) 43 | err_pixel: [f32; 3], // error pixel [R, G, B] 44 | val: f32, // value will be added to the calculation 45 | ) { 46 | // R 47 | self.get_pixel_mut(origin_pixel.0, origin_pixel.1).0[0] = (self 48 | .get_pixel(origin_pixel.0, origin_pixel.1) 49 | .0[0] as f32 50 | + err_pixel[0] * val / 16.0) 51 | as u8; 52 | // G 53 | self.get_pixel_mut(origin_pixel.0, origin_pixel.1).0[1] = (self 54 | .get_pixel(origin_pixel.0, origin_pixel.1) 55 | .0[1] as f32 56 | + err_pixel[1] * val / 16.0) 57 | as u8; 58 | // B 59 | self.get_pixel_mut(origin_pixel.0, origin_pixel.1).0[2] = (self 60 | .get_pixel(origin_pixel.0, origin_pixel.1) 61 | .0[2] as f32 62 | + err_pixel[2] * val / 16.0) 63 | as u8; 64 | } 65 | } 66 | 67 | // this helper function to calculate the rgb values for the floyed dither algorithm. 68 | fn find_closest_color(pixel: [u8; 4], factor: f32) -> [u8; 4] { 69 | [ 70 | ((factor * pixel[0] as f32 / 255.0).ceil() * (255.0 / factor)) as u8, 71 | ((factor * pixel[1] as f32 / 255.0).ceil() * (255.0 / factor)) as u8, 72 | ((factor * pixel[2] as f32 / 255.0).ceil() * (255.0 / factor)) as u8, 73 | pixel[3], 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /src/tai/operations/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ascii; 2 | pub mod braille; 3 | pub mod dither; 4 | pub mod onechar; 5 | pub mod otsu_threshold; 6 | -------------------------------------------------------------------------------- /src/tai/operations/onechar.rs: -------------------------------------------------------------------------------- 1 | use crate::tai::arguments::config::Config; 2 | use crate::tai::operations::otsu_threshold::OtsuThreshold; 3 | use crate::tai::utils::get_luma_buffer; 4 | use image::Luma; 5 | 6 | // will make the image to ONLY black and white 7 | // by converting the the "grays" to black or white based on the scale. 8 | // source: https://en.wikipedia.org/wiki/Thresholding_(image_processing) 9 | // below we are using Otsu's thresholding which is automatically finds 10 | // the best threshold value 11 | // https://en.wikipedia.org/wiki/Otsu%27s_method 12 | pub fn img_to_onechar(config: Config, reverse: bool) -> String { 13 | let mut img: image::ImageBuffer, Vec> = match get_luma_buffer(&config) { 14 | Some(img) => img, 15 | None => return "".to_string(), 16 | }; 17 | let mut result = "".to_string(); 18 | 19 | img.threshold(); 20 | for y in 0..img.height() { 21 | for x in 0..img.width() { 22 | let pixel = img.get_pixel(x, y); 23 | if *pixel == Luma([255]) { 24 | result.push(if !reverse { config.onechar } else { ' ' }); 25 | } else { 26 | result.push(if reverse { config.onechar } else { ' ' }); 27 | } 28 | } 29 | result.push('\n'); 30 | } 31 | result.push('\n'); 32 | 33 | result 34 | } 35 | -------------------------------------------------------------------------------- /src/tai/operations/otsu_threshold.rs: -------------------------------------------------------------------------------- 1 | use image::GrayImage; 2 | 3 | pub trait OtsuThreshold { 4 | fn get_histogram(&mut self) -> [usize; 256]; 5 | fn get_otsu_value(&mut self) -> u8; 6 | fn threshold(&mut self); 7 | } 8 | 9 | impl OtsuThreshold for GrayImage { 10 | fn get_histogram(&mut self) -> [usize; 256] { 11 | let mut out = [0; 256]; 12 | self.iter().for_each(|p| { 13 | out[*p as usize] += 1; 14 | }); 15 | out 16 | } 17 | fn get_otsu_value(&mut self) -> u8 { 18 | let img_histogram: [usize; 256] = self.get_histogram(); 19 | let total_weight = self.width() as f64 * self.height() as f64; 20 | let mut bg_sum = 0.0; 21 | let mut bg_weight = 0.0; 22 | let mut max_variance = 0.0; 23 | let mut best_threshold = 0; 24 | let sum_intensity: f64 = img_histogram 25 | .iter() 26 | .enumerate() 27 | .fold(0f64, |acu, (t, c)| acu + (t * c) as f64); 28 | 29 | for (threshold, count) in img_histogram.iter().enumerate() { 30 | let fg_weight = total_weight - bg_weight; 31 | if fg_weight > 0.0 && bg_weight > 0.0 { 32 | let fg_mean = (sum_intensity - bg_sum) / fg_weight; 33 | let val = (bg_weight * fg_weight * ((bg_sum / bg_weight) - fg_mean)).powi(2); 34 | if val >= max_variance { 35 | best_threshold = threshold as u8; 36 | max_variance = val; 37 | } 38 | } 39 | bg_weight += *count as f64; 40 | bg_sum += (threshold * count) as f64; 41 | } 42 | 43 | best_threshold 44 | } 45 | fn threshold(&mut self) { 46 | let best_threshold = self.get_otsu_value(); 47 | self.iter_mut() 48 | .for_each(|p| *p = if *p < best_threshold { 0 } else { 255 }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/tai/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::tai::arguments::config::Config; 2 | use image::{DynamicImage, GenericImageView, GrayImage, RgbaImage}; 3 | 4 | // luminance formula credits: https://stackoverflow.com/a/596243 5 | // >>> Luminance = 0.2126*R + 0.7152*G + 0.0722*B <<< 6 | // calculate RGB values to get luminance of the pixel 7 | pub fn get_luminance(r: u8, g: u8, b: u8) -> f32 { 8 | let r = 0.2126 * (r as f32); 9 | let g = 0.7152 * (g as f32); 10 | let b = 0.0722 * (b as f32); 11 | r + g + b 12 | } 13 | 14 | // colorize a character by surrounding it with true term colors 15 | pub fn colorize(rgb: &[u8; 3], ch: char, bg_fg: u8) -> String { 16 | let prefix = format!("\x1B[{};2;{};{};{}m", bg_fg, rgb[0], rgb[1], rgb[2]); 17 | let postfix = "\x1B[0m"; 18 | format!("{}{}{}", prefix, ch, postfix) 19 | } 20 | 21 | //rescale the image and convert to image buffer 22 | pub fn open_and_resize(config: &Config) -> Option { 23 | let img = if let Ok(image) = image::load_from_memory(&config.image_file_u8) { 24 | image 25 | } else { 26 | eprintln!("Image path is not correct, OR image format is not supported!\n try -h | --help"); 27 | return None; 28 | }; 29 | let width = match config.original_size { 30 | true => img.width(), 31 | false => ((img.width() / config.scale) / 2) as u32, 32 | }; 33 | let height = match config.original_size { 34 | true => img.height(), 35 | false => ((img.height() / config.scale) / 4) as u32, 36 | }; 37 | let img = img.resize_exact(width, height, image::imageops::FilterType::Lanczos3); 38 | let img = if config.colored { 39 | img.into_rgba8() 40 | } else { 41 | img.grayscale().into_rgba8() 42 | }; 43 | Some(img) 44 | } 45 | 46 | pub fn resize(img: DynamicImage, config: &Config) -> RgbaImage { 47 | let (width, height) = match config.original_size { 48 | false => { 49 | let width = ((img.width() / config.scale) / 2) as u32; 50 | let height = ((img.height() / config.scale) / 4) as u32; 51 | (width, height) 52 | } 53 | true => (img.width(), img.height()), 54 | }; 55 | img.resize(width, height, image::imageops::FilterType::Lanczos3) 56 | .to_rgba8() 57 | } 58 | 59 | // this will open the image path, 60 | // and resize the image and turn it into image buffer; 61 | pub fn get_luma_buffer(config: &Config) -> Option { 62 | let img = if let Ok(image) = image::load_from_memory(&config.image_file_u8) { 63 | image 64 | } else { 65 | eprintln!("Image path is not correct, OR image format is not supported!\n try -h | --help"); 66 | return None; 67 | }; 68 | let width = match config.original_size { 69 | true => img.width(), 70 | false => ((img.width() / config.scale) / 2) as u32, 71 | }; 72 | let height = match config.original_size { 73 | true => img.height(), 74 | false => ((img.height() / config.scale) / 4) as u32, 75 | }; 76 | let img = img.resize_exact(width, height, image::imageops::FilterType::Lanczos3); 77 | let img = img.to_luma8(); 78 | Some(img) 79 | } 80 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn log(data: String) { 2 | web_sys::console::log_3( 3 | &"%cin Rust".to_string().into(), 4 | &"background:#faad14;border-radius: 0.5em;color: white;font-weight: bold;padding: 2px 0.5em" 5 | .to_string() 6 | .into(), 7 | &data.into(), 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Rust-image-ascii 6 | 7 | 8 | 9 |
10 |
11 | WASM 文件加载中…
12 |
13 |

Rust-image-ascii

14 | 15 | 19 | 29 |
30 | 31 | 32 |
33 | 34 |

 42 | 
 43 |     
110 |     
111 |     
112 | 灰度算法对比 113 | 简单版本 114 | Tai 版本 115 |
116 | 117 | 118 | --------------------------------------------------------------------------------