├── .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 | 
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 | 
70 |
71 | ### Tai 版本
72 |
73 | 看到一个支持 ASCII 种类挺多的 Rust 项目 https://github.com/MustafaSalih1993/tai ,于是将这个项目的 IO 部分进行了修改,适配 WASM 进行了编译处理。
74 |
75 | 
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 |
89 | 原图
90 |
91 | |
92 |
93 | 最大值法
94 |
95 | |
96 |
97 | 平均值法
98 |
99 | |
100 |
101 |
102 |
103 | 加权平均值法:0.2126 * r + 0.7152 * g + 0.0722 * b
104 |
105 | |
106 |
107 | 加权平均值法:0.299 * r + 0.587 * g + 0.114 * b
108 |
109 | |
110 |
111 |
112 | 加权平均值法: Math.sqrt( (0.299 * r) ** 2 + (0.587 * g) ** 2 +
113 | (0.114 * b) ** 2 )
114 |
115 |
116 | |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
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 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
53 |
54 |
102 |
103 |
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 | 
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 | 
--------------------------------------------------------------------------------
/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 |
116 |
117 |
118 |
--------------------------------------------------------------------------------