refactor: serve full-res images via endpoint instead of base64
- Lightbox now fetches /image?path=<encoded> from review server - Removes full-res base64 embedding from HTML (much smaller page) - Thumbnails still embedded as base64 for fast initial load - Added urlencode/urldecode helpers - Removed make_fullres_data_uri function
This commit is contained in:
@@ -33,6 +33,9 @@ pub fn launch_review(groups: &[DuplicateGroup], entries: &[ImageEntry]) {
|
|||||||
let header = Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap();
|
let header = Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap();
|
||||||
let _ = req.respond(Response::from_string(&html).with_header(header));
|
let _ = req.respond(Response::from_string(&html).with_header(header));
|
||||||
}
|
}
|
||||||
|
(Method::Get, u) if u.starts_with("/image?") => {
|
||||||
|
handle_serve_image(req, u);
|
||||||
|
}
|
||||||
(Method::Post, "/delete") => {
|
(Method::Post, "/delete") => {
|
||||||
handle_delete(req);
|
handle_delete(req);
|
||||||
}
|
}
|
||||||
@@ -86,6 +89,71 @@ fn handle_delete(mut req: tiny_http::Request) -> usize {
|
|||||||
deleted
|
deleted
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_serve_image(req: tiny_http::Request, url: &str) {
|
||||||
|
let path = url.strip_prefix("/image?path=").unwrap_or("");
|
||||||
|
let path = urldecode(path);
|
||||||
|
let file_path = std::path::Path::new(&path);
|
||||||
|
|
||||||
|
if !file_path.exists() {
|
||||||
|
let _ = req.respond(Response::from_string("not found").with_status_code(404));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = match fs::read(file_path) {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => {
|
||||||
|
let _ = req.respond(Response::from_string("read error").with_status_code(500));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let content_type = match file_path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()).as_deref() {
|
||||||
|
Some("png") => "image/png",
|
||||||
|
Some("gif") => "image/gif",
|
||||||
|
Some("webp") => "image/webp",
|
||||||
|
Some("bmp") => "image/bmp",
|
||||||
|
_ => "image/jpeg",
|
||||||
|
};
|
||||||
|
|
||||||
|
let header = Header::from_bytes("Content-Type", content_type).unwrap();
|
||||||
|
let _ = req.respond(Response::from_data(data).with_header(header));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urldecode(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len());
|
||||||
|
let mut chars = s.bytes();
|
||||||
|
while let Some(b) = chars.next() {
|
||||||
|
if b == b'%' {
|
||||||
|
let h = chars.next().unwrap_or(b'0');
|
||||||
|
let l = chars.next().unwrap_or(b'0');
|
||||||
|
let hex = [h, l];
|
||||||
|
if let Ok(val) = u8::from_str_radix(&String::from_utf8_lossy(&hex), 16) {
|
||||||
|
result.push(val as char);
|
||||||
|
}
|
||||||
|
} else if b == b'+' {
|
||||||
|
result.push(' ');
|
||||||
|
} else {
|
||||||
|
result.push(b as char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn urlencode(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len() * 3);
|
||||||
|
for b in s.bytes() {
|
||||||
|
match b {
|
||||||
|
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
|
||||||
|
result.push(b as char);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
result.push_str(&format!("%{:02X}", b));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_ignore(
|
fn handle_ignore(
|
||||||
mut req: tiny_http::Request,
|
mut req: tiny_http::Request,
|
||||||
groups: &[DuplicateGroup],
|
groups: &[DuplicateGroup],
|
||||||
@@ -153,26 +221,6 @@ fn make_thumbnail_data_uri(path: &std::path::Path) -> String {
|
|||||||
format!("data:image/jpeg;base64,{b64}")
|
format!("data:image/jpeg;base64,{b64}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_fullres_data_uri(path: &std::path::Path) -> String {
|
|
||||||
let data = match fs::read(path) {
|
|
||||||
Ok(d) => d,
|
|
||||||
Err(_) => return String::from("data:image/png;base64,"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let img = match image::ImageReader::new(std::io::Cursor::new(&data))
|
|
||||||
.with_guessed_format()
|
|
||||||
.ok()
|
|
||||||
.and_then(|r| r.decode().ok())
|
|
||||||
{
|
|
||||||
Some(i) => i,
|
|
||||||
None => return String::from("data:image/png;base64,"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut buf = std::io::Cursor::new(Vec::new());
|
|
||||||
img.write_to(&mut buf, image::ImageFormat::Jpeg).unwrap_or(());
|
|
||||||
let b64 = base64::engine::general_purpose::STANDARD.encode(buf.into_inner());
|
|
||||||
format!("data:image/jpeg;base64,{b64}")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_review_html(groups: &[DuplicateGroup]) -> String {
|
fn build_review_html(groups: &[DuplicateGroup]) -> String {
|
||||||
let mut html = String::from(r#"<!DOCTYPE html>
|
let mut html = String::from(r#"<!DOCTYPE html>
|
||||||
@@ -254,15 +302,15 @@ h1 { text-align: center; margin-bottom: 20px; color: #e94560; }
|
|||||||
|
|
||||||
for (pidx, path) in group.paths.iter().enumerate() {
|
for (pidx, path) in group.paths.iter().enumerate() {
|
||||||
let data_uri = make_thumbnail_data_uri(path);
|
let data_uri = make_thumbnail_data_uri(path);
|
||||||
let full_uri = make_fullres_data_uri(path);
|
|
||||||
let display_path = path.display();
|
let display_path = path.display();
|
||||||
|
let encoded_path = urlencode(&display_path.to_string());
|
||||||
let size = fs::metadata(path)
|
let size = fs::metadata(path)
|
||||||
.map(|m| format_size(m.len()))
|
.map(|m| format_size(m.len()))
|
||||||
.unwrap_or_else(|_| "?".to_string());
|
.unwrap_or_else(|_| "?".to_string());
|
||||||
|
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
r#"<div class="image-card" id="card-{idx}-{pidx}">
|
r#"<div class="image-card" id="card-{idx}-{pidx}">
|
||||||
<img src="{data_uri}" data-full="{full_uri}" alt="{display_path}" title="Click for full size" onclick="openLightbox(this.dataset.full, '{display_path}')">
|
<img src="{data_uri}" data-fullpath="/image?path={encoded_path}" alt="{display_path}" title="Click for full size" onclick="openLightbox(this.dataset.fullpath, '{display_path}')">
|
||||||
<div class="path">{display_path}</div>
|
<div class="path">{display_path}</div>
|
||||||
<div class="size">{size}</div>
|
<div class="size">{size}</div>
|
||||||
<label><input type="checkbox" class="del-check" data-group="{idx}" value="{display_path}"> Delete this</label>
|
<label><input type="checkbox" class="del-check" data-group="{idx}" value="{display_path}"> Delete this</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user