From 4b26f69b40cfb30c171b7105bee84c731897fd16 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Mon, 10 Jun 2024 00:26:27 +0200 Subject: [PATCH] Added the abiltiy to have a (currently purposely undocumented) `?format=square` parameter to image previews --- ...49a24a29fad1ce6d6041ee8a0aa91c6ff375.json} | 4 +- server/Cargo.toml | 10 ++- server/main-api/src/details.rs | 2 + server/main-api/src/maps/mod.rs | 60 ++++++++++---- server/main-api/src/maps/overlay_map.rs | 77 ++++++++++-------- server/main-api/src/maps/static/pin.png | Bin 0 -> 3640 bytes server/main-api/src/maps/static/pin.svg | 6 ++ server/main-api/src/maps/static/pin.webp | Bin 706 -> 0 bytes server/main-api/src/utils.rs | 15 +++- 9 files changed, 119 insertions(+), 55 deletions(-) rename server/.sqlx/{query-ac0c4c7f53ab4b463efbd65489063e8eab53f8270085dc4b9f04daa600649585.json => query-e2c20ca8f6c7924335e607304f6849a24a29fad1ce6d6041ee8a0aa91c6ff375.json} (78%) create mode 100644 server/main-api/src/maps/static/pin.png create mode 100644 server/main-api/src/maps/static/pin.svg delete mode 100644 server/main-api/src/maps/static/pin.webp diff --git a/server/.sqlx/query-ac0c4c7f53ab4b463efbd65489063e8eab53f8270085dc4b9f04daa600649585.json b/server/.sqlx/query-e2c20ca8f6c7924335e607304f6849a24a29fad1ce6d6041ee8a0aa91c6ff375.json similarity index 78% rename from server/.sqlx/query-ac0c4c7f53ab4b463efbd65489063e8eab53f8270085dc4b9f04daa600649585.json rename to server/.sqlx/query-e2c20ca8f6c7924335e607304f6849a24a29fad1ce6d6041ee8a0aa91c6ff375.json index 701a926c3..10230d26d 100644 --- a/server/.sqlx/query-ac0c4c7f53ab4b463efbd65489063e8eab53f8270085dc4b9f04daa600649585.json +++ b/server/.sqlx/query-e2c20ca8f6c7924335e607304f6849a24a29fad1ce6d6041ee8a0aa91c6ff375.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT key, visible_id, type\n FROM aliases\n WHERE alias = $1 OR key = $1\n LIMIT 1", + "query": "\n SELECT key, visible_id, type\n FROM aliases\n WHERE alias = $1 AND key <> alias\n LIMIT 1", "describe": { "columns": [ { @@ -30,5 +30,5 @@ false ] }, - "hash": "ac0c4c7f53ab4b463efbd65489063e8eab53f8270085dc4b9f04daa600649585" + "hash": "e2c20ca8f6c7924335e607304f6849a24a29fad1ce6d6041ee8a0aa91c6ff375" } diff --git a/server/Cargo.toml b/server/Cargo.toml index 8f5c0209f..7453fe732 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -6,7 +6,13 @@ resolver = "2" strip = true lto = "thin" -# Enable max optimizations for dependencies, but not for our code +# Enable max optimizations for some dependencies, but not for our code # nessesary to get acceptable performance out of the image processing code -[profile.dev.package."*"] +[profile.dev.package.image] +opt-level = 3 + +[profile.dev.package.imageproc] +opt-level = 3 + +[profile.dev.package.ab_glyph] opt-level = 3 diff --git a/server/main-api/src/details.rs b/server/main-api/src/details.rs index 913f75cea..2de81adfd 100644 --- a/server/main-api/src/details.rs +++ b/server/main-api/src/details.rs @@ -1,5 +1,6 @@ use actix_web::{get, web, HttpResponse}; use log::error; +use sqlx::Error::RowNotFound; use sqlx::PgPool; use crate::models::LocationKeyAlias; @@ -76,6 +77,7 @@ async fn get_alias_and_redirect(conn: &PgPool, query: &str) -> Option<(String, S }; Some((d[0].key.clone(), redirect_url)) } + Err(RowNotFound) => None, Err(e) => { error!("Error requesting alias for {query}: {e:?}"); None diff --git a/server/main-api/src/maps/mod.rs b/server/main-api/src/maps/mod.rs index 9a1682ab0..07d7b2d4f 100644 --- a/server/main-api/src/maps/mod.rs +++ b/server/main-api/src/maps/mod.rs @@ -2,9 +2,10 @@ use std::io::Cursor; use actix_web::http::header::LOCATION; use actix_web::{get, web, HttpResponse}; -use image::Rgba; +use image::{ImageBuffer, Rgba}; use log::{debug, error, warn}; +use serde::Deserialize; use sqlx::PgPool; use tokio::time::Instant; use unicode_truncate::UnicodeTruncateStr; @@ -58,32 +59,47 @@ async fn get_localised_data( } } -async fn construct_image_from_data(data: Location) -> Option> { +async fn construct_image_from_data(data: Location, format: PreviewFormat) -> Option> { let start_time = Instant::now(); - let mut img = image::RgbaImage::new(1200, 630); + let mut img = match format { + PreviewFormat::OpenGraph => image::RgbaImage::new(1200, 630), + PreviewFormat::Square => image::RgbaImage::new(1200, 1200), + }; // add the map if !OverlayMapTask::with(&data).draw_onto(&mut img).await { return None; } debug!("map draw {:?}", start_time.elapsed()); + draw_pin(&mut img); draw_bottom(&data, &mut img); debug!("overlay finish {:?}", start_time.elapsed()); Some(wrap_image_in_response(&img)) } +/// add the location pin image to the center +fn draw_pin(img: &mut ImageBuffer, Vec>) { + let pin = image::load_from_memory(include_bytes!("static/pin.png")).unwrap(); + image::imageops::overlay( + img, + &pin, + (img.width() as i64) / 2 - i64::from(pin.width()) / 2, + ((img.height() as i64) - 125) / 2 - i64::from(pin.height()), + ); +} + fn wrap_image_in_response(img: &image::RgbaImage) -> Vec { let mut w = Cursor::new(Vec::new()); img.write_to(&mut w, image::ImageFormat::Png).unwrap(); w.into_inner() } - +const WHITE_PIXEL: Rgba = Rgba([255, 255, 255, 255]); fn draw_bottom(data: &Location, img: &mut image::RgbaImage) { // draw background white - for x in 0..1200 { - for y in 630 - 125..630 { - img.put_pixel(x, y, Rgba([255, 255, 255, 255])); + for x in 0..img.width() { + for y in img.height() - 125..img.height() { + img.put_pixel(x, y, WHITE_PIXEL); } } // add our logo so the bottom @@ -92,7 +108,7 @@ fn draw_bottom(data: &Location, img: &mut image::RgbaImage) { img, &logo, 15, - 630 - (125 / 2) - (i64::from(logo.height()) / 2) + 9, + img.height() as i64 - (125 / 2) - (i64::from(logo.height()) / 2) + 9, ); let name = if data.name.chars().count() >= 45 { format!("{}...", data.name.unicode_truncate(45).0) @@ -122,7 +138,7 @@ async fn get_possible_redirect_url(conn: &PgPool, query: &str) -> Option r#" SELECT key, visible_id, type FROM aliases - WHERE alias = $1 OR key = $1 + WHERE alias = $1 AND key <> alias LIMIT 1"#, query ) @@ -137,10 +153,27 @@ async fn get_possible_redirect_url(conn: &PgPool, query: &str) -> Option } } +#[derive(Deserialize, Default, Debug, Copy, Clone)] +#[serde(rename_all = "snake_case")] +enum PreviewFormat { + #[default] + OpenGraph, + Square, +} + +#[derive(Deserialize, Default, Debug)] +#[serde(rename_all = "snake_case")] +#[serde(default)] +struct QueryArgs { + #[serde(flatten)] + lang: utils::LangQueryArgs, + format: PreviewFormat, +} + #[get("/{id}")] pub async fn maps_handler( params: web::Path, - web::Query(args): web::Query, + web::Query(args): web::Query, data: web::Data, ) -> HttpResponse { let start_time = Instant::now(); @@ -152,13 +185,13 @@ pub async fn maps_handler( res.insert_header((LOCATION, redirect_url)); return res.finish(); } - let data = match get_localised_data(&data.db, &id, args.should_use_english()).await { + let data = match get_localised_data(&data.db, &id, args.lang.should_use_english()).await { Ok(data) => data, Err(e) => { - return e.into(); + return e; } }; - let img = construct_image_from_data(data) + let img = construct_image_from_data(data, args.format) .await .unwrap_or_else(load_default_image); @@ -169,5 +202,4 @@ pub async fn maps_handler( HttpResponse::Ok() .content_type("image/png") .body(img) - .into() } diff --git a/server/main-api/src/maps/overlay_map.rs b/server/main-api/src/maps/overlay_map.rs index 85859a0b8..deaeef25b 100644 --- a/server/main-api/src/maps/overlay_map.rs +++ b/server/main-api/src/maps/overlay_map.rs @@ -1,5 +1,6 @@ use futures::{stream::FuturesUnordered, StreamExt}; use log::warn; +use std::ops::Range; use crate::maps::fetch_tile::FetchTileTask; use crate::models::Location; @@ -10,6 +11,8 @@ pub struct OverlayMapTask { pub z: u32, } +const POSSIBLE_INDEX_RANGE: Range = 0..7; + impl OverlayMapTask { pub fn with(entry: &Location) -> Self { let zoom = match entry.r#type.as_str() { @@ -27,26 +30,28 @@ impl OverlayMapTask { } pub async fn draw_onto(&self, img: &mut image::RgbaImage) -> bool { // coordinate system is centered around the center of the image - // around this center there is a 5*3 grid of tiles - // ----------------------------------------- - // | -2/ 1 | -1/ 1 | 0/ 1 | 1/ 1 | 2/ 1 | - // | -2/ 0 | -1/ 0 | x | 1/ 0 | 2/ 0 | - // | -2/-1 | -1/-1 | 0/-1 | 1/-1 | 2/-1 | - // ----------------------------------------- - // we can now filter for "is on the 1200*630 image" and append them to a work queue + // around this center there is a 5*5 grid of tiles + // ------------------------------- + // | -1 / 1 | 0 / 1 | 1 / 1 | + // | -1 / 0 | x | 1 / 0 | + // | -1 / -1 | 0 / -1 | 1 / -1 | + // ------------------------------- + // we can now filter for "is on the image" and append them to a work queue let x_pixels = (512.0 * (self.x - self.x.floor())) as u32; let y_pixels = (512.0 * (self.y - self.y.floor())) as u32; - let (x_img_coords, y_img_coords) = center_to_top_left_coordinates(x_pixels, y_pixels); - // the queue can have 3...4*2 entries, because 630-125=505=> max.2 Tiles and 1200=> max 4 tiles + let (x_img_coords, y_img_coords) = center_to_top_left_coordinates(img, x_pixels, y_pixels); + // is_in_range is quite cheap => we over-check this one to cope with different image formats let mut work_queue = FuturesUnordered::new(); - for x_index in 0..5 { - for y_index in 0..3 { - if is_in_range(x_img_coords, y_img_coords, x_index, y_index) { + for index_x in POSSIBLE_INDEX_RANGE.clone() { + for index_y in POSSIBLE_INDEX_RANGE.clone() { + if is_on_image(img, (x_img_coords, y_img_coords), (index_x, index_y)) { + let offset_x = (index_x as i32) - ((POSSIBLE_INDEX_RANGE.end / 2) as i32); + let offset_y = (index_y as i32) - ((POSSIBLE_INDEX_RANGE.end / 2) as i32); work_queue.push( FetchTileTask::from(self) - .offset_by((x_index as i32) - 2, (y_index as i32) - 1) - .with_index(x_index, y_index) + .offset_by(offset_x, offset_y) + .with_index(index_x, index_y) .fulfill(), ); } @@ -65,15 +70,6 @@ impl OverlayMapTask { } } } - - // add the location pin image to the center - let pin = image::load_from_memory(include_bytes!("static/pin.webp")).unwrap(); - image::imageops::overlay( - img, - &pin, - 1200 / 2 - i64::from(pin.width()) / 2, - (630 - 125) / 2 - i64::from(pin.height()), - ); true } } @@ -86,19 +82,28 @@ fn lat_lon_z_to_xyz(lat_deg: f64, lon_deg: f64, zoom: u32) -> (f64, f64, u32) { (xtile, ytile, zoom) } -fn center_to_top_left_coordinates(x_pixels: u32, y_pixels: u32) -> (u32, u32) { - // the center coordniates are usefull for orienting ourselves in one tile, - // but for drawing them, top left is better - let y_to_img_border = 512 + y_pixels; - let y_img_coords = y_to_img_border - (630 - 125) / 2; - let x_to_img_border = 512 * 2 + x_pixels; - let x_img_coords = x_to_img_border - 1200 / 2; +/// The center coordinates are usefully for orienting ourselves in one tile +/// For drawing them, top left is better +fn center_to_top_left_coordinates( + img: &image::RgbaImage, + x_pixels: u32, + y_pixels: u32, +) -> (u32, u32) { + let y_to_img_border = 512 * (POSSIBLE_INDEX_RANGE.end / 2) + y_pixels; + let y_img_coords = y_to_img_border - (img.height() - 125) / 2; + let x_to_img_border = 512 * (POSSIBLE_INDEX_RANGE.end / 2) + x_pixels; + let x_img_coords = x_to_img_border - img.width() / 2; (x_img_coords, y_img_coords) } -fn is_in_range(x_pixels: u32, y_pixels: u32, x_index: u32, y_index: u32) -> bool { - let x_in_range = x_pixels <= (x_index + 1) * 512 && x_pixels + 1200 >= x_index * 512; - let y_in_range = y_pixels <= (y_index + 1) * 512 && y_pixels + (630 - 125) >= y_index * 512; +fn is_on_image( + img: &image::RgbaImage, + (x_pixel, y_pixel): (u32, u32), + (x_index, y_index): (u32, u32), +) -> bool { + let x_in_range = (x_index + 1) * 512 >= x_pixel && x_index * 512 <= x_pixel + img.width(); + let y_in_range = + (y_index + 1) * 512 >= y_pixel && y_index * 512 <= y_pixel + (img.height() - 125); x_in_range && y_in_range } @@ -137,12 +142,16 @@ mod tests { expected_x: (u32, u32), expected_y: (u32, u32), ) { + let img = image::RgbaImage::new(600, 200); for x in 0..10 { for y in 0..10 { let (x_min, x_max) = expected_x; let (y_min, y_max) = expected_y; let expected_result = x <= x_max && x >= x_min && y <= y_max && y >= y_min; - assert_eq!(is_in_range(x_pixels, y_pixels, x, y), expected_result); + assert_eq!( + is_on_image(&img, (x_pixels, y_pixels), (x, y)), + expected_result + ); } } } diff --git a/server/main-api/src/maps/static/pin.png b/server/main-api/src/maps/static/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..3ab173f971a9e5e6cde5538cdf57458eacaee13e GIT binary patch literal 3640 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 KMNUMnLSTX`DClee literal 0 HcmV?d00001 diff --git a/server/main-api/src/maps/static/pin.svg b/server/main-api/src/maps/static/pin.svg new file mode 100644 index 000000000..f5011743e --- /dev/null +++ b/server/main-api/src/maps/static/pin.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/server/main-api/src/maps/static/pin.webp b/server/main-api/src/maps/static/pin.webp deleted file mode 100644 index b6e399aa33f7a4fdc4edb25dcbfedc762871a3a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 706 zcmV;z0zLgwNk&Gx0ssJ4MM6+kP&iDj0ssIn7{CY+_vE0BBuA>*`vBni!+Io_!*+cj z;kc0`MT$N6p8q~F-@*byVNYQv2NEiqPJ8aTnMH<36w^a14*)WxJj`Kg6LO@FuyV;TA_$}$-nx|A`gf{kc zt%Efg=IdWZq=S%(zQr+b`udd-smKUJDu(0Hc%Ewd>RS-fK`@wu@7NrW%kw8aLfNO!UAoxoj5pN;|@F0`o^7?&Ob z8xlco8E{uR4^+GG!Y(+jNnp3LrJD|Jt}t#bFgO7$GgSAGY;nfM#0X&}rc6k!bi(50CT>}j098hTj3l77jNI!se zNh|@rNn=NWhM=-;;Ju^=z`mS-v@O6_Nt!(gEDC4?uJswc26}2^il%_y^4|jXfa3cBTAXt=UBDBC-UFtJUH)F+hJvTh09(_R1IIpuY2XO365yYb o{fYmMl8!ETE{`Uscb*qY<$d6$QaXKtqa&%&E1oFTfApUN00>`P00000 diff --git a/server/main-api/src/utils.rs b/server/main-api/src/utils.rs index 98219e562..c442d8fdf 100644 --- a/server/main-api/src/utils.rs +++ b/server/main-api/src/utils.rs @@ -1,12 +1,21 @@ use serde::Deserialize; -#[derive(Deserialize)] +#[derive(Deserialize, Copy, Clone, Debug, Eq, PartialEq, Default)] +#[serde(rename_all = "snake_case")] +enum LanguageOptions { + #[default] + De, + En, +} + +#[derive(Deserialize, Copy, Clone, Debug, Eq, PartialEq, Default)] +#[serde(default)] pub struct LangQueryArgs { - lang: Option, + lang: LanguageOptions, } impl LangQueryArgs { pub fn should_use_english(&self) -> bool { - self.lang.as_ref().map_or(false, |c| c == "en") + self.lang == LanguageOptions::En } }