diff --git a/editoast/.gitignore b/editoast/.gitignore index 88f342d2063..6b5ca9496b0 100644 --- a/editoast/.gitignore +++ b/editoast/.gitignore @@ -1,3 +1,4 @@ target *.snap.new -assets/sprites/*/sprites* +assets/sprites/*/sprites.* +assets/fonts/glyphs/* diff --git a/editoast/Dockerfile b/editoast/Dockerfile index 920414f0f1e..503e50e4912 100644 --- a/editoast/Dockerfile +++ b/editoast/Dockerfile @@ -23,11 +23,15 @@ COPY --from=static_assets . /assets ####################### # Build assets # ####################### -FROM alpine:latest AS editoast_assets +FROM ubuntu:latest AS editoast_assets +RUN apt update && apt install --yes wget RUN wget https://github.com/flother/spreet/releases/download/v0.11.0/spreet-x86_64-unknown-linux-musl.tar.gz RUN tar xvf spreet-x86_64-unknown-linux-musl.tar.gz --directory /usr/bin +RUN wget http://github.com/stadiamaps/sdf_font_tools/releases/download/cli-v1.4.2/build_pbf_glyphs.x86_64-unknown-linux-gnu -O /usr/bin/build_pbf_glyphs +RUN chmod +x /usr/bin/build_pbf_glyphs COPY ./assets /assets RUN /assets/sprites/generate-atlas.sh +RUN /assets/fonts/generate-glyphs.sh ###################### # Testing env: build # @@ -44,6 +48,7 @@ ENV RUSTFLAGS="-Cinstrument-coverage -C target-feature=-crt-static -C link-arg=- ENV LLVM_PROFILE_FILE="editoast-%p-%m.profraw" RUN cargo chef cook --tests --recipe-path recipe.json COPY . . +COPY --from=editoast_assets /assets /editoast/assets ####################### # Running env : build # diff --git a/editoast/README.md b/editoast/README.md index 253fbea269d..65315b85980 100644 --- a/editoast/README.md +++ b/editoast/README.md @@ -24,6 +24,8 @@ $ diesel migration run # build the assets $ cargo install spreet $ ./assets/sprites/generate-atlas.sh +$ cargo install build_pbf_glyphs +$ ./assets/fonts/generate-glyphs.sh # Build and run $ cargo build $ cargo run -- runserver diff --git a/editoast/assets/fonts/Roboto Bold.ttf b/editoast/assets/fonts/Roboto Bold.ttf new file mode 100644 index 00000000000..91ec2122786 Binary files /dev/null and b/editoast/assets/fonts/Roboto Bold.ttf differ diff --git a/editoast/assets/fonts/Roboto Condensed Italic.ttf b/editoast/assets/fonts/Roboto Condensed Italic.ttf new file mode 100644 index 00000000000..5ed712c9bde Binary files /dev/null and b/editoast/assets/fonts/Roboto Condensed Italic.ttf differ diff --git a/editoast/assets/fonts/Roboto Condensed.ttf b/editoast/assets/fonts/Roboto Condensed.ttf new file mode 100644 index 00000000000..6e7575a16a4 Binary files /dev/null and b/editoast/assets/fonts/Roboto Condensed.ttf differ diff --git a/editoast/assets/fonts/Roboto Italic.ttf b/editoast/assets/fonts/Roboto Italic.ttf new file mode 100644 index 00000000000..9f49ffd8691 Binary files /dev/null and b/editoast/assets/fonts/Roboto Italic.ttf differ diff --git a/editoast/assets/fonts/Roboto Medium.ttf b/editoast/assets/fonts/Roboto Medium.ttf new file mode 100644 index 00000000000..87983419893 Binary files /dev/null and b/editoast/assets/fonts/Roboto Medium.ttf differ diff --git a/editoast/assets/fonts/Roboto Regular.ttf b/editoast/assets/fonts/Roboto Regular.ttf new file mode 100644 index 00000000000..7d9a6c4c32d Binary files /dev/null and b/editoast/assets/fonts/Roboto Regular.ttf differ diff --git a/editoast/assets/fonts/SNCF.ttf b/editoast/assets/fonts/SNCF.ttf new file mode 100644 index 00000000000..1f95b977dc4 Binary files /dev/null and b/editoast/assets/fonts/SNCF.ttf differ diff --git a/editoast/assets/fonts/generate-glyphs.sh b/editoast/assets/fonts/generate-glyphs.sh new file mode 100755 index 00000000000..158d6ab5c93 --- /dev/null +++ b/editoast/assets/fonts/generate-glyphs.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# This script should be used to generate glyphs from ttf fonts. +# Those glyphs are used to display text on the map +# You will need build_pbf_glyphs, you can install it with: +# `$ cargo install build_pbf_glyphs` +set -e + +fonts_directory=$(dirname "$(realpath "$0")") +echo "Converting fonts in ${fonts_directory} to glyphs" +build_pbf_glyphs "${fonts_directory}" "${fonts_directory}"/glyphs/ diff --git a/editoast/openapi.yaml b/editoast/openapi.yaml index 3b9fc6d2d60..47ee4e5ac6c 100644 --- a/editoast/openapi.yaml +++ b/editoast/openapi.yaml @@ -299,6 +299,29 @@ paths: application/json: schema: $ref: '#/components/schemas/InternalError' + /fonts/{font}/{glyph}: + get: + tags: + - fonts + summary: This endpoint is used by map libre to retrieve the fonts. They are separated by font and unicode block + parameters: + - name: font + in: path + description: Requested font + required: true + schema: + type: string + - name: glyph + in: path + description: Requested unicode block + required: true + schema: + type: string + responses: + '200': + description: Glyphs in PBF format of the font at the requested unicode block + '404': + description: Font not found /health: get: responses: @@ -4807,6 +4830,7 @@ components: - $ref: '#/components/schemas/EditoastEditionErrorSplitTrackSectionBadOffset' - $ref: '#/components/schemas/EditoastEditoastUrlErrorInvalidUrl' - $ref: '#/components/schemas/EditoastElectricalProfilesErrorNotFound' + - $ref: '#/components/schemas/EditoastFontErrorsFileNotFound' - $ref: '#/components/schemas/EditoastGeometryErrorUnexpectedGeometry' - $ref: '#/components/schemas/EditoastGetObjectsErrorsDuplicateIdsProvided' - $ref: '#/components/schemas/EditoastGetObjectsErrorsObjectIdNotFound' @@ -4889,6 +4913,30 @@ components: description: Generated error type for Editoast discriminator: propertyName: type + EditoastFontErrorsFileNotFound: + type: object + required: + - type + - status + - message + properties: + context: + type: object + required: + - file + properties: + file: + type: string + message: + type: string + status: + type: integer + enum: + - 404 + type: + type: string + enum: + - editoast:fonts:FileNotFound EditoastGeometryErrorUnexpectedGeometry: type: object required: diff --git a/editoast/src/views/fonts.rs b/editoast/src/views/fonts.rs new file mode 100644 index 00000000000..fa80f841734 --- /dev/null +++ b/editoast/src/views/fonts.rs @@ -0,0 +1,77 @@ +use axum::extract::Path; +use axum::extract::Request; +use axum::response::IntoResponse; +use editoast_derive::EditoastError; +use thiserror::Error; +use tower::ServiceExt; +use tower_http::services::ServeFile; + +use crate::client::get_dynamic_assets_path; +use crate::error::Result; + +crate::routes! { + "/fonts/{font}/{glyph}" => fonts, +} + +#[derive(Debug, Error, EditoastError)] +#[editoast_error(base_id = "fonts")] +enum FontErrors { + #[error("File '{file}' not found")] + #[editoast_error(status = 404)] + FileNotFound { file: String }, +} + +/// This endpoint is used by map libre to retrieve the fonts. They are separated by font and unicode block +#[utoipa::path( + get, path = "", + tag = "fonts", + params( + ("font" = String, Path, description = "Requested font"), + ("glyph" = String, Path, description = "Requested unicode block"), + ), + responses( + (status = 200, description = "Glyphs in PBF format of the font at the requested unicode block"), + (status = 404, description = "Font not found"), + ), +)] +async fn fonts( + Path((font, file_name)): Path<(String, String)>, + request: Request, +) -> Result { + let path = get_dynamic_assets_path().join(format!("fonts/glyphs/{font}/{file_name}")); + + if !path.is_file() { + return Err(FontErrors::FileNotFound { file: file_name }.into()); + } + + Ok(ServeFile::new(&path).oneshot(request).await) +} + +#[cfg(test)] +mod tests { + use crate::views::test_app::TestAppBuilder; + + use super::*; + use axum::http::StatusCode; + use rstest::rstest; + + #[rstest] + async fn test_font() { + let app = TestAppBuilder::default_app(); + let request = app.get("/fonts/Roboto%20Bold/0-255.pbf"); + let response = app.fetch(request).assert_status(StatusCode::OK); + assert_eq!("application/octet-stream", response.content_type()); + let response = response.bytes(); + let expected = + std::fs::read(get_dynamic_assets_path().join("fonts/glyphs/Roboto Bold/0-255.pbf")) + .unwrap(); + assert_eq!(response, expected); + } + + #[rstest] + async fn test_font_not_found() { + let app = TestAppBuilder::default_app(); + let request = app.get("/fonts/Comic%20Sans/0-255.pbf"); + app.fetch(request).assert_status(StatusCode::NOT_FOUND); + } +} diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 25f4f61b9e6..a3e17b0cd02 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -1,6 +1,7 @@ mod authz; mod documents; pub mod electrical_profiles; +pub mod fonts; pub mod infra; mod layers; mod openapi; @@ -99,6 +100,7 @@ crate::routes! { &authz, &documents, &electrical_profiles, + &fonts, &infra, &layers, &projects, diff --git a/front/public/locales/en/errors.json b/front/public/locales/en/errors.json index 0fcd50950b3..88d3ab0d560 100644 --- a/front/public/locales/en/errors.json +++ b/front/public/locales/en/errors.json @@ -61,6 +61,9 @@ "electrical_profiles": { "NotFound": "Electrical Profile Set '{{electrical_profile_set_id}}', could not be found" }, + "fonts": { + "FileNotFound": "File for the requested font not found" + }, "geometry": { "UnexpectedGeometry": "Expected geometry {{expected}} but got {{actual}}" }, diff --git a/front/public/locales/fr/errors.json b/front/public/locales/fr/errors.json index edb8f1f1abc..ed1367cbde4 100644 --- a/front/public/locales/fr/errors.json +++ b/front/public/locales/fr/errors.json @@ -61,6 +61,9 @@ "electrical_profiles": { "NotFound": "Profil électrique '{{electrical_profile_set_id}}' non trouvé" }, + "fonts": { + "FileNotFound": "Fichier pour la police de charactère demandée non trouvé" + }, "geometry": { "UnexpectedGeometry": "Géometrie {{expected}} attendue mais {{actual}} reçue" }, diff --git a/front/src/common/Map/Layers/blankStyle.ts b/front/src/common/Map/Layers/blankStyle.ts index 4654653721a..54ef0864a7f 100644 --- a/front/src/common/Map/Layers/blankStyle.ts +++ b/front/src/common/Map/Layers/blankStyle.ts @@ -66,7 +66,7 @@ export const useMapBlankStyle = (): MapProps['mapStyle'] => { name: 'Blank', sources: {}, sprite, - glyphs: 'https://static.osm.osrd.fr/fonts/{fontstack}/{range}.pbf', + glyphs: `${window.location.origin}${baseURL}/fonts/{fontstack}/{range}.pbf`, layers: [ { id: 'emptyBackground', diff --git a/front/src/common/api/generatedEditoastApi.ts b/front/src/common/api/generatedEditoastApi.ts index 6c56d89df4c..04cad90036f 100644 --- a/front/src/common/api/generatedEditoastApi.ts +++ b/front/src/common/api/generatedEditoastApi.ts @@ -4,6 +4,7 @@ export const addTagTypes = [ 'authz', 'documents', 'electrical_profiles', + 'fonts', 'infra', 'rolling_stock', 'delimited_area', @@ -138,6 +139,13 @@ const injectedRtkApi = api }), providesTags: ['electrical_profiles'], }), + getFontsByFontAndGlyph: build.query< + GetFontsByFontAndGlyphApiResponse, + GetFontsByFontAndGlyphApiArg + >({ + query: (queryArg) => ({ url: `/fonts/${queryArg.font}/${queryArg.glyph}` }), + providesTags: ['fonts'], + }), getHealth: build.query({ query: () => ({ url: `/health` }), }), @@ -1169,6 +1177,13 @@ export type GetElectricalProfileSetByElectricalProfileSetIdLevelOrderApiResponse export type GetElectricalProfileSetByElectricalProfileSetIdLevelOrderApiArg = { electricalProfileSetId: number; }; +export type GetFontsByFontAndGlyphApiResponse = unknown; +export type GetFontsByFontAndGlyphApiArg = { + /** Requested font */ + font: string; + /** Requested unicode block */ + glyph: string; +}; export type GetHealthApiResponse = unknown; export type GetHealthApiArg = void; export type GetInfraApiResponse = /** status 200 All infras, paginated */ PaginationStats & {