Skip to content

Commit

Permalink
Added the abiltiy to have a (currently purposely undocumented) `?form…
Browse files Browse the repository at this point in the history
…at=square` parameter to image previews
  • Loading branch information
CommanderStorm committed Jun 10, 2024
1 parent 8453961 commit 4b26f69
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 55 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions server/main-api/src/details.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use actix_web::{get, web, HttpResponse};
use log::error;
use sqlx::Error::RowNotFound;
use sqlx::PgPool;

use crate::models::LocationKeyAlias;
Expand Down Expand Up @@ -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
Expand Down
60 changes: 46 additions & 14 deletions server/main-api/src/maps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,32 +59,47 @@ async fn get_localised_data(
}
}

async fn construct_image_from_data(data: Location) -> Option<Vec<u8>> {
async fn construct_image_from_data(data: Location, format: PreviewFormat) -> Option<Vec<u8>> {
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<Rgba<u8>, Vec<u8>>) {
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<u8> {
let mut w = Cursor::new(Vec::new());
img.write_to(&mut w, image::ImageFormat::Png).unwrap();
w.into_inner()
}

const WHITE_PIXEL: Rgba<u8> = 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
Expand All @@ -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)
Expand Down Expand Up @@ -122,7 +138,7 @@ async fn get_possible_redirect_url(conn: &PgPool, query: &str) -> Option<String>
r#"
SELECT key, visible_id, type
FROM aliases
WHERE alias = $1 OR key = $1
WHERE alias = $1 AND key <> alias
LIMIT 1"#,
query
)
Expand All @@ -137,10 +153,27 @@ async fn get_possible_redirect_url(conn: &PgPool, query: &str) -> Option<String>
}
}

#[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<String>,
web::Query(args): web::Query<utils::LangQueryArgs>,
web::Query(args): web::Query<QueryArgs>,
data: web::Data<crate::AppData>,
) -> HttpResponse {
let start_time = Instant::now();
Expand All @@ -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);

Expand All @@ -169,5 +202,4 @@ pub async fn maps_handler(
HttpResponse::Ok()
.content_type("image/png")
.body(img)
.into()
}
77 changes: 43 additions & 34 deletions server/main-api/src/maps/overlay_map.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,6 +11,8 @@ pub struct OverlayMapTask {
pub z: u32,
}

const POSSIBLE_INDEX_RANGE: Range<u32> = 0..7;

impl OverlayMapTask {
pub fn with(entry: &Location) -> Self {
let zoom = match entry.r#type.as_str() {
Expand All @@ -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(),
);
}
Expand All @@ -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
}
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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
);
}
}
}
Expand Down
Binary file added server/main-api/src/maps/static/pin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions server/main-api/src/maps/static/pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed server/main-api/src/maps/static/pin.webp
Binary file not shown.
15 changes: 12 additions & 3 deletions server/main-api/src/utils.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
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
}
}

0 comments on commit 4b26f69

Please sign in to comment.