Skip to main content

bitmapfont_creator/
xml_generator.rs

1//! XML generation module for Phaser BitmapText format
2
3use crate::error::Result;
4use crate::packer::PackedChar;
5
6/// Character metrics for XML output
7#[derive(Debug, Clone)]
8pub struct CharMetrics {
9    /// The character
10    pub char: String,
11    /// X position in atlas
12    pub x: u32,
13    /// Y position in atlas
14    pub y: u32,
15    /// Width (original, without padding)
16    pub width: u32,
17    /// Height (original, without padding)
18    pub height: u32,
19    /// X offset
20    pub xoffset: i32,
21    /// Y offset
22    pub yoffset: i32,
23    /// X advance (horizontal step)
24    pub xadvance: u32,
25    /// Padding
26    pub padding: u32,
27}
28
29impl From<PackedChar> for CharMetrics {
30    fn from(packed: PackedChar) -> Self {
31        // x, y point directly to the character position in atlas (with padding offset)
32        // Padding is only used for texture atlas spacing, not for rendering
33        CharMetrics {
34            char: packed.char,
35            x: packed.x + packed.padding,  // Point to actual char position (skip padding)
36            y: packed.y + packed.padding,
37            width: packed.original_width,
38            height: packed.original_height,
39            xoffset: 0,  // No offset needed
40            yoffset: 0,  // No offset needed
41            xadvance: packed.original_width,  // Only character width, no padding
42            padding: packed.padding,
43        }
44    }
45}
46
47/// Generate Phaser-compatible XML font file
48pub fn generate_xml(
49    font_name: &str,
50    atlas_width: u32,
51    atlas_height: u32,
52    chars: &[CharMetrics],
53) -> Result<String> {
54    let mut xml = String::new();
55
56    // XML header
57    xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
58
59    // Font element
60    xml.push_str(&format!(
61        "<font>\n"
62    ));
63
64    // Info element - use first char's padding for the font info
65    let padding = chars.first().map(|c| c.padding).unwrap_or(4);
66    xml.push_str(&format!(
67        "  <info face=\"{}\" size=\"32\" bold=\"0\" italic=\"0\" charset=\"\" unicode=\"1\" stretchH=\"100\" smooth=\"1\" aa=\"1\" padding=\"{},{},{},{}\" spacing=\"1,1\" outline=\"0\"/>\n",
68        escape_xml(font_name),
69        padding, padding, padding, padding
70    ));
71
72    // Calculate lineHeight and base from actual character heights
73    let max_height = chars.iter().map(|c| c.height).max().unwrap_or(32);
74    let line_height = max_height;
75    let base = max_height;  // Base is typically the same as line height for bitmap fonts
76
77    // Common element
78    xml.push_str(&format!(
79        "  <common lineHeight=\"{}\" base=\"{}\" scaleW=\"{}\" scaleH=\"{}\" pages=\"1\" packed=\"0\" alphaChnl=\"0\" redChnl=\"4\" greenChnl=\"4\" blueChnl=\"4\"/>\n",
80        line_height, base, atlas_width, atlas_height
81    ));
82
83    // Pages element
84    xml.push_str("  <pages>\n");
85    xml.push_str(&format!("    <page id=\"0\" file=\"{}.png\"/>\n", escape_xml(font_name)));
86    xml.push_str("  </pages>\n");
87
88    // Chars element
89    xml.push_str(&format!("  <chars count=\"{}\">\n", chars.len()));
90
91    for char_metrics in chars {
92        // Get character code (use first char of string)
93        let char_code = char_metrics.char.chars().next()
94            .map(|c| c as u32)
95            .unwrap_or(0);
96
97        xml.push_str(&format!(
98            "    <char id=\"{}\" x=\"{}\" y=\"{}\" width=\"{}\" height=\"{}\" xoffset=\"{}\" yoffset=\"{}\" xadvance=\"{}\" page=\"0\" chnl=\"15\"/>\n",
99            char_code,
100            char_metrics.x,
101            char_metrics.y,
102            char_metrics.width,
103            char_metrics.height,
104            char_metrics.xoffset,
105            char_metrics.yoffset,
106            char_metrics.xadvance
107        ));
108    }
109
110    xml.push_str("  </chars>\n");
111    xml.push_str("</font>\n");
112
113    Ok(xml)
114}
115
116/// Escape special XML characters
117fn escape_xml(s: &str) -> String {
118    s.replace('&', "&amp;")
119        .replace('<', "&lt;")
120        .replace('>', "&gt;")
121        .replace('"', "&quot;")
122        .replace('\'', "&apos;")
123}