Skip to main content

fop_layout/layout/properties/
extraction.rs

1//! Core trait extraction: extract_traits, font size resolution, text measurement
2
3use crate::area::{
4    BorderStyle, Direction, DisplayAlign, FontStretch, FontStyle, FontVariant, Span, TextTransform,
5    TraitSet, WritingMode,
6};
7use crate::layout::TextAlign;
8use fop_core::{PropertyId, PropertyList};
9use fop_types::Length;
10
11use super::misc::{extract_border_radius, extract_opacity, extract_overflow, OverflowBehavior};
12use super::spacing::{extract_letter_spacing, extract_line_height, extract_word_spacing};
13
14/// Extract font-size from a property value, resolving relative sizes
15///
16/// If the value is a relative font size (larger, smaller, or size keywords),
17/// it will be resolved using the parent font size from the property list.
18/// Returns None if the value cannot be converted to a length.
19pub(super) fn extract_font_size(
20    properties: &PropertyList,
21    value: &fop_core::PropertyValue,
22) -> Option<Length> {
23    // First check if it's a direct length
24    if let Some(len) = value.as_length() {
25        return Some(len);
26    }
27
28    // Check if it's a relative font size
29    if value.as_relative_font_size().is_some() {
30        // Get parent font size for resolution
31        let parent_font_size = if let Some(parent) = properties.parent() {
32            parent
33                .get(PropertyId::FontSize)
34                .ok()
35                .and_then(|v| extract_font_size(parent, &v))
36                .unwrap_or(Length::from_pt(12.0)) // Default to 12pt if no parent
37        } else {
38            Length::from_pt(12.0) // Default to 12pt if no parent
39        };
40
41        return value.resolve_font_size(parent_font_size);
42    }
43
44    None
45}
46
47/// Parse border style from string value
48pub(super) fn parse_border_style(s: Option<&str>) -> Option<BorderStyle> {
49    match s? {
50        "none" => Some(BorderStyle::None),
51        "solid" => Some(BorderStyle::Solid),
52        "dashed" => Some(BorderStyle::Dashed),
53        "dotted" => Some(BorderStyle::Dotted),
54        "double" => Some(BorderStyle::Double),
55        "groove" => Some(BorderStyle::Groove),
56        "ridge" => Some(BorderStyle::Ridge),
57        "inset" => Some(BorderStyle::Inset),
58        "outset" => Some(BorderStyle::Outset),
59        "hidden" => Some(BorderStyle::Hidden),
60        _ => None,
61    }
62}
63
64/// Extract rendering traits from a property list
65pub fn extract_traits(properties: &PropertyList) -> TraitSet {
66    let mut traits = TraitSet::default();
67
68    // Color
69    if let Ok(value) = properties.get(PropertyId::Color) {
70        traits.color = value.as_color();
71    }
72
73    // Background color
74    if let Ok(value) = properties.get(PropertyId::BackgroundColor) {
75        traits.background_color = value.as_color();
76    }
77
78    // Font family
79    if let Ok(value) = properties.get(PropertyId::FontFamily) {
80        traits.font_family = value.as_string().map(|s| s.to_string());
81    }
82
83    // Font size
84    if let Ok(value) = properties.get(PropertyId::FontSize) {
85        traits.font_size = extract_font_size(properties, &value);
86    }
87
88    // Font weight
89    if let Ok(value) = properties.get(PropertyId::FontWeight) {
90        if let Some(num) = value.as_integer() {
91            traits.font_weight = Some(num as u16);
92        }
93    }
94
95    // Font style
96    if let Ok(value) = properties.get(PropertyId::FontStyle) {
97        if let Some(enum_val) = value.as_enum() {
98            traits.font_style = match enum_val {
99                1 => Some(FontStyle::Italic),
100                2 => Some(FontStyle::Oblique),
101                _ => Some(FontStyle::Normal),
102            };
103        }
104    }
105
106    // Margins (for spacing calculations - reserved for future use)
107    let _margin_top = properties
108        .get(PropertyId::MarginTop)
109        .ok()
110        .and_then(|v| v.as_length())
111        .unwrap_or(Length::ZERO);
112    let _margin_right = properties
113        .get(PropertyId::MarginRight)
114        .ok()
115        .and_then(|v| v.as_length())
116        .unwrap_or(Length::ZERO);
117    let _margin_bottom = properties
118        .get(PropertyId::MarginBottom)
119        .ok()
120        .and_then(|v| v.as_length())
121        .unwrap_or(Length::ZERO);
122    let _margin_left = properties
123        .get(PropertyId::MarginLeft)
124        .ok()
125        .and_then(|v| v.as_length())
126        .unwrap_or(Length::ZERO);
127
128    // Padding
129    let padding_top = properties
130        .get(PropertyId::PaddingTop)
131        .ok()
132        .and_then(|v| v.as_length())
133        .unwrap_or(Length::ZERO);
134    let padding_right = properties
135        .get(PropertyId::PaddingRight)
136        .ok()
137        .and_then(|v| v.as_length())
138        .unwrap_or(Length::ZERO);
139    let padding_bottom = properties
140        .get(PropertyId::PaddingBottom)
141        .ok()
142        .and_then(|v| v.as_length())
143        .unwrap_or(Length::ZERO);
144    let padding_left = properties
145        .get(PropertyId::PaddingLeft)
146        .ok()
147        .and_then(|v| v.as_length())
148        .unwrap_or(Length::ZERO);
149
150    traits.padding = Some([padding_top, padding_right, padding_bottom, padding_left]);
151
152    // Border width
153    let border_top = properties
154        .get(PropertyId::BorderTopWidth)
155        .ok()
156        .and_then(|v| v.as_length())
157        .unwrap_or(Length::ZERO);
158    let border_right = properties
159        .get(PropertyId::BorderRightWidth)
160        .ok()
161        .and_then(|v| v.as_length())
162        .unwrap_or(Length::ZERO);
163    let border_bottom = properties
164        .get(PropertyId::BorderBottomWidth)
165        .ok()
166        .and_then(|v| v.as_length())
167        .unwrap_or(Length::ZERO);
168    let border_left = properties
169        .get(PropertyId::BorderLeftWidth)
170        .ok()
171        .and_then(|v| v.as_length())
172        .unwrap_or(Length::ZERO);
173
174    traits.border_width = Some([border_top, border_right, border_bottom, border_left]);
175
176    // Border colors
177    let border_top_color = properties
178        .get(PropertyId::BorderTopColor)
179        .ok()
180        .and_then(|v| v.as_color())
181        .unwrap_or(fop_types::Color::BLACK);
182    let border_right_color = properties
183        .get(PropertyId::BorderRightColor)
184        .ok()
185        .and_then(|v| v.as_color())
186        .unwrap_or(fop_types::Color::BLACK);
187    let border_bottom_color = properties
188        .get(PropertyId::BorderBottomColor)
189        .ok()
190        .and_then(|v| v.as_color())
191        .unwrap_or(fop_types::Color::BLACK);
192    let border_left_color = properties
193        .get(PropertyId::BorderLeftColor)
194        .ok()
195        .and_then(|v| v.as_color())
196        .unwrap_or(fop_types::Color::BLACK);
197
198    traits.border_color = Some([
199        border_top_color,
200        border_right_color,
201        border_bottom_color,
202        border_left_color,
203    ]);
204
205    // Border styles
206    let border_top_style = properties
207        .get(PropertyId::BorderTopStyle)
208        .ok()
209        .and_then(|v| parse_border_style(v.as_string()))
210        .unwrap_or(BorderStyle::Solid);
211    let border_right_style = properties
212        .get(PropertyId::BorderRightStyle)
213        .ok()
214        .and_then(|v| parse_border_style(v.as_string()))
215        .unwrap_or(BorderStyle::Solid);
216    let border_bottom_style = properties
217        .get(PropertyId::BorderBottomStyle)
218        .ok()
219        .and_then(|v| parse_border_style(v.as_string()))
220        .unwrap_or(BorderStyle::Solid);
221    let border_left_style = properties
222        .get(PropertyId::BorderLeftStyle)
223        .ok()
224        .and_then(|v| parse_border_style(v.as_string()))
225        .unwrap_or(BorderStyle::Solid);
226
227    traits.border_style = Some([
228        border_top_style,
229        border_right_style,
230        border_bottom_style,
231        border_left_style,
232    ]);
233
234    // Text alignment
235    if let Ok(value) = properties.get(PropertyId::TextAlign) {
236        if let Some(s) = value.as_string() {
237            traits.text_align = match s {
238                "left" => Some(TextAlign::Left),
239                "right" => Some(TextAlign::Right),
240                "center" => Some(TextAlign::Center),
241                "justify" => Some(TextAlign::Justify),
242                _ => None,
243            };
244        }
245    }
246
247    // Line height
248    traits.line_height = extract_line_height(properties);
249
250    // Letter spacing
251    traits.letter_spacing = extract_letter_spacing(properties);
252
253    // Word spacing
254    traits.word_spacing = extract_word_spacing(properties);
255
256    // Border radius
257    traits.border_radius = extract_border_radius(properties);
258
259    // Overflow
260    let overflow = extract_overflow(properties);
261    // Only set if not the default (Visible)
262    if overflow != OverflowBehavior::Visible {
263        traits.overflow = Some(overflow);
264    }
265
266    // Opacity
267    let opacity = extract_opacity(properties);
268    // Only set if not the default (1.0)
269    if (opacity - 1.0).abs() > f64::EPSILON {
270        traits.opacity = Some(opacity);
271    }
272
273    // Text transform
274    if let Ok(value) = properties.get(PropertyId::TextTransform) {
275        if let Some(s) = value.as_string() {
276            traits.text_transform = match s {
277                "uppercase" => Some(TextTransform::Uppercase),
278                "lowercase" => Some(TextTransform::Lowercase),
279                "capitalize" => Some(TextTransform::Capitalize),
280                _ => None,
281            };
282        }
283    }
284
285    // Font variant
286    if let Ok(value) = properties.get(PropertyId::FontVariant) {
287        if let Some(s) = value.as_string() {
288            traits.font_variant = match s {
289                "small-caps" => Some(FontVariant::SmallCaps),
290                _ => None,
291            };
292        }
293    }
294
295    // Display align
296    if let Ok(value) = properties.get(PropertyId::DisplayAlign) {
297        if let Some(s) = value.as_string() {
298            traits.display_align = match s {
299                "center" => Some(DisplayAlign::Center),
300                "after" => Some(DisplayAlign::After),
301                _ => Some(DisplayAlign::Before),
302            };
303        }
304    }
305
306    // Baseline shift (super/sub/length)
307    if let Ok(value) = properties.get(PropertyId::BaselineShift) {
308        if let Some(s) = value.as_string() {
309            traits.baseline_shift = match s {
310                "super" => Some(0.5), // 50% of font-size upward
311                "sub" => Some(-0.3),  // 30% of font-size downward
312                "baseline" | "0" => Some(0.0),
313                _ => None,
314            };
315        } else if let Some(len) = value.as_length() {
316            // Store as pt value; will be divided by font-size later
317            traits.baseline_shift = Some(len.to_pt() / 12.0); // relative to 12pt default
318        }
319    }
320
321    // Hyphenation
322    if let Ok(value) = properties.get(PropertyId::Hyphenate) {
323        if let Some(s) = value.as_string() {
324            traits.hyphenate = Some(s == "true");
325        } else if let Some(b) = value.as_boolean() {
326            traits.hyphenate = Some(b);
327        }
328    }
329
330    if let Ok(value) = properties.get(PropertyId::HyphenationPushCharacterCount) {
331        if let Some(i) = value.as_integer() {
332            traits.hyphenation_push_chars = Some(i as u32);
333        }
334    }
335
336    if let Ok(value) = properties.get(PropertyId::HyphenationRemainCharacterCount) {
337        if let Some(i) = value.as_integer() {
338            traits.hyphenation_remain_chars = Some(i as u32);
339        }
340    }
341
342    // Font stretch
343    if let Ok(value) = properties.get(PropertyId::FontStretch) {
344        if let Some(s) = value.as_string() {
345            traits.font_stretch = match s {
346                "ultra-condensed" => Some(FontStretch::UltraCondensed),
347                "extra-condensed" => Some(FontStretch::ExtraCondensed),
348                "condensed" => Some(FontStretch::Condensed),
349                "semi-condensed" => Some(FontStretch::SemiCondensed),
350                "semi-expanded" => Some(FontStretch::SemiExpanded),
351                "expanded" => Some(FontStretch::Expanded),
352                "extra-expanded" => Some(FontStretch::ExtraExpanded),
353                "ultra-expanded" => Some(FontStretch::UltraExpanded),
354                _ => None, // "normal" -> None (default)
355            };
356        }
357    }
358
359    // Text align last
360    if let Ok(value) = properties.get(PropertyId::TextAlignLast) {
361        if let Some(s) = value.as_string() {
362            traits.text_align_last = match s {
363                "left" | "start" => Some(TextAlign::Left),
364                "right" | "end" => Some(TextAlign::Right),
365                "center" => Some(TextAlign::Center),
366                "justify" => Some(TextAlign::Justify),
367                _ => None,
368            };
369        }
370    }
371
372    // Change bar color
373    if let Ok(value) = properties.get(PropertyId::ChangeBarColor) {
374        traits.change_bar_color = value.as_color();
375    }
376
377    // span property (for multi-column spanning)
378    if let Ok(value) = properties.get(PropertyId::Span) {
379        if value.as_string() == Some("all") {
380            traits.span = Span::All;
381        }
382    }
383
384    // role attribute for accessibility tagging
385    if let Ok(value) = properties.get(PropertyId::Role) {
386        traits.role = value.as_string().map(|s| s.to_string());
387    }
388
389    // xml:lang for language tagging
390    if let Ok(value) = properties.get(PropertyId::XmlLang) {
391        traits.xml_lang = value.as_string().map(|s| s.to_string());
392    }
393
394    // writing-mode
395    if let Ok(value) = properties.get(PropertyId::WritingMode) {
396        if let Some(s) = value.as_string() {
397            traits.writing_mode = match s {
398                "rl-tb" | "rl" => WritingMode::RlTb,
399                "tb-rl" | "tb" => WritingMode::TbRl,
400                "tb-lr" => WritingMode::TbLr,
401                _ => WritingMode::LrTb,
402            };
403        }
404    }
405
406    // direction
407    if let Ok(value) = properties.get(PropertyId::Direction) {
408        if let Some(s) = value.as_string() {
409            traits.direction = match s {
410                "rtl" => Direction::Rtl,
411                _ => Direction::Ltr,
412            };
413        }
414    }
415
416    traits
417}
418
419/// Estimate the pixel width of a text string given font properties.
420/// Uses approximate character width metrics — a full implementation would
421/// use actual TTF glyph advances via `ttf-parser`.
422///
423/// Returns estimated width in points.
424///
425/// # Examples
426///
427/// ```
428/// use fop_layout::measure_text_width;
429/// use fop_types::Length;
430///
431/// let width = measure_text_width("Hello", Length::from_pt(12.0), None);
432/// assert!(width.to_pt() > 0.0);
433/// ```
434pub fn measure_text_width(text: &str, font_size: Length, font_weight: Option<u16>) -> Length {
435    if text.is_empty() {
436        return Length::ZERO;
437    }
438
439    // Average character width approximation:
440    // - Normal weight: ~0.5 × font-size per character (for Latin)
441    // - Bold: ~0.55 × font-size
442    // - CJK characters: ~1.0 × font-size (full-width)
443    let weight_factor = match font_weight {
444        Some(w) if w >= 600 => 0.55,
445        _ => 0.50,
446    };
447
448    let mut total_width = 0.0_f64;
449    for ch in text.chars() {
450        let char_factor = if is_cjk(ch) {
451            1.0
452        } else if ch == ' ' {
453            0.25
454        } else if r#"iIl1!|.,;'"#.contains(ch) {
455            0.3
456        } else if "mMwW".contains(ch) {
457            0.7
458        } else {
459            weight_factor
460        };
461        total_width += char_factor;
462    }
463
464    Length::from_pt(total_width * font_size.to_pt())
465}
466
467/// Check if a character is a CJK (Chinese/Japanese/Korean) character
468fn is_cjk(ch: char) -> bool {
469    matches!(ch,
470        '\u{3000}'..='\u{9FFF}'   // CJK Unified Ideographs + common CJK
471        | '\u{AC00}'..='\u{D7AF}' // Korean Hangul syllables
472        | '\u{F900}'..='\u{FAFF}' // CJK Compatibility Ideographs
473        | '\u{FF00}'..='\u{FFEF}' // Halfwidth and Fullwidth Forms
474    )
475}