mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
faz 5
This commit is contained in:
4100
backend/Cargo.lock
generated
Normal file
4100
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
backend/Cargo.toml
Normal file
18
backend/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "dreport-backend"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dreport-core = { path = "../core" }
|
||||||
|
axum = "0.8"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
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"
|
||||||
BIN
backend/fonts/NotoSans-Bold.ttf
Normal file
BIN
backend/fonts/NotoSans-Bold.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/NotoSans-BoldItalic.ttf
Normal file
BIN
backend/fonts/NotoSans-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/NotoSans-Italic.ttf
Normal file
BIN
backend/fonts/NotoSans-Italic.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/NotoSans-Regular.ttf
Normal file
BIN
backend/fonts/NotoSans-Regular.ttf
Normal file
Binary file not shown.
BIN
backend/fonts/NotoSansMono-Regular.ttf
Normal file
BIN
backend/fonts/NotoSansMono-Regular.ttf
Normal file
Binary file not shown.
32
backend/src/main.rs
Normal file
32
backend/src/main.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use axum::{Router, serve};
|
||||||
|
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 cors = CorsLayer::new()
|
||||||
|
.allow_origin(Any)
|
||||||
|
.allow_methods(Any)
|
||||||
|
.allow_headers(Any);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.merge(routes::router())
|
||||||
|
.layer(cors)
|
||||||
|
.with_state(fonts);
|
||||||
|
|
||||||
|
let listener = TcpListener::bind("0.0.0.0:3001").await?;
|
||||||
|
println!("dreport backend listening on http://localhost:3001");
|
||||||
|
serve(listener, app).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
1
backend/src/models/mod.rs
Normal file
1
backend/src/models/mod.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub use dreport_core::models::*;
|
||||||
21
backend/src/routes/health.rs
Normal file
21
backend/src/routes/health.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use axum::{Router, routing::get, Json};
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use typst_kit::fonts::Fonts;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HealthResponse {
|
||||||
|
status: &'static str,
|
||||||
|
version: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn health() -> Json<HealthResponse> {
|
||||||
|
Json(HealthResponse {
|
||||||
|
status: "ok",
|
||||||
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router() -> Router<Arc<Fonts>> {
|
||||||
|
Router::new().route("/api/health", get(health))
|
||||||
|
}
|
||||||
12
backend/src/routes/mod.rs
Normal file
12
backend/src/routes/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
mod health;
|
||||||
|
mod render;
|
||||||
|
|
||||||
|
use axum::Router;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use typst_kit::fonts::Fonts;
|
||||||
|
|
||||||
|
pub fn router() -> Router<Arc<Fonts>> {
|
||||||
|
Router::new()
|
||||||
|
.merge(health::router())
|
||||||
|
.merge(render::router())
|
||||||
|
}
|
||||||
52
backend/src/routes/render.rs
Normal file
52
backend/src/routes/render.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use axum::{
|
||||||
|
Router,
|
||||||
|
extract::State,
|
||||||
|
http::{StatusCode, header},
|
||||||
|
response::IntoResponse,
|
||||||
|
routing::post,
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
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 {
|
||||||
|
pub template: Template,
|
||||||
|
pub data: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /api/render — Template + Data → PDF
|
||||||
|
pub async fn render(
|
||||||
|
State(fonts): State<Arc<Fonts>>,
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
Ok(pdf_bytes) => (
|
||||||
|
StatusCode::OK,
|
||||||
|
[(header::CONTENT_TYPE, "application/pdf")],
|
||||||
|
pdf_bytes,
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(err) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("PDF derleme hatasi: {}", err),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router() -> Router<Arc<Fonts>> {
|
||||||
|
Router::new().route("/api/render", post(render))
|
||||||
|
}
|
||||||
117
backend/src/typst_engine/compiler.rs
Normal file
117
backend/src/typst_engine/compiler.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
17
backend/src/typst_engine/fonts.rs
Normal file
17
backend/src/typst_engine/fonts.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
4
backend/src/typst_engine/mod.rs
Normal file
4
backend/src/typst_engine/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod compiler;
|
||||||
|
pub mod fonts;
|
||||||
|
|
||||||
|
pub use dreport_core::template_to_typst;
|
||||||
Reference in New Issue
Block a user