1use crate::error::Result;
2use crate::font::Font;
3use crate::stream::calculate_checksum;
4
5#[derive(Debug, Clone)]
7pub struct ValidationReport {
8 pub is_valid: bool,
9 pub errors: Vec<ValidationError>,
10 pub warnings: Vec<ValidationWarning>,
11}
12
13#[derive(Debug, Clone)]
14pub struct ValidationError {
15 pub error_type: ValidationErrorType,
16 pub message: String,
17 pub table: Option<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct ValidationWarning {
22 pub warning_type: ValidationWarningType,
23 pub message: String,
24 pub table: Option<String>,
25}
26
27#[derive(Debug, Clone, PartialEq)]
28pub enum ValidationErrorType {
29 InvalidSignature,
30 InvalidChecksum,
31 MissingRequiredTable,
32 InvalidTableStructure,
33 InvalidGlyphData,
34 InvalidCmapData,
35}
36
37#[derive(Debug, Clone, PartialEq)]
38pub enum ValidationWarningType {
39 NonStandardTable,
40 UnexpectedTableVersion,
41 DeprecatedTable,
42 PotentiallyProblematic,
43}
44
45impl Font {
46 pub fn validate(&self) -> Result<ValidationReport> {
48 let mut report = ValidationReport {
49 is_valid: true,
50 errors: Vec::new(),
51 warnings: Vec::new(),
52 };
53
54 if self.sfnt_version != 0x00010000 && self.sfnt_version != 0x4F54544F {
56 report.errors.push(ValidationError {
57 error_type: ValidationErrorType::InvalidSignature,
58 message: format!("Invalid SFNT version: {:#x}", self.sfnt_version),
59 table: None,
60 });
61 report.is_valid = false;
62 }
63
64 let required_tables = [
66 (b"cmap", "character to glyph mapping"),
67 (b"head", "font header"),
68 (b"hhea", "horizontal header"),
69 (b"hmtx", "horizontal metrics"),
70 (b"maxp", "maximum profile"),
71 (b"name", "naming table"),
72 (b"OS/2", "OS/2 and Windows metrics"),
73 (b"post", "PostScript information"),
74 ];
75
76 for (tag, description) in &required_tables {
77 if self.get_table_record(tag).is_none() {
78 report.errors.push(ValidationError {
79 error_type: ValidationErrorType::MissingRequiredTable,
80 message: format!("Missing required table: {} ({})",
81 String::from_utf8_lossy(tag.as_slice()), description),
82 table: Some(String::from_utf8_lossy(tag.as_slice()).to_string()),
83 });
84 report.is_valid = false;
85 }
86 }
87
88 if self.sfnt_version == 0x00010000 {
90 let ttf_required = [(b"glyf", "glyph data"), (b"loca", "index to location")];
91 for (tag, description) in &ttf_required {
92 if self.get_table_record(tag).is_none() {
93 report.errors.push(ValidationError {
94 error_type: ValidationErrorType::MissingRequiredTable,
95 message: format!("Missing required table for TrueType: {} ({})",
96 String::from_utf8_lossy(tag.as_slice()), description),
97 table: Some(String::from_utf8_lossy(tag.as_slice()).to_string()),
98 });
99 report.is_valid = false;
100 }
101 }
102 }
103
104 for record in &self.table_records {
106 if let Some(table_data) = self.get_table_data(&record.table_tag) {
107 let calculated_checksum = calculate_checksum(&table_data);
108
109 if record.table_tag == *b"head" {
111 } else if calculated_checksum != record.checksum {
114 report.warnings.push(ValidationWarning {
115 warning_type: ValidationWarningType::PotentiallyProblematic,
116 message: format!(
117 "Checksum mismatch for table {}: expected {:#x}, got {:#x}",
118 String::from_utf8_lossy(&record.table_tag),
119 record.checksum,
120 calculated_checksum
121 ),
122 table: Some(String::from_utf8_lossy(&record.table_tag).to_string()),
123 });
124 }
125 }
126 }
127
128 if let Ok(head) = self.head_table() {
130 if head.magic_number != 0x5F0F3CF5 {
131 report.errors.push(ValidationError {
132 error_type: ValidationErrorType::InvalidTableStructure,
133 message: format!("Invalid magic number in head table: {:#x}", head.magic_number),
134 table: Some("head".to_string()),
135 });
136 report.is_valid = false;
137 }
138
139 if head.units_per_em == 0 || head.units_per_em > 16384 {
141 report.warnings.push(ValidationWarning {
142 warning_type: ValidationWarningType::PotentiallyProblematic,
143 message: format!("Unusual units_per_em value: {}", head.units_per_em),
144 table: Some("head".to_string()),
145 });
146 }
147 }
148
149 if let Ok(maxp) = self.maxp_table() {
151 if maxp.num_glyphs == 0 {
152 report.errors.push(ValidationError {
153 error_type: ValidationErrorType::InvalidGlyphData,
154 message: "Font has no glyphs".to_string(),
155 table: Some("maxp".to_string()),
156 });
157 report.is_valid = false;
158 }
159 }
160
161 if let Ok(cmap) = self.cmap_table() {
163 if cmap.subtables.is_empty() {
164 report.errors.push(ValidationError {
165 error_type: ValidationErrorType::InvalidCmapData,
166 message: "Cmap table has no subtables".to_string(),
167 table: Some("cmap".to_string()),
168 });
169 report.is_valid = false;
170 }
171 }
172
173 let standard_tables = [
175 "cmap", "head", "hhea", "hmtx", "maxp", "name", "OS/2", "post",
176 "glyf", "loca", "kern", "GPOS", "GSUB", "BASE", "GDEF", "JSTF",
177 "vhea", "vmtx", "VORG", "CVT ", "fpgm", "prep", "gasp", "EBSC",
178 "trak", "ltsh", "PCLT", "VDMX", "hdmx", "CBDT", "CBLC", "COLR",
179 "CPAL", "sbix", "acnt", "avar", "bdat", "bloc", "bsln", "cvar",
180 "fdsc", "feat", "fmtx", "fvar", "gvar", "gcid", "glyf", "hvar",
181 "just", "lcar", "mort", "morx", "opbd", "prop", "trak", "Zapf",
182 "Silf", "Glat", "Gloc", "Feat", "Sill",
183 ];
184
185 for record in &self.table_records {
186 let tag_str = String::from_utf8_lossy(&record.table_tag).to_string();
187 if !standard_tables.contains(&tag_str.as_str()) {
188 report.warnings.push(ValidationWarning {
189 warning_type: ValidationWarningType::NonStandardTable,
190 message: format!("Non-standard table: {}", tag_str),
191 table: Some(tag_str),
192 });
193 }
194 }
195
196 Ok(report)
197 }
198
199 pub fn is_valid(&self) -> Result<bool> {
201 Ok(self.validate()?.is_valid)
202 }
203}
204
205impl ValidationReport {
206 pub fn summary(&self) -> String {
208 let mut summary = String::new();
209
210 if self.is_valid {
211 summary.push_str("✓ Font is valid\n");
212 } else {
213 summary.push_str("✗ Font is invalid\n");
214 }
215
216 if !self.errors.is_empty() {
217 summary.push_str(&format!("\nErrors ({}):\n", self.errors.len()));
218 for error in &self.errors {
219 let table = error.table.as_ref().map(|t| format!("[{}] ", t)).unwrap_or_default();
220 summary.push_str(&format!(" ✗ {}: {}\n", table, error.message));
221 }
222 }
223
224 if !self.warnings.is_empty() {
225 summary.push_str(&format!("\nWarnings ({}):\n", self.warnings.len()));
226 for warning in &self.warnings {
227 let table = warning.table.as_ref().map(|t| format!("[{}] ", t)).unwrap_or_default();
228 summary.push_str(&format!(" ⚠ {}: {}\n", table, warning.message));
229 }
230 }
231
232 summary
233 }
234}