diff --git a/Cargo.lock b/Cargo.lock index 7ffb7265b..7e03c0724 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -442,6 +442,21 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -1174,6 +1189,27 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "freetype-rs" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59c337e64822dd56a3a83ed75a662a470736bdb3a9fabfb588dff276b94a4e0" +dependencies = [ + "bitflags 1.3.2", + "freetype-sys", + "libc", +] + +[[package]] +name = "freetype-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643148ca6cbad6bec384b52fbe1968547d578c4efe83109e035c43a71734ff88" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "fs4" version = "0.6.6" @@ -1737,6 +1773,7 @@ dependencies = [ "actix-rt", "actix-web", "async-trait", + "bit-set", "brotli", "cargo-husky", "clap", @@ -1753,6 +1790,7 @@ dependencies = [ "martin-mbtiles", "martin-tile-utils", "num_cpus", + "pbf_font_tools", "pmtiles", "postgis", "postgres", @@ -2071,6 +2109,20 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbf_font_tools" +version = "2.4.0" +dependencies = [ + "futures", + "glob", + "protobuf", + "protobuf-codegen", + "protoc-bin-vendored", + "sdf_glyph_renderer", + "thiserror", + "tokio", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2299,6 +2351,107 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" +dependencies = [ + "anyhow", + "indexmap 1.9.3", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + [[package]] name = "quote" version = "1.0.33" @@ -2381,9 +2534,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.1" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -2393,9 +2546,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -2668,6 +2821,14 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdf_glyph_renderer" +version = "0.6.0" +dependencies = [ + "freetype-rs", + "thiserror", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -3820,6 +3981,18 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "whoami" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 9531ce2e1..b53cfa48e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ actix-rt = "2" actix-web = "4" anyhow = "1.0" async-trait = "0.1" +bit-set = "0.5.3" brotli = "3" cargo-husky = { version = "1", features = ["user-hooks"], default-features = false } clap = { version = "4", features = ["derive"] } @@ -36,6 +37,7 @@ log = "0.4" martin-mbtiles = { path = "./martin-mbtiles", version = "0.6.0", default-features = false } martin-tile-utils = { path = "./martin-tile-utils", version = "0.1.0" } num_cpus = "1" +pbf_font_tools = { version = "2.4.0", features = ["freetype"], path = "../sdf_font_tools/pbf_font_tools" } pmtiles = { version = "0.3", features = ["mmap-async-tokio", "tilejson"] } postgis = "0.9" postgres = { version = "0.19", features = ["with-time-0_3", "with-uuid-1", "with-serde_json-1"] } diff --git a/martin-mbtiles/src/errors.rs b/martin-mbtiles/src/errors.rs index cb732b66e..be9336df0 100644 --- a/martin-mbtiles/src/errors.rs +++ b/martin-mbtiles/src/errors.rs @@ -5,6 +5,8 @@ use sqlite_hashes::rusqlite; use crate::MbtType; +use crate::mbtiles::MbtType; + #[derive(thiserror::Error, Debug)] pub enum MbtError { #[error("The source and destination MBTiles files are the same: {}", .0.display())] diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 750dca484..e8d7f8e5a 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -57,6 +57,7 @@ actix-rt.workspace = true actix-web.workspace = true actix.workspace = true async-trait.workspace = true +bit-set.workspace = true brotli.workspace = true clap.workspace = true deadpool-postgres.workspace = true @@ -69,6 +70,7 @@ log.workspace = true martin-mbtiles.workspace = true martin-tile-utils.workspace = true num_cpus.workspace = true +pbf_font_tools.workspace = true pmtiles.workspace = true postgis.workspace = true postgres-protocol.workspace = true diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 1201279b9..3e9054a9c 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -10,7 +10,7 @@ use crate::args::srv::SrvArgs; use crate::args::State::{Ignore, Share, Take}; use crate::config::Config; use crate::file_config::FileConfigEnum; -use crate::{Error, Result}; +use crate::{Error, OneOrMany, Result}; #[derive(Parser, Debug, PartialEq, Default)] #[command(about, version)] @@ -44,6 +44,9 @@ pub struct MetaArgs { /// Export a directory with SVG files as a sprite source. Can be specified multiple times. #[arg(short, long)] pub sprite: Vec, + /// Export a directory with font files as a font source. Can be specified multiple times. + #[arg(short, long)] + pub font: Vec, } impl Args { @@ -81,6 +84,10 @@ impl Args { config.sprites = FileConfigEnum::new(self.meta.sprite); } + if !self.meta.font.is_empty() { + config.fonts = OneOrMany::new_opt(self.meta.font); + } + cli_strings.check() } } diff --git a/martin/src/config.rs b/martin/src/config.rs index b477fd6f4..d0b2459ee 100644 --- a/martin/src/config.rs +++ b/martin/src/config.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fs::File; use std::future::Future; use std::io::prelude::*; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::pin::Pin; use futures::future::try_join_all; @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; use subst::VariableMap; use crate::file_config::{resolve_files, FileConfigEnum}; +use crate::fonts::{resolve_fonts, FontSources}; use crate::mbtiles::MbtSource; use crate::pg::PgConfig; use crate::pmtiles::PmtSource; @@ -24,6 +25,7 @@ pub type UnrecognizedValues = HashMap; pub struct AllSources { pub sources: Sources, pub sprites: SpriteSources, + pub fonts: FontSources, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] @@ -43,6 +45,9 @@ pub struct Config { #[serde(skip_serializing_if = "Option::is_none")] pub sprites: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fonts: Option>, + #[serde(flatten)] pub unrecognized: UnrecognizedValues, } @@ -83,6 +88,14 @@ impl Config { false }; + any |= if let Some(cfg) = &mut self.fonts { + // TODO: support for unrecognized fonts? + // res.extend(cfg.finalize("fonts.")?); + !cfg.is_empty() + } else { + false + }; + if any { Ok(res) } else { @@ -123,6 +136,7 @@ impl Config { }) .sort(), sprites: resolve_sprites(&mut self.sprites)?, + fonts: resolve_fonts(&mut self.fonts)?, }) } } diff --git a/martin/src/fonts/mod.rs b/martin/src/fonts/mod.rs new file mode 100644 index 000000000..dc079112b --- /dev/null +++ b/martin/src/fonts/mod.rs @@ -0,0 +1,359 @@ +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; + +use bit_set::BitSet; +use log::{debug, info, warn}; +use pbf_font_tools::freetype::{Face, Library}; +use pbf_font_tools::protobuf::Message; +use pbf_font_tools::{render_sdf_glyph, Fontstack, Glyphs, PbfFontError}; +use serde::{Deserialize, Serialize}; + +use crate::fonts::FontError::IoError; +use crate::OneOrMany; + +const MAX_UNICODE_CP: usize = 0xFFFF; +const CP_RANGE_SIZE: usize = 256; +const FONT_SIZE: usize = 24; +#[allow(clippy::cast_possible_wrap)] +const CHAR_HEIGHT: isize = (FONT_SIZE as isize) << 6; +const BUFFER_SIZE: usize = 3; +const RADIUS: usize = 8; +const CUTOFF: f64 = 0.25_f64; + +/// Each range is 256 codepoints long, so the highest range ID is 0xFFFF / 256 = 255. +const MAX_UNICODE_CP_RANGE_ID: usize = MAX_UNICODE_CP / CP_RANGE_SIZE; + +#[derive(thiserror::Error, Debug)] +pub enum FontError { + #[error("Font {0} not found")] + FontNotFound(String), + + #[error("Font range start ({0}) must be <= end ({1})")] + InvalidFontRangeStartEnd(u32, u32), + + #[error("Font range start ({0}) must be multiple of {CP_RANGE_SIZE} (e.g. 0, 256, 512, ...)")] + InvalidFontRangeStart(u32), + + #[error( + "Font range end ({0}) must be multiple of {CP_RANGE_SIZE} - 1 (e.g. 255, 511, 767, ...)" + )] + InvalidFontRangeEnd(u32), + + #[error("Given font range {0}-{1} is invalid. It must be {CP_RANGE_SIZE} characters long (e.g. 0-255, 256-511, ...)")] + InvalidFontRange(u32, u32), + + #[error("FreeType font error: {0}")] + FreeType(#[from] pbf_font_tools::freetype::Error), + + #[error("IO error accessing {}: {0}", .1.display())] + IoError(std::io::Error, PathBuf), + + #[error("Font {0} uses bad file {}", .1.display())] + InvalidFontFilePath(String, PathBuf), + + #[error("No font files found in {}", .0.display())] + NoFontFilesFound(PathBuf), + + #[error("Font {} could not be loaded", .0.display())] + UnableToReadFont(PathBuf), + + #[error("{0} in file {}", .1.display())] + FontProcessingError(spreet::error::Error, PathBuf), + + #[error("Font {0} is missing a family name")] + MissingFamilyName(PathBuf), + + #[error("PBF Font error: {0}")] + PbfFontError(#[from] PbfFontError), + + #[error("Error serializing protobuf: {0}")] + ErrorSerializingProtobuf(#[from] pbf_font_tools::protobuf::Error), +} + +fn recurse_dirs( + lib: &Library, + path: &Path, + fonts: &mut HashMap, + catalog: &mut HashMap, +) -> Result<(), FontError> { + for dir_entry in path + .read_dir() + .map_err(|e| IoError(e, path.to_path_buf()))? + .flatten() + { + let path = dir_entry.path(); + + if path.is_dir() { + recurse_dirs(lib, &path, fonts, catalog)?; + continue; + } + + if !path + .extension() + .and_then(OsStr::to_str) + .is_some_and(|e| ["otf", "ttf", "ttc"].contains(&e)) + { + continue; + } + + let mut face = lib.new_face(&path, 0)?; + let num_faces = face.num_faces() as isize; + for i in 0..num_faces { + if i > 0 { + face = lib.new_face(&path, i)?; + } + let Some(family) = face.family_name() else { + return Err(FontError::MissingFamilyName(path.clone())); + }; + let mut name = family.clone(); + let style = face.style_name(); + if let Some(style) = &style { + name.push(' '); + name.push_str(style); + } + // Make sure font name has no slashes or commas, replacing them with spaces and de-duplicating spaces + name = name + .replace(['/', ','], " ") + .replace(" ", " ") + .replace(" ", " "); + + match fonts.entry(name) { + Entry::Occupied(v) => { + warn!("Ignoring duplicate font source {} from {} because it was already configured for {}", + v.key(), path.display(), v.get().path.display()); + } + Entry::Vacant(v) => { + let key = v.key(); + let Some((codepoints, count, ranges )) = get_available_codepoints(&mut face) else { + warn!("Ignoring font source {key} from {} because it has no available glyphs", path.display()); + continue + }; + + let start = ranges.first().map(|(s, _)| *s).unwrap(); + let end = ranges.last().map(|(_, e)| *e).unwrap(); + info!( + "Configured font source {key} with {count} glyphs ({start:04X}-{end:04X}) from {}", + path.display() + ); + debug!( + "Available font ranges: {}", + ranges + .iter() + .map(|(s, e)| if s == e { + format!("{s:02X}") + } else { + format!("{s:02X}-{e:02X}") + }) + .collect::>() + .join(", "), + ); + + catalog.insert( + v.key().clone(), + FontEntry { + family, + style, + total_glyphs: count, + start, + end, + }, + ); + + v.insert(FontSource { + path: path.clone(), + face_index: i, + codepoints, + }); + } + } + } + } + + Ok(()) +} + +type GetGlyphInfo = (BitSet, usize, Vec<(usize, usize)>); + +fn get_available_codepoints(face: &mut Face) -> Option { + let mut codepoints = BitSet::with_capacity(MAX_UNICODE_CP); + let mut spans = Vec::new(); + let mut first: Option = None; + let mut count = 0; + + for cp in 0..=MAX_UNICODE_CP { + if face.get_char_index(cp) != 0 { + codepoints.insert(cp); + count += 1; + if first.is_none() { + first = Some(cp); + } + } else if let Some(start) = first { + spans.push((start, cp - 1)); + first = None; + } + } + + if count == 0 { + None + } else { + Some((codepoints, count, spans)) + } +} + +pub fn resolve_fonts(config: &mut Option>) -> Result { + let Some(cfg) = config else { + return Ok(FontSources::default()); + }; + + let mut fonts = HashMap::new(); + let mut catalog = HashMap::new(); + let lib = Library::init()?; + + for path in cfg.iter() { + let disp_path = path.display(); + if path.exists() { + recurse_dirs(&lib, path, &mut fonts, &mut catalog)?; + } else { + warn!("Ignoring non-existent font source {disp_path}"); + }; + } + + let mut masks = Vec::with_capacity(MAX_UNICODE_CP_RANGE_ID + 1); + + let mut bs = BitSet::with_capacity(CP_RANGE_SIZE); + for v in 0..=MAX_UNICODE_CP { + bs.insert(v); + if v % CP_RANGE_SIZE == (CP_RANGE_SIZE - 1) { + masks.push(bs); + bs = BitSet::with_capacity(CP_RANGE_SIZE); + } + } + + Ok(FontSources { + fonts, + masks, + catalog: FontCatalog { fonts: catalog }, + }) +} + +#[derive(Debug, Clone, Default)] +pub struct FontSources { + fonts: HashMap, + masks: Vec, + catalog: FontCatalog, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct FontCatalog { + // TODO: Use pre-sorted BTreeMap instead + fonts: HashMap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct FontEntry { + pub family: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, + pub total_glyphs: usize, + pub start: usize, + pub end: usize, +} + +impl FontSources { + #[must_use] + pub fn get_catalog(&self) -> &FontCatalog { + &self.catalog + } + + /// Given a list of IDs in a format "id1,id2,id3", return a combined font. + #[allow(clippy::cast_possible_truncation)] + pub fn get_font_range(&self, ids: &str, start: u32, end: u32) -> Result, FontError> { + if start > end { + return Err(FontError::InvalidFontRangeStartEnd(start, end)); + } + if start % (CP_RANGE_SIZE as u32) != 0 { + return Err(FontError::InvalidFontRangeStart(start)); + } + if end % (CP_RANGE_SIZE as u32) != (CP_RANGE_SIZE as u32 - 1) { + return Err(FontError::InvalidFontRangeEnd(end)); + } + if (end - start) != (CP_RANGE_SIZE as u32 - 1) { + return Err(FontError::InvalidFontRange(start, end)); + } + + let mut needed = self.masks[(start as usize) / CP_RANGE_SIZE].clone(); + let fonts = ids + .split(',') + .filter_map(|id| match self.fonts.get(id) { + None => Some(Err(FontError::FontNotFound(id.to_string()))), + Some(v) => { + let mut ds = needed.clone(); + ds.intersect_with(&v.codepoints); + if ds.is_empty() { + None + } else { + needed.difference_with(&v.codepoints); + Some(Ok((id, v, ds))) + } + } + }) + .collect::, FontError>>()?; + + if fonts.is_empty() { + return Ok(Vec::new()); + } + + let lib = Library::init()?; + let mut stack = Fontstack::new(); + + for (id, font, ds) in fonts { + if stack.has_name() { + let name = stack.mut_name(); + name.push_str(", "); + name.push_str(id); + } else { + stack.set_name(id.to_string()); + } + + let face = lib.new_face(&font.path, font.face_index)?; + + // FreeType conventions: char width or height of zero means "use the same value" + // and setting both resolution values to zero results in the default value + // of 72 dpi. + // + // See https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html#ft_set_char_size + // and https://www.freetype.org/freetype2/docs/tutorial/step1.html for details. + face.set_char_size(0, CHAR_HEIGHT, 0, 0)?; + + for cp in ds.iter() { + let glyph = render_sdf_glyph(&face, cp as u32, BUFFER_SIZE, RADIUS, CUTOFF)?; + stack.glyphs.push(glyph); + } + } + + stack.set_range(format!("{start}-{end}")); + + let mut glyphs = Glyphs::new(); + glyphs.stacks.push(stack); + let mut result = Vec::new(); + glyphs.write_to_vec(&mut result)?; + Ok(result) + } +} + +#[derive(Clone, Debug)] +pub struct FontSource { + path: PathBuf, + face_index: isize, + codepoints: BitSet, +} + +// #[cfg(test)] +// mod tests { +// use std::path::PathBuf; +// +// use super::*; +// } diff --git a/martin/src/lib.rs b/martin/src/lib.rs index bdc115c76..57a5943eb 100644 --- a/martin/src/lib.rs +++ b/martin/src/lib.rs @@ -11,6 +11,7 @@ pub mod args; mod config; pub mod file_config; +pub mod fonts; pub mod mbtiles; pub mod pg; pub mod pmtiles; diff --git a/martin/src/srv/server.rs b/martin/src/srv/server.rs index 345b992ca..c8cf41236 100755 --- a/martin/src/srv/server.rs +++ b/martin/src/srv/server.rs @@ -23,6 +23,7 @@ use serde::Deserialize; use tilejson::{tilejson, TileJSON}; use crate::config::AllSources; +use crate::fonts::{FontError, FontSources}; use crate::source::{Source, Sources, UrlQuery, Xyz}; use crate::sprites::{SpriteError, SpriteSources}; use crate::srv::config::{SrvConfig, KEEP_ALIVE_DEFAULT, LISTEN_ADDRESSES_DEFAULT}; @@ -69,6 +70,19 @@ pub fn map_sprite_error(e: SpriteError) -> actix_web::Error { } } +pub fn map_font_error(e: FontError) -> actix_web::Error { + #[allow(clippy::enum_glob_use)] + use FontError::*; + match e { + FontNotFound(_) => error::ErrorNotFound(e.to_string()), + InvalidFontRangeStartEnd(_, _) + | InvalidFontRangeStart(_) + | InvalidFontRangeEnd(_) + | InvalidFontRange(_, _) => ErrorBadRequest(e.to_string()), + _ => map_internal_error(e), + } +} + /// Root path will eventually have a web front. For now, just a stub. #[route("/", method = "GET", method = "HEAD")] #[allow(clippy::unused_async)] @@ -130,6 +144,34 @@ async fn get_sprite_json( Ok(HttpResponse::Ok().json(sheet.get_index())) } +#[derive(Deserialize, Debug)] +struct FontRequest { + fontstack: String, + start: u32, + end: u32, +} + +#[route( + "/font/{fontstack}/{start}-{end}", + method = "GET", + wrap = "middleware::Compress::default()" +)] +#[allow(clippy::unused_async)] +async fn get_font(path: Path, fonts: Data) -> Result { + let data = fonts + .get_font_range(&path.fontstack, path.start, path.end) + .map_err(map_font_error)?; + Ok(HttpResponse::Ok() + .content_type("application/x-protobuf") + .body(data)) +} + +#[route("/font", method = "GET", wrap = "middleware::Compress::default()")] +#[allow(clippy::unused_async)] +async fn get_font_catalog(fonts: Data) -> HttpResponse { + HttpResponse::Ok().json(fonts.get_catalog()) +} + #[route( "/{source_ids}", method = "GET", @@ -406,10 +448,12 @@ pub fn router(cfg: &mut web::ServiceConfig) { cfg.service(get_health) .service(get_index) .service(get_catalog) + .service(get_font_catalog) .service(git_source_info) .service(get_tile) .service(get_sprite_json) - .service(get_sprite_png); + .service(get_sprite_png) + .service(get_font); } /// Create a new initialized Actix `App` instance together with the listening address. @@ -428,6 +472,7 @@ pub fn new_server(config: SrvConfig, all_sources: AllSources) -> crate::Result<( App::new() .app_data(Data::new(all_sources.sources.clone())) .app_data(Data::new(all_sources.sprites.clone())) + .app_data(Data::new(all_sources.fonts.clone())) .wrap(cors_middleware) .wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly)) .wrap(middleware::Logger::default()) diff --git a/martin/src/utils/error.rs b/martin/src/utils/error.rs index 44829c34e..59073dc0b 100644 --- a/martin/src/utils/error.rs +++ b/martin/src/utils/error.rs @@ -2,6 +2,7 @@ use std::io; use std::path::PathBuf; use crate::file_config::FileError; +use crate::fonts::FontError; use crate::pg::PgError; use crate::sprites::SpriteError; @@ -38,4 +39,7 @@ pub enum Error { #[error("{0}")] SpriteError(#[from] SpriteError), + + #[error("{0}")] + FontError(#[from] FontError), }