├── .github ├── ISSUE_TEMPLATE │ ├── python-버그-제보.md │ ├── rust-버그-제보.md │ ├── 기능-추가-요청.md │ └── 질문과-도움.md ├── pull_request_template.md └── workflows │ ├── ci.yaml │ └── python.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── crates ├── hwp │ ├── Cargo.toml │ ├── README.md │ ├── src │ │ ├── hwp │ │ │ ├── bin_data.rs │ │ │ ├── body.rs │ │ │ ├── color_ref.rs │ │ │ ├── doc_info │ │ │ │ ├── bin_data.rs │ │ │ │ ├── border_fill.rs │ │ │ │ ├── bullet.rs │ │ │ │ ├── change_tracking.rs │ │ │ │ ├── change_tracking_author.rs │ │ │ │ ├── char_shape.rs │ │ │ │ ├── compatible_document.rs │ │ │ │ ├── font.rs │ │ │ │ ├── id_mappings.rs │ │ │ │ ├── memo_shape.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── numbering.rs │ │ │ │ ├── paragraph_shape.rs │ │ │ │ ├── properties.rs │ │ │ │ ├── style.rs │ │ │ │ ├── tab_definition.rs │ │ │ │ └── track_change.rs │ │ │ ├── header.rs │ │ │ ├── mod.rs │ │ │ ├── paragraph │ │ │ │ ├── char.rs │ │ │ │ ├── char_list.rs │ │ │ │ ├── char_shape.rs │ │ │ │ ├── control │ │ │ │ │ ├── book_mark.rs │ │ │ │ │ ├── column.rs │ │ │ │ │ ├── common_properties.rs │ │ │ │ │ ├── draw_text.rs │ │ │ │ │ ├── element_properties.rs │ │ │ │ │ ├── equation.rs │ │ │ │ │ ├── footnote_endnote.rs │ │ │ │ │ ├── header_footer.rs │ │ │ │ │ ├── hidden_comment.rs │ │ │ │ │ ├── index_mark.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── number.rs │ │ │ │ │ ├── over_type.rs │ │ │ │ │ ├── page_definition.rs │ │ │ │ │ ├── page_hiding.rs │ │ │ │ │ ├── page_number_control.rs │ │ │ │ │ ├── page_number_position.rs │ │ │ │ │ ├── paragraph_list.rs │ │ │ │ │ ├── section.rs │ │ │ │ │ ├── shape_object │ │ │ │ │ │ ├── arc.rs │ │ │ │ │ │ ├── container.rs │ │ │ │ │ │ ├── content.rs │ │ │ │ │ │ ├── curve.rs │ │ │ │ │ │ ├── ellipse.rs │ │ │ │ │ │ ├── gen_shape_object.rs │ │ │ │ │ │ ├── line.rs │ │ │ │ │ │ ├── mod.rs │ │ │ │ │ │ ├── ole.rs │ │ │ │ │ │ ├── picture.rs │ │ │ │ │ │ ├── polygon.rs │ │ │ │ │ │ └── rectangle.rs │ │ │ │ │ ├── sub_text.rs │ │ │ │ │ ├── table.rs │ │ │ │ │ └── unknown.rs │ │ │ │ ├── header.rs │ │ │ │ ├── line_segment.rs │ │ │ │ ├── mod.rs │ │ │ │ └── range_tag.rs │ │ │ ├── parameter_set │ │ │ │ └── mod.rs │ │ │ ├── record │ │ │ │ ├── mod.rs │ │ │ │ ├── reader.rs │ │ │ │ └── tags.rs │ │ │ ├── section.rs │ │ │ ├── unknown.rs │ │ │ ├── utils │ │ │ │ ├── bits.rs │ │ │ │ ├── crypto.rs │ │ │ │ ├── mod.rs │ │ │ │ └── random.rs │ │ │ └── version.rs │ │ ├── hwpx │ │ │ └── mod.rs │ │ └── lib.rs │ └── tests │ │ ├── integration │ │ ├── README.md │ │ ├── hancom │ │ │ ├── README.md │ │ │ ├── files │ │ │ │ ├── 한글문서파일형식_5.0_revision1.3.hwp │ │ │ │ └── 한글문서파일형식_수식_revision1.3.hwp │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── naver_documents │ │ │ ├── README.md │ │ │ ├── files │ │ │ │ ├── annual_report.hwp │ │ │ │ └── work_report.hwp │ │ │ └── mod.rs │ │ └── project │ │ │ ├── files │ │ │ ├── bookmark.hwp │ │ │ ├── color_fill.hwp │ │ │ ├── draw_text.hwp │ │ │ ├── dutmal.hwp │ │ │ ├── gradation_fill.hwp │ │ │ ├── group_with_draw_text.hwp │ │ │ ├── hello_world.hwp │ │ │ ├── image_fill.hwp │ │ │ ├── outline.hwp │ │ │ ├── over_type.hwp │ │ │ ├── range.hwp │ │ │ └── shadow.hwp │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── unit │ │ ├── mod.rs │ │ └── section.rs │ │ └── utils │ │ └── mod.rs ├── macro │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── python │ ├── Cargo.toml │ ├── README.md │ ├── pyproject.toml │ └── src │ ├── bin_data.rs │ ├── lib.rs │ ├── paragraph │ ├── char.rs │ ├── control │ │ ├── common_properties.rs │ │ ├── equation.rs │ │ ├── footnote_endnote.rs │ │ ├── header_footer.rs │ │ ├── mod.rs │ │ └── table.rs │ └── mod.rs │ ├── section.rs │ └── version.rs └── docs └── development.md /.github/ISSUE_TEMPLATE/python-버그-제보.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Python 버그 제보 3 | about: hwppy의 버그를 알려주세요 4 | title: '' 5 | labels: python 6 | assignees: hahnlee 7 | 8 | --- 9 | 10 | **버그 설명** 11 | 버그를 설명해주세요 12 | 13 | **재현방법** 14 | 재현 방법을 설명해주세요 15 | 1. '...' 16 | 17 | **예상되는 동작** 18 | 예상되는 동작을 설명해주세요 19 | 20 | **스크린샷** 21 | 만약 스크린샷이 있다면 업로드 해주세요 22 | 23 | **환경:** 24 | - OS: [e.g. macOS] 25 | - Python 버전 26 | - libhwp 버전 27 | - 테스트한 아래아한글 문서 버전 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/rust-버그-제보.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Rust 버그 제보 3 | about: hwp-rs의 버그를 알려주세요. (잘 모르겠다면 이곳으로 만들어주세요) 4 | title: "[버그]" 5 | labels: Rust 6 | assignees: hahnlee 7 | 8 | --- 9 | 10 | **버그 설명** 11 | 버그를 설명해주세요 12 | 13 | **재현방법** 14 | 재현 방법을 설명해주세요 15 | 1. '...' 16 | 17 | **예상되는 동작** 18 | 예상되는 동작을 설명해주세요 19 | 20 | **스크린샷** 21 | 만약 스크린샷이 있다면 업로드 해주세요 22 | 23 | **환경:** 24 | - OS: [e.g. macOS] 25 | - Rust 버전 26 | - hwp-rs 버전 27 | - 테스트한 아래아한글 문서 버전 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/기능-추가-요청.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 기능 추가 요청 3 | about: 기능 추가 요청은 가급적 discussions를 이용해주세요. 4 | title: "[기능요청]" 5 | labels: enhancement 6 | assignees: hahnlee 7 | 8 | --- 9 | 10 | 기능 추가 요청은 가급적 discussions를 이용해주세요. 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/질문과-도움.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 질문과 도움 3 | about: 질문과 도움은 discussions에 올려주세요. 이슈 생성시 예고없이 close 될 수 있습니다. 4 | title: "[질문]" 5 | labels: question 6 | assignees: hahnlee 7 | 8 | --- 9 | 10 | 질문과 도움은 [discussions](https://github.com/hahnlee/hwp-rs/discussions/categories/%EC%A7%88%EB%AC%B8)에 올려주세요. 이슈 생성시 예고없이 close 될 수 있습니다. 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## PR 제목 2 | https://www.conventionalcommits.org/ko/v1.0.0/ 3 | 4 | Conventional Commits 가이드라인에 맞추어 PR 제목을 만들어주세요. 5 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - develop 7 | paths: 8 | - crates/** 9 | - Cargo.lock 10 | - Cargo.toml 11 | pull_request: 12 | paths: 13 | - crates/** 14 | - Cargo.lock 15 | - Cargo.toml 16 | 17 | concurrency: 18 | group: ci-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Set up checkout 26 | uses: actions/checkout@v2 27 | with: 28 | submodules: true 29 | 30 | - name: Set up cache cargo 31 | uses: actions/cache@v2 32 | with: 33 | path: | 34 | ~/.cargo/registry 35 | ~/.cargo/git 36 | target 37 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 38 | restore-keys: | 39 | ${{ runner.os }}-cargo- 40 | 41 | - name: Set up rust 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | profile: minimal 46 | 47 | - name: Test rust 48 | run: cargo test 49 | -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - crates/** 9 | - Cargo.lock 10 | - Cargo.toml 11 | release: 12 | types: [published] 13 | 14 | concurrency: 15 | group: python-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | linux: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - uses: messense/maturin-action@v1 24 | with: 25 | manylinux: auto 26 | command: build 27 | args: --release --sdist -o dist --find-interpreter -m crates/python/Cargo.toml 28 | - name: Upload wheels 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: wheels 32 | path: dist 33 | 34 | windows: 35 | runs-on: windows-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - uses: messense/maturin-action@v1 39 | with: 40 | command: build 41 | args: --release -o dist --find-interpreter -m crates/python/Cargo.toml 42 | - name: Upload wheels 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: wheels 46 | path: dist 47 | 48 | macos: 49 | runs-on: macos-latest 50 | steps: 51 | - uses: actions/checkout@v3 52 | - uses: messense/maturin-action@v1 53 | with: 54 | command: build 55 | args: --release -o dist --universal2 --find-interpreter -m crates/python/Cargo.toml 56 | - name: Upload wheels 57 | uses: actions/upload-artifact@v2 58 | with: 59 | name: wheels 60 | path: dist 61 | 62 | release: 63 | name: Release 64 | runs-on: ubuntu-latest 65 | if: "startsWith(github.ref, 'refs/tags/')" 66 | needs: [macos, windows, linux] 67 | steps: 68 | - uses: actions/download-artifact@v2 69 | with: 70 | name: wheels 71 | - name: Publish to PyPI 72 | uses: messense/maturin-action@v1 73 | env: 74 | MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} 75 | with: 76 | command: upload 77 | args: --skip-existing * 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Rust ### 2 | /target 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | # General 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | # Icon must end with two \r 26 | Icon 27 | 28 | 29 | # Thumbnails 30 | ._* 31 | 32 | # Files that might appear in the root of a volume 33 | .DocumentRevisions-V100 34 | .fseventsd 35 | .Spotlight-V100 36 | .TemporaryItems 37 | .Trashes 38 | .VolumeIcon.icns 39 | .com.apple.timemachine.donotpresent 40 | 41 | # Directories potentially created on remote AFP share 42 | .AppleDB 43 | .AppleDesktop 44 | Network Trash Folder 45 | Temporary Items 46 | .apdisk 47 | 48 | ### macOS Patch ### 49 | # iCloud generated files 50 | *.icloud 51 | 52 | ### Windows ### 53 | # Windows thumbnail cache files 54 | Thumbs.db 55 | Thumbs.db:encryptable 56 | ehthumbs.db 57 | ehthumbs_vista.db 58 | 59 | # Dump file 60 | *.stackdump 61 | 62 | # Folder config file 63 | [Dd]esktop.ini 64 | 65 | # Recycle Bin used on file shares 66 | $RECYCLE.BIN/ 67 | 68 | # Windows Installer files 69 | *.cab 70 | *.msi 71 | *.msix 72 | *.msm 73 | *.msp 74 | 75 | # Windows shortcuts 76 | *.lnk 77 | 78 | ### PyO3 ### 79 | /dist 80 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/hwp", 4 | "crates/macro", 5 | "crates/python", 6 | ] 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HWP-RS 2 | > 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. 3 | 4 | Rust로 작성된 hwp파서와 각종 도구들 5 | 6 | [hwp-rs와 libhwp를 공개합니다](https://blog.hanlee.io/2022/hwp-rs) 7 | 8 | 9 | - [hwp-rs](./crates/hwp) Rust로 작성된 로우레벨 hwp 파서 10 | - [libhwp](./crates/python) Rust로 작성된 Python hwp 리더 라이브러리 11 | ```python 12 | from libhwp import HWPReader 13 | 14 | hwp = HWPReader('<파일 경로>') 15 | 16 | # 모든 문단 출력 (표, 캡션 포함) 17 | for paragraph in hwp.find_all('paragraph'): 18 | print(paragraph) 19 | 20 | # 테이블 내용 출력 21 | for table in hwp.find_all('table'): 22 | for cell in table.cells: 23 | for paragraph in cell.paragraphs: 24 | print(paragraph) 25 | 26 | # 문서에 사용된 파일 저장 27 | for file in hwp.bin_data: 28 | with open(file.name, 'wb') as f: 29 | f.write(file.data) 30 | ``` 31 | 32 | # 개발가이드 33 | [가이드 문서](./docs/development.md)를 참고해주세요 34 | 35 | # 다른언어 바인딩은 지원 계획이 있나요? 36 | 없습니다. 37 | 38 | 하지만 [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen)을 이용한 웹 어셈블리 바인딩은 준비중이며, [napi-rs](https://napi.rs)를 사용한 node 바인딩은 고려하고 있습니다. 39 | 40 | 필요시 hwp-rs를 직접 바인딩하여 사용해주세요. 만약 장기적인 관리를 해주실 수 있다면 [discussions](https://github.com/hahnlee/hwp-rs/discussions)에 알려주세요. 41 | 42 | # License 43 | ``` 44 | Copyright Han Lee and other contributors 45 | 46 | Licensed under the Apache License, Version 2.0 (the "License"); 47 | you may not use this file except in compliance with the License. 48 | You may obtain a copy of the License at 49 | 50 | http://www.apache.org/licenses/LICENSE-2.0 51 | 52 | Unless required by applicable law or agreed to in writing, software 53 | distributed under the License is distributed on an "AS IS" BASIS, 54 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 55 | See the License for the specific language governing permissions and 56 | limitations under the License. 57 | ``` 58 | -------------------------------------------------------------------------------- /crates/hwp/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hwp" 3 | version = "0.2.0" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "낮은 수준의 hwp 파서" 7 | repository = "https://github.com/hahnlee/hwp-rs" 8 | 9 | [dependencies] 10 | aes = "0.8" 11 | byteorder = "1" 12 | cfb = "0.7" 13 | hwp_macro = { path = "../macro", version = "0.2.0" } 14 | flate2 = "1.0" 15 | num = "0.4" 16 | num-traits = "0.2" 17 | num-derive = "0.3" 18 | -------------------------------------------------------------------------------- /crates/hwp/README.md: -------------------------------------------------------------------------------- 1 | # HWP-RS 2 | > 본 제품은 한글과컴퓨터의 한/글 문서 파일(.hwp) 공개 문서를 참고하여 개발하였습니다. 3 | 4 | 러스트로 작성된 낮은 수준의 HWP 파서. 5 | 6 | hwp의 완전한 해독을 목표로 개발중이며, hwp파일에 최대한 가까운 구조를 유지하려고 합니다. 7 | 8 | # License 9 | ``` 10 | Copyright Han Lee and other contributors 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | ``` 24 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/bin_data.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone)] 2 | pub struct File { 3 | pub name: String, 4 | pub data: Vec, 5 | } 6 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/body.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Seek}; 2 | 3 | use cfb::CompoundFile; 4 | 5 | use super::{header::Header, section::Section}; 6 | 7 | #[derive(Debug)] 8 | pub struct Body { 9 | pub sections: Vec
, 10 | } 11 | 12 | impl Body { 13 | pub fn from_cfb(cfb: &mut CompoundFile, header: &Header) -> Self { 14 | let body_text = cfb.read_storage("/BodyText").unwrap(); 15 | 16 | let size = body_text.count(); 17 | let mut sections: Vec
= Vec::with_capacity(size); 18 | for i in 0..size { 19 | let mut stream = cfb.open_stream(format!("/BodyText/Section{}", i)).unwrap(); 20 | let section = Section::from_stream(&mut stream, header); 21 | sections.push(section); 22 | } 23 | 24 | Self { sections } 25 | } 26 | 27 | pub fn from_distributed(cfb: &mut CompoundFile, header: &Header) -> Self { 28 | let view_text = cfb.read_storage("/ViewText").unwrap(); 29 | let size = view_text.count(); 30 | 31 | let mut sections: Vec
= Vec::with_capacity(size); 32 | 33 | for i in 0..size { 34 | let mut stream = cfb.open_stream(format!("/ViewText/Section{}", i)).unwrap(); 35 | let section = Section::from_distributed(&mut stream, header); 36 | sections.push(section); 37 | } 38 | 39 | Self { sections } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/color_ref.rs: -------------------------------------------------------------------------------- 1 | use super::utils::bits::get_value_range; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct ColorRef { 5 | pub red: u32, 6 | pub blue: u32, 7 | pub green: u32, 8 | } 9 | 10 | impl ColorRef { 11 | pub fn from_u32(number: u32) -> Self { 12 | let red = get_value_range(number, 0, 7); 13 | let blue = get_value_range(number, 8, 15); 14 | let green = get_value_range(number, 16, 23); 15 | 16 | Self { red, blue, green } 17 | } 18 | 19 | pub fn to_hex(&self) -> String { 20 | format!("#{:02X}{:02X}{:02X}", self.red, self.blue, self.green) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/bin_data.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use num::FromPrimitive; 3 | use num_derive::FromPrimitive; 4 | 5 | use crate::hwp::{ 6 | header::Header, 7 | record::{reader::RecordReader, tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 8 | utils::bits::get_value_range, 9 | version::Version, 10 | }; 11 | 12 | #[derive(Debug)] 13 | pub struct BinData { 14 | pub properties: BinDataProperties, 15 | pub absolute_path: Option, 16 | pub relative_path: Option, 17 | pub id: Option, 18 | pub extension: Option, 19 | } 20 | 21 | impl BinData { 22 | pub fn cfb_file_name(&self) -> Option { 23 | if self.properties.kind != BinDataKind::Embedding { 24 | return None; 25 | } 26 | 27 | let mut extension = self.extension.clone().unwrap(); 28 | extension.make_ascii_lowercase(); 29 | 30 | let id = self.id.unwrap(); 31 | 32 | Some(format!("BIN{:0>4X}.{extension}", id)) 33 | } 34 | 35 | pub fn compressed(&self, header: &Header) -> bool { 36 | match self.properties.compress_mode { 37 | CompressMode::Default => header.flags.compressed, 38 | CompressMode::Compress => true, 39 | _ => false, 40 | } 41 | } 42 | } 43 | 44 | impl FromRecordCursor for BinData { 45 | fn from_record_cursor(cursor: &mut RecordCursor, _: &Version) -> Self { 46 | let record = cursor.current(); 47 | 48 | assert_eq!( 49 | record.tag_id, 50 | DocInfoRecord::HWPTAG_BIN_DATA as u32, 51 | "올바르지 않은 정보" 52 | ); 53 | 54 | let mut data = record.get_data_reader(); 55 | let properties = data.read_u16::().unwrap(); 56 | let properties = BinDataProperties::from_bits(properties); 57 | 58 | let absolute_path = if properties.kind == BinDataKind::Link { 59 | Some(data.read_string::().unwrap()) 60 | } else { 61 | None 62 | }; 63 | 64 | let relative_path = if properties.kind == BinDataKind::Link { 65 | Some(data.read_string::().unwrap()) 66 | } else { 67 | None 68 | }; 69 | 70 | let id = if properties.kind == BinDataKind::Embedding 71 | || properties.kind == BinDataKind::Storage 72 | { 73 | Some(data.read_u16::().unwrap()) 74 | } else { 75 | None 76 | }; 77 | 78 | let extension = if properties.kind == BinDataKind::Embedding { 79 | Some(data.read_string::().unwrap()) 80 | } else { 81 | None 82 | }; 83 | 84 | Self { 85 | properties, 86 | absolute_path, 87 | relative_path, 88 | id, 89 | extension, 90 | } 91 | } 92 | } 93 | 94 | #[repr(u8)] 95 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 96 | pub enum BinDataKind { 97 | /// 그림 외부 파일 참조 98 | Link, 99 | /// 그림 파일 포함 100 | Embedding, 101 | /// OLE 포함 102 | Storage, 103 | } 104 | 105 | #[repr(u8)] 106 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 107 | pub enum CompressMode { 108 | /// 스토리지의 디폴트 모드 따라감 109 | Default, 110 | /// 무조건 압축 111 | Compress, 112 | /// 무조건 압축하지 않음 113 | None, 114 | } 115 | 116 | #[repr(u8)] 117 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 118 | pub enum BinDataStatus { 119 | /// 아직 access 된 적이 없는 상태 120 | Initial, 121 | /// access에 성공하여 파일을 찾은 상태 122 | Success, 123 | /// access가 실패한 에러 상태 124 | Failed, 125 | /// 링크 access가 실패했으나 무시된 상태 126 | Ignored, 127 | } 128 | 129 | #[derive(Debug)] 130 | pub struct BinDataProperties { 131 | /// 타입 132 | pub kind: BinDataKind, 133 | /// 압축 모드 134 | pub compress_mode: CompressMode, 135 | /// 상태 136 | pub status: BinDataStatus, 137 | } 138 | 139 | impl BinDataProperties { 140 | pub fn from_bits(bits: u16) -> Self { 141 | Self { 142 | kind: BinDataKind::from_u16(get_value_range(bits, 0, 3)).unwrap(), 143 | compress_mode: CompressMode::from_u16(get_value_range(bits, 4, 5)).unwrap(), 144 | status: BinDataStatus::from_u16(get_value_range(bits, 8, 9)).unwrap(), 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/bullet.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use byteorder::{LittleEndian, ReadBytesExt}; 4 | use num::FromPrimitive; 5 | use num_derive::FromPrimitive; 6 | 7 | use crate::hwp::{ 8 | doc_info::numbering::ParagraphHead, 9 | record::{tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 10 | version::Version, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct Bullet { 15 | /// 문단 머리의 정보 16 | pub paragraph_head: ParagraphHead, 17 | /// 글머리표 문자 18 | pub bullet_char: char, 19 | /// 이미지 글머리표 여부 20 | pub use_image: bool, 21 | /// 이미지 정보 22 | pub image: Image, 23 | /// 체크 글머리표 문자 24 | pub checked_char: char, 25 | } 26 | 27 | impl FromRecordCursor for Bullet { 28 | fn from_record_cursor(cursor: &mut RecordCursor, _: &Version) -> Self { 29 | let record = cursor.current(); 30 | assert_eq!(record.tag_id, DocInfoRecord::HWPTAG_BULLET as u32); 31 | 32 | let mut reader = record.get_data_reader(); 33 | let paragraph_head = ParagraphHead::from_reader(&mut reader, false); 34 | let bullet_char = 35 | char::from_u32(reader.read_u16::().unwrap().into()).unwrap(); 36 | let use_image = reader.read_u32::().unwrap() > 0; 37 | let image = Image::from_reader(&mut reader); 38 | let checked_char = 39 | char::from_u32(reader.read_u16::().unwrap().into()).unwrap(); 40 | 41 | Self { 42 | paragraph_head, 43 | bullet_char, 44 | use_image, 45 | image, 46 | checked_char, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Clone)] 52 | pub struct Image { 53 | /// 밝기 54 | pub bright: u8, 55 | /// 명암 56 | pub contrast: u8, 57 | /// 그림 효과 58 | pub effect: ImageEffect, 59 | /// BinItem의 아이디 참조값 60 | pub bin_item_id: u16, 61 | } 62 | 63 | impl Image { 64 | pub fn from_reader(reader: &mut T) -> Self { 65 | Self { 66 | bright: reader.read_u8().unwrap(), 67 | contrast: reader.read_u8().unwrap(), 68 | effect: ImageEffect::from_u8(reader.read_u8().unwrap()).unwrap(), 69 | bin_item_id: reader.read_u16::().unwrap(), 70 | } 71 | } 72 | } 73 | 74 | #[repr(u8)] 75 | #[derive(Debug, Clone, PartialEq, Eq, FromPrimitive)] 76 | pub enum ImageEffect { 77 | /// 원래 그림에서 78 | RealPic, 79 | /// 그레이스케일로 80 | GrayScale, 81 | /// 흑백으로 82 | BlackWhite, 83 | /// 패턴 8x8 84 | Pattern8x8, 85 | } 86 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/change_tracking.rs: -------------------------------------------------------------------------------- 1 | use crate::hwp::{ 2 | record::{tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 3 | version::Version, 4 | }; 5 | 6 | // TODO: (@hahnlee) 7 | #[derive(Debug)] 8 | pub struct ChangeTracking {} 9 | 10 | impl FromRecordCursor for ChangeTracking { 11 | fn from_record_cursor(cursor: &mut RecordCursor, _: &Version) -> Self { 12 | let record = cursor.current(); 13 | assert_eq!(record.tag_id, DocInfoRecord::HWPTAG_TRACK_CHANGE as u32); 14 | 15 | Self {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/change_tracking_author.rs: -------------------------------------------------------------------------------- 1 | use crate::hwp::{ 2 | record::{tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 3 | version::Version, 4 | }; 5 | 6 | // TODO: (@hahnlee) 7 | #[derive(Debug)] 8 | pub struct ChangeTrackingAuthor {} 9 | 10 | impl FromRecordCursor for ChangeTrackingAuthor { 11 | fn from_record_cursor(cursor: &mut RecordCursor, _: &Version) -> Self { 12 | let record = cursor.current(); 13 | assert_eq!( 14 | record.tag_id, 15 | DocInfoRecord::HWPTAG_TRACK_CHANGE_AUTHOR as u32 16 | ); 17 | 18 | Self {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/char_shape.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use num::FromPrimitive; 3 | use num_derive::FromPrimitive; 4 | 5 | use crate::hwp::{ 6 | color_ref::ColorRef, 7 | record::{tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 8 | utils::bits::{get_flag, get_value_range}, 9 | version::Version, 10 | }; 11 | 12 | use super::border_fill::BorderKind; 13 | 14 | #[derive(Debug)] 15 | pub struct CharShape { 16 | /// 언어별 글꼴 ID(FaceID) 참조 값 17 | pub font_ids: [u16; 7], 18 | /// 언어별 장평, 50%~200% 19 | pub font_scales: [u8; 7], 20 | /// 언어별 자간 21 | pub font_spacings: [i8; 7], 22 | /// 언어별 상대 크기, 10%~250% 23 | pub font_sizes: [u8; 7], 24 | /// 언어별 글자 위치, -100%~100% 25 | pub font_positions: [i8; 7], 26 | /// 기준 크기, 0pt~4096pt 27 | pub base_size: i32, 28 | /// 기울임 여부 29 | pub italic: bool, 30 | /// 진하게 여부 31 | pub bold: bool, 32 | /// 밑줄 종류 33 | pub underline_kind: UnderlineKind, 34 | /// 밑줄 모양 35 | pub underline_shape: BorderKind, 36 | /// 외곽선 종류 37 | pub outline_kind: OutlineKind, 38 | /// 그림자 종류 39 | pub shadow_kind: ShadowKind, 40 | /// 양각여부 41 | pub emboss: bool, 42 | /// 음각여부 43 | pub engrave: bool, 44 | /// 위 첨자 여부 45 | pub supscript: bool, 46 | /// 아래 첨자 여부 47 | pub subscript: bool, 48 | /// 취소선 여부 49 | pub strike: bool, 50 | /// 강조점 종류 51 | pub sym_mark: SymMark, 52 | /// 글꼴에 어울리는 빈칸 사용 여부 53 | pub use_font_space: bool, 54 | /// 취소선 모양 55 | pub strike_shape: BorderKind, 56 | /// Kerning 여부 57 | pub use_kerning: bool, 58 | /// 그림자 간격 X, -100%~100% 59 | pub shadow_offset_x: u8, 60 | /// 그림자 간격 Y, -100%~100% 61 | pub shadow_offset_y: u8, 62 | /// 글자 색 63 | pub color: ColorRef, 64 | /// 밑줄 색 65 | pub underline_color: ColorRef, 66 | /// 음영 색 67 | pub shade_color: ColorRef, 68 | /// 그림자 색 69 | pub shadow_color: ColorRef, 70 | /// 글자 테두리/배경 ID 참조 값 (5.0.2.1 이상) 71 | pub border_fill_id: Option, 72 | /// 취소선 색 (5.0.3.0 이상) 73 | pub strike_color: Option, 74 | } 75 | 76 | impl FromRecordCursor for CharShape { 77 | fn from_record_cursor(cursor: &mut RecordCursor, version: &Version) -> Self { 78 | let record = cursor.current(); 79 | assert_eq!( 80 | record.tag_id, 81 | DocInfoRecord::HWPTAG_CHAR_SHAPE as u32, 82 | "올바르지 않은 정보" 83 | ); 84 | 85 | let mut reader = record.get_data_reader(); 86 | 87 | let font_ids = [ 88 | reader.read_u16::().unwrap(), 89 | reader.read_u16::().unwrap(), 90 | reader.read_u16::().unwrap(), 91 | reader.read_u16::().unwrap(), 92 | reader.read_u16::().unwrap(), 93 | reader.read_u16::().unwrap(), 94 | reader.read_u16::().unwrap(), 95 | ]; 96 | 97 | let font_scales = [ 98 | reader.read_u8().unwrap(), 99 | reader.read_u8().unwrap(), 100 | reader.read_u8().unwrap(), 101 | reader.read_u8().unwrap(), 102 | reader.read_u8().unwrap(), 103 | reader.read_u8().unwrap(), 104 | reader.read_u8().unwrap(), 105 | ]; 106 | 107 | let font_spacings = [ 108 | reader.read_i8().unwrap(), 109 | reader.read_i8().unwrap(), 110 | reader.read_i8().unwrap(), 111 | reader.read_i8().unwrap(), 112 | reader.read_i8().unwrap(), 113 | reader.read_i8().unwrap(), 114 | reader.read_i8().unwrap(), 115 | ]; 116 | 117 | let font_sizes = [ 118 | reader.read_u8().unwrap(), 119 | reader.read_u8().unwrap(), 120 | reader.read_u8().unwrap(), 121 | reader.read_u8().unwrap(), 122 | reader.read_u8().unwrap(), 123 | reader.read_u8().unwrap(), 124 | reader.read_u8().unwrap(), 125 | ]; 126 | 127 | let font_positions = [ 128 | reader.read_i8().unwrap(), 129 | reader.read_i8().unwrap(), 130 | reader.read_i8().unwrap(), 131 | reader.read_i8().unwrap(), 132 | reader.read_i8().unwrap(), 133 | reader.read_i8().unwrap(), 134 | reader.read_i8().unwrap(), 135 | ]; 136 | 137 | let base_size = reader.read_i32::().unwrap(); 138 | 139 | let attribute = reader.read_u32::().unwrap(); 140 | let italic = get_flag(attribute, 0); 141 | let bold = get_flag(attribute, 1); 142 | let underline_kind = UnderlineKind::from_u32(get_value_range(attribute, 2, 3)).unwrap(); 143 | let underline_shape = BorderKind::from_u32(get_value_range(attribute, 4, 7)).unwrap(); 144 | let outline_kind = OutlineKind::from_u32(get_value_range(attribute, 8, 10)).unwrap(); 145 | let shadow_kind = ShadowKind::from_u32(get_value_range(attribute, 11, 12)).unwrap(); 146 | let emboss = get_flag(attribute, 13); 147 | let engrave = get_flag(attribute, 14); 148 | let supscript = get_flag(attribute, 15); 149 | let subscript = get_flag(attribute, 16); 150 | let strike = get_value_range(attribute, 18, 20) > 0; 151 | let sym_mark = SymMark::from_u32(get_value_range(attribute, 21, 24)).unwrap(); 152 | let use_font_space = get_flag(attribute, 25); 153 | let strike_shape = BorderKind::from_u32(get_value_range(attribute, 26, 29)).unwrap(); 154 | let use_kerning = get_flag(attribute, 30); 155 | 156 | let shadow_offset_x = reader.read_u8().unwrap(); 157 | let shadow_offset_y = reader.read_u8().unwrap(); 158 | 159 | let color = ColorRef::from_u32(reader.read_u32::().unwrap()); 160 | let underline_color = ColorRef::from_u32(reader.read_u32::().unwrap()); 161 | let shade_color = ColorRef::from_u32(reader.read_u32::().unwrap()); 162 | let shadow_color = ColorRef::from_u32(reader.read_u32::().unwrap()); 163 | 164 | let border_fill_id = if *version >= Version::from_str("5.0.2.1") { 165 | Some(reader.read_u16::().unwrap()) 166 | } else { 167 | None 168 | }; 169 | 170 | let strike_color = if *version >= Version::from_str("5.0.3.0") { 171 | Some(ColorRef::from_u32( 172 | reader.read_u32::().unwrap(), 173 | )) 174 | } else { 175 | None 176 | }; 177 | 178 | Self { 179 | font_ids, 180 | font_scales, 181 | font_spacings, 182 | font_sizes, 183 | font_positions, 184 | base_size, 185 | italic, 186 | bold, 187 | underline_kind, 188 | underline_shape, 189 | outline_kind, 190 | shadow_kind, 191 | emboss, 192 | engrave, 193 | supscript, 194 | subscript, 195 | strike, 196 | sym_mark, 197 | use_font_space, 198 | strike_shape, 199 | use_kerning, 200 | shadow_offset_x, 201 | shadow_offset_y, 202 | color, 203 | underline_color, 204 | shade_color, 205 | shadow_color, 206 | border_fill_id, 207 | strike_color, 208 | } 209 | } 210 | } 211 | 212 | #[repr(u8)] 213 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 214 | pub enum UnderlineKind { 215 | None, 216 | Bottom, 217 | Top, 218 | } 219 | 220 | #[repr(u8)] 221 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 222 | pub enum OutlineKind { 223 | /// 없음 224 | None, 225 | /// 실선 226 | Solid, 227 | /// 점선 228 | Dot, 229 | /// 굵은 실선(두꺼운 선) 230 | Tick, 231 | /// 파선(긴 점선) 232 | Dash, 233 | /// 일점쇄선 (-.-.-.-.) 234 | DashDot, 235 | /// 이점쇄선 (-..-..-..) 236 | DashDotDot, 237 | } 238 | 239 | #[repr(u8)] 240 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 241 | pub enum ShadowKind { 242 | /// 없음 243 | None, 244 | /// 비연속 245 | Drop, 246 | /// 연속 247 | Continuous, 248 | } 249 | 250 | #[repr(u8)] 251 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 252 | pub enum SymMark { 253 | /// 없음 254 | None, 255 | /// 검정 동그라미 강조점 256 | DotAbove, 257 | /// 속 빈 동그라미 강조점̊̊̊̊̊̊̊̊̊ 258 | RingAbove, 259 | /// ˇ 260 | Caron, 261 | /// ̃ 262 | Tilde, 263 | /// ・ 264 | DotMiddle, 265 | /// : 266 | Colon, 267 | } 268 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/compatible_document.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | use num::FromPrimitive; 3 | use num_derive::FromPrimitive; 4 | 5 | use crate::hwp::record::{tags::DocInfoRecord, RecordCursor}; 6 | 7 | #[derive(Debug)] 8 | pub struct CompatibleDocument { 9 | /// 대상 프로그램 10 | pub target_program: TargetProgram, 11 | /// 레이아웃 호환성 12 | pub layout_compatibility: LayoutCompatibility, 13 | } 14 | 15 | impl CompatibleDocument { 16 | pub fn from_record_cursor(cursor: &mut RecordCursor) -> Self { 17 | let record = cursor.current(); 18 | let mut reader = record.get_data_reader(); 19 | 20 | let target_program = 21 | TargetProgram::from_u32(reader.read_u32::().unwrap()).unwrap(); 22 | 23 | assert_eq!(reader.position(), record.size.into()); 24 | 25 | let layout_compatibility = LayoutCompatibility::from_record_cursor(cursor); 26 | 27 | Self { 28 | target_program, 29 | layout_compatibility, 30 | } 31 | } 32 | } 33 | 34 | #[repr(u8)] 35 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 36 | pub enum TargetProgram { 37 | /// 한/글 문서(현재 버전) 38 | HWP201X, 39 | /// 한/글 2007 호환 문서 40 | HWP200X, 41 | /// MS 워드 호환 문서 42 | MSWord, 43 | } 44 | 45 | #[derive(Debug)] 46 | pub struct LayoutCompatibility { 47 | /// 글자 단위 서식 48 | pub text_attribute: u32, 49 | /// 문단 단위 서식 50 | pub paragraph_attribute: u32, 51 | /// 구역 단위 서식 52 | pub section_attribute: u32, 53 | /// 개체 단위 서식 54 | pub object_attribute: u32, 55 | /// 필드 단위 서식 56 | pub field_attribute: u32, 57 | } 58 | 59 | impl LayoutCompatibility { 60 | pub fn from_record_cursor(cursor: &mut RecordCursor) -> Self { 61 | let record = cursor.current(); 62 | assert_eq!( 63 | record.tag_id, 64 | DocInfoRecord::HWPTAG_LAYOUT_COMPATIBILITY as u32 65 | ); 66 | 67 | let mut reader = record.get_data_reader(); 68 | 69 | // NOTE: (@hahnlee) 문서와 되어있지 않음, 정확한 정보는 HWPX와 대조해서 유추해야함 70 | let text_attribute = reader.read_u32::().unwrap(); 71 | let paragraph_attribute = reader.read_u32::().unwrap(); 72 | let section_attribute = reader.read_u32::().unwrap(); 73 | let object_attribute = reader.read_u32::().unwrap(); 74 | let field_attribute = reader.read_u32::().unwrap(); 75 | 76 | Self { 77 | text_attribute, 78 | paragraph_attribute, 79 | section_attribute, 80 | object_attribute, 81 | field_attribute, 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/font.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use byteorder::{LittleEndian, ReadBytesExt}; 4 | use num::FromPrimitive; 5 | use num_derive::FromPrimitive; 6 | 7 | use crate::hwp::{ 8 | record::{reader::RecordReader, tags::DocInfoRecord, FromRecordCursor, RecordCursor}, 9 | utils::bits::get_flag, 10 | version::Version, 11 | }; 12 | 13 | #[derive(Debug)] 14 | pub struct Font { 15 | /// 글꼴 이름 16 | pub name: String, 17 | /// 기본 글꼴 이름 18 | pub default_font_name: Option, 19 | /// 글꼴유형정보 20 | pub panose: Option, 21 | /// 대체 글꼴 유형 22 | pub alternative_kind: Option, 23 | /// 대체 글꼴 이름 24 | pub alternative_font_name: Option, 25 | } 26 | 27 | impl FromRecordCursor for Font { 28 | fn from_record_cursor(cursor: &mut RecordCursor, _: &Version) -> Self { 29 | let record = cursor.current(); 30 | 31 | assert_eq!( 32 | record.tag_id, 33 | DocInfoRecord::HWPTAG_FACE_NAME as u32, 34 | "올바르지 않은 정보" 35 | ); 36 | 37 | let mut reader = record.get_data_reader(); 38 | 39 | let properties = reader.read_u8().unwrap(); 40 | let name = reader.read_string::().unwrap(); 41 | 42 | let has_alternative = get_flag(properties, 7); 43 | let has_panose = get_flag(properties, 6); 44 | let has_default_font = get_flag(properties, 5); 45 | 46 | let alternative_kind = if has_alternative { 47 | Some(AlternativeKind::from_u8(reader.read_u8().unwrap()).unwrap()) 48 | } else { 49 | None 50 | }; 51 | let alternative_font_name = if has_alternative { 52 | Some(reader.read_string::().unwrap()) 53 | } else { 54 | None 55 | }; 56 | 57 | let panose = if has_panose { 58 | Some(Panose::from_reader(&mut reader)) 59 | } else { 60 | None 61 | }; 62 | 63 | let default_font_name = if has_default_font { 64 | Some(reader.read_string::().unwrap()) 65 | } else { 66 | None 67 | }; 68 | 69 | Self { 70 | name, 71 | default_font_name, 72 | panose, 73 | alternative_kind, 74 | alternative_font_name, 75 | } 76 | } 77 | } 78 | 79 | /// https://en.wikipedia.org/wiki/PANOSE 80 | /// https://monotype.github.io/panose/pan1.htm 81 | #[derive(Debug)] 82 | pub struct Panose { 83 | /// 글꼴 계열 84 | pub kind: u8, 85 | /// 세리프 유형 86 | pub serif_style: u8, 87 | /// 굵기 88 | pub weight: u8, 89 | /// 비례 90 | pub proportion: u8, 91 | /// 대조 92 | pub contrast: u8, 93 | /// 스트로크 편차 94 | pub stroke_variation: u8, 95 | /// 자획 유형 96 | pub arm_style: u8, 97 | /// 글자형 98 | pub letterform: u8, 99 | /// 중간선 100 | pub midline: u8, 101 | /// X-높이 102 | pub x_height: u8, 103 | } 104 | 105 | impl Panose { 106 | pub fn from_reader(reader: &mut T) -> Self { 107 | Self { 108 | kind: reader.read_u8().unwrap(), 109 | serif_style: reader.read_u8().unwrap(), 110 | weight: reader.read_u8().unwrap(), 111 | proportion: reader.read_u8().unwrap(), 112 | contrast: reader.read_u8().unwrap(), 113 | stroke_variation: reader.read_u8().unwrap(), 114 | arm_style: reader.read_u8().unwrap(), 115 | letterform: reader.read_u8().unwrap(), 116 | midline: reader.read_u8().unwrap(), 117 | x_height: reader.read_u8().unwrap(), 118 | } 119 | } 120 | } 121 | 122 | #[repr(u8)] 123 | #[derive(Debug, PartialEq, Eq, FromPrimitive)] 124 | pub enum AlternativeKind { 125 | /// 원래 종류를 알 수 없을 때 126 | Unknown, 127 | /// 트루타입 글꼴 128 | TTF, 129 | /// 한/글 전용 글꼴 130 | HFT, 131 | } 132 | -------------------------------------------------------------------------------- /crates/hwp/src/hwp/doc_info/id_mappings.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{LittleEndian, ReadBytesExt}; 2 | 3 | use crate::hwp::{ 4 | record::{read_items, tags::DocInfoRecord, RecordCursor}, 5 | version::Version, 6 | }; 7 | 8 | use super::{ 9 | bin_data::BinData, border_fill::BorderFill, bullet::Bullet, change_tracking::ChangeTracking, 10 | change_tracking_author::ChangeTrackingAuthor, char_shape::CharShape, font::Font, 11 | memo_shape::MemoShape, numbering::Numbering, paragraph_shape::ParagraphShape, style::Style, 12 | tab_definition::TabDefinition, 13 | }; 14 | 15 | #[derive(Debug)] 16 | pub struct IDMappings { 17 | /// 바이너리 데이터 18 | pub binary_data: Vec, 19 | /// 한글 글꼴 20 | pub korean_fonts: Vec, 21 | /// 영어 글꼴 22 | pub english_fonts: Vec, 23 | /// 한자 글꼴 24 | pub chinese_characters_fonts: Vec, 25 | /// 일어 글꼴 26 | pub japanese_fonts: Vec, 27 | /// 기타 글꼴 28 | pub etc_fonts: Vec, 29 | /// 기호 글꼴 30 | pub symbol_fonts: Vec, 31 | /// 사용자 글꼴 32 | pub user_fonts: Vec, 33 | /// 테두리/배경 34 | pub border_fills: Vec, 35 | /// 글자 모양 36 | pub char_shapes: Vec, 37 | /// 탭 정의 38 | pub tab_definitions: Vec, 39 | /// 문단 번호 40 | pub numberings: Vec, 41 | /// 글머리표 42 | pub bullets: Vec, 43 | /// 문단 모양 44 | pub paragraph_shapes: Vec, 45 | /// 스타일(문단 스타일) 46 | pub styles: Vec