1use crate::archive::EpubArchive;
2use crate::book::Book;
3use crate::deobfuscate::FontDeobfuscator;
4use crate::error::EbookError;
5use crate::layout::RenditionLayout;
6use crate::metadata::{Metadata, PageProgressionDirection, SpineItem};
7use crate::nav::NavPoint;
8use crate::opf::OpfPackage;
9use crate::section::Section;
10use ahash::AHashMap;
11
12pub struct RtfBook;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16struct RtfState {
17 bold: bool,
18 italic: bool,
19 underline: bool,
20 strike: bool,
21 is_destination: bool,
22 dest_name: String,
23 uc: usize,
24}
25
26impl Default for RtfState {
27 fn default() -> Self {
28 Self {
29 bold: false,
30 italic: false,
31 underline: false,
32 strike: false,
33 is_destination: false,
34 dest_name: String::new(),
35 uc: 1,
36 }
37 }
38}
39
40impl RtfBook {
41 pub fn parse(bytes: &[u8], title_fallback: &str) -> Result<Book, EbookError> {
43 let text = match std::str::from_utf8(bytes) {
44 Ok(s) => s.to_string(),
45 Err(_) => String::from_utf8_lossy(bytes).to_string(),
46 };
47
48 if !text.starts_with("{\\rtf") && !text.contains("{\\rtf") {
49 return Err(EbookError::InvalidFormat(
50 "Not a valid RTF document (missing {\\rtf header)".to_string(),
51 ));
52 }
53
54 let mut archive = EpubArchive::empty();
55 let mut title = title_fallback.to_string();
56 let mut creators = Vec::new();
57 let mut description = None;
58
59 let mut sections: Vec<Section> = Vec::new();
60 let mut spine: Vec<SpineItem> = Vec::new();
61 let mut toc: Vec<NavPoint> = Vec::new();
62
63 let mut current_html = String::new();
64 let mut current_text = String::new();
65 let mut cur_run = String::new();
66 let mut section_index = 0;
67 let mut img_counter = 0;
68
69 let chars: Vec<char> = text.chars().collect();
70 let len = chars.len();
71 let mut i = 0;
72
73 let mut state_stack: Vec<RtfState> = vec![RtfState::default()];
74 let mut cur_state = RtfState::default();
75
76 let mut dest_buffer = String::new();
77 let mut in_pict = false;
78 let mut pict_hex = String::new();
79 let mut pict_type = "png";
80
81 let mut in_table = false;
82 let mut cell_start_idx = 0;
83 let mut table_cells: Vec<String> = Vec::new();
84
85 let flush_run =
86 |html: &mut String, text: &mut String, run: &mut String, state: &RtfState| {
87 if run.is_empty() {
88 return;
89 }
90 let mut formatted = xml_escape(run);
91 if state.strike {
92 formatted = format!("<s>{}</s>", formatted);
93 }
94 if state.underline {
95 formatted = format!("<u>{}</u>", formatted);
96 }
97 if state.italic {
98 formatted = format!("<em>{}</em>", formatted);
99 }
100 if state.bold {
101 formatted = format!("<strong>{}</strong>", formatted);
102 }
103 html.push_str(&formatted);
104 text.push_str(run);
105 run.clear();
106 };
107
108 let flush_section = |sec_idx: usize,
109 cur_html: &str,
110 cur_txt: &str,
111 sections: &mut Vec<Section>,
112 spine: &mut Vec<SpineItem>,
113 toc: &mut Vec<NavPoint>| {
114 if cur_txt.trim().is_empty() && !sections.is_empty() {
115 return;
116 }
117 let href = format!("section_{}.html", sec_idx);
118 let full_html = format!("<div class=\"rtf-section\">\n{}\n</div>", cur_html);
119 let char_count = cur_txt.chars().count();
120 let plain_text_lower = cur_txt.to_lowercase();
121
122 sections.push(Section {
123 index: sec_idx,
124 idref: format!("sec_{}", sec_idx),
125 href: href.clone(),
126 full_path: href.clone(),
127 raw_html: full_html.clone(),
128 processed_html: full_html,
129 plain_text: cur_txt.to_string(),
130 plain_text_lower,
131 char_count,
132 viewport_width: None,
133 viewport_height: None,
134 });
135
136 spine.push(SpineItem {
137 idref: format!("sec_{}", sec_idx),
138 linear: true,
139 properties: Vec::new(),
140 index: sec_idx,
141 href: href.clone(),
142 media_type: "application/xhtml+xml".to_string(),
143 });
144
145 toc.push(NavPoint {
146 id: format!("nav_{}", toc.len() + 1),
147 label: format!("Section {}", sec_idx + 1),
148 href: href.clone(),
149 full_path: href,
150 subitems: Vec::new(),
151 });
152 };
153
154 while i < len {
155 match chars[i] {
156 '{' => {
157 flush_run(
158 &mut current_html,
159 &mut current_text,
160 &mut cur_run,
161 &cur_state,
162 );
163 state_stack.push(cur_state.clone());
164 cur_state.is_destination = false;
165 cur_state.dest_name.clear();
166 i += 1;
167 }
168 '}' => {
169 flush_run(
170 &mut current_html,
171 &mut current_text,
172 &mut cur_run,
173 &cur_state,
174 );
175 if in_pict {
176 if !pict_hex.is_empty() {
177 if let Ok(bin) = hex_to_bytes(&pict_hex) {
178 img_counter += 1;
179 let img_name = format!("images/img_{}.{}", img_counter, pict_type);
180 archive.insert(&img_name, bin);
181 current_html.push_str(&format!(
182 "<p><img src=\"{}\" alt=\"image\" /></p>\n",
183 img_name
184 ));
185 }
186 }
187 in_pict = false;
188 pict_hex.clear();
189 }
190
191 if cur_state.is_destination {
192 let dest = cur_state.dest_name.as_str();
193 let buf_clean = dest_buffer.trim().to_string();
194 if !buf_clean.is_empty() {
195 match dest {
196 "title" => title = buf_clean,
197 "author" => creators.push(buf_clean),
198 "doccomm" | "subject" => description = Some(buf_clean),
199 _ => {}
200 }
201 }
202 dest_buffer.clear();
203 }
204
205 if let Some(prev) = state_stack.pop() {
206 cur_state = prev;
207 }
208 i += 1;
209 }
210 '\\' => {
211 i += 1;
212 if i >= len {
213 break;
214 }
215
216 match chars[i] {
218 '{' | '}' | '\\' => {
219 let c = chars[i];
220 if cur_state.is_destination {
221 dest_buffer.push(c);
222 } else if in_pict {
223 pict_hex.push(c);
224 } else {
225 cur_run.push(c);
226 }
227 i += 1;
228 continue;
229 }
230 '\'' => {
231 if i + 2 < len {
233 let hex_str: String = chars[i + 1..=i + 2].iter().collect();
234 if let Ok(byte) = u8::from_str_radix(&hex_str, 16) {
235 let s = if byte < 0x80 {
236 (byte as char).to_string()
237 } else {
238 let byte_arr = [byte];
239 let (cow, _) = encoding_rs::WINDOWS_1252
240 .decode_without_bom_handling(&byte_arr);
241 cow.to_string()
242 };
243 if cur_state.is_destination {
244 dest_buffer.push_str(&s);
245 } else {
246 cur_run.push_str(&s);
247 }
248 }
249 i += 3;
250 continue;
251 }
252 }
253 '*' => {
254 cur_state.is_destination = true;
256 i += 1;
257 continue;
258 }
259 _ => {}
260 }
261
262 let mut word = String::new();
264 while i < len && chars[i].is_ascii_alphabetic() {
265 word.push(chars[i]);
266 i += 1;
267 }
268
269 let mut is_neg = false;
271 if i < len && chars[i] == '-' {
272 is_neg = true;
273 i += 1;
274 }
275 let mut param_str = String::new();
276 while i < len && chars[i].is_ascii_digit() {
277 param_str.push(chars[i]);
278 i += 1;
279 }
280 let param: Option<i32> = if !param_str.is_empty() {
281 let p = param_str.parse::<i32>().unwrap_or(0);
282 Some(if is_neg { -p } else { p })
283 } else {
284 None
285 };
286
287 if i < len && chars[i] == ' ' {
289 i += 1;
290 }
291
292 match word.as_str() {
293 "b" => {
294 let next_bold = param != Some(0);
295 if next_bold != cur_state.bold {
296 flush_run(
297 &mut current_html,
298 &mut current_text,
299 &mut cur_run,
300 &cur_state,
301 );
302 cur_state.bold = next_bold;
303 }
304 }
305 "i" => {
306 let next_italic = param != Some(0);
307 if next_italic != cur_state.italic {
308 flush_run(
309 &mut current_html,
310 &mut current_text,
311 &mut cur_run,
312 &cur_state,
313 );
314 cur_state.italic = next_italic;
315 }
316 }
317 "ul" => {
318 let next_ul = param != Some(0);
319 if next_ul != cur_state.underline {
320 flush_run(
321 &mut current_html,
322 &mut current_text,
323 &mut cur_run,
324 &cur_state,
325 );
326 cur_state.underline = next_ul;
327 }
328 }
329 "ulnone" => {
330 if cur_state.underline {
331 flush_run(
332 &mut current_html,
333 &mut current_text,
334 &mut cur_run,
335 &cur_state,
336 );
337 cur_state.underline = false;
338 }
339 }
340 "strike" => {
341 let next_strike = param != Some(0);
342 if next_strike != cur_state.strike {
343 flush_run(
344 &mut current_html,
345 &mut current_text,
346 &mut cur_run,
347 &cur_state,
348 );
349 cur_state.strike = next_strike;
350 }
351 }
352 "par" => {
353 flush_run(
354 &mut current_html,
355 &mut current_text,
356 &mut cur_run,
357 &cur_state,
358 );
359 if !cur_state.is_destination && !in_pict {
360 current_html.push_str("<br/>\n");
361 current_text.push('\n');
362 }
363 }
364 "line" => {
365 flush_run(
366 &mut current_html,
367 &mut current_text,
368 &mut cur_run,
369 &cur_state,
370 );
371 if !cur_state.is_destination && !in_pict {
372 current_html.push_str("<br/>");
373 current_text.push('\n');
374 }
375 }
376 "page" => {
377 flush_run(
378 &mut current_html,
379 &mut current_text,
380 &mut cur_run,
381 &cur_state,
382 );
383 if !cur_state.is_destination && !in_pict && !current_text.is_empty() {
384 flush_section(
385 section_index,
386 ¤t_html,
387 ¤t_text,
388 &mut sections,
389 &mut spine,
390 &mut toc,
391 );
392 section_index += 1;
393 current_html.clear();
394 current_text.clear();
395 }
396 }
397 "tab" => {
398 flush_run(
399 &mut current_html,
400 &mut current_text,
401 &mut cur_run,
402 &cur_state,
403 );
404 if !cur_state.is_destination && !in_pict {
405 current_html.push_str(" ");
406 current_text.push('\t');
407 }
408 }
409 "uc" => {
410 if let Some(n) = param {
411 cur_state.uc = (n.max(0)) as usize;
412 }
413 }
414 "u" => {
415 if let Some(code) = param {
416 let unsigned_code = if code < 0 {
417 (code + 65536) as u32
418 } else {
419 code as u32
420 };
421 if let Some(ch) = char::from_u32(unsigned_code) {
422 if cur_state.is_destination {
423 dest_buffer.push(ch);
424 } else {
425 cur_run.push(ch);
426 }
427 }
428 let mut skipped = 0;
430 while i < len && skipped < cur_state.uc {
431 if chars[i] == '{' || chars[i] == '}' {
432 break;
433 }
434 if chars[i] == '\\' {
435 if i + 3 < len && chars[i + 1] == '\'' {
436 i += 4;
437 skipped += 1;
438 continue;
439 } else {
440 break;
441 }
442 }
443 i += 1;
444 skipped += 1;
445 }
446 }
447 }
448 "info" | "title" | "author" | "subject" | "doccomm" | "keywords"
449 | "fonttbl" | "colortbl" | "stylesheet" => {
450 flush_run(
451 &mut current_html,
452 &mut current_text,
453 &mut cur_run,
454 &cur_state,
455 );
456 cur_state.is_destination = true;
457 cur_state.dest_name = word;
458 }
459 "pict" => {
460 flush_run(
461 &mut current_html,
462 &mut current_text,
463 &mut cur_run,
464 &cur_state,
465 );
466 in_pict = true;
467 pict_hex.clear();
468 }
469 "pngblip" => pict_type = "png",
470 "jpegblip" => pict_type = "jpg",
471 "trowd" => {
472 flush_run(
473 &mut current_html,
474 &mut current_text,
475 &mut cur_run,
476 &cur_state,
477 );
478 in_table = true;
479 table_cells.clear();
480 cell_start_idx = current_html.len();
481 }
482 "cell" => {
483 flush_run(
484 &mut current_html,
485 &mut current_text,
486 &mut cur_run,
487 &cur_state,
488 );
489 if in_table && current_html.len() >= cell_start_idx {
490 let cell_content = current_html.split_off(cell_start_idx);
491 table_cells.push(cell_content);
492 cell_start_idx = current_html.len();
493 }
494 }
495 "row" => {
496 flush_run(
497 &mut current_html,
498 &mut current_text,
499 &mut cur_run,
500 &cur_state,
501 );
502 if in_table {
503 if current_html.len() > cell_start_idx {
504 let cell_content = current_html.split_off(cell_start_idx);
505 table_cells.push(cell_content);
506 }
507 current_html.push_str("<table>\n<tr>\n");
508 for cell in &table_cells {
509 current_html.push_str(&format!(" <td>{}</td>\n", cell));
510 }
511 current_html.push_str("</tr>\n</table>\n");
512 table_cells.clear();
513 in_table = false;
514 }
515 }
516 _ => {}
517 }
518 }
519 c => {
520 if in_pict {
521 if c.is_ascii_hexdigit() {
522 pict_hex.push(c);
523 }
524 } else if cur_state.is_destination {
525 dest_buffer.push(c);
526 } else if c != '\r' && c != '\n' {
527 cur_run.push(c);
528 }
529 i += 1;
530 }
531 }
532 }
533
534 flush_run(
536 &mut current_html,
537 &mut current_text,
538 &mut cur_run,
539 &cur_state,
540 );
541 flush_section(
542 section_index,
543 ¤t_html,
544 ¤t_text,
545 &mut sections,
546 &mut spine,
547 &mut toc,
548 );
549
550 let metadata = Metadata {
551 title,
552 creators,
553 publishers: Vec::new(),
554 languages: vec!["en".to_string()],
555 rights: None,
556 description,
557 identifier: None,
558 pub_date: None,
559 modified_date: None,
560 subjects: vec!["Document".to_string()],
561 cover_id: None,
562 cover_href: None,
563 direction: PageProgressionDirection::Ltr,
564 meta_properties: AHashMap::new(),
565 accessibility: Default::default(),
566 };
567
568 let opf = OpfPackage {
569 version: "3.0".to_string(),
570 opf_path: "content.opf".to_string(),
571 opf_dir: "".to_string(),
572 metadata,
573 manifest: AHashMap::new(),
574 spine,
575 guide: Vec::new(),
576 toc_item_id: None,
577 nav_item_id: None,
578 };
579
580 let mut book = Book {
581 archive,
582 opf,
583 layout: RenditionLayout::default(),
584 toc,
585 landmarks: Vec::new(),
586 page_list: Vec::new(),
587 sections,
588 locations: crate::locations::Locations::default(),
589 annotations: crate::annotations::AnnotationManager::default(),
590 before_display_hooks: Vec::new(),
591 font_deobfuscator: FontDeobfuscator::parse_encryption_xml(""),
592 media_overlays: AHashMap::new(),
593 render_cache: parking_lot::Mutex::new(AHashMap::new()),
594 };
595
596 book.generate_locations(1000);
597 Ok(book)
598 }
599}
600
601fn hex_to_bytes(hex: &str) -> Result<Vec<u8>, String> {
602 let clean: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
603 if !clean.len().is_multiple_of(2) {
604 return Err("Invalid hex length".to_string());
605 }
606 let mut bytes = Vec::with_capacity(clean.len() / 2);
607 let chars: Vec<char> = clean.chars().collect();
608 for chunk in chars.chunks(2) {
609 let pair: String = chunk.iter().collect();
610 let b = u8::from_str_radix(&pair, 16).map_err(|e| e.to_string())?;
611 bytes.push(b);
612 }
613 Ok(bytes)
614}
615
616fn xml_escape(input: &str) -> String {
617 input
618 .replace('&', "&")
619 .replace('<', "<")
620 .replace('>', ">")
621 .replace('"', """)
622 .replace('\'', "'")
623}