Compare commits

..

No commits in common. "16d4306763cd48d57abb2e247040a082600973f1" and "5fc6ce7636eb355daa84ec9d5121b0c15b898fd6" have entirely different histories.

1794 changed files with 3495 additions and 1229 deletions

3
.gitignore vendored
View file

@ -2,6 +2,3 @@ node_modules/
target/ target/
pkg/ pkg/
.DS_Store .DS_Store
dist/
*.iml
.idea

View file

@ -1,17 +1,7 @@
**Spiegel** image filter project **Spiegel** image filter project (Work In Progress)
- rust/webassembly for image processing - rust
- vanilla javascript for the user interface - wasm
- 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.
sample output sample output
![sample](https://github.com/shautvast/spiegel-web/blob/main/unsplash.png) ![sample](https://github.com/shautvast/spiegel-web/blob/main/webclient/output.jpg)

BIN
dist/11dc9380193954aac8c4.module.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

BIN
dist/66d531fb4651c57454ae.module.wasm vendored Normal file

Binary file not shown.

1
dist/796.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

523
dist/bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

42
dist/crate_pkg_photon_rs_js.bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
dist/df3fbe72e3ad368ae5bf.module.wasm vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
dist/f49f7893ad0a3ad10176.module.wasm vendored Normal file

Binary file not shown.

29
dist/index.html vendored Normal file
View 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>

View file

@ -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);

View file

@ -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);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -2,61 +2,29 @@
<html> <html>
<head> <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> <title>Spiegel</title>
<link rel="stylesheet" href="/css/styles.css">
</head> </head>
<body> <body>
<h1>Klaer Lightende Spiegel der Verfkonst</h1> <div class="main">
<h4>Paints your image in the colors of the <label>
<a class="link" target="_blank" <h2>Blur factor</h2>
href="https://nl.wikipedia.org/wiki/Klaer_Lightende_Spiegel_der_Verfkonst">handpainted <h4 id="msg">(blurring can take up to 15 seconds, please be patient)</h4>
book by A. Boogert</a>, </label>
<img id="spieghel" alt="spieghel" src="" class="hide"> <div class="slidecontainer">
the 1692 version of the Pantone color book.</h4> <input type="range" id="slider" value="0" min="0" max="100" class="slider" />
</div>
</label>
<div class="main_content">
<div id="progress"></div>
<div class="content" id="images">
<div id="image_container"></div>
<div class="main"> <canvas id="canvas"></canvas>
</div>
<label>
<h4>Upload or drag&drop a (jpeg) picture and move the slider, for a quick preview</h4>
<h4>Then press Apply</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>
</label>
<div class="main-content">
<div class="content" id="images">
<div id="image-container" class="border" ondrop="drop(event)" ondragover="allowDrop(event)"></div>
<canvas id="canvas" style="visibility: hidden;"></canvas>
</div> </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> </body>
</html> </html>

127
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "0.1.0", "version": "0.1.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"http-proxy-middleware": "^2.0.6" "@silvia-odwyer/photon": "^0.1.0"
}, },
"bin": { "bin": {
"create-rust-webpack": ".bin/create-rust-webpack.js" "create-rust-webpack": ".bin/create-rust-webpack.js"
@ -20,6 +20,7 @@
"css-loader": "^6.10.0", "css-loader": "^6.10.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.0", "html-webpack-plugin": "^5.6.0",
"js-loader": "^0.1.1",
"style-loader": "^3.3.4", "style-loader": "^3.3.4",
"webpack": "^5.91.0", "webpack": "^5.91.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
@ -153,11 +154,16 @@
"node": ">=14" "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": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/connect": "*", "@types/connect": "*",
"@types/node": "*" "@types/node": "*"
@ -176,7 +182,7 @@
"version": "3.4.38", "version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@ -221,7 +227,7 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.33", "@types/express-serve-static-core": "^4.17.33",
@ -233,7 +239,7 @@
"version": "4.17.43", "version": "4.17.43",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==", "integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"@types/qs": "*", "@types/qs": "*",
@ -261,12 +267,13 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
"devOptional": true "dev": true
}, },
"node_modules/@types/http-proxy": { "node_modules/@types/http-proxy": {
"version": "1.17.14", "version": "1.17.14",
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz",
"integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==",
"dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@ -281,7 +288,7 @@
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"devOptional": true "dev": true
}, },
"node_modules/@types/minimatch": { "node_modules/@types/minimatch": {
"version": "5.1.2", "version": "5.1.2",
@ -293,6 +300,7 @@
"version": "20.11.30", "version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -310,13 +318,13 @@
"version": "6.9.14", "version": "6.9.14",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz",
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==", "integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==",
"devOptional": true "dev": true
}, },
"node_modules/@types/range-parser": { "node_modules/@types/range-parser": {
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
"devOptional": true "dev": true
}, },
"node_modules/@types/retry": { "node_modules/@types/retry": {
"version": "0.12.2", "version": "0.12.2",
@ -328,7 +336,7 @@
"version": "0.17.4", "version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/mime": "^1", "@types/mime": "^1",
"@types/node": "*" "@types/node": "*"
@ -347,7 +355,7 @@
"version": "1.15.5", "version": "1.15.5",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
"devOptional": true, "dev": true,
"dependencies": { "dependencies": {
"@types/http-errors": "*", "@types/http-errors": "*",
"@types/mime": "*", "@types/mime": "*",
@ -971,6 +979,7 @@
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
}, },
@ -1340,6 +1349,19 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "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": { "node_modules/connect-history-api-fallback": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz",
@ -1903,7 +1925,8 @@
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "4.0.7", "version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"dev": true
}, },
"node_modules/events": { "node_modules/events": {
"version": "3.3.0", "version": "3.3.0",
@ -2118,6 +2141,15 @@
"node": ">=0.10.0" "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": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@ -2182,6 +2214,7 @@
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -2233,6 +2266,7 @@
"version": "1.15.6", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -2758,6 +2792,7 @@
"version": "1.18.1", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"dev": true,
"dependencies": { "dependencies": {
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0", "follow-redirects": "^1.0.0",
@ -2771,6 +2806,7 @@
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
"dev": true,
"dependencies": { "dependencies": {
"@types/http-proxy": "^1.17.8", "@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
@ -2794,6 +2830,7 @@
"version": "4.0.5", "version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@ -2991,6 +3028,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -3008,6 +3046,7 @@
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"dependencies": { "dependencies": {
"is-extglob": "^2.1.1" "is-extglob": "^2.1.1"
}, },
@ -3049,6 +3088,7 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -3090,6 +3130,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
"dev": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@ -3217,6 +3258,24 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "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": { "node_modules/json-parse-even-better-errors": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "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", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@ -4501,7 +4561,8 @@
"node_modules/requires-port": { "node_modules/requires-port": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "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": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
@ -5433,6 +5494,15 @@
"node": ">=6" "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": { "node_modules/terser": {
"version": "5.29.2", "version": "5.29.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
@ -5547,6 +5617,7 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@ -5582,10 +5653,23 @@
"node": ">= 0.6" "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": { "node_modules/undici-types": {
"version": "5.26.5", "version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "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": { "node_modules/union-value": {
"version": "1.0.1", "version": "1.0.1",
@ -5788,6 +5872,21 @@
"node": ">= 0.8" "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": { "node_modules/watchpack": {
"version": "1.7.5", "version": "1.7.5",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz",

View file

@ -1,8 +1,8 @@
{ {
"author": "Sander Hautvast", "author": "Silvia O'Dwyer <silviaodwyerdev@gmail.com>",
"name": "Klaer Lightende Spiegel der Verfkonst", "name": "photon-wasm",
"version": "1.0.0", "version": "0.1.0",
"repository": "https://github.com/shautvast/spiegel-web", "repository": "https://github.com/silvia-odwyer/photon",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"start": "webpack-dev-server", "start": "webpack-dev-server",

1353
server/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

20
server/Cargo.toml Normal file
View 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
View 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
View 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))
}

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 63 KiB

View file

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View file

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Some files were not shown because too many files have changed in this diff Show more