This commit is contained in:
2026-03-29 19:17:09 +03:00
parent 9b17d2aef4
commit 1cbe42ed75
34 changed files with 4690 additions and 3105 deletions

View File

@@ -5,6 +5,7 @@ edition = "2024"
[dependencies]
dreport-core = { path = "../core" }
dreport-layout = { path = "../layout-engine" }
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
@@ -12,7 +13,3 @@ serde_json = "1"
tower-http = { version = "0.6", features = ["cors"] }
thiserror = "2"
anyhow = "1"
typst = "0.14"
typst-pdf = "0.14"
typst-kit = { version = "0.14", features = ["fonts"] }
chrono = "0.4"

View File

@@ -1,18 +1,17 @@
use axum::{Router, serve};
use dreport_layout::FontData;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_http::cors::{Any, CorsLayer};
mod models;
mod routes;
mod typst_engine;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Fontları bir kez yükle — tüm request'lerde paylaşılacak
println!("Fontlar yukleniyor...");
let fonts = Arc::new(typst_engine::fonts::load_fonts());
println!("Fontlar yuklendi ({} font)", fonts.fonts.len());
let fonts = Arc::new(load_fonts());
println!("Fontlar yuklendi ({} font dosyasi)", fonts.len());
let cors = CorsLayer::new()
.allow_origin(Any)
@@ -30,3 +29,31 @@ 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

@@ -1,7 +1,7 @@
use axum::{Router, routing::get, Json};
use dreport_layout::FontData;
use serde::Serialize;
use std::sync::Arc;
use typst_kit::fonts::Fonts;
#[derive(Serialize)]
struct HealthResponse {
@@ -16,6 +16,6 @@ async fn health() -> Json<HealthResponse> {
})
}
pub fn router() -> Router<Arc<Fonts>> {
pub fn router() -> Router<Arc<Vec<FontData>>> {
Router::new().route("/api/health", get(health))
}

View File

@@ -2,10 +2,10 @@ mod health;
mod render;
use axum::Router;
use dreport_layout::FontData;
use std::sync::Arc;
use typst_kit::fonts::Fonts;
pub fn router() -> Router<Arc<Fonts>> {
pub fn router() -> Router<Arc<Vec<FontData>>> {
Router::new()
.merge(health::router())
.merge(render::router())

View File

@@ -6,13 +6,11 @@ use axum::{
routing::post,
Json,
};
use dreport_layout::FontData;
use serde::Deserialize;
use std::sync::Arc;
use crate::models::Template;
use crate::typst_engine::compiler::compile_pdf;
use crate::typst_engine::template_to_typst::{self, RenderMode};
use typst_kit::fonts::Fonts;
#[derive(Deserialize)]
pub struct RenderRequest {
@@ -22,17 +20,14 @@ pub struct RenderRequest {
/// POST /api/render — Template + Data → PDF
pub async fn render(
State(fonts): State<Arc<Fonts>>,
State(fonts): State<Arc<Vec<FontData>>>,
Json(payload): Json<RenderRequest>,
) -> impl IntoResponse {
// 1. Template JSON → Typst markup
let typst_markup = template_to_typst::template_to_typst(&payload.template, &payload.data, RenderMode::Pdf);
// 1. Layout hesapla
let layout = dreport_layout::compute_layout(&payload.template, &payload.data, &fonts);
// 2. Base64 image'ları çıkar
let files = template_to_typst::extract_image_files(&payload.template);
// 3. Typst markup → PDF
match compile_pdf(typst_markup, &fonts, files) {
// 2. PDF render
match dreport_layout::pdf_render::render_pdf(&layout, &fonts) {
Ok(pdf_bytes) => (
StatusCode::OK,
[(header::CONTENT_TYPE, "application/pdf")],
@@ -41,12 +36,12 @@ pub async fn render(
.into_response(),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("PDF derleme hatasi: {}", err),
format!("PDF render hatasi: {}", err),
)
.into_response(),
}
}
pub fn router() -> Router<Arc<Fonts>> {
pub fn router() -> Router<Arc<Vec<FontData>>> {
Router::new().route("/api/render", post(render))
}

View File

@@ -1,117 +0,0 @@
use std::collections::HashMap;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime, Smart};
use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::{Library, LibraryExt, World};
use typst_kit::fonts::Fonts;
use typst_pdf::{PdfOptions, pdf};
/// Typst World implementasyonu — dreport backend için.
/// Fonts referans olarak tutulur (clone edilemez).
pub struct DreportWorld<'a> {
library: LazyHash<Library>,
book: LazyHash<FontBook>,
fonts: &'a Fonts,
main_source: Source,
/// Sanal dosyalar (ör: base64 image'lar)
files: HashMap<String, Bytes>,
}
impl<'a> DreportWorld<'a> {
pub fn new(typst_markup: String, fonts: &'a Fonts, files: HashMap<String, Vec<u8>>) -> Self {
let main_id = FileId::new_fake(VirtualPath::new("main.typ"));
Self {
library: LazyHash::new(Library::default()),
book: LazyHash::new(fonts.book.clone()),
fonts,
main_source: Source::new(main_id, typst_markup),
files: files.into_iter().map(|(k, v)| (k, Bytes::new(v))).collect(),
}
}
}
impl World for DreportWorld<'_> {
fn library(&self) -> &LazyHash<Library> {
&self.library
}
fn book(&self) -> &LazyHash<FontBook> {
&self.book
}
fn main(&self) -> FileId {
self.main_source.id()
}
fn source(&self, id: FileId) -> FileResult<Source> {
if id == self.main_source.id() {
Ok(self.main_source.clone())
} else {
Err(FileError::NotFound(id.vpath().as_rooted_path().into()))
}
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
let path = id.vpath().as_rooted_path();
let path_str = path.to_string_lossy();
// Baştaki "/" veya "./" kaldır
let clean_path = path_str.trim_start_matches('/').trim_start_matches("./");
if let Some(bytes) = self.files.get(clean_path) {
Ok(bytes.clone())
} else {
Err(FileError::NotFound(path.into()))
}
}
fn font(&self, index: usize) -> Option<Font> {
self.fonts.fonts.get(index)?.get()
}
fn today(&self, offset: Option<i64>) -> Option<Datetime> {
let now = chrono::Utc::now();
let offset_secs = offset.unwrap_or(0) * 3600;
let tz = chrono::FixedOffset::east_opt(offset_secs as i32)?;
let local = now.with_timezone(&tz);
use chrono::Datelike;
Datetime::from_ymd(
local.year(),
local.month().try_into().ok()?,
local.day().try_into().ok()?,
)
}
}
/// Typst markup → PDF bytes
pub fn compile_pdf(typst_markup: String, fonts: &Fonts, files: HashMap<String, Vec<u8>>) -> Result<Vec<u8>, String> {
let world = DreportWorld::new(typst_markup, fonts, files);
// Derleme
let warned = typst::compile::<PagedDocument>(&world);
let document = warned.output.map_err(|errs| {
errs.into_iter()
.map(|e| e.message.to_string())
.collect::<Vec<_>>()
.join("; ")
})?;
// PDF export
let options = PdfOptions {
ident: Smart::Auto,
timestamp: None,
page_ranges: None,
standards: Default::default(),
tagged: false,
};
let pdf_bytes = pdf(&document, &options).map_err(|errs| {
errs.into_iter()
.map(|e| e.message.to_string())
.collect::<Vec<_>>()
.join("; ")
})?;
Ok(pdf_bytes)
}

View File

@@ -1,17 +0,0 @@
use std::path::PathBuf;
use typst_kit::fonts::{FontSearcher, Fonts};
/// Proje fontlarını yükler (backend/fonts/ dizininden).
/// Uygulama başlangıcında bir kez çağrılır ve paylaşılır.
pub fn load_fonts() -> Fonts {
let font_dir = font_dir();
FontSearcher::new()
.include_system_fonts(false)
.search_with(&[font_dir])
}
fn font_dir() -> PathBuf {
// Cargo manifest dizinine göre fonts/ klasörü
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(manifest_dir).join("fonts")
}

View File

@@ -1,4 +0,0 @@
pub mod compiler;
pub mod fonts;
pub use dreport_core::template_to_typst;