mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-02 02:49:16 +00:00
0.2.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dreport-layout"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
description = "Layout engine for dreport (taffy + cosmic-text)"
|
||||
license = "MIT"
|
||||
@@ -11,8 +11,9 @@ publish = ["gitea"]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
dreport-core = { version = "0.1.0", path = "../core", registry = "gitea" }
|
||||
dreport-core = { version = "0.2.0", path = "../core", registry = "gitea" }
|
||||
dexpr = { version = "0.1.0", registry = "gitea" }
|
||||
rust_decimal = "1.41"
|
||||
taffy = "0.9"
|
||||
cosmic-text = { version = "0.18", default-features = false, features = ["std", "swash"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -81,20 +81,27 @@ pub fn apply_format_with_config(value: &str, format: Option<&str>, config: &drep
|
||||
}
|
||||
|
||||
fn format_currency(value: &str, config: &dreport_core::models::FormatConfig) -> String {
|
||||
if let Ok(n) = value.parse::<f64>() {
|
||||
let abs = n.abs();
|
||||
let integer = abs.floor() as i64;
|
||||
let frac = ((abs - abs.floor()) * 100.0).round() as i64;
|
||||
use dexpr::Decimal;
|
||||
|
||||
let int_str = format_with_thousands(integer, &config.thousands_separator);
|
||||
let sign = if n < 0.0 { "-" } else { "" };
|
||||
if config.currency_position == "prefix" {
|
||||
format!("{}{}{}{}{:02}", config.currency_symbol, sign, int_str, config.decimal_separator, frac)
|
||||
} else {
|
||||
format!("{}{}{}{:02} {}", sign, int_str, config.decimal_separator, frac, config.currency_symbol)
|
||||
}
|
||||
let Ok(d) = value.parse::<Decimal>() else {
|
||||
return value.to_string();
|
||||
};
|
||||
|
||||
// Round to 2 decimal places using Decimal — no float precision loss
|
||||
// MidpointAwayFromZero: 1.005 → 1.01 (currency convention)
|
||||
let rounded = d.abs().round_dp_with_strategy(2, rust_decimal::RoundingStrategy::MidpointAwayFromZero);
|
||||
// Extract integer and fractional parts from the rounded Decimal
|
||||
let truncated = rounded.trunc();
|
||||
let frac_part = rounded - truncated;
|
||||
let integer = truncated.to_string().parse::<i64>().unwrap_or(0);
|
||||
let frac = (frac_part * Decimal::from(100)).trunc().to_string().parse::<i64>().unwrap_or(0);
|
||||
|
||||
let int_str = format_with_thousands(integer, &config.thousands_separator);
|
||||
let sign = if d.is_sign_negative() { "-" } else { "" };
|
||||
if config.currency_position == "prefix" {
|
||||
format!("{}{}{}{}{:02}", config.currency_symbol, sign, int_str, config.decimal_separator, frac)
|
||||
} else {
|
||||
value.to_string()
|
||||
format!("{}{}{}{:02} {}", sign, int_str, config.decimal_separator, frac, config.currency_symbol)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,4 +272,49 @@ mod tests {
|
||||
"2880"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_basic() {
|
||||
let config = dreport_core::models::FormatConfig::default();
|
||||
assert_eq!(format_currency("1500", &config), "1.500,00 ₺");
|
||||
assert_eq!(format_currency("0", &config), "0,00 ₺");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_fractional() {
|
||||
let config = dreport_core::models::FormatConfig::default();
|
||||
assert_eq!(format_currency("19.99", &config), "19,99 ₺");
|
||||
assert_eq!(format_currency("100.50", &config), "100,50 ₺");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_rounding_edge_case() {
|
||||
// 1.005 should round to 1.01, not 1.00
|
||||
let config = dreport_core::models::FormatConfig::default();
|
||||
let result = format_currency("1.005", &config);
|
||||
assert_eq!(result, "1,01 ₺");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_negative() {
|
||||
let config = dreport_core::models::FormatConfig::default();
|
||||
assert_eq!(format_currency("-250.75", &config), "-250,75 ₺");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_large_number() {
|
||||
let config = dreport_core::models::FormatConfig::default();
|
||||
assert_eq!(format_currency("1234567.89", &config), "1.234.567,89 ₺");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency_format_prefix_position() {
|
||||
let config = dreport_core::models::FormatConfig {
|
||||
currency_symbol: "$".to_string(),
|
||||
currency_position: "prefix".to_string(),
|
||||
thousands_separator: ",".to_string(),
|
||||
decimal_separator: ".".to_string(),
|
||||
};
|
||||
assert_eq!(format_currency("1500.25", &config), "$1,500.25");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +711,29 @@ fn render_line(
|
||||
surface.set_fill(None);
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ImageFormat {
|
||||
Png,
|
||||
Jpeg,
|
||||
Gif,
|
||||
WebP,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
fn detect_image_format(data: &[u8]) -> ImageFormat {
|
||||
if data.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
|
||||
ImageFormat::Png
|
||||
} else if data.starts_with(&[0xFF, 0xD8, 0xFF]) {
|
||||
ImageFormat::Jpeg
|
||||
} else if data.starts_with(b"GIF8") {
|
||||
ImageFormat::Gif
|
||||
} else if data.len() >= 12 && &data[8..12] == b"WEBP" {
|
||||
ImageFormat::WebP
|
||||
} else {
|
||||
ImageFormat::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fn render_image(
|
||||
surface: &mut krilla::surface::Surface<'_>,
|
||||
x: f32,
|
||||
@@ -735,16 +758,35 @@ fn render_image(
|
||||
return;
|
||||
};
|
||||
|
||||
// Tüm formatları image crate ile decode edip PNG'ye çevir (krilla JPEG desteği sınırlı)
|
||||
let png_data = match decode_to_png(&decoded) {
|
||||
Some(data) => data,
|
||||
None => {
|
||||
eprintln!("[dreport] Image decode/re-encode hatası, ham veri deneniyor");
|
||||
decoded
|
||||
// Magic bytes ile format tespit et, krilla'nın native desteğini kullan
|
||||
let img_result = match detect_image_format(&decoded) {
|
||||
ImageFormat::Png => krilla::image::Image::from_png(decoded.into(), true),
|
||||
ImageFormat::Jpeg => krilla::image::Image::from_jpeg(decoded.into(), true),
|
||||
ImageFormat::Gif => krilla::image::Image::from_gif(decoded.into(), true),
|
||||
ImageFormat::WebP => krilla::image::Image::from_webp(decoded.into(), true),
|
||||
ImageFormat::Unknown => {
|
||||
match decode_to_png(&decoded) {
|
||||
Some(png_data) => krilla::image::Image::from_png(png_data.into(), true),
|
||||
None => {
|
||||
eprintln!("[dreport] Image decode/re-encode hatası");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
embed_png(surface, x, y, w, h, &png_data);
|
||||
let Ok(img) = img_result else {
|
||||
eprintln!("[dreport] Image krilla embed hatası");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(size) = Size::from_wh(w, h) else {
|
||||
return;
|
||||
};
|
||||
|
||||
surface.push_transform(&Transform::from_translate(x, y));
|
||||
surface.draw_image(img, size);
|
||||
surface.pop();
|
||||
}
|
||||
|
||||
fn render_barcode(
|
||||
@@ -1391,4 +1433,37 @@ mod tests {
|
||||
std::fs::write(&out_path, &pdf).unwrap();
|
||||
println!("Full pipeline PDF: {}", out_path.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_png() {
|
||||
let data = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
|
||||
assert_eq!(detect_image_format(&data), ImageFormat::Png);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_jpeg() {
|
||||
let data = [0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10];
|
||||
assert_eq!(detect_image_format(&data), ImageFormat::Jpeg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_gif() {
|
||||
assert_eq!(detect_image_format(b"GIF89a..."), ImageFormat::Gif);
|
||||
assert_eq!(detect_image_format(b"GIF87a..."), ImageFormat::Gif);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_webp() {
|
||||
// RIFF____WEBP
|
||||
let mut data = vec![0u8; 12];
|
||||
data[0..4].copy_from_slice(b"RIFF");
|
||||
data[8..12].copy_from_slice(b"WEBP");
|
||||
assert_eq!(detect_image_format(&data), ImageFormat::WebP);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_unknown() {
|
||||
assert_eq!(detect_image_format(&[0x00, 0x01, 0x02]), ImageFormat::Unknown);
|
||||
assert_eq!(detect_image_format(&[]), ImageFormat::Unknown);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
use dreport_core::models::*;
|
||||
|
||||
use crate::data_resolve::ResolvedData;
|
||||
use crate::text_measure::TextMeasurer;
|
||||
|
||||
/// Cache for expanded table containers.
|
||||
/// Key: hash of (table JSON + resolved rows + available width).
|
||||
pub type TableExpandCache = HashMap<u64, ContainerElement>;
|
||||
|
||||
fn table_cache_key(
|
||||
table: &RepeatingTableElement,
|
||||
rows: &[Vec<String>],
|
||||
available_width_mm: f64,
|
||||
) -> u64 {
|
||||
let mut hasher = std::hash::DefaultHasher::new();
|
||||
// Serialize table definition (id, columns, style, etc.)
|
||||
if let Ok(json) = serde_json::to_string(table) {
|
||||
json.hash(&mut hasher);
|
||||
}
|
||||
// Hash resolved row data
|
||||
for row in rows {
|
||||
for cell in row {
|
||||
cell.hash(&mut hasher);
|
||||
}
|
||||
row.len().hash(&mut hasher);
|
||||
}
|
||||
rows.len().hash(&mut hasher);
|
||||
// Hash available width (as bits to avoid float hashing issues)
|
||||
available_width_mm.to_bits().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Her auto sütun için header + tüm data satırlarındaki en geniş text'i ölç,
|
||||
/// doğal genişliklerini Fixed olarak ata.
|
||||
/// Fr sütunları olduğu gibi bırak (kalan alanı taffy dağıtır).
|
||||
@@ -138,6 +168,29 @@ fn compute_auto_column_widths(
|
||||
result
|
||||
}
|
||||
|
||||
/// Cache-aware table expansion.
|
||||
/// Verilen cache'e bakar, hit varsa clone döner. Miss'te expand edip cache'e yazar.
|
||||
pub fn expand_table_cached(
|
||||
table: &RepeatingTableElement,
|
||||
resolved: &ResolvedData,
|
||||
measurer: &mut TextMeasurer,
|
||||
available_width_mm: f64,
|
||||
cache: &mut TableExpandCache,
|
||||
) -> ContainerElement {
|
||||
let rows = resolved.tables.get(&table.id)
|
||||
.map(|t| t.rows.as_slice())
|
||||
.unwrap_or(&[]);
|
||||
let key = table_cache_key(table, rows, available_width_mm);
|
||||
|
||||
if let Some(cached) = cache.get(&key) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let result = expand_table(table, resolved, measurer, available_width_mm);
|
||||
cache.insert(key, result.clone());
|
||||
result
|
||||
}
|
||||
|
||||
/// RepeatingTable element'ini bir container ağacına expand eder.
|
||||
/// Tablo → column container (header row + data rows)
|
||||
/// Her row → row container (cell'ler → static_text)
|
||||
@@ -708,4 +761,61 @@ mod tests {
|
||||
_ => panic!("Expected Container"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_cache_hit() {
|
||||
let table = make_table(2);
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["A".to_string(), "1".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
let mut cache = TableExpandCache::new();
|
||||
|
||||
// First call — cache miss
|
||||
let result1 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
// Second call — same inputs — cache hit
|
||||
let result2 = expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||
assert_eq!(cache.len(), 1); // no new entry
|
||||
assert_eq!(result1.id, result2.id);
|
||||
assert_eq!(result1.children.len(), result2.children.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_cache_miss_on_data_change() {
|
||||
let table = make_table(2);
|
||||
let resolved1 = make_resolved("tbl", vec![
|
||||
vec!["A".to_string(), "1".to_string()],
|
||||
]);
|
||||
let resolved2 = make_resolved("tbl", vec![
|
||||
vec!["B".to_string(), "2".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
let mut cache = TableExpandCache::new();
|
||||
|
||||
expand_table_cached(&table, &resolved1, &mut measurer, 180.0, &mut cache);
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
// Different data — cache miss
|
||||
expand_table_cached(&table, &resolved2, &mut measurer, 180.0, &mut cache);
|
||||
assert_eq!(cache.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_table_cache_miss_on_width_change() {
|
||||
let table = make_table(2);
|
||||
let resolved = make_resolved("tbl", vec![
|
||||
vec!["A".to_string(), "1".to_string()],
|
||||
]);
|
||||
let mut measurer = make_measurer();
|
||||
let mut cache = TableExpandCache::new();
|
||||
|
||||
expand_table_cached(&table, &resolved, &mut measurer, 180.0, &mut cache);
|
||||
assert_eq!(cache.len(), 1);
|
||||
|
||||
// Different available width — cache miss
|
||||
expand_table_cached(&table, &resolved, &mut measurer, 160.0, &mut cache);
|
||||
assert_eq!(cache.len(), 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use taffy::prelude::*;
|
||||
|
||||
use crate::data_resolve::ResolvedData;
|
||||
use crate::sizing::{self, mm_to_pt, pt_to_mm};
|
||||
use crate::table_layout;
|
||||
use crate::table_layout::{self, TableExpandCache};
|
||||
use crate::text_measure::TextMeasurer;
|
||||
use crate::{ElementLayout, LayoutError, LayoutResult, ResolvedContent, ResolvedStyle};
|
||||
|
||||
@@ -55,6 +55,7 @@ pub fn compute(
|
||||
let mut taffy = TaffyTree::<MeasureContext>::new();
|
||||
taffy.disable_rounding();
|
||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||
let mut table_cache = TableExpandCache::new();
|
||||
|
||||
let page_width_mm = template.page.width;
|
||||
let root_node = build_container(
|
||||
@@ -65,6 +66,7 @@ pub fn compute(
|
||||
None,
|
||||
measurer,
|
||||
page_width_mm,
|
||||
&mut table_cache,
|
||||
)?;
|
||||
|
||||
// Sayfa wrapper: sayfa genişliğinde ama yükseklik sınırsız (auto)
|
||||
@@ -131,8 +133,9 @@ fn compute_section(
|
||||
let mut taffy = TaffyTree::<MeasureContext>::new();
|
||||
taffy.disable_rounding();
|
||||
let mut node_map: HashMap<NodeId, NodeInfo> = HashMap::new();
|
||||
let mut table_cache = TableExpandCache::new();
|
||||
|
||||
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None, measurer, page_width_mm)?;
|
||||
let section_node = build_container(container, &mut taffy, &mut node_map, resolved, None, measurer, page_width_mm, &mut table_cache)?;
|
||||
|
||||
let wrapper_style = Style {
|
||||
display: Display::Flex,
|
||||
@@ -214,6 +217,7 @@ fn build_container(
|
||||
parent_direction: Option<&str>,
|
||||
measurer: &mut TextMeasurer,
|
||||
page_width_mm: f64,
|
||||
table_cache: &mut TableExpandCache,
|
||||
) -> Result<NodeId, LayoutError> {
|
||||
let style = sizing::container_to_style(el, parent_direction);
|
||||
let direction = el.direction.as_str();
|
||||
@@ -232,7 +236,7 @@ fn build_container(
|
||||
let mut children_ids = Vec::new();
|
||||
|
||||
for child in &el.children {
|
||||
let child_node = build_element(child, taffy, node_map, resolved, Some(direction), measurer, content_width_mm)?;
|
||||
let child_node = build_element(child, taffy, node_map, resolved, Some(direction), measurer, content_width_mm, table_cache)?;
|
||||
child_nodes.push(child_node);
|
||||
children_ids.push(child.id().to_string());
|
||||
}
|
||||
@@ -269,10 +273,11 @@ fn build_element(
|
||||
parent_direction: Option<&str>,
|
||||
measurer: &mut TextMeasurer,
|
||||
page_width_mm: f64,
|
||||
table_cache: &mut TableExpandCache,
|
||||
) -> Result<NodeId, LayoutError> {
|
||||
match el {
|
||||
TemplateElement::Container(e) => {
|
||||
build_container(e, taffy, node_map, resolved, parent_direction, measurer, page_width_mm)
|
||||
build_container(e, taffy, node_map, resolved, parent_direction, measurer, page_width_mm, table_cache)
|
||||
}
|
||||
TemplateElement::StaticText(e) => build_text_leaf(
|
||||
taffy,
|
||||
@@ -440,8 +445,8 @@ fn build_element(
|
||||
Ok(node)
|
||||
}
|
||||
TemplateElement::RepeatingTable(e) => {
|
||||
// Tabloyu container ağacına expand et (measurer ile auto sütun genişlikleri hesaplanır)
|
||||
let expanded = table_layout::expand_table(e, resolved, measurer, page_width_mm);
|
||||
// Tabloyu container ağacına expand et (cache ile)
|
||||
let expanded = table_layout::expand_table_cached(e, resolved, measurer, page_width_mm, table_cache);
|
||||
|
||||
// Expand edilmiş tablo cell'lerinin text'lerini resolved'a ekle
|
||||
// (expand_table StaticText'ler üretir, bunların text'leri zaten content'te)
|
||||
@@ -460,6 +465,7 @@ fn build_element(
|
||||
parent_direction,
|
||||
measurer,
|
||||
page_width_mm,
|
||||
table_cache,
|
||||
)
|
||||
}
|
||||
TemplateElement::Shape(e) => {
|
||||
|
||||
21
layout-engine/tests/common/mod.rs
Normal file
21
layout-engine/tests/common/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use dreport_layout::FontData;
|
||||
|
||||
pub fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let data = std::fs::read(&path).unwrap();
|
||||
if let Some(fd) = FontData::from_bytes(data) {
|
||||
fonts.push(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
@@ -8,27 +8,10 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use dreport_core::models::*;
|
||||
use dreport_layout::{compute_layout, FontData, LayoutResult, ResolvedContent};
|
||||
use dreport_layout::{compute_layout, LayoutResult, ResolvedContent};
|
||||
|
||||
fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let data = std::fs::read(&path).unwrap();
|
||||
if let Some(fd) = FontData::from_bytes(data) {
|
||||
fonts.push(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
mod common;
|
||||
use common::load_test_fonts;
|
||||
|
||||
fn base_template() -> Template {
|
||||
Template {
|
||||
|
||||
@@ -1,27 +1,10 @@
|
||||
//! Integration tests for the layout engine's compute_layout() public API.
|
||||
|
||||
use dreport_core::models::*;
|
||||
use dreport_layout::{compute_layout, FontData, LayoutResult};
|
||||
use dreport_layout::{compute_layout, LayoutResult};
|
||||
|
||||
fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let data = std::fs::read(&path).unwrap();
|
||||
if let Some(fd) = FontData::from_bytes(data) {
|
||||
fonts.push(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
mod common;
|
||||
use common::load_test_fonts;
|
||||
|
||||
fn simple_template() -> Template {
|
||||
Template {
|
||||
|
||||
@@ -4,27 +4,10 @@
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
use dreport_core::models::*;
|
||||
use dreport_layout::{compute_layout, FontData};
|
||||
use dreport_layout::compute_layout;
|
||||
|
||||
fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in std::fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let data = std::fs::read(&path).unwrap();
|
||||
if let Some(fd) = FontData::from_bytes(data) {
|
||||
fonts.push(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
mod common;
|
||||
use common::load_test_fonts;
|
||||
|
||||
fn simple_template() -> Template {
|
||||
Template {
|
||||
@@ -320,10 +303,8 @@ fn test_page_break_produces_multiple_pages() {
|
||||
let pdf_bytes = dreport_layout::pdf_render::render_pdf(&layout, &fonts).unwrap();
|
||||
assert!(pdf_bytes.starts_with(b"%PDF"));
|
||||
|
||||
// Write PDF for manual inspection
|
||||
let out_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent().unwrap()
|
||||
.join("test_page_break.pdf");
|
||||
// Write PDF to temp dir for manual inspection
|
||||
let out_path = std::env::temp_dir().join("dreport_test_page_break.pdf");
|
||||
std::fs::write(&out_path, &pdf_bytes).unwrap();
|
||||
println!("Wrote: {}", out_path.display());
|
||||
}
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
|
||||
#![cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
mod common;
|
||||
|
||||
mod visual {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use dreport_core::models::Template;
|
||||
use dreport_layout::{compute_layout, FontData, ResolvedContent};
|
||||
use dreport_layout::{compute_layout, ResolvedContent};
|
||||
use dreport_layout::pdf_render::render_pdf;
|
||||
|
||||
use crate::common::load_test_fonts;
|
||||
|
||||
fn fixtures_dir() -> std::path::PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
|
||||
}
|
||||
@@ -24,26 +28,6 @@ mod visual {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots")
|
||||
}
|
||||
|
||||
fn load_test_fonts() -> Vec<FontData> {
|
||||
let font_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("backend/fonts");
|
||||
|
||||
let mut fonts = Vec::new();
|
||||
for entry in fs::read_dir(&font_dir).expect("backend/fonts directory not found") {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|e| e == "ttf") {
|
||||
let data = fs::read(&path).unwrap();
|
||||
if let Some(fd) = FontData::from_bytes(data) {
|
||||
fonts.push(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
fonts
|
||||
}
|
||||
|
||||
fn generate_test_pdf(template_name: &str, data_name: &str) -> Vec<u8> {
|
||||
let template_json = fs::read_to_string(fixtures_dir().join(template_name)).unwrap();
|
||||
let data_json = fs::read_to_string(fixtures_dir().join(data_name)).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user