mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
visual testing & renderer improvement
This commit is contained in:
@@ -84,9 +84,9 @@ function shapeStyle(el: ElementLayout): Record<string, string> {
|
|||||||
function lineStyle(el: ElementLayout): Record<string, string> {
|
function lineStyle(el: ElementLayout): Record<string, string> {
|
||||||
const st = el.style
|
const st = el.style
|
||||||
return {
|
return {
|
||||||
borderTop: `${(st.strokeWidth ?? 0.5) * props.scale}px solid ${st.strokeColor ?? '#000'}`,
|
backgroundColor: st.strokeColor ?? '#000',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '0',
|
height: '100%',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +96,17 @@ async function renderBarcodeToCanvas(canvas: HTMLCanvasElement, format: string,
|
|||||||
if (!value || !generateBarcode) return
|
if (!value || !generateBarcode) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// WASM'dan yüksek çözünürlüklü pixel verisi al
|
// Eleman boyutlarından piksel boyutlarını hesapla (PDF ile aynı mantık: pt * 4)
|
||||||
// QR her zaman kare
|
// data-el-w ve data-el-h mm cinsinden, MM_TO_PT = 72/25.4 = 2.83465
|
||||||
|
const elWmm = parseFloat(canvas.dataset.elW || '30')
|
||||||
|
const elHmm = parseFloat(canvas.dataset.elH || '15')
|
||||||
|
const MM_TO_PT = 72 / 25.4
|
||||||
const isQr = format === 'qr'
|
const isQr = format === 'qr'
|
||||||
const size = isQr ? 300 : 400
|
const wPt = elWmm * MM_TO_PT
|
||||||
const height = isQr ? 300 : 150
|
const hPt = elHmm * MM_TO_PT
|
||||||
const result = await generateBarcode(format, value, size, height, isQr ? false : includeText)
|
const size = Math.max(1, Math.round(wPt * 4))
|
||||||
|
const barcodeHeight = Math.max(1, Math.round(hPt * 4))
|
||||||
|
const result = await generateBarcode(format, value, size, barcodeHeight, isQr ? false : includeText)
|
||||||
if (!result) return
|
if (!result) return
|
||||||
|
|
||||||
// Canvas boyutlarını WASM çıktısına ayarla (crisp rendering)
|
// Canvas boyutlarını WASM çıktısına ayarla (crisp rendering)
|
||||||
@@ -248,6 +253,8 @@ watch(
|
|||||||
:data-format="el.content.format"
|
:data-format="el.content.format"
|
||||||
:data-value="el.content.value"
|
:data-value="el.content.value"
|
||||||
:data-include-text="el.style.barcodeIncludeText ?? (el.content.format === 'ean13' || el.content.format === 'ean8')"
|
:data-include-text="el.style.barcodeIncludeText ?? (el.content.format === 'ean13' || el.content.format === 'ean8')"
|
||||||
|
:data-el-w="el.width_mm"
|
||||||
|
:data-el-h="el.height_mm"
|
||||||
:style="{ width: '100%', height: '100%', display: 'block' }"
|
:style="{ width: '100%', height: '100%', display: 'block' }"
|
||||||
/>
|
/>
|
||||||
<div v-else class="layout-el__placeholder">
|
<div v-else class="layout-el__placeholder">
|
||||||
|
|||||||
@@ -46,6 +46,52 @@ fn parse_color(hex: &str) -> rgb::Color {
|
|||||||
rgb::Color::new(r, g, b)
|
rgb::Color::new(r, g, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rounded rectangle path oluştur. radius 0 ise düz dikdörtgen.
|
||||||
|
fn build_rect_path(x: f32, y: f32, w: f32, h: f32, radius: f32) -> Option<krilla::geom::Path> {
|
||||||
|
let mut pb = PathBuilder::new();
|
||||||
|
if radius <= 0.0 {
|
||||||
|
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
||||||
|
pb.push_rect(rect);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Radius'u yarım kenar uzunluğuyla sınırla
|
||||||
|
let r = radius.min(w / 2.0).min(h / 2.0);
|
||||||
|
// Cubic bezier kappa faktörü (daire yaklaşımı)
|
||||||
|
let k = r * 0.5522848;
|
||||||
|
|
||||||
|
// Sağ üst köşeden başla, saat yönünde
|
||||||
|
pb.move_to(x + r, y);
|
||||||
|
pb.line_to(x + w - r, y);
|
||||||
|
pb.cubic_to(x + w - r + k, y, x + w, y + r - k, x + w, y + r);
|
||||||
|
pb.line_to(x + w, y + h - r);
|
||||||
|
pb.cubic_to(x + w, y + h - r + k, x + w - r + k, y + h, x + w - r, y + h);
|
||||||
|
pb.line_to(x + r, y + h);
|
||||||
|
pb.cubic_to(x + r - k, y + h, x, y + h - r + k, x, y + h - r);
|
||||||
|
pb.line_to(x, y + r);
|
||||||
|
pb.cubic_to(x, y + r - k, x + r - k, y, x + r, y);
|
||||||
|
pb.close();
|
||||||
|
}
|
||||||
|
pb.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ellipse path oluştur (4 cubic bezier ile)
|
||||||
|
fn build_ellipse_path(x: f32, y: f32, w: f32, h: f32) -> Option<krilla::geom::Path> {
|
||||||
|
let mut pb = PathBuilder::new();
|
||||||
|
let cx = x + w / 2.0;
|
||||||
|
let cy = y + h / 2.0;
|
||||||
|
let rx = w / 2.0;
|
||||||
|
let ry = h / 2.0;
|
||||||
|
let kx = rx * 0.5522848;
|
||||||
|
let ky = ry * 0.5522848;
|
||||||
|
pb.move_to(cx, cy - ry);
|
||||||
|
pb.cubic_to(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
|
||||||
|
pb.cubic_to(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
|
||||||
|
pb.cubic_to(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
|
||||||
|
pb.cubic_to(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
|
||||||
|
pb.close();
|
||||||
|
pb.finish()
|
||||||
|
}
|
||||||
|
|
||||||
fn fill_from_color(color: rgb::Color) -> Fill {
|
fn fill_from_color(color: rgb::Color) -> Fill {
|
||||||
Fill {
|
Fill {
|
||||||
paint: color.into(),
|
paint: color.into(),
|
||||||
@@ -54,18 +100,30 @@ fn fill_from_color(color: rgb::Color) -> Fill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Font metrikleri — ascender ve descender oranları (unitsPerEm'e bölünmüş)
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct FontMetrics {
|
||||||
|
/// sTypoAscender / unitsPerEm (pozitif, genelde 0.7–1.1)
|
||||||
|
ascender: f32,
|
||||||
|
/// |sTypoDescender| / unitsPerEm (pozitif, genelde 0.2–0.4)
|
||||||
|
descender: f32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Font koleksiyonu — family + weight + italic → KrillaFont mapping
|
/// Font koleksiyonu — family + weight + italic → KrillaFont mapping
|
||||||
struct FontCollection {
|
struct FontCollection {
|
||||||
/// (family_lower, is_bold, is_italic) → KrillaFont
|
/// (family_lower, is_bold, is_italic) → KrillaFont
|
||||||
fonts: HashMap<(String, bool, bool), KrillaFont>,
|
fonts: HashMap<(String, bool, bool), KrillaFont>,
|
||||||
/// Fallback font (ilk yüklenen regular)
|
/// Fallback font (ilk yüklenen regular)
|
||||||
default: Option<KrillaFont>,
|
default: Option<KrillaFont>,
|
||||||
|
/// Font metrikleri: (family_lower, is_bold) → FontMetrics
|
||||||
|
metrics: HashMap<(String, bool), FontMetrics>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontCollection {
|
impl FontCollection {
|
||||||
fn new(font_data: &[FontData]) -> Self {
|
fn new(font_data: &[FontData]) -> Self {
|
||||||
let mut fonts = HashMap::new();
|
let mut fonts = HashMap::new();
|
||||||
let mut default = None;
|
let mut default = None;
|
||||||
|
let mut metrics = HashMap::new();
|
||||||
|
|
||||||
for fd in font_data {
|
for fd in font_data {
|
||||||
let Some(font) = KrillaFont::new(
|
let Some(font) = KrillaFont::new(
|
||||||
@@ -84,6 +142,11 @@ impl FontCollection {
|
|||||||
default = Some(font.clone());
|
default = Some(font.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Font metriklerini OS/2 tablosundan oku
|
||||||
|
if let Some(m) = read_font_metrics(&fd.data) {
|
||||||
|
metrics.insert((family_lower.clone(), is_bold), m);
|
||||||
|
}
|
||||||
|
|
||||||
fonts.insert((family_lower.clone(), is_bold, is_italic), font);
|
fonts.insert((family_lower.clone(), is_bold, is_italic), font);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +157,7 @@ impl FontCollection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self { fonts, default }
|
Self { fonts, default, metrics }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get(&self, family: Option<&str>, weight: Option<&str>) -> Option<&KrillaFont> {
|
fn get(&self, family: Option<&str>, weight: Option<&str>) -> Option<&KrillaFont> {
|
||||||
@@ -107,6 +170,80 @@ impl FontCollection {
|
|||||||
.or_else(|| self.fonts.get(&(family_lower, false, false)))
|
.or_else(|| self.fonts.get(&(family_lower, false, false)))
|
||||||
.or(self.default.as_ref())
|
.or(self.default.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CSS line-height: 1.2 modeline uygun baseline offset hesapla (pt cinsinden).
|
||||||
|
///
|
||||||
|
/// CSS modeli:
|
||||||
|
/// content_height = (ascender + |descender|) * font_size
|
||||||
|
/// half_leading = (line_height - content_height) / 2
|
||||||
|
/// baseline_from_top = half_leading + ascender * font_size
|
||||||
|
fn baseline_offset(&self, family: Option<&str>, weight: Option<&str>, font_size: f32) -> f32 {
|
||||||
|
let is_bold = matches!(weight, Some("bold"));
|
||||||
|
let family_lower = family.unwrap_or("noto sans").to_lowercase();
|
||||||
|
|
||||||
|
let m = self.metrics
|
||||||
|
.get(&(family_lower.clone(), is_bold))
|
||||||
|
.or_else(|| self.metrics.get(&(family_lower, false)))
|
||||||
|
.copied();
|
||||||
|
|
||||||
|
match m {
|
||||||
|
Some(m) => {
|
||||||
|
let content_height = (m.ascender + m.descender) * font_size;
|
||||||
|
let line_height = font_size * 1.2;
|
||||||
|
let half_leading = (line_height - content_height) / 2.0;
|
||||||
|
half_leading + m.ascender * font_size
|
||||||
|
}
|
||||||
|
None => font_size * 0.8, // Fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TTF OS/2 tablosundan font metriklerini oku
|
||||||
|
fn read_font_metrics(data: &[u8]) -> Option<FontMetrics> {
|
||||||
|
let units_per_em = read_units_per_em(data)?;
|
||||||
|
if units_per_em == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let table_offset = find_os2_table(data)?;
|
||||||
|
// sTypoAscender: offset 68 (int16), sTypoDescender: offset 70 (int16, negatif)
|
||||||
|
if table_offset + 72 > data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ascender = i16::from_be_bytes([data[table_offset + 68], data[table_offset + 69]]);
|
||||||
|
let descender = i16::from_be_bytes([data[table_offset + 70], data[table_offset + 71]]);
|
||||||
|
|
||||||
|
Some(FontMetrics {
|
||||||
|
ascender: ascender as f32 / units_per_em as f32,
|
||||||
|
descender: descender.unsigned_abs() as f32 / units_per_em as f32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TTF head tablosundan unitsPerEm oku
|
||||||
|
fn read_units_per_em(data: &[u8]) -> Option<u16> {
|
||||||
|
if data.len() < 12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let num_tables = u16::from_be_bytes([data[4], data[5]]) as usize;
|
||||||
|
let mut offset = 12;
|
||||||
|
for _ in 0..num_tables {
|
||||||
|
if offset + 16 > data.len() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let tag = &data[offset..offset + 4];
|
||||||
|
if tag == b"head" {
|
||||||
|
let table_offset =
|
||||||
|
u32::from_be_bytes([data[offset + 8], data[offset + 9], data[offset + 10], data[offset + 11]])
|
||||||
|
as usize;
|
||||||
|
// unitsPerEm: head tablosunda offset 18 (uint16)
|
||||||
|
if table_offset + 20 <= data.len() {
|
||||||
|
return Some(u16::from_be_bytes([data[table_offset + 18], data[table_offset + 19]]));
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
offset += 16;
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TTF OS/2 tablosunun offset'ini bul
|
/// TTF OS/2 tablosunun offset'ini bul
|
||||||
@@ -228,7 +365,7 @@ fn render_element(
|
|||||||
render_text(surface, x, y, w, h, value, &el.style, fonts, measurer);
|
render_text(surface, x, y, w, h, value, &el.style, fonts, measurer);
|
||||||
}
|
}
|
||||||
ResolvedContent::Line => {
|
ResolvedContent::Line => {
|
||||||
render_line(surface, x, y, w, &el.style);
|
render_line(surface, x, y, w, h, &el.style);
|
||||||
}
|
}
|
||||||
ResolvedContent::Image { src } => {
|
ResolvedContent::Image { src } => {
|
||||||
render_image(surface, x, y, w, h, src);
|
render_image(surface, x, y, w, h, src);
|
||||||
@@ -275,61 +412,69 @@ fn render_shape(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shape_type = match content {
|
||||||
|
Some(ResolvedContent::Shape { shape_type }) => shape_type.as_str(),
|
||||||
|
_ => "rectangle",
|
||||||
|
};
|
||||||
|
|
||||||
|
let rect_radius = |s: &ResolvedStyle| -> f32 {
|
||||||
|
if shape_type == "rounded_rectangle" {
|
||||||
|
s.border_radius.map(|r| mm(r)).unwrap_or(mm(3.0))
|
||||||
|
} else {
|
||||||
|
s.border_radius.map(|r| mm(r)).unwrap_or(0.0)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if has_border {
|
||||||
|
let border_width = mm(style.border_width.unwrap_or(0.5));
|
||||||
|
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
||||||
|
let inset = border_width / 2.0;
|
||||||
|
|
||||||
|
// Fill + stroke tek path ile — anti-aliasing uyumu
|
||||||
if let Some(ref bg) = style.background_color {
|
if let Some(ref bg) = style.background_color {
|
||||||
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
||||||
} else {
|
} else {
|
||||||
surface.set_fill(None);
|
surface.set_fill(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if has_border {
|
|
||||||
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
|
||||||
let border_width = mm(style.border_width.unwrap_or(0.5));
|
|
||||||
surface.set_stroke(Some(Stroke {
|
surface.set_stroke(Some(Stroke {
|
||||||
paint: border_color.into(),
|
paint: border_color.into(),
|
||||||
width: border_width,
|
width: border_width,
|
||||||
opacity: NormalizedF32::ONE,
|
opacity: NormalizedF32::ONE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
} else {
|
|
||||||
surface.set_stroke(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let shape_type = match content {
|
|
||||||
Some(ResolvedContent::Shape { shape_type }) => shape_type.as_str(),
|
|
||||||
_ => "rectangle",
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = match shape_type {
|
let path = match shape_type {
|
||||||
"ellipse" => {
|
"ellipse" => build_ellipse_path(
|
||||||
let mut pb = PathBuilder::new();
|
x + inset, y + inset,
|
||||||
let cx = x + w / 2.0;
|
w - border_width, h - border_width,
|
||||||
let cy = y + h / 2.0;
|
),
|
||||||
let rx = w / 2.0;
|
|
||||||
let ry = h / 2.0;
|
|
||||||
// Approximate ellipse with 4 cubic bezier curves
|
|
||||||
let kx = rx * 0.5522848;
|
|
||||||
let ky = ry * 0.5522848;
|
|
||||||
pb.move_to(cx, cy - ry);
|
|
||||||
pb.cubic_to(cx + kx, cy - ry, cx + rx, cy - ky, cx + rx, cy);
|
|
||||||
pb.cubic_to(cx + rx, cy + ky, cx + kx, cy + ry, cx, cy + ry);
|
|
||||||
pb.cubic_to(cx - kx, cy + ry, cx - rx, cy + ky, cx - rx, cy);
|
|
||||||
pb.cubic_to(cx - rx, cy - ky, cx - kx, cy - ry, cx, cy - ry);
|
|
||||||
pb.close();
|
|
||||||
pb.finish()
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
// rectangle / rounded_rectangle
|
let radius = rect_radius(style);
|
||||||
let mut pb = PathBuilder::new();
|
build_rect_path(
|
||||||
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
x + inset, y + inset,
|
||||||
pb.push_rect(rect);
|
w - border_width, h - border_width,
|
||||||
}
|
(radius - inset).max(0.0),
|
||||||
pb.finish()
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(p) = path {
|
if let Some(p) = path {
|
||||||
surface.draw_path(&p);
|
surface.draw_path(&p);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Sadece fill, border yok
|
||||||
|
surface.set_fill(Some(fill_from_color(parse_color(
|
||||||
|
style.background_color.as_deref().unwrap_or("#ffffff"),
|
||||||
|
))));
|
||||||
|
surface.set_stroke(None);
|
||||||
|
|
||||||
|
let path = match shape_type {
|
||||||
|
"ellipse" => build_ellipse_path(x, y, w, h),
|
||||||
|
_ => build_rect_path(x, y, w, h, rect_radius(style)),
|
||||||
|
};
|
||||||
|
if let Some(p) = path {
|
||||||
|
surface.draw_path(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
surface.set_fill(None);
|
surface.set_fill(None);
|
||||||
surface.set_stroke(None);
|
surface.set_stroke(None);
|
||||||
@@ -346,8 +491,9 @@ fn render_checkbox(
|
|||||||
) {
|
) {
|
||||||
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#333333"));
|
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#333333"));
|
||||||
let border_width = mm(style.border_width.unwrap_or(0.3));
|
let border_width = mm(style.border_width.unwrap_or(0.3));
|
||||||
|
let inset = border_width / 2.0;
|
||||||
|
|
||||||
// Draw box outline
|
// Draw box outline (inset for CSS border-box match)
|
||||||
surface.set_fill(None);
|
surface.set_fill(None);
|
||||||
surface.set_stroke(Some(Stroke {
|
surface.set_stroke(Some(Stroke {
|
||||||
paint: border_color.into(),
|
paint: border_color.into(),
|
||||||
@@ -356,14 +502,11 @@ fn render_checkbox(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let rect_path = {
|
if let Some(p) = build_rect_path(
|
||||||
let mut pb = PathBuilder::new();
|
x + inset, y + inset,
|
||||||
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
w - border_width, h - border_width,
|
||||||
pb.push_rect(rect);
|
0.0,
|
||||||
}
|
) {
|
||||||
pb.finish()
|
|
||||||
};
|
|
||||||
if let Some(p) = rect_path {
|
|
||||||
surface.draw_path(&p);
|
surface.draw_path(&p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,40 +556,44 @@ fn render_container_bg(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill
|
let radius = style.border_radius.map(|r| mm(r)).unwrap_or(0.0);
|
||||||
|
|
||||||
|
if has_border {
|
||||||
|
let border_width = mm(style.border_width.unwrap_or(0.5));
|
||||||
|
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
||||||
|
let inset = border_width / 2.0;
|
||||||
|
|
||||||
|
// CSS border-box: stroke path'i border_width/2 içeri çek.
|
||||||
|
// Tek draw_path ile hem fill hem stroke çizerek anti-aliasing uyumunu sağla.
|
||||||
if let Some(ref bg) = style.background_color {
|
if let Some(ref bg) = style.background_color {
|
||||||
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
surface.set_fill(Some(fill_from_color(parse_color(bg))));
|
||||||
} else {
|
} else {
|
||||||
surface.set_fill(None);
|
surface.set_fill(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stroke
|
|
||||||
if has_border {
|
|
||||||
let border_color = parse_color(style.border_color.as_deref().unwrap_or("#000000"));
|
|
||||||
let border_width = mm(style.border_width.unwrap_or(0.5));
|
|
||||||
surface.set_stroke(Some(Stroke {
|
surface.set_stroke(Some(Stroke {
|
||||||
paint: border_color.into(),
|
paint: border_color.into(),
|
||||||
width: border_width,
|
width: border_width,
|
||||||
opacity: NormalizedF32::ONE,
|
opacity: NormalizedF32::ONE,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
} else {
|
if let Some(path) = build_rect_path(
|
||||||
surface.set_stroke(None);
|
x + inset, y + inset,
|
||||||
}
|
w - border_width, h - border_width,
|
||||||
|
(radius - inset).max(0.0),
|
||||||
let rect_path = {
|
) {
|
||||||
let mut pb = PathBuilder::new();
|
|
||||||
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
|
||||||
pb.push_rect(rect);
|
|
||||||
}
|
|
||||||
pb.finish()
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(path) = rect_path {
|
|
||||||
surface.draw_path(&path);
|
surface.draw_path(&path);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Sadece background, border yok
|
||||||
|
surface.set_fill(Some(fill_from_color(parse_color(
|
||||||
|
style.background_color.as_deref().unwrap_or("#ffffff"),
|
||||||
|
))));
|
||||||
|
surface.set_stroke(None);
|
||||||
|
if let Some(path) = build_rect_path(x, y, w, h, radius) {
|
||||||
|
surface.draw_path(&path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset
|
|
||||||
surface.set_fill(None);
|
surface.set_fill(None);
|
||||||
surface.set_stroke(None);
|
surface.set_stroke(None);
|
||||||
}
|
}
|
||||||
@@ -483,8 +630,12 @@ fn render_text(
|
|||||||
surface.set_fill(Some(fill_from_color(color)));
|
surface.set_fill(Some(fill_from_color(color)));
|
||||||
surface.set_stroke(None);
|
surface.set_stroke(None);
|
||||||
|
|
||||||
// Text baseline: y + ascent (yaklaşık font_size * 0.8)
|
// Text baseline: CSS line-height 1.2 modeline uygun hesapla
|
||||||
let baseline_y = y + font_size * 0.8;
|
let baseline_y = y + fonts.baseline_offset(
|
||||||
|
style.font_family.as_deref(),
|
||||||
|
style.font_weight.as_deref(),
|
||||||
|
font_size,
|
||||||
|
);
|
||||||
|
|
||||||
// Hizalama — cosmic-text ile text genişliğini ölçerek gerçek pozisyon hesapla
|
// Hizalama — cosmic-text ile text genişliğini ölçerek gerçek pozisyon hesapla
|
||||||
let text_x = match style.text_align.as_deref() {
|
let text_x = match style.text_align.as_deref() {
|
||||||
@@ -561,7 +712,7 @@ fn render_rich_text(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.font_size.map(|f| f as f32).unwrap_or(default_font_size))
|
.map(|s| s.font_size.map(|f| f as f32).unwrap_or(default_font_size))
|
||||||
.fold(0.0f32, f32::max);
|
.fold(0.0f32, f32::max);
|
||||||
let baseline_y = y + max_font_size * 0.8;
|
let baseline_y = y + fonts.baseline_offset(default_family, default_weight, max_font_size);
|
||||||
|
|
||||||
let mut cursor_x = line_start_x;
|
let mut cursor_x = line_start_x;
|
||||||
|
|
||||||
@@ -607,6 +758,7 @@ fn render_line(
|
|||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
w: f32,
|
w: f32,
|
||||||
|
h: f32,
|
||||||
style: &ResolvedStyle,
|
style: &ResolvedStyle,
|
||||||
) {
|
) {
|
||||||
let stroke_color = style
|
let stroke_color = style
|
||||||
@@ -614,29 +766,27 @@ fn render_line(
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(parse_color)
|
.map(parse_color)
|
||||||
.unwrap_or(rgb::Color::new(0, 0, 0));
|
.unwrap_or(rgb::Color::new(0, 0, 0));
|
||||||
let stroke_width = mm(style.stroke_width.unwrap_or(0.5));
|
|
||||||
|
|
||||||
surface.set_fill(None);
|
// Çizgiyi filled rectangle olarak çiz — CSS borderTop ile aynı davranış.
|
||||||
surface.set_stroke(Some(Stroke {
|
// Stroke kullanmak sub-pixel anti-aliasing farkları yaratır.
|
||||||
paint: stroke_color.into(),
|
surface.set_fill(Some(fill_from_color(stroke_color)));
|
||||||
width: stroke_width,
|
surface.set_stroke(None);
|
||||||
opacity: NormalizedF32::ONE,
|
|
||||||
..Default::default()
|
|
||||||
}));
|
|
||||||
|
|
||||||
let line_y = y + stroke_width / 2.0;
|
let rect_path = {
|
||||||
let path = {
|
|
||||||
let mut pb = PathBuilder::new();
|
let mut pb = PathBuilder::new();
|
||||||
pb.move_to(x, line_y);
|
// Eleman yüksekliği layout engine tarafından stroke_width olarak hesaplandı.
|
||||||
pb.line_to(x + w, line_y);
|
// Tüm eleman alanını dolduran ince dikdörtgen çiz.
|
||||||
|
if let Some(rect) = krilla::geom::Rect::from_xywh(x, y, w, h) {
|
||||||
|
pb.push_rect(rect);
|
||||||
|
}
|
||||||
pb.finish()
|
pb.finish()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(p) = path {
|
if let Some(p) = rect_path {
|
||||||
surface.draw_path(&p);
|
surface.draw_path(&p);
|
||||||
}
|
}
|
||||||
|
|
||||||
surface.set_stroke(None);
|
surface.set_fill(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_image(
|
fn render_image(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
BIN
test_output.pdf
BIN
test_output.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user