diff --git a/src/extractor.rs b/src/extractor.rs index d0c451e..b8800e9 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -26,6 +26,10 @@ pub struct TextItem { pub font_size: f32, /// Page number (1-indexed) pub page: u32, + /// Whether the font is bold + pub is_bold: bool, + /// Whether the font is italic + pub is_italic: bool, } /// A line of text (grouped text items) @@ -38,6 +42,81 @@ pub struct TextLine { impl TextLine { pub fn text(&self) -> String { + self.text_with_formatting(false, false) + } + + /// Get text with optional bold/italic markdown formatting + pub fn text_with_formatting(&self, format_bold: bool, format_italic: bool) -> String { + if !format_bold && !format_italic { + return self.text_plain(); + } + + let mut result = String::new(); + let mut current_bold = false; + let mut current_italic = false; + + for (i, item) in self.items.iter().enumerate() { + let text = item.text.as_str(); + let text_trimmed = text.trim(); + + // Skip empty items + if text_trimmed.is_empty() { + continue; + } + + // Determine spacing + let needs_space = if i == 0 || result.is_empty() { + false + } else { + let prev_item = &self.items[i - 1]; + self.needs_space_between(prev_item, item, &result) + }; + + // Check for style changes + let item_bold = format_bold && item.is_bold; + let item_italic = format_italic && item.is_italic; + + // Close previous styles if they change + if current_italic && !item_italic { + result.push('*'); + current_italic = false; + } + if current_bold && !item_bold { + result.push_str("**"); + current_bold = false; + } + + // Add space after closing markers if needed + if needs_space { + result.push(' '); + } + + // Open new styles + if item_bold && !current_bold { + result.push_str("**"); + current_bold = true; + } + if item_italic && !current_italic { + result.push('*'); + current_italic = true; + } + + result.push_str(text_trimmed); + } + + // Close any remaining open styles + if current_italic { + result.push('*'); + } + if current_bold { + result.push_str("**"); + } + + result + } + + /// Get plain text without formatting + fn text_plain(&self) -> String { let mut result = String::new(); for (i, item) in self.items.iter().enumerate() { let text = item.text.as_str(); @@ -45,53 +124,49 @@ impl TextLine { result.push_str(text); } else { let prev_item = &self.items[i - 1]; - - // Don't add space before/after hyphens for hyphenated words - let prev_ends_with_hyphen = result.ends_with('-'); - let curr_is_hyphen = text.trim() == "-"; - let curr_starts_with_hyphen = text.starts_with('-'); - - // Detect subscript/superscript: smaller font size and/or Y offset - // Subscripts/superscripts are typically 60-80% of normal font size - // and have a vertical offset of 1-3 points - let font_ratio = item.font_size / prev_item.font_size; - let reverse_font_ratio = prev_item.font_size / item.font_size; - let y_diff = (item.y - prev_item.y).abs(); - - // Current item is subscript/superscript (smaller than previous) - let is_sub_super = font_ratio < 0.85 && y_diff > 1.0; - // Previous item was subscript/superscript (returning to normal size) - let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0; - - // Use position-based spacing detection - // This is more reliable than character-case heuristics for determining - // whether text fragments should be joined (e.g., "CONST" + "ANCIA" → "CONSTANCIA") - let should_join = should_join_items(prev_item, item); - - // Check if space already exists to avoid double spaces - let prev_ends_with_space = result.ends_with(' '); - let curr_starts_with_space = text.starts_with(' '); - let space_already_exists = prev_ends_with_space || curr_starts_with_space; - - if prev_ends_with_hyphen - || curr_is_hyphen - || curr_starts_with_hyphen - || is_sub_super - || was_sub_super - || should_join - || space_already_exists - { - // No space for hyphenated words, subscript/superscript, closely positioned items, - // or when space already exists - result.push_str(text); - } else { + if self.needs_space_between(prev_item, item, &result) { result.push(' '); - result.push_str(text); } + result.push_str(text); } } result } + + /// Determine if a space is needed between two items + fn needs_space_between(&self, prev_item: &TextItem, item: &TextItem, result: &str) -> bool { + let text = item.text.as_str(); + + // Don't add space before/after hyphens for hyphenated words + let prev_ends_with_hyphen = result.ends_with('-'); + let curr_is_hyphen = text.trim() == "-"; + let curr_starts_with_hyphen = text.starts_with('-'); + + // Detect subscript/superscript: smaller font size and/or Y offset + let font_ratio = item.font_size / prev_item.font_size; + let reverse_font_ratio = prev_item.font_size / item.font_size; + let y_diff = (item.y - prev_item.y).abs(); + + let is_sub_super = font_ratio < 0.85 && y_diff > 1.0; + let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0; + + // Use position-based spacing detection + let should_join = should_join_items(prev_item, item); + + // Check if space already exists + let prev_ends_with_space = result.ends_with(' '); + let curr_starts_with_space = text.starts_with(' '); + let space_already_exists = prev_ends_with_space || curr_starts_with_space; + + // Add space unless one of these conditions applies + !(prev_ends_with_hyphen + || curr_is_hyphen + || curr_starts_with_hyphen + || is_sub_super + || was_sub_super + || should_join + || space_already_exists) + } } /// Determine if two adjacent text items should be joined without a space @@ -388,6 +463,11 @@ fn extract_page_text_items( // Transform position through CTM let combined = multiply_matrices(&text_matrix, &ctm); let (x, y) = (combined[4], combined[5]); + // Detect bold/italic from font name + let base_font = font_base_names + .get(¤t_font) + .map(|s| s.as_str()) + .unwrap_or(¤t_font); items.push(TextItem { text, x, @@ -397,6 +477,8 @@ fn extract_page_text_items( font: current_font.clone(), font_size: rendered_size, page: page_num, + is_bold: is_bold_font(base_font), + is_italic: is_italic_font(base_font), }); } } @@ -426,6 +508,11 @@ fn extract_page_text_items( // Transform position through CTM let combined = multiply_matrices(&text_matrix, &ctm); let (x, y) = (combined[4], combined[5]); + // Detect bold/italic from font name + let base_font = font_base_names + .get(¤t_font) + .map(|s| s.as_str()) + .unwrap_or(¤t_font); items.push(TextItem { text: combined_text, x, @@ -435,6 +522,8 @@ fn extract_page_text_items( font: current_font.clone(), font_size: rendered_size, page: page_num, + is_bold: is_bold_font(base_font), + is_italic: is_italic_font(base_font), }); } } @@ -460,6 +549,11 @@ fn extract_page_text_items( // Transform position through CTM let combined = multiply_matrices(&text_matrix, &ctm); let (x, y) = (combined[4], combined[5]); + // Detect bold/italic from font name + let base_font = font_base_names + .get(¤t_font) + .map(|s| s.as_str()) + .unwrap_or(¤t_font); items.push(TextItem { text, x, @@ -469,6 +563,8 @@ fn extract_page_text_items( font: current_font.clone(), font_size: rendered_size, page: page_num, + is_bold: is_bold_font(base_font), + is_italic: is_italic_font(base_font), }); } } @@ -503,6 +599,42 @@ fn effective_font_size(base_size: f32, text_matrix: &[f32; 6]) -> f32 { base_size * scale } +/// Detect if a font name indicates bold style +/// Common patterns: "Bold", "Bd", "Black", "Heavy", "Demi", "Semi" (semi-bold) +pub fn is_bold_font(font_name: &str) -> bool { + let lower = font_name.to_lowercase(); + + // Check for common bold indicators + // Note: Need to be careful with "Oblique" not matching "Obl" + false positive for bold + lower.contains("bold") + || lower.contains("-bd") + || lower.contains("_bd") + || lower.contains("black") + || lower.contains("heavy") + || lower.contains("demibold") + || lower.contains("semibold") + || lower.contains("demi-bold") + || lower.contains("semi-bold") + || lower.contains("extrabold") + || lower.contains("ultrabold") + || lower.contains("medium") && !lower.contains("mediumitalic") // Some fonts use Medium for semi-bold +} + +/// Detect if a font name indicates italic/oblique style +/// Common patterns: "Italic", "It", "Oblique", "Obl", "Slant", "Inclined" +pub fn is_italic_font(font_name: &str) -> bool { + let lower = font_name.to_lowercase(); + + // Check for common italic indicators + lower.contains("italic") + || lower.contains("oblique") + || lower.contains("-it") + || lower.contains("_it") + || lower.contains("slant") + || lower.contains("inclined") + || lower.contains("kursiv") // German for italic +} + /// Extract text from a text operand, handling encoding fn extract_text_from_operand( obj: &Object, @@ -881,6 +1013,8 @@ mod tests { font: "F1".into(), font_size: 12.0, page: 1, + is_bold: false, + is_italic: false, }, TextItem { text: "World".into(), @@ -891,6 +1025,8 @@ mod tests { font: "F1".into(), font_size: 12.0, page: 1, + is_bold: false, + is_italic: false, }, TextItem { text: "Next line".into(), @@ -901,6 +1037,8 @@ mod tests { font: "F1".into(), font_size: 12.0, page: 1, + is_bold: false, + is_italic: false, }, ]; @@ -909,4 +1047,32 @@ mod tests { assert_eq!(lines[0].text(), "Hello World"); assert_eq!(lines[1].text(), "Next line"); } + + #[test] + fn test_bold_italic_detection() { + // Test bold detection + assert!(is_bold_font("Arial-Bold")); + assert!(is_bold_font("TimesNewRoman-Bold")); + assert!(is_bold_font("Helvetica-BoldOblique")); + assert!(is_bold_font("ABCDEF+ArialMT-Bold")); + assert!(is_bold_font("NotoSans-Black")); + assert!(is_bold_font("Roboto-SemiBold")); + assert!(!is_bold_font("Arial")); + assert!(!is_bold_font("TimesNewRoman-Italic")); + + // Test italic detection + assert!(is_italic_font("Arial-Italic")); + assert!(is_italic_font("TimesNewRoman-Italic")); + assert!(is_italic_font("Helvetica-Oblique")); + assert!(is_italic_font("ABCDEF+ArialMT-Italic")); + assert!(is_italic_font("Helvetica-BoldOblique")); + assert!(!is_italic_font("Arial")); + assert!(!is_italic_font("TimesNewRoman-Bold")); + + // Test bold-italic detection + assert!(is_bold_font("Arial-BoldItalic")); + assert!(is_italic_font("Arial-BoldItalic")); + assert!(is_bold_font("Helvetica-BoldOblique")); + assert!(is_italic_font("Helvetica-BoldOblique")); + } } diff --git a/src/markdown.rs b/src/markdown.rs index 85a390a..f46ae8d 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -28,6 +28,10 @@ pub struct MarkdownOptions { pub format_urls: bool, /// Fix hyphenation (broken words across lines) pub fix_hyphenation: bool, + /// Detect and format bold text from font names + pub detect_bold: bool, + /// Detect and format italic text from font names + pub detect_italic: bool, } impl Default for MarkdownOptions { @@ -40,6 +44,8 @@ impl Default for MarkdownOptions { remove_page_numbers: true, format_urls: true, fix_hyphenation: true, + detect_bold: true, + detect_italic: true, } } } @@ -214,6 +220,7 @@ fn to_markdown_from_lines_with_tables( let mut prev_y = f32::MAX; let mut in_list = false; let mut in_paragraph = false; + let mut last_list_x: Option = None; let mut inserted_tables: HashSet<(u32, usize)> = HashSet::new(); for line in lines { @@ -265,27 +272,29 @@ fn to_markdown_from_lines_with_tables( // Paragraph break (large Y gap) let y_gap = prev_y - line.y; let is_para_break = y_gap > base_size * 1.8; // Slightly lower threshold - if is_para_break { - if in_paragraph { - output.push_str("\n\n"); - in_paragraph = false; - } - if in_list { - in_list = false; - } + if is_para_break && in_paragraph { + output.push_str("\n\n"); + in_paragraph = false; } + // Don't immediately end list on paragraph break + // Let the continuation check below decide if we're still in a list prev_y = line.y; - let text = line.text(); + // Get text with optional bold/italic formatting + let text = line.text_with_formatting(options.detect_bold, options.detect_italic); let trimmed = text.trim(); + // Also get plain text for pattern matching (list detection, captions, etc.) + let plain_text = line.text(); + let plain_trimmed = plain_text.trim(); + if trimmed.is_empty() { continue; } // Detect figure/table captions and source citations // These should be on their own line followed by a paragraph break - if is_caption_line(trimmed) { + if is_caption_line(plain_trimmed) { if in_paragraph { output.push_str("\n\n"); in_paragraph = false; @@ -296,7 +305,8 @@ fn to_markdown_from_lines_with_tables( } // Detect headers by font size - if options.detect_headers && trimmed.len() > 3 { + // Note: Headers typically shouldn't have bold markers since they're already emphasized + if options.detect_headers && plain_trimmed.len() > 3 { let line_font_size = line.items.first().map(|i| i.font_size).unwrap_or(base_size); if let Some(header_level) = detect_header_level(line_font_size, base_size) { if in_paragraph { @@ -304,14 +314,15 @@ fn to_markdown_from_lines_with_tables( in_paragraph = false; } let prefix = "#".repeat(header_level); - output.push_str(&format!("{} {}\n\n", prefix, trimmed)); + // Use plain text for headers to avoid redundant formatting + output.push_str(&format!("{} {}\n\n", prefix, plain_trimmed)); in_list = false; continue; } } // Detect list items - if options.detect_lists && is_list_item(trimmed) { + if options.detect_lists && is_list_item(plain_trimmed) { if in_paragraph { output.push_str("\n\n"); in_paragraph = false; @@ -320,9 +331,37 @@ fn to_markdown_from_lines_with_tables( output.push_str(&formatted); output.push('\n'); in_list = true; + last_list_x = line.items.first().map(|i| i.x); continue; - } else if in_list && !trimmed.starts_with(char::is_whitespace) { - in_list = false; + } else if in_list { + // Check if this line is a continuation of the previous list item + // Continuations have similar X position and reasonable Y gap + let line_x = line.items.first().map(|i| i.x); + let is_continuation = if let (Some(list_x), Some(curr_x)) = (last_list_x, line_x) { + // Continuation criteria: + // 1. X is at or past the list text position + // 2. Y gap is not too large (max ~5 line heights) + // 3. Not a new list item + let x_ok = curr_x >= list_x - 5.0 && curr_x <= list_x + 50.0; + let y_ok = y_gap < base_size * 7.0; + x_ok && y_ok && !is_list_item(plain_trimmed) + } else { + false + }; + + if is_continuation { + // Append to previous list item with a space + if output.ends_with('\n') { + output.pop(); + output.push(' '); + } + output.push_str(trimmed); + output.push('\n'); + continue; + } else { + in_list = false; + last_list_x = None; + } } // Detect code blocks by font @@ -333,7 +372,8 @@ fn to_markdown_from_lines_with_tables( output.push_str("\n\n"); in_paragraph = false; } - output.push_str(&format!("```\n{}\n```\n", trimmed)); + // Use plain text for code blocks + output.push_str(&format!("```\n{}\n```\n", plain_trimmed)); continue; } } @@ -390,6 +430,7 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> let mut prev_y = f32::MAX; let mut in_list = false; let mut in_paragraph = false; + let mut last_list_x: Option = None; for line in lines { // Page break @@ -403,32 +444,36 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> } current_page = line.page; prev_y = f32::MAX; + in_list = false; + last_list_x = None; } // Paragraph break (large Y gap) let y_gap = prev_y - line.y; let is_para_break = y_gap > base_size * 1.8; // Slightly lower threshold - if is_para_break { - if in_paragraph { - output.push_str("\n\n"); - in_paragraph = false; - } - if in_list { - in_list = false; - } + if is_para_break && in_paragraph { + output.push_str("\n\n"); + in_paragraph = false; } + // Don't immediately end list on paragraph break + // Let the continuation check below decide if we're still in a list prev_y = line.y; - let text = line.text(); + // Get text with optional bold/italic formatting + let text = line.text_with_formatting(options.detect_bold, options.detect_italic); let trimmed = text.trim(); + // Also get plain text for pattern matching + let plain_text = line.text(); + let plain_trimmed = plain_text.trim(); + if trimmed.is_empty() { continue; } // Detect figure/table captions and source citations // These should be on their own line followed by a paragraph break - if is_caption_line(trimmed) { + if is_caption_line(plain_trimmed) { if in_paragraph { output.push_str("\n\n"); in_paragraph = false; @@ -440,7 +485,7 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> // Detect headers by font size // Skip very short text (likely drop caps or labels) - if options.detect_headers && trimmed.len() > 3 { + if options.detect_headers && plain_trimmed.len() > 3 { let line_font_size = line.items.first().map(|i| i.font_size).unwrap_or(base_size); if let Some(header_level) = detect_header_level(line_font_size, base_size) { if in_paragraph { @@ -448,14 +493,15 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> in_paragraph = false; } let prefix = "#".repeat(header_level); - output.push_str(&format!("{} {}\n\n", prefix, trimmed)); + // Use plain text for headers to avoid redundant formatting + output.push_str(&format!("{} {}\n\n", prefix, plain_trimmed)); in_list = false; continue; } } // Detect list items - if options.detect_lists && is_list_item(trimmed) { + if options.detect_lists && is_list_item(plain_trimmed) { if in_paragraph { output.push_str("\n\n"); in_paragraph = false; @@ -464,11 +510,35 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> output.push_str(&formatted); output.push('\n'); in_list = true; + last_list_x = line.items.first().map(|i| i.x); continue; } else if in_list { - // Check if continuing list or ending - if !trimmed.starts_with(char::is_whitespace) { + // Check if this line is a continuation of the previous list item + let line_x = line.items.first().map(|i| i.x); + let is_continuation = if let (Some(list_x), Some(curr_x)) = (last_list_x, line_x) { + // Continuation criteria: + // 1. X is at or past the list text position + // 2. Y gap is not too large (max ~5 line heights) + // 3. Not a new list item + let x_ok = curr_x >= list_x - 5.0 && curr_x <= list_x + 50.0; + let y_ok = y_gap < base_size * 7.0; + x_ok && y_ok && !is_list_item(plain_trimmed) + } else { + false + }; + + if is_continuation { + // Append to previous list item with a space + if output.ends_with('\n') { + output.pop(); + output.push(' '); + } + output.push_str(trimmed); + output.push('\n'); + continue; + } else { in_list = false; + last_list_x = None; } } @@ -480,7 +550,8 @@ pub fn to_markdown_from_lines(lines: Vec, options: MarkdownOptions) -> output.push_str("\n\n"); in_paragraph = false; } - output.push_str(&format!("```\n{}\n```\n", trimmed)); + // Use plain text for code blocks + output.push_str(&format!("```\n{}\n```\n", plain_trimmed)); continue; } } @@ -949,7 +1020,7 @@ fn is_page_number_line(trimmed: &str) -> bool { } // Pattern 4: "- X -" centered page number - if trimmed.starts_with('-') && trimmed.ends_with('-') { + if trimmed.len() >= 3 && trimmed.starts_with('-') && trimmed.ends_with('-') { let inner = trimmed[1..trimmed.len() - 1].trim(); if inner.chars().all(|c| c.is_ascii_digit()) && !inner.is_empty() { return true; diff --git a/src/tables.rs b/src/tables.rs index d86cdd8..a9df0aa 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -222,6 +222,12 @@ fn detect_table_in_region(items: &[(usize, &TextItem)]) -> Option { return None; } + // Validation 8: Check for Table of Contents pattern + // TOCs have dots (leader lines) and page numbers, not real table data + if is_table_of_contents(&cells) { + return None; + } + Some(Table { columns, rows, @@ -305,9 +311,9 @@ fn has_consistent_columns(cells: &[Vec]) -> bool { consistent_rows as f32 / cells.len() as f32 > 0.4 } -/// Check if the content looks like table data (numbers, short values) +/// Check if the content looks like table data (numbers, short values, specs) fn has_table_like_content(cells: &[Vec]) -> bool { - let mut numeric_cells = 0; + let mut data_like_cells = 0; let mut total_cells = 0; for row in cells.iter().skip(1) { @@ -316,9 +322,9 @@ fn has_table_like_content(cells: &[Vec]) -> bool { let trimmed = cell.trim(); if !trimmed.is_empty() { total_cells += 1; - // Check if it looks like a number (including decimals) - if looks_like_number(trimmed) { - numeric_cells += 1; + // Check if it looks like table data + if looks_like_table_data(trimmed) { + data_like_cells += 1; } } } @@ -328,12 +334,65 @@ fn has_table_like_content(cells: &[Vec]) -> bool { return false; } - // At least 20% numeric content suggests a data table + // At least 20% data-like content suggests a data table // OR the table has many columns (structural table) - let pct_numeric = numeric_cells as f32 / total_cells as f32; + let pct_data = data_like_cells as f32 / total_cells as f32; let num_cols = cells.first().map(|r| r.len()).unwrap_or(0); - pct_numeric > 0.2 || num_cols >= 5 + pct_data > 0.2 || num_cols >= 5 +} + +/// Check if a cell value looks like table data +/// Includes: numbers, part numbers, specifications with units, codes +fn looks_like_table_data(s: &str) -> bool { + let s = s.trim(); + if s.is_empty() { + return false; + } + + // Pure numbers + if looks_like_number(s) { + return true; + } + + // Part numbers / model codes (alphanumeric, typically short) + // e.g., "NA555", "NE555", "LM358" + if s.len() <= 10 + && s.chars().all(|c| c.is_alphanumeric()) + && s.chars().any(|c| c.is_ascii_digit()) + { + return true; + } + + // Specifications with units (contains numbers and unit symbols) + // e.g., "–40°C to +105°C", "5V", "200mA", "8-pin" + let has_number = s.chars().any(|c| c.is_ascii_digit()); + let has_unit = s.contains('°') + || s.contains('V') + || s.contains('A') + || s.contains("Hz") + || s.contains("mA") + || s.contains("µ") + || s.contains("pin") + || s.contains("MHz") + || s.contains("kHz"); + if has_number && has_unit { + return true; + } + + // Package designations with parentheses + // e.g., "D (SOIC, 8)", "P (PDIP, 8)" + if s.contains('(') && s.contains(')') && s.chars().any(|c| c.is_ascii_digit()) { + return true; + } + + // Temperature ranges + // e.g., "TA = –40°C to +105°C" + if (s.contains("°C") || s.contains("°F")) && s.contains("to") { + return true; + } + + false } /// Check if a string looks like a number @@ -349,6 +408,56 @@ fn looks_like_number(s: &str) -> bool { && s.chars().any(|c| c.is_ascii_digit()) } +/// Check if this looks like a Table of Contents +/// TOCs have characteristic patterns: leader dots, page numbers, section names +fn is_table_of_contents(cells: &[Vec]) -> bool { + if cells.is_empty() { + return false; + } + + let mut dot_cells = 0; + let mut page_number_cells = 0; + let mut total_cells = 0; + + for row in cells { + for cell in row { + let trimmed = cell.trim(); + if trimmed.is_empty() { + continue; + } + total_cells += 1; + + // Check for leader dots (sequences of periods) + // TOCs often have "........" or ". . . ." patterns + let dot_count = trimmed.chars().filter(|&c| c == '.').count(); + let is_mostly_dots = dot_count > trimmed.len() / 2 && dot_count >= 3; + if is_mostly_dots { + dot_cells += 1; + } + + // Check for standalone page numbers (1-4 digits, possibly with spaces) + let digits_only: String = trimmed.chars().filter(|c| !c.is_whitespace()).collect(); + if digits_only.len() <= 4 + && !digits_only.is_empty() + && digits_only.chars().all(|c| c.is_ascii_digit()) + { + page_number_cells += 1; + } + } + } + + if total_cells == 0 { + return false; + } + + // If a significant portion of cells are dots or page numbers, it's likely a TOC + let dot_ratio = dot_cells as f32 / total_cells as f32; + let page_num_ratio = page_number_cells as f32 / total_cells as f32; + + // TOC typically has >15% dot cells and >10% page number cells + dot_ratio > 0.15 || (dot_ratio > 0.05 && page_num_ratio > 0.15) +} + /// Check what fraction of items align to detected columns fn check_column_alignment(items: &[(usize, &TextItem)], columns: &[f32]) -> f32 { let tolerance = 40.0; @@ -817,6 +926,8 @@ mod tests { font: "F1".into(), font_size, page: 1, + is_bold: false, + is_italic: false, } } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index da1c123..82a7f85 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -18,6 +18,8 @@ fn make_text_item(text: &str, x: f32, y: f32, font_size: f32, page: u32) -> Text font: "Helvetica".to_string(), font_size, page, + is_bold: false, + is_italic: false, } } @@ -29,6 +31,7 @@ fn make_text_item_with_font( font: &str, page: u32, ) -> TextItem { + use pdf_inspector::extractor::{is_bold_font, is_italic_font}; TextItem { text: text.to_string(), x, @@ -38,6 +41,8 @@ fn make_text_item_with_font( font: font.to_string(), font_size, page, + is_bold: is_bold_font(font), + is_italic: is_italic_font(font), } } @@ -253,6 +258,8 @@ fn test_markdown_options_custom() { remove_page_numbers: false, format_urls: false, fix_hyphenation: false, + detect_bold: false, + detect_italic: false, }; assert!(!opts.detect_headers); assert!(opts.detect_lists); @@ -261,6 +268,8 @@ fn test_markdown_options_custom() { assert!(!opts.remove_page_numbers); assert!(!opts.format_urls); assert!(!opts.fix_hyphenation); + assert!(!opts.detect_bold); + assert!(!opts.detect_italic); } // ============================================================================