mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
bug fixes & improvements & missing features & font loader
This commit is contained in:
164
backend/src/font_registry.rs
Normal file
164
backend/src/font_registry.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use std::collections::HashMap;
|
||||
use dreport_layout::FontData;
|
||||
use dreport_layout::font_meta::{self, FontFamilyInfo, FontVariantKey};
|
||||
use dreport_layout::font_provider::FontProvider;
|
||||
|
||||
/// Font registry — manages all available fonts from embedded defaults + external directory.
|
||||
pub struct FontRegistry {
|
||||
/// family_lower -> variant_key -> FontData
|
||||
families: HashMap<String, HashMap<FontVariantKey, FontData>>,
|
||||
/// Original-case family names
|
||||
family_names: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl FontRegistry {
|
||||
pub fn new() -> Self {
|
||||
let mut registry = Self {
|
||||
families: HashMap::new(),
|
||||
family_names: HashMap::new(),
|
||||
};
|
||||
|
||||
// Load embedded default fonts
|
||||
registry.load_embedded_defaults();
|
||||
|
||||
// Load fonts from DREPORT_FONTS_DIR if set
|
||||
if let Ok(dir) = std::env::var("DREPORT_FONTS_DIR") {
|
||||
registry.load_from_directory(&dir);
|
||||
}
|
||||
|
||||
registry
|
||||
}
|
||||
|
||||
fn load_embedded_defaults(&mut self) {
|
||||
let embedded: &[(&str, &[u8])] = &[
|
||||
("NotoSans-Regular", include_bytes!("../fonts/NotoSans-Regular.ttf")),
|
||||
("NotoSans-Bold", include_bytes!("../fonts/NotoSans-Bold.ttf")),
|
||||
("NotoSans-Italic", include_bytes!("../fonts/NotoSans-Italic.ttf")),
|
||||
("NotoSans-BoldItalic", include_bytes!("../fonts/NotoSans-BoldItalic.ttf")),
|
||||
("NotoSansMono-Regular", include_bytes!("../fonts/NotoSansMono-Regular.ttf")),
|
||||
];
|
||||
|
||||
for (_name, data) in embedded {
|
||||
self.register_font(data.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_directory(&mut self, dir: &str) {
|
||||
let path = std::path::Path::new(dir);
|
||||
if !path.is_dir() {
|
||||
eprintln!("DREPORT_FONTS_DIR dizini bulunamadı: {}", dir);
|
||||
return;
|
||||
}
|
||||
|
||||
let entries = match std::fs::read_dir(path) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!("DREPORT_FONTS_DIR okunamadı: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let p = entry.path();
|
||||
if p.extension().is_some_and(|e| e == "ttf" || e == "otf") {
|
||||
if let Ok(data) = std::fs::read(&p) {
|
||||
if self.register_font(data) {
|
||||
println!(" Font yüklendi: {}", p.display());
|
||||
} else {
|
||||
eprintln!(" Font parse edilemedi: {}", p.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a font from raw bytes. Returns true if successful.
|
||||
fn register_font(&mut self, data: Vec<u8>) -> bool {
|
||||
let Some(meta) = font_meta::parse_font_meta(&data) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let family_lower = meta.family.to_lowercase();
|
||||
let variant_key = meta.variant_key();
|
||||
|
||||
self.family_names
|
||||
.entry(family_lower.clone())
|
||||
.or_insert_with(|| meta.family.clone());
|
||||
|
||||
let font_data = FontData::new(meta.family, meta.weight, meta.italic, data);
|
||||
|
||||
self.families
|
||||
.entry(family_lower)
|
||||
.or_default()
|
||||
.insert(variant_key, font_data);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Get a specific font's raw bytes
|
||||
pub fn get_font_bytes(&self, family: &str, weight: u16, italic: bool) -> Option<&[u8]> {
|
||||
let family_lower = family.to_lowercase();
|
||||
let key = FontVariantKey { weight, italic };
|
||||
self.families
|
||||
.get(&family_lower)
|
||||
.and_then(|variants| variants.get(&key))
|
||||
.map(|fd| fd.data.as_slice())
|
||||
}
|
||||
|
||||
/// Get all FontData for given family names (for passing to layout engine)
|
||||
pub fn fonts_for_families(&self, families: &[String]) -> Vec<FontData> {
|
||||
let mut result = Vec::new();
|
||||
let mut loaded = std::collections::HashSet::new();
|
||||
|
||||
// Always include default family
|
||||
let default_lower = "noto sans".to_string();
|
||||
let mut to_load: Vec<String> = vec![default_lower.clone()];
|
||||
for f in families {
|
||||
let fl = f.to_lowercase();
|
||||
if !to_load.contains(&fl) {
|
||||
to_load.push(fl);
|
||||
}
|
||||
}
|
||||
|
||||
for family_lower in &to_load {
|
||||
if loaded.contains(family_lower) {
|
||||
continue;
|
||||
}
|
||||
if let Some(variants) = self.families.get(family_lower) {
|
||||
for fd in variants.values() {
|
||||
result.push(fd.clone());
|
||||
}
|
||||
loaded.insert(family_lower.clone());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl FontProvider for FontRegistry {
|
||||
fn list_families(&self) -> Vec<FontFamilyInfo> {
|
||||
self.families
|
||||
.iter()
|
||||
.map(|(family_lower, variants)| {
|
||||
let family = self.family_names
|
||||
.get(family_lower)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| family_lower.clone());
|
||||
FontFamilyInfo {
|
||||
family,
|
||||
variants: variants.keys().cloned().collect(),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_font(&self, family: &str, weight: u16, italic: bool) -> Option<FontData> {
|
||||
let family_lower = family.to_lowercase();
|
||||
let key = FontVariantKey { weight, italic };
|
||||
self.families
|
||||
.get(&family_lower)
|
||||
.and_then(|variants| variants.get(&key))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
use axum::{Router, serve};
|
||||
use dreport_layout::FontData;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
mod font_registry;
|
||||
mod models;
|
||||
mod routes;
|
||||
|
||||
use font_registry::FontRegistry;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
println!("Fontlar yukleniyor...");
|
||||
let fonts = Arc::new(load_fonts());
|
||||
println!("Fontlar yuklendi ({} font dosyasi)", fonts.len());
|
||||
println!("Font registry başlatılıyor...");
|
||||
let registry = Arc::new(FontRegistry::new());
|
||||
|
||||
let family_count = dreport_layout::font_provider::FontProvider::list_families(registry.as_ref()).len();
|
||||
println!("Font registry hazır ({} font ailesi)", family_count);
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
@@ -21,7 +25,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
let app = Router::new()
|
||||
.merge(routes::router())
|
||||
.layer(cors)
|
||||
.with_state(fonts);
|
||||
.with_state(registry);
|
||||
|
||||
let listener = TcpListener::bind("0.0.0.0:3001").await?;
|
||||
println!("dreport backend listening on http://localhost:3001");
|
||||
@@ -29,31 +33,3 @@ async fn main() -> anyhow::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Proje fontlarını yükler (backend/fonts/ dizininden).
|
||||
fn load_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("fonts");
|
||||
let mut fonts = Vec::new();
|
||||
|
||||
let entries = std::fs::read_dir(&font_dir).expect("backend/fonts dizini bulunamadi");
|
||||
for entry in entries {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf" || e == "otf") {
|
||||
let data = std::fs::read(&path).unwrap();
|
||||
let family = if path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.contains("Mono")
|
||||
{
|
||||
"Noto Sans Mono".to_string()
|
||||
} else {
|
||||
"Noto Sans".to_string()
|
||||
};
|
||||
fonts.push(FontData { family, data });
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
||||
77
backend/src/routes/fonts.rs
Normal file
77
backend/src/routes/fonts.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use axum::{
|
||||
Router,
|
||||
extract::{Path, State},
|
||||
http::{StatusCode, header},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json,
|
||||
};
|
||||
use dreport_layout::font_provider::FontProvider;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::font_registry::FontRegistry;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FontFamilyResponse {
|
||||
family: String,
|
||||
variants: Vec<FontVariantResponse>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FontVariantResponse {
|
||||
weight: u16,
|
||||
italic: bool,
|
||||
}
|
||||
|
||||
/// GET /api/fonts — list all available font families
|
||||
async fn list_fonts(
|
||||
State(registry): State<Arc<FontRegistry>>,
|
||||
) -> Json<Vec<FontFamilyResponse>> {
|
||||
let families = registry.list_families();
|
||||
let response: Vec<FontFamilyResponse> = families
|
||||
.into_iter()
|
||||
.map(|f| FontFamilyResponse {
|
||||
family: f.family,
|
||||
variants: f.variants
|
||||
.into_iter()
|
||||
.map(|v| FontVariantResponse {
|
||||
weight: v.weight,
|
||||
italic: v.italic,
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect();
|
||||
Json(response)
|
||||
}
|
||||
|
||||
/// GET /api/fonts/:family/:weight/:italic — serve font binary
|
||||
async fn get_font(
|
||||
State(registry): State<Arc<FontRegistry>>,
|
||||
Path((family, weight, italic)): Path<(String, u16, String)>,
|
||||
) -> impl IntoResponse {
|
||||
let is_italic = italic == "true" || italic == "1";
|
||||
|
||||
match registry.get_font_bytes(&family, weight, is_italic) {
|
||||
Some(data) => (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "font/ttf")],
|
||||
data.to_vec(),
|
||||
)
|
||||
.into_response(),
|
||||
None => (
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"Font bulunamadı: {} weight={} italic={}",
|
||||
family, weight, is_italic
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||
Router::new()
|
||||
.route("/api/fonts", get(list_fonts))
|
||||
.route("/api/fonts/{family}/{weight}/{italic}", get(get_font))
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
use axum::{Router, routing::get, Json};
|
||||
use dreport_layout::FontData;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::font_registry::FontRegistry;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct HealthResponse {
|
||||
status: &'static str,
|
||||
@@ -16,6 +17,6 @@ async fn health() -> Json<HealthResponse> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
||||
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||
Router::new().route("/api/health", get(health))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
mod fonts;
|
||||
mod health;
|
||||
mod render;
|
||||
|
||||
use axum::Router;
|
||||
use dreport_layout::FontData;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
||||
use crate::font_registry::FontRegistry;
|
||||
|
||||
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||
Router::new()
|
||||
.merge(health::router())
|
||||
.merge(render::router())
|
||||
.merge(fonts::router())
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ use axum::{
|
||||
routing::post,
|
||||
Json,
|
||||
};
|
||||
use dreport_layout::FontData;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::font_registry::FontRegistry;
|
||||
use crate::models::Template;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -20,28 +20,39 @@ pub struct RenderRequest {
|
||||
|
||||
/// POST /api/render — Template + Data → PDF
|
||||
pub async fn render(
|
||||
State(fonts): State<Arc<Vec<FontData>>>,
|
||||
State(registry): State<Arc<FontRegistry>>,
|
||||
Json(payload): Json<RenderRequest>,
|
||||
) -> impl IntoResponse {
|
||||
// 1. Layout hesapla
|
||||
let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts);
|
||||
// CPU-intensive layout + PDF render'ı blocking thread'de çalıştır
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
// Template'in fonts alanına göre sadece gerekli fontları yükle
|
||||
let fonts = registry.fonts_for_families(&payload.template.fonts);
|
||||
let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts)
|
||||
.map_err(|e| format!("Layout error: {}", e))?;
|
||||
dreport_layout::pdf_render::render_pdf(&layout, &fonts)
|
||||
})
|
||||
.await;
|
||||
|
||||
// 2. PDF render
|
||||
match dreport_layout::pdf_render::render_pdf(&layout, &fonts) {
|
||||
Ok(pdf_bytes) => (
|
||||
match result {
|
||||
Ok(Ok(pdf_bytes)) => (
|
||||
StatusCode::OK,
|
||||
[(header::CONTENT_TYPE, "application/pdf")],
|
||||
pdf_bytes,
|
||||
)
|
||||
.into_response(),
|
||||
Ok(Err(err)) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("PDF render hatası: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("PDF render hatasi: {}", err),
|
||||
format!("Task hatası: {}", err),
|
||||
)
|
||||
.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn router() -> Router<Arc<Vec<FontData>>> {
|
||||
pub fn router() -> Router<Arc<FontRegistry>> {
|
||||
Router::new().route("/api/render", post(render))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user