Compare commits
No commits in common. "16d4306763cd48d57abb2e247040a082600973f1" and "5fc6ce7636eb355daa84ec9d5121b0c15b898fd6" have entirely different histories.
16d4306763
...
5fc6ce7636
3
.gitignore
vendored
|
|
@ -2,6 +2,3 @@ node_modules/
|
|||
target/
|
||||
pkg/
|
||||
.DS_Store
|
||||
dist/
|
||||
*.iml
|
||||
.idea
|
||||
18
README.md
|
|
@ -1,17 +1,7 @@
|
|||
**Spiegel** image filter project
|
||||
**Spiegel** image filter project (Work In Progress)
|
||||
|
||||
- rust/webassembly for image processing
|
||||
- vanilla javascript for the user interface
|
||||
- no server (just static pages)
|
||||
|
||||
Live demo at https://shautvast.github.io/spiegel-demo/
|
||||
|
||||
* Sorry for the poor performance, especially on larger images.
|
||||
* It uses the median image filter from image.rs. That in itself can be pretty slow.
|
||||
(Although the implementation uses a _sliding window histogram_, which I think is pretty nifty).
|
||||
* And on top of that, I created this custom flood fill algorithm,
|
||||
that instead of filling it with with a single color, looks up a sample from the
|
||||
Spiegel book (that has a corresponding color) and takes the pixels from that.
|
||||
- rust
|
||||
- wasm
|
||||
|
||||
sample output
|
||||

|
||||

|
||||
|
|
|
|||
BIN
dist/11dc9380193954aac8c4.module.wasm
vendored
Normal file
BIN
dist/630027c604733df47374e4f5adb1b7c5.jpg
vendored
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
dist/66d531fb4651c57454ae.module.wasm
vendored
Normal file
1
dist/796.bundle.js
vendored
Normal file
BIN
dist/93be161a4640fe100e05af248fa4c52e.png
vendored
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
dist/95cc0985b7bd6242de9b9665261616a3.jpg
vendored
Normal file
|
After Width: | Height: | Size: 192 KiB |
523
dist/bundle.js
vendored
Normal file
42
dist/crate_pkg_photon_rs_js.bundle.js
vendored
Normal file
42
dist/crate_pkg_spiegel_client_js.bundle.js
vendored
Normal file
BIN
dist/df3fbe72e3ad368ae5bf.module.wasm
vendored
Normal file
BIN
dist/ea872833a419652a23b47d50be7fe0ef.jpg
vendored
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
dist/f49f7893ad0a3ad10176.module.wasm
vendored
Normal file
29
dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>Spiegel</title>
|
||||
<script defer src="bundle.js"></script></head>
|
||||
|
||||
<body>
|
||||
<div class="default">
|
||||
<div class="main">
|
||||
<label>
|
||||
<h2>Blur factor</h2< /label>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" id="slider" value="0" min="0" max="100" class="slider" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="main_content">
|
||||
<div class="content" id="images">
|
||||
<div id="image_container"></div>
|
||||
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
use image::{GenericImage, GenericImageView, Pixel, Rgba, RgbaImage};
|
||||
use photon_rs::PhotonImage;
|
||||
use std::collections::LinkedList;
|
||||
use image::imageops::FilterType;
|
||||
|
||||
mod samples;
|
||||
|
||||
use samples::log;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
static BLACK: Rgba<u8> = Rgba([0, 0, 0, 0]);
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn spiegel(photon_image: PhotonImage, median_kernelsize: u32, preview: bool) -> PhotonImage {
|
||||
samples::read_jpeg_bytes();
|
||||
|
||||
let width = photon_image.get_width();
|
||||
let height = photon_image.get_height();
|
||||
|
||||
let raw_pixels = photon_image.get_raw_pixels().to_vec();
|
||||
let i1 = RgbaImage::from_vec(width, height, raw_pixels).unwrap();
|
||||
|
||||
let i2 = if preview {
|
||||
image::imageops::resize(&i1, u32::min(500, width >> 1), u32::min(500, height >> 1), FilterType::Nearest)
|
||||
} else {
|
||||
image::imageops::resize(&i1, u32::min(500, width), u32::min(500, height), FilterType::Nearest)
|
||||
};
|
||||
let mut i3 = imageproc::filter::median_filter(&i2, median_kernelsize, median_kernelsize);
|
||||
let i4 = if !preview {
|
||||
apply_samples_to_image(&mut i3)
|
||||
} else {
|
||||
i3
|
||||
};
|
||||
let i5 = image::imageops::resize(&i4, width, height, FilterType::Nearest);
|
||||
PhotonImage::new(i5.into_raw(), width, height)
|
||||
}
|
||||
|
||||
fn apply_samples_to_image(src: &mut RgbaImage) -> RgbaImage {
|
||||
let mut out = RgbaImage::new(src.width(), src.height());
|
||||
unsafe {
|
||||
for y in 0..src.height() {
|
||||
log(&format!("{}", y));
|
||||
for x in 0..src.width() {
|
||||
if out.unsafe_get_pixel(x, y) == BLACK {
|
||||
let pixel = src.unsafe_get_pixel(x, y);
|
||||
if pixel != BLACK {
|
||||
let sample = samples::get_closest_color(pixel[0], pixel[1], pixel[2])
|
||||
.image
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
fill(src, sample, &mut out, pixel, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn fill(
|
||||
src: &mut RgbaImage,
|
||||
sample: &RgbaImage,
|
||||
dest: &mut RgbaImage,
|
||||
color: Rgba<u8>,
|
||||
px: u32,
|
||||
py: u32,
|
||||
) {
|
||||
unsafe {
|
||||
let height = sample.height();
|
||||
let width = sample.width();
|
||||
let mut points = LinkedList::new();
|
||||
if is_same(src.unsafe_get_pixel(px, py), color) {
|
||||
points.push_back(Coord(px, py));
|
||||
}
|
||||
|
||||
while !points.is_empty() {
|
||||
if let Some(coord) = points.pop_back() {
|
||||
let orig_pixel = src.unsafe_get_pixel(coord.0, coord.1);
|
||||
let x = coord.0;
|
||||
let y = coord.1;
|
||||
if src.unsafe_get_pixel(x, y) != BLACK {
|
||||
if is_same(orig_pixel, color) {
|
||||
let mut xx = x;
|
||||
let mut yy = y;
|
||||
while xx >= width {
|
||||
xx -= width;
|
||||
}
|
||||
while yy >= height {
|
||||
yy -= height;
|
||||
}
|
||||
dest.unsafe_put_pixel(x, y, sample.unsafe_get_pixel(xx, yy));
|
||||
src.unsafe_put_pixel(x, y, BLACK);
|
||||
if x > 1 {
|
||||
points.push_front(Coord(x - 1, y));
|
||||
}
|
||||
if y > 1 {
|
||||
points.push_front(Coord(x, y - 1));
|
||||
}
|
||||
if x < src.width() - 1 {
|
||||
points.push_front(Coord(x + 1, y));
|
||||
}
|
||||
if y < src.height() - 1 {
|
||||
points.push_front(Coord(x, y + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("break");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_same(p1: Rgba<u8>, p2: Rgba<u8>) -> bool {
|
||||
let p1 = p1.channels();
|
||||
let p2 = p2.channels();
|
||||
i16::abs(p1[0] as i16 - p2[0] as i16) < 4
|
||||
&& i16::abs(p1[1] as i16 - p2[1] as i16) < 4
|
||||
&& i16::abs(p1[2] as i16 - p2[2] as i16) < 4
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Coord(u32, u32);
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
use image::{load_from_memory_with_format, RgbaImage};
|
||||
use std::sync::OnceLock;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use include_dir::{include_dir, Dir, DirEntry};
|
||||
|
||||
static mut SAMPLES: OnceLock<Vec<ColorSample>> = OnceLock::new();
|
||||
static SAMPLES_DIR: Dir = include_dir!("src/samples");
|
||||
|
||||
pub fn read_jpeg_bytes() {
|
||||
unsafe {
|
||||
SAMPLES.get_or_init(|| {
|
||||
log("reading image samples");
|
||||
read_color_samples().unwrap()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_closest_color<'a>(r: u8, g: u8, b: u8) -> &'static ColorSample {
|
||||
unsafe {
|
||||
let color_samples = SAMPLES.get_mut().unwrap();
|
||||
|
||||
let mut closest = None;
|
||||
let mut min_diff: f32 = 4294967295.0; //0xFFFFFFFF
|
||||
for sample in color_samples {
|
||||
let diff = get_distance(sample.r, sample.g, sample.b, r, g, b);
|
||||
if diff < min_diff {
|
||||
closest = Some(sample);
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
let closest = closest.unwrap();
|
||||
if closest.image.is_none() {
|
||||
let sample_image =
|
||||
load_from_memory_with_format(closest.raw_bytes, image::ImageFormat::Jpeg)
|
||||
.unwrap()
|
||||
.to_rgba8();
|
||||
closest.image = Some(sample_image);
|
||||
}
|
||||
closest
|
||||
}
|
||||
}
|
||||
|
||||
/// returns squared euclidian color distance
|
||||
/// as if colors were points in 3d space
|
||||
fn get_distance(r1: u8, g1: u8, b1: u8, r2: u8, g2: u8, b2: u8) -> f32 {
|
||||
let red_dif = r1 as f32 - r2 as f32;
|
||||
let green_dif = g1 as f32 - g2 as f32;
|
||||
let blue_dif = b1 as f32 - b2 as f32;
|
||||
return red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif;
|
||||
}
|
||||
|
||||
/// read all sample jpegs into memory
|
||||
pub fn read_color_samples() -> anyhow::Result<Vec<ColorSample>> {
|
||||
let mut color_samples: Vec<ColorSample> = Vec::new();
|
||||
|
||||
for entry in SAMPLES_DIR.entries() {
|
||||
if let DirEntry::File(f) = entry {
|
||||
let filename = entry.path().file_name().unwrap().to_str().unwrap();
|
||||
let hex_r = &filename[0..2];
|
||||
let hex_g = &filename[2..4];
|
||||
let hex_b = &filename[4..6];
|
||||
|
||||
color_samples.push(ColorSample {
|
||||
filename: filename.into(),
|
||||
r: u8::from_str_radix(&hex_r, 16)?,
|
||||
g: u8::from_str_radix(&hex_g, 16)?,
|
||||
b: u8::from_str_radix(&hex_b, 16)?,
|
||||
raw_bytes: f.contents(),
|
||||
image: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
log("Done reading image samples");
|
||||
Ok(color_samples)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ColorSample {
|
||||
pub filename: String,
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub raw_bytes: &'static [u8],
|
||||
pub image: Option<RgbaImage>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(js_namespace = console)]
|
||||
pub fn log(s: &str);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
fn log_u32(a: u32);
|
||||
|
||||
#[wasm_bindgen(js_namespace = console, js_name = log)]
|
||||
fn log_many(a: &str, b: &str);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 26 KiB |
54
index.html
|
|
@ -2,61 +2,29 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>Spiegel</title>
|
||||
<link rel="stylesheet" href="/css/styles.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Klaer Lightende Spiegel der Verfkonst</h1>
|
||||
<h4>Paints your image in the colors of the
|
||||
<a class="link" target="_blank"
|
||||
href="https://nl.wikipedia.org/wiki/Klaer_Lightende_Spiegel_der_Verfkonst">handpainted
|
||||
book by A. Boogert</a>,
|
||||
<img id="spieghel" alt="spieghel" src="" class="hide">
|
||||
the 1692 version of the Pantone color book.</h4>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<div class="main">
|
||||
<label>
|
||||
<h4>Upload or drag&drop a (jpeg) picture and move the slider, for a quick preview</h4>
|
||||
<h4>Then press Apply</h4>
|
||||
|
||||
<h2>Blur factor</h2>
|
||||
<h4 id="msg">(blurring can take up to 15 seconds, please be patient)</h4>
|
||||
</label>
|
||||
<div><input id="upload" type="file" accept="image/jpeg"></div>
|
||||
<div id="slidecontainer" class="hide">
|
||||
<label for="slider">Brush stroke size</label><input type="range" id="slider" value="0" min="0" max="100" class="slider"/>
|
||||
<button id="apply">Apply</button>
|
||||
<div class="slidecontainer">
|
||||
<input type="range" id="slider" value="0" min="0" max="100" class="slider" />
|
||||
</div>
|
||||
</label>
|
||||
<div class="main-content">
|
||||
<div class="main_content">
|
||||
<div id="progress"></div>
|
||||
<div class="content" id="images">
|
||||
<div id="image-container" class="border" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
|
||||
<div id="image_container"></div>
|
||||
|
||||
<canvas id="canvas" style="visibility: hidden;"></canvas>
|
||||
<canvas id="canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4>(Painting can take long, be patient</h4>
|
||||
<h4>all image processing is done in your browser).</h4>
|
||||
<h5>And ignore all the warnings! It will be all right.</h5>
|
||||
<img id="browserwarning" src="">
|
||||
</div>
|
||||
<script>
|
||||
function allowDrop(event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function drop(event) {
|
||||
event.preventDefault();
|
||||
let dt = event.dataTransfer;
|
||||
|
||||
console.log(dt.files);
|
||||
|
||||
document.querySelector("#source-image").src = URL.createObjectURL(dt.files[0]);
|
||||
document.querySelector('#image-container').setAttribute("class", "no-border");
|
||||
document.querySelector("#slidecontainer").setAttribute("class", "slidecontainer");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
127
package-lock.json
generated
|
|
@ -9,7 +9,7 @@
|
|||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"http-proxy-middleware": "^2.0.6"
|
||||
"@silvia-odwyer/photon": "^0.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"create-rust-webpack": ".bin/create-rust-webpack.js"
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"css-loader": "^6.10.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"js-loader": "^0.1.1",
|
||||
"style-loader": "^3.3.4",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
|
|
@ -153,11 +154,16 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@silvia-odwyer/photon": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@silvia-odwyer/photon/-/photon-0.1.0.tgz",
|
||||
"integrity": "sha512-7dPuzcHvnI7A/6C29xTDxzEhKIQJ0KsjwlPXKKqxs+PJNbqK1gKQj/GMQUbzX3Ec+MB4JBKElsUS920vNL7MAw=="
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
|
|
@ -176,7 +182,7 @@
|
|||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
|
|
@ -221,7 +227,7 @@
|
|||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
|
|
@ -233,7 +239,7 @@
|
|||
"version": "4.17.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
|
||||
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
|
|
@ -261,12 +267,13 @@
|
|||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/http-proxy": {
|
||||
"version": "1.17.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz",
|
||||
"integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
|
|
@ -281,7 +288,7 @@
|
|||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
|
|
@ -293,6 +300,7 @@
|
|||
"version": "20.11.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
|
|
@ -310,13 +318,13 @@
|
|||
"version": "6.9.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz",
|
||||
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"devOptional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/retry": {
|
||||
"version": "0.12.2",
|
||||
|
|
@ -328,7 +336,7 @@
|
|||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
|
|
@ -347,7 +355,7 @@
|
|||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
|
||||
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/mime": "*",
|
||||
|
|
@ -971,6 +979,7 @@
|
|||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
|
|
@ -1340,6 +1349,19 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/connect": {
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmjs.org/connect/-/connect-0.5.8.tgz",
|
||||
"integrity": "sha512-N/eXBtibhXlXU/M3mYaIaTAg9OPx0560l48h+kxbuY7FQ4+6ByrJLTZe6q//Kw7wBTy1d4eQGW3fqBDMLoK6/g==",
|
||||
"deprecated": "connect 0.x series is deprecated",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"qs": ">= 0.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/connect-history-api-fallback": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
|
||||
|
|
@ -1903,7 +1925,8 @@
|
|||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
|
|
@ -2118,6 +2141,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eyes": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
|
||||
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "> 0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
|
@ -2182,6 +2214,7 @@
|
|||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
|
|
@ -2233,6 +2266,7 @@
|
|||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
|
|
@ -2758,6 +2792,7 @@
|
|||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
|
|
@ -2771,6 +2806,7 @@
|
|||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
|
|
@ -2794,6 +2830,7 @@
|
|||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"picomatch": "^2.3.1"
|
||||
|
|
@ -2991,6 +3028,7 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -3008,6 +3046,7 @@
|
|||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
|
|
@ -3049,6 +3088,7 @@
|
|||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
|
|
@ -3090,6 +3130,7 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
|
||||
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -3217,6 +3258,24 @@
|
|||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/js-loader": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-loader/-/js-loader-0.1.1.tgz",
|
||||
"integrity": "sha512-3QBYVT2BkoyzBVYjPBiJ1w0Bpclhww+SH9OdObgODb1KxjtYyjb4MH3kwGqZVuGBIQK2lmPJkFL2ohouMQzQUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"connect": "=0.5.8",
|
||||
"temp": "=0.2.0",
|
||||
"uglify-js": "=0.0.4",
|
||||
"vows": "=0.5.6"
|
||||
},
|
||||
"bin": {
|
||||
"jsloader": "bin/jsloader"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/json-parse-even-better-errors": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
|
||||
|
|
@ -4097,6 +4156,7 @@
|
|||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
|
@ -4501,7 +4561,8 @@
|
|||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
|
|
@ -5433,6 +5494,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/temp": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/temp/-/temp-0.2.0.tgz",
|
||||
"integrity": "sha512-YDBo1L4ZLi2+8u7rcY8asynPBwrXuNQ97sdemNwv/v8OJLCj/duAiuGYMtFIMUNKZG4zO1O5oQD7u+Y0c2O6ZA==",
|
||||
"dev": true,
|
||||
"engines": [
|
||||
"node >=0.1.90"
|
||||
]
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
|
||||
|
|
@ -5547,6 +5617,7 @@
|
|||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
|
@ -5582,10 +5653,23 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/uglify-js": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-0.0.4.tgz",
|
||||
"integrity": "sha512-cGiAIcNECVjqL3wNiWiBctBJr94T6WS+q0c/H/I+pGsAQ/+8ZKbJxw9GJiaL+ZYbGDU275Shz+h2Gq4S2Oc35Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"uglifyjs": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/union-value": {
|
||||
"version": "1.0.1",
|
||||
|
|
@ -5788,6 +5872,21 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vows": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/vows/-/vows-0.5.6.tgz",
|
||||
"integrity": "sha512-IGLEetjz/nPKHYPlA3LTGMxj6nbfsyZ3CwFTR6hzE6zRLLY7yu5ya4v9ymY7cit9wnogDv6Cj12nU/myCeDwgQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eyes": ">=0.1.6"
|
||||
},
|
||||
"bin": {
|
||||
"vows": "bin/vows"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.2.6"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"author": "Sander Hautvast",
|
||||
"name": "Klaer Lightende Spiegel der Verfkonst",
|
||||
"version": "1.0.0",
|
||||
"repository": "https://github.com/shautvast/spiegel-web",
|
||||
"author": "Silvia O'Dwyer <silviaodwyerdev@gmail.com>",
|
||||
"name": "photon-wasm",
|
||||
"version": "0.1.0",
|
||||
"repository": "https://github.com/silvia-odwyer/photon",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server",
|
||||
|
|
|
|||
1353
server/Cargo.lock
generated
Normal file
20
server/Cargo.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[package]
|
||||
edition = "2021"
|
||||
name = "spiegel-server"
|
||||
version = "0.1.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.7" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.36", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
image = "0.24.1"
|
||||
anyhow = "1.0"
|
||||
include_dir = "0.7.3"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
tower = { version = "0.4", features = ["util"] }
|
||||
tower-http = { version = "0.5.0", features = ["fs", "trace"] }
|
||||
74
server/src/lib.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
use std::sync::OnceLock;
|
||||
|
||||
use include_dir::{include_dir, Dir, DirEntry};
|
||||
|
||||
static SAMPLES: OnceLock<Vec<ColorSample>> = OnceLock::new();
|
||||
static SAMPLES_DIR: Dir = include_dir!("src/samples");
|
||||
|
||||
pub fn init() {
|
||||
SAMPLES.get_or_init(|| {
|
||||
println!("reading image samples");
|
||||
read_color_samples().unwrap()
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get_closest_color<'a>(color: &String) -> &'a ColorSample {
|
||||
let color_samples = SAMPLES.get().unwrap();
|
||||
let mut closest = None;
|
||||
let mut min_diff: f32 = 4294967295.0; //0xFFFFFFFF
|
||||
for sample in color_samples {
|
||||
let diff = get_distance(sample.r, sample.g, sample.b, color);
|
||||
if diff < min_diff {
|
||||
closest = Some(sample);
|
||||
min_diff = diff;
|
||||
}
|
||||
}
|
||||
|
||||
closest.unwrap()
|
||||
}
|
||||
|
||||
/// returns squared euclidian color distance
|
||||
/// as if colors were points in 3d space
|
||||
fn get_distance(r1: u8, g1: u8, b1: u8, rgb: &String) -> f32 {
|
||||
let r2 = u8::from_str_radix(&rgb[0..2], 16).unwrap();
|
||||
let g2 = u8::from_str_radix(&rgb[2..4], 16).unwrap();
|
||||
let b2 = u8::from_str_radix(&rgb[4..6], 16).unwrap();
|
||||
let red_dif = r1 as f32 - r2 as f32;
|
||||
let green_dif = g1 as f32 - g2 as f32;
|
||||
let blue_dif = b1 as f32 - b2 as f32;
|
||||
return red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif;
|
||||
}
|
||||
|
||||
/// read all sample jpegs into memory
|
||||
pub fn read_color_samples() -> anyhow::Result<Vec<ColorSample>> {
|
||||
let mut color_samples: Vec<ColorSample> = Vec::new();
|
||||
|
||||
for entry in SAMPLES_DIR.entries() {
|
||||
if let DirEntry::File(f) = entry {
|
||||
let sample_image = f.contents();
|
||||
|
||||
let filename = entry.path().file_name().unwrap().to_str().unwrap();
|
||||
let hex_r = &filename[0..2];
|
||||
let hex_g = &filename[2..4];
|
||||
let hex_b = &filename[4..6];
|
||||
color_samples.push(ColorSample {
|
||||
filename: filename.into(),
|
||||
r: u8::from_str_radix(&hex_r, 16)?,
|
||||
g: u8::from_str_radix(&hex_g, 16)?,
|
||||
b: u8::from_str_radix(&hex_b, 16)?,
|
||||
image: sample_image,
|
||||
});
|
||||
}
|
||||
}
|
||||
println!("Done reading image samples");
|
||||
Ok(color_samples)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ColorSample {
|
||||
pub filename: String,
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
pub image: &'static [u8],
|
||||
}
|
||||
32
server/src/main.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use axum::extract::Path;
|
||||
use axum::http::{header, StatusCode};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{routing::get, Router};
|
||||
use spiegel_server::{get_closest_color, init};
|
||||
|
||||
/// serves images from memory
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
init();
|
||||
let app = Router::new().route("/color/:rgb_hex", get(fetch_nearest_color));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_nearest_color(
|
||||
Path(rgb_hex): Path<String>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if rgb_hex.len() != 6 {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"input should be color hex, eg AA11CC".into(),
|
||||
));
|
||||
}
|
||||
let closest = get_closest_color(&rgb_hex);
|
||||
|
||||
let headers = [(header::CONTENT_TYPE, "image/jpeg")];
|
||||
Ok((headers, closest.image))
|
||||
}
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |