bug fixes & improvements & missing features & font loader

This commit is contained in:
2026-04-07 00:36:21 +03:00
parent e95606d18b
commit 9f658f5615
54 changed files with 4087 additions and 1843 deletions

View 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()
}
}

View File

@@ -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
}

View 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))
}

View File

@@ -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))
}

View File

@@ -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())
}

View File

@@ -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))
}