From a2c34c42da145c84b8c9af3d62b103daeb8946f5 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 1 Mar 2026 20:53:09 +0400 Subject: [PATCH 01/16] Feat(pptx): init --- parser/src/lib.rs | 1 - parser/src/parsers/mod.rs | 6 ++++-- parser/src/parsers/pptx.rs | 3 +++ parser/tests/test.rs | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 parser/src/parsers/pptx.rs diff --git a/parser/src/lib.rs b/parser/src/lib.rs index bcf1b0f..6bedfa3 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -6,7 +6,6 @@ mod parsers; use pyo3::prelude::*; use pyo3::{PyResult, types::PyModule}; - /// Модуль для реализации функций модуля `docs_parser` mod parser { use pyo3::prelude::*; diff --git a/parser/src/parsers/mod.rs b/parser/src/parsers/mod.rs index e4c0d96..af76190 100644 --- a/parser/src/parsers/mod.rs +++ b/parser/src/parsers/mod.rs @@ -1,6 +1,8 @@ //! Модуль для реализации парсеров -mod xml; pub(crate) mod docx; pub(crate) mod image; -pub(crate) mod text; pub(crate) mod pdf; +pub(crate) mod pptx; +pub(crate) mod text; + +mod xml; diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs new file mode 100644 index 0000000..b72755f --- /dev/null +++ b/parser/src/parsers/pptx.rs @@ -0,0 +1,3 @@ +//! Парсинг pptx файлов +//! +//! Для парсинга используется crate zip diff --git a/parser/tests/test.rs b/parser/tests/test.rs index e69de29..8b13789 100644 --- a/parser/tests/test.rs +++ b/parser/tests/test.rs @@ -0,0 +1 @@ + From 87de315182a5bf1d711422efa542ea33bd10ba91 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 08:24:08 +0400 Subject: [PATCH 02/16] Feat(cargo): add rustypptx crate --- parser/Cargo.lock | 336 +++++++++++++++++++++++++++++++++++++++++++++- parser/Cargo.toml | 1 + 2 files changed, 334 insertions(+), 3 deletions(-) diff --git a/parser/Cargo.lock b/parser/Cargo.lock index a520a46..7509547 100644 --- a/parser/Cargo.lock +++ b/parser/Cargo.lock @@ -55,6 +55,56 @@ dependencies = [ "equator", ] +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -66,6 +116,9 @@ name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "arg_enum_proc_macro" @@ -251,6 +304,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + [[package]] name = "bzip2" version = "0.6.1" @@ -260,6 +322,16 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cbc" version = "0.1.2" @@ -334,12 +406,64 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.116", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + [[package]] name = "color_quant" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -364,6 +488,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -429,6 +568,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "digest" version = "0.10.7" @@ -440,6 +590,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "docx-rs" version = "0.4.19" @@ -479,6 +640,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equator" version = "0.4.2" @@ -614,9 +798,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -788,6 +974,12 @@ dependencies = [ "syn 2.0.116", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -803,6 +995,30 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.116", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -950,6 +1166,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "lzma-rust2" version = "0.16.2" @@ -959,6 +1185,17 @@ dependencies = [ "sha2", ] +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "maybe-rayon" version = "0.1.1" @@ -1121,6 +1358,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parser" version = "0.1.0" @@ -1131,8 +1374,9 @@ dependencies = [ "mime", "pdf-extract", "pyo3", - "quick-xml", + "quick-xml 0.39.2", "rayon", + "rustypptx", "tesseract", "thiserror 2.0.18", "zip 8.1.0", @@ -1214,6 +1458,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "postscript" version = "0.14.1" @@ -1361,6 +1614,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.39.2" @@ -1550,6 +1812,23 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rustypptx" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1265abd01a89e4f51fb62d3db2be3eb31004023b3957e3a482cfbc26d23509" +dependencies = [ + "clap", + "env_logger", + "log", + "quick-xml 0.37.5", + "rayon", + "serde", + "serde_json", + "thiserror 2.0.18", + "zip 2.4.2", +] + [[package]] name = "semver" version = "1.0.27" @@ -1665,6 +1944,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1882,6 +2167,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.21.0" @@ -2212,6 +2503,15 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + [[package]] name = "y4m" version = "0.8.0" @@ -2270,6 +2570,36 @@ dependencies = [ "flate2", ] +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2 0.5.2", + "constant_time_eq 0.3.1", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + [[package]] name = "zip" version = "8.1.0" @@ -2277,8 +2607,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72" dependencies = [ "aes", - "bzip2", - "constant_time_eq", + "bzip2 0.6.1", + "constant_time_eq 0.4.2", "crc32fast", "deflate64", "flate2", diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 0c35600..6569198 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -20,6 +20,7 @@ mime = "0.3.17" # NOTE: Для парсинга форматов офиса docx-rs = "0.4.19" +rustypptx = "0.2.0" zip = "8.1.0" quick-xml = "0.39.2" From 2ef4162eb4e27633c4802ed860b5734b5f029353 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 08:24:44 +0400 Subject: [PATCH 03/16] Feat(errors): add convert from rustypptx error to ParserError --- parser/src/errors.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/parser/src/errors.rs b/parser/src/errors.rs index f83b5b6..bb0a2e9 100644 --- a/parser/src/errors.rs +++ b/parser/src/errors.rs @@ -55,6 +55,12 @@ pub enum ParserError { #[error("Docx error: {0}")] DocxError(#[from] docx_rs::ReaderError), + /// Ошибка чтения pptx + /// + /// Ошибки библиотеки для работы с pptx + #[error("Docx error: {0}")] + PptxError(#[from] rustypptx::PptxError), + /// Ошибка tesseract::InitializeError #[error("Tesseract init error: {0}")] TesseractInitError(#[from] tesseract::InitializeError), From 1c126a6e670845c8ddef13a2da2600cdc4e3f1b0 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 08:34:04 +0400 Subject: [PATCH 04/16] Chore(docx): update comments --- parser/src/parsers/docx.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/parser/src/parsers/docx.rs b/parser/src/parsers/docx.rs index 43a7c94..5101f7b 100644 --- a/parser/src/parsers/docx.rs +++ b/parser/src/parsers/docx.rs @@ -25,8 +25,10 @@ pub(crate) struct DocxParser { pub images: HashMap, } +// FIX: переделать под выдачу и текста и картинки с метакми в тексте (типа этот текст из картинки +// такой-то) impl DocxParser { - /// Creates a new [`DocxParser`]. + /// Создает новый [`DocxParser`]. pub(crate) fn new() -> Self { Self { images: HashMap::new(), @@ -40,7 +42,14 @@ impl DocxParser { /// /// # Returns /// - Ok([`String`]) - возвращает текст - /// - Err([`ParserError::DocxError`]) - ошибка во время парсинга docx файла + /// - Err([`ParserError`]) - ошибка во время парсинга docx файла + /// + /// # Errors + /// - [`ParserError::DocxError`] - ошибка во время docx + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - [`ParserError::ZipError`] - ошибка во время парсинга docx как zip + /// - [`ParserError::XmlError`] - ошибка во время парсинга конфигурационного файла docx + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки pub(crate) fn get_from_docx(&mut self, data: &[u8]) -> Result { let dox = read_docx(data)?; // Вытаскиваем все картинки @@ -172,11 +181,11 @@ impl DocxParser { Ok(images_with_id) } - // ***************************************************************************** + // ************************************************************************* // Работа с элементами docx - // ***************************************************************************** + // ************************************************************************* - /// Проходится по всем детям `Paragraph` и извлекает из них текст + /// Проходится по всем детям [`docx_rs::Paragraph`] и извлекает из них текст fn paragraph_unwrap(&self, paragraph: &docx_rs::Paragraph) -> String { paragraph .children @@ -188,7 +197,7 @@ impl DocxParser { .collect::() } - /// Проходится по всем детям `Run` и извлекает из них текст + /// Проходится по всем детям [`docx_rs::Run`] и извлекает из них текст fn run_unwrap(&self, run: &docx_rs::Run) -> String { run.children .iter() @@ -200,7 +209,7 @@ impl DocxParser { .collect::() } - /// Извлекает текст из `Drawing`, если он есть + /// Извлекает текст из [`docx_rs::Drawing`], если он есть fn drawing_unwrap(&self, drawing: &docx_rs::Drawing) -> Result> { Ok(match &drawing.data { Some(docx_rs::DrawingData::Pic(pic)) => Some(self.pic_unwrap(pic)?), @@ -209,6 +218,7 @@ impl DocxParser { }) } + /// Подставляет текст с нужной картинки вместо [`docx_rs::Pic`] fn pic_unwrap(&self, pic: &docx_rs::Pic) -> Result { match self.images.get(&pic.id) { Some(text) => Ok(text.clone()), @@ -216,7 +226,7 @@ impl DocxParser { } } - /// Извлекает текст из `TextBox` + /// Извлекает текст из [`docx_rs::TextBox`] fn text_box_unwrap(&self, text_box: &docx_rs::TextBox) -> String { text_box .children @@ -230,7 +240,7 @@ impl DocxParser { .collect::() } - /// Проходится по всем детям `Table` и извлекает из них текст + /// Проходится по всем детям [`docx_rs::Table`] и извлекает из них текст fn table_unwrap(&self, table: &docx_rs::Table) -> String { table .rows @@ -241,7 +251,7 @@ impl DocxParser { .collect::() } - /// Извлекает текст из `TableRow` + /// Извлекает текст из [`docx_rs::TableRow`] fn table_row_unwrap(&self, table_row: &docx_rs::TableRow) -> String { table_row .cells @@ -256,7 +266,7 @@ impl DocxParser { .collect::() } - /// Извлекает текст из `TableCell` + /// Извлекает текст из [`docx_rs::TableCell`] fn table_cell_unwrap(&self, cell: &docx_rs::TableCell) -> String { cell.children .iter() From 23e01cc49cec666f7ff25ebb6bca1034d29309e1 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 10:09:55 +0400 Subject: [PATCH 05/16] Feat(pptx): init PptxParser struct --- parser/src/parsers/pptx.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index b72755f..29f2acf 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -1,3 +1,20 @@ //! Парсинг pptx файлов //! -//! Для парсинга используется crate zip +//! Для парсинга используется crate rustypptx + +use std::collections::HashMap; + +use crate::{errors::ParserError, parsers::image::get_from_image}; + +type Result = std::result::Result; +type SlideIndex = u32; +type ImgOnSlideNum = u32; +type ImagesInfo = HashMap<(SlideIndex, ImgOnSlideNum), Vec>; + +pub(crate) struct PptxParser { + /// HashMap для сопоставления байтов картинки с её местом в тексте слайда + pub slides_img_info: ImagesInfo, + /// HashMap из индекса слайда и текста извлеченного из него + pub slides_text: HashMap, +} + From 43e7628c63cf2df2a3b56e02524ddceee56d331c Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 10:10:25 +0400 Subject: [PATCH 06/16] Feat(pptx): add new method --- parser/src/parsers/pptx.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 29f2acf..5c4e916 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -18,3 +18,13 @@ pub(crate) struct PptxParser { pub slides_text: HashMap, } +impl PptxParser { + /// Создает новый [`PptxParser`]. + pub(crate) fn new() -> Self { + Self { + slides_img_info: HashMap::new(), + slides_text: HashMap::new(), + } + } + +} From 36fcc02073e29990ad318b0624b83e2bc1421226 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 10:11:07 +0400 Subject: [PATCH 07/16] Feat(pptx): add get from pptx method --- parser/src/parsers/pptx.rs | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 5c4e916..7a22cf5 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -27,4 +27,72 @@ impl PptxParser { } } + /// Извлекает текстовые данные и текст из картинок + /// + /// # Arguments + /// - `data` - слайс байтов данных из файла + /// + /// # Returns + /// - Ok([`String`]) - возвращает текст + /// - Err([`ParserError`]) - ошибка во время парсинга pptx файла + /// + /// # Errors + /// - [`ParserError::PptxError`] - ошибка во время парсинга pptx + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки + pub(crate) fn get_from_pptx(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { + let pptx_doc = rustypptx::parse_pptx_bytes(data)?; + + let mut result_text = String::new(); + if let Some(title) = pptx_doc.metadata.title { + result_text.push_str(&format!("Название: {title}")); + } + + for slide in pptx_doc.slides.iter() { + self.slides_img_info = + slide + .images + .iter() + .enumerate() + .fold(HashMap::new(), |mut info, (ind, img)| { + info.insert((slide.index, ind as u32), img.data.clone()); + info + }); + self.slides_text = + slide + .text_elements + .iter() + .fold(HashMap::new(), |mut sl_text, text_element| { + sl_text.insert(slide.index, text_element.text.clone()); + sl_text + }) + } + + result_text = self + .slides_text + .iter_mut() + .map(|(sl_ind, text)| { + text.push_str( + &self + .slides_img_info + .iter() + .filter(|((ind, _), _)| *ind == *sl_ind) + .map(|((_, img_num), data)| { + Ok(format!( + "\n/********slide = {sl_ind}; img_num = {img_num}********/\n \ + {}\n \ + /****************************/", + get_from_image(data)? + )) + }) + .collect::>>()? + .join("\n"), + ); + Ok(text.clone()) + }) + .collect::>>()? + .join("\n"); + + Ok((result_text, self.slides_img_info)) + } } From 4877793ec3961a398e2a2b5954bf273dccc357fa Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 10:18:05 +0400 Subject: [PATCH 08/16] Feat(pptx): move set slides text and img info logic in another fn --- parser/src/parsers/pptx.rs | 44 +++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 7a22cf5..bf97100 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -44,29 +44,11 @@ impl PptxParser { let pptx_doc = rustypptx::parse_pptx_bytes(data)?; let mut result_text = String::new(); - if let Some(title) = pptx_doc.metadata.title { + if let Some(title) = &pptx_doc.metadata.title { result_text.push_str(&format!("Название: {title}")); } - for slide in pptx_doc.slides.iter() { - self.slides_img_info = - slide - .images - .iter() - .enumerate() - .fold(HashMap::new(), |mut info, (ind, img)| { - info.insert((slide.index, ind as u32), img.data.clone()); - info - }); - self.slides_text = - slide - .text_elements - .iter() - .fold(HashMap::new(), |mut sl_text, text_element| { - sl_text.insert(slide.index, text_element.text.clone()); - sl_text - }) - } + self.set_slides_text_and_img_info(pptx_doc); result_text = self .slides_text @@ -95,4 +77,26 @@ impl PptxParser { Ok((result_text, self.slides_img_info)) } + + fn set_slides_text_and_img_info(&mut self, pptx_doc: rustypptx::PptxDocument) { + for slide in pptx_doc.slides.iter() { + self.slides_img_info = + slide + .images + .iter() + .enumerate() + .fold(HashMap::new(), |mut info, (ind, img)| { + info.insert((slide.index, ind as u32), img.data.clone()); + info + }); + self.slides_text = + slide + .text_elements + .iter() + .fold(HashMap::new(), |mut sl_text, text_element| { + sl_text.insert(slide.index, text_element.text.clone()); + sl_text + }) + } + } } From e475a990404a8de76c0fb71ccfa0f50e95fe0278 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sat, 7 Mar 2026 10:21:03 +0400 Subject: [PATCH 09/16] Feat(pptx): move add text from img in slides logic in another fn --- parser/src/parsers/pptx.rs | 51 ++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index bf97100..8e03785 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -50,30 +50,7 @@ impl PptxParser { self.set_slides_text_and_img_info(pptx_doc); - result_text = self - .slides_text - .iter_mut() - .map(|(sl_ind, text)| { - text.push_str( - &self - .slides_img_info - .iter() - .filter(|((ind, _), _)| *ind == *sl_ind) - .map(|((_, img_num), data)| { - Ok(format!( - "\n/********slide = {sl_ind}; img_num = {img_num}********/\n \ - {}\n \ - /****************************/", - get_from_image(data)? - )) - }) - .collect::>>()? - .join("\n"), - ); - Ok(text.clone()) - }) - .collect::>>()? - .join("\n"); + result_text = self.add_text_from_img_in_slides()?; Ok((result_text, self.slides_img_info)) } @@ -99,4 +76,30 @@ impl PptxParser { }) } } + fn add_text_from_img_in_slides(&mut self) -> Result { + Ok(self + .slides_text + .iter_mut() + .map(|(sl_ind, text)| { + text.push_str( + &self + .slides_img_info + .iter() + .filter(|((ind, _), _)| *ind == *sl_ind) + .map(|((_, img_num), data)| { + Ok(format!( + "\n/********slide = {sl_ind}; img_num = {img_num}********/\n \ + {}\n \ + /****************************/", + get_from_image(data)? + )) + }) + .collect::>>()? + .join("\n"), + ); + Ok(text.clone()) + }) + .collect::>>()? + .join("\n")) + } } From bf8c3af5a8a4abdbe8514de24affbcd0414dba57 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 01:01:27 +0400 Subject: [PATCH 10/16] Feat(pptx): rework slides_text from hashmap to Vec --- parser/src/parsers/pptx.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 8e03785..6239344 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -15,7 +15,7 @@ pub(crate) struct PptxParser { /// HashMap для сопоставления байтов картинки с её местом в тексте слайда pub slides_img_info: ImagesInfo, /// HashMap из индекса слайда и текста извлеченного из него - pub slides_text: HashMap, + pub slides_text: Vec, } impl PptxParser { @@ -23,13 +23,14 @@ impl PptxParser { pub(crate) fn new() -> Self { Self { slides_img_info: HashMap::new(), - slides_text: HashMap::new(), + slides_text: Vec::new(), } } /// Извлекает текстовые данные и текст из картинок /// /// # Arguments + /// - `mut `[`self`] - сам парсер (забирает владение над парсером) /// - `data` - слайс байтов данных из файла /// /// # Returns @@ -66,31 +67,37 @@ impl PptxParser { info.insert((slide.index, ind as u32), img.data.clone()); info }); - self.slides_text = + self.slides_text.push(format!( + "\n/*****************slide = {} ***************/\n {}\n", + slide.index, slide .text_elements .iter() - .fold(HashMap::new(), |mut sl_text, text_element| { - sl_text.insert(slide.index, text_element.text.clone()); + .fold(String::new(), |mut sl_text, text_element| { + sl_text.push_str(&text_element.text); + sl_text.push('\n'); sl_text }) + )); } } + fn add_text_from_img_in_slides(&mut self) -> Result { Ok(self .slides_text .iter_mut() + .enumerate() .map(|(sl_ind, text)| { text.push_str( &self .slides_img_info .iter() - .filter(|((ind, _), _)| *ind == *sl_ind) - .map(|((_, img_num), data)| { + .filter(|((ind, _), _)| *ind as usize == sl_ind + 1) + .map(|((ind, img_num), data)| { Ok(format!( - "\n/********slide = {sl_ind}; img_num = {img_num}********/\n \ - {}\n \ - /****************************/", + "\n/********slide = {ind}; img_num = {img_num}********/\n\ + {}\n\ + /*****************************************************/", get_from_image(data)? )) }) From 1e969a88f137bfe467d8301675fe31bc201d173a Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 01:04:50 +0400 Subject: [PATCH 11/16] Feat(pptx): parallelize slides images parsing --- parser/src/parsers/pptx.rs | 39 +++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 6239344..e2a1776 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -4,6 +4,8 @@ use std::collections::HashMap; +use rayon::prelude::*; + use crate::{errors::ParserError, parsers::image::get_from_image}; type Result = std::result::Result; @@ -14,7 +16,7 @@ type ImagesInfo = HashMap<(SlideIndex, ImgOnSlideNum), Vec>; pub(crate) struct PptxParser { /// HashMap для сопоставления байтов картинки с её местом в тексте слайда pub slides_img_info: ImagesInfo, - /// HashMap из индекса слайда и текста извлеченного из него + /// Текст слайда (индекс слайда на 1 больше индекса в slides_text) pub slides_text: Vec, } @@ -85,26 +87,25 @@ impl PptxParser { fn add_text_from_img_in_slides(&mut self) -> Result { Ok(self .slides_text - .iter_mut() + .par_iter() .enumerate() .map(|(sl_ind, text)| { - text.push_str( - &self - .slides_img_info - .iter() - .filter(|((ind, _), _)| *ind as usize == sl_ind + 1) - .map(|((ind, img_num), data)| { - Ok(format!( - "\n/********slide = {ind}; img_num = {img_num}********/\n\ - {}\n\ - /*****************************************************/", - get_from_image(data)? - )) - }) - .collect::>>()? - .join("\n"), - ); - Ok(text.clone()) + let mut res_slide_text = String::from(text); + + for ((ind, img_num), data) in self + .slides_img_info + .iter() + .filter(|((ind, _), _)| *ind as usize == sl_ind + 1) + { + res_slide_text.push_str(&format!( + "\n/********slide = {ind}; img_num = {img_num}********/\n" + )); + + res_slide_text.push_str(&get_from_image(data)?); + res_slide_text + .push_str("\n/*****************************************************/"); + } + Ok(res_slide_text) }) .collect::>>()? .join("\n")) From 5c08e2c8a32ff74ab405e5f1c6ea3912aa0af7f2 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 01:05:45 +0400 Subject: [PATCH 12/16] Fix(pptx): set up slides img info --- parser/src/parsers/pptx.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index e2a1776..c47556d 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -60,15 +60,11 @@ impl PptxParser { fn set_slides_text_and_img_info(&mut self, pptx_doc: rustypptx::PptxDocument) { for slide in pptx_doc.slides.iter() { - self.slides_img_info = - slide - .images - .iter() - .enumerate() - .fold(HashMap::new(), |mut info, (ind, img)| { - info.insert((slide.index, ind as u32), img.data.clone()); - info - }); + for (ind, img) in slide.images.iter().enumerate() { + self.slides_img_info + .insert((slide.index, ind as u32), img.data.clone()); + } + self.slides_text.push(format!( "\n/*****************slide = {} ***************/\n {}\n", slide.index, From 26e89c8eea0b83b4e72c554a86214ff708b0e47b Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 01:06:30 +0400 Subject: [PATCH 13/16] Feat(match_parsers): add pptx parser usage --- parser/src/match_parsers.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/parser/src/match_parsers.rs b/parser/src/match_parsers.rs index 9a48a25..230238a 100644 --- a/parser/src/match_parsers.rs +++ b/parser/src/match_parsers.rs @@ -11,7 +11,7 @@ use crate::{ APPLICATION_XLS, APPLICATION_XLSX, }, errors::ParserError, - parsers::{docx, image::get_from_image, pdf::get_from_pdf, text::get_from_text}, + parsers::{docx, image::get_from_image, pdf::get_from_pdf, pptx, text::get_from_text}, }; type Result = std::result::Result; @@ -41,7 +41,11 @@ pub fn get_text(file_name: &str) -> Result { docx_parser.get_from_docx(&file_data) } Some(mime) if mime == APPLICATION_XLSX => todo!(), - Some(mime) if mime == APPLICATION_PPTX => todo!(), + Some(mime) if mime == APPLICATION_PPTX => { + let pptx_parser = pptx::PptxParser::new(); + let (res, _) = pptx_parser.get_from_pptx(&file_data)?; + Ok(res) + } Some(mime) if mime == APPLICATION_PDF => get_from_pdf(&file_data), Some(mime) if mime.type_() == TEXT => get_from_text(&file_data), Some(mime) if mime.type_() == IMAGE => get_from_image(&file_data), From f99c871fdd0481ec8b2396e4889e7c75d5dc9bb5 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 01:06:54 +0400 Subject: [PATCH 14/16] Chore(app): add usage pptx example --- app/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index 5639639..e04abfa 100644 --- a/app/main.py +++ b/app/main.py @@ -7,4 +7,5 @@ # print(docs_parser.get_text("parser/assets/text_from_img.png")) # print(docs_parser.get_text("parser/assets/main.typ")) # print(docs_parser.get_text("parser/assets/main.pdf")) -print(docs_parser.get_text("parser/assets/too_many_png.docx")) +# print(docs_parser.get_text("parser/assets/too_many_png.docx")) +print(docs_parser.get_text("parser/assets/Presentation.pptx")) From a23b7d1659093ca28afd96d842f8dffcd3eeec4e Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 14:52:33 +0400 Subject: [PATCH 15/16] Feat(pptx): add doc comments --- parser/src/parsers/pptx.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index c47556d..53a4bcf 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -45,19 +45,20 @@ impl PptxParser { /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки pub(crate) fn get_from_pptx(mut self, data: &[u8]) -> Result<(String, ImagesInfo)> { let pptx_doc = rustypptx::parse_pptx_bytes(data)?; - let mut result_text = String::new(); + if let Some(title) = &pptx_doc.metadata.title { result_text.push_str(&format!("Название: {title}")); } self.set_slides_text_and_img_info(pptx_doc); - result_text = self.add_text_from_img_in_slides()?; Ok((result_text, self.slides_img_info)) } + /// Заполняет текущий парсер данными из pptx файла для дальнейшей обработки + /// (текст и картинки со слайдов) fn set_slides_text_and_img_info(&mut self, pptx_doc: rustypptx::PptxDocument) { for slide in pptx_doc.slides.iter() { for (ind, img) in slide.images.iter().enumerate() { @@ -80,6 +81,16 @@ impl PptxParser { } } + /// Собирает текст из всех слайдов в единый текст, извлекая и подставляя + /// текст из картинок сладов в нужные места + /// + /// # Returns + /// - Ok([`String`]) - возвращает текст всех слайдов + /// - Err([`ParserError`]) - ошибка во время парсинга pptx файла + /// + /// # Errors + /// - [`ParserError::ImageError`] - ошибка во время парсинга картинки + /// - Остальные [`ParserError`] связанные с Tesseract ошибки во время парсинга картинки fn add_text_from_img_in_slides(&mut self) -> Result { Ok(self .slides_text From 9e1cc63a950aa02d64f9655d0629dc7c9dbb3675 Mon Sep 17 00:00:00 2001 From: aragami3070 Date: Sun, 8 Mar 2026 15:14:43 +0400 Subject: [PATCH 16/16] Feat(pptx): add tests extract text with and without png --- parser/assets/pres_with_png.pptx | Bin 0 -> 32136 bytes parser/assets/pres_without_png.pptx | Bin 0 -> 30637 bytes .../extract_text_from_pptx_with_png.txt | 15 +++++++ .../extract_text_from_pptx_without_png.txt | 11 +++++ parser/src/parsers/pptx.rs | 40 ++++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 parser/assets/pres_with_png.pptx create mode 100644 parser/assets/pres_without_png.pptx create mode 100644 parser/assets/tests_results/extract_text_from_pptx_with_png.txt create mode 100644 parser/assets/tests_results/extract_text_from_pptx_without_png.txt diff --git a/parser/assets/pres_with_png.pptx b/parser/assets/pres_with_png.pptx new file mode 100644 index 0000000000000000000000000000000000000000..84e994995a6d44f1e503223cfcedd392896690a9 GIT binary patch literal 32136 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 literal 0 HcmV?d00001 diff --git a/parser/assets/pres_without_png.pptx b/parser/assets/pres_without_png.pptx new file mode 100644 index 0000000000000000000000000000000000000000..923a0086295bbc577ee779457ed14e262120aa91 GIT binary patch literal 30637 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 literal 0 HcmV?d00001 diff --git a/parser/assets/tests_results/extract_text_from_pptx_with_png.txt b/parser/assets/tests_results/extract_text_from_pptx_with_png.txt new file mode 100644 index 0000000..233fd74 --- /dev/null +++ b/parser/assets/tests_results/extract_text_from_pptx_with_png.txt @@ -0,0 +1,15 @@ + +/*****************slide = 1 ***************/ + Тема презентации +Подзаголовок презентации + + + +/*****************slide = 2 ***************/ + Текст заголовка со слайда +Абиба + + +/********slide = 2; img_num = 0********/ +МЯУ=191919 +/*****************************************************/ \ No newline at end of file diff --git a/parser/assets/tests_results/extract_text_from_pptx_without_png.txt b/parser/assets/tests_results/extract_text_from_pptx_without_png.txt new file mode 100644 index 0000000..397c389 --- /dev/null +++ b/parser/assets/tests_results/extract_text_from_pptx_without_png.txt @@ -0,0 +1,11 @@ + +/*****************slide = 1 ***************/ + Тема презентации +Подзаголовок презентации + + + +/*****************slide = 2 ***************/ + Текст заголовка со слайда +Абиба + diff --git a/parser/src/parsers/pptx.rs b/parser/src/parsers/pptx.rs index 53a4bcf..c7b7821 100644 --- a/parser/src/parsers/pptx.rs +++ b/parser/src/parsers/pptx.rs @@ -118,3 +118,43 @@ impl PptxParser { .join("\n")) } } + +#[cfg(test)] +mod tests { + use crate::{errors::ParserError, parsers::pptx::PptxParser}; + + type Result = std::result::Result; + + /// Считывает данные из файла ввиде byte vec + fn read_data_from_file(file_name: &str) -> Result> { + Ok(std::fs::read(file_name)?) + } + + fn extract_text_from_pptx(extract_file: &str, check_file: &str) -> Result<()> { + let data = read_data_from_file(extract_file)?; + let pars = PptxParser::new(); + let (res, _) = pars.get_from_pptx(&data)?; + + assert_eq!( + res.trim(), + String::from_utf8(read_data_from_file(check_file)?)?.trim() + ); + Ok(()) + } + + #[test] + fn extract_text_from_pptx_without_png() -> Result<()> { + extract_text_from_pptx( + "assets/pres_without_png.pptx", + "assets/tests_results/extract_text_from_pptx_without_png.txt", + ) + } + + #[test] + fn extract_text_from_pptx_with_png() -> Result<()> { + extract_text_from_pptx( + "assets/pres_with_png.pptx", + "assets/tests_results/extract_text_from_pptx_with_png.txt", + ) + } +}