HyunsangJoo commited on
Commit
eea5466
ยท
1 Parent(s): 7a527a3

Refactor PPTX generation logic to separate module

Browse files

Moved all PowerPoint generation functions from app.py to a new module dots_ocr/utils/pptx_generator.py for better modularity and maintainability. Added PPT_DATA_SCHEMA.md to document the required input data structure. Included local_test.py and test.json for local testing of PPTX generation.

Files changed (5) hide show
  1. PPT_DATA_SCHEMA.md +65 -0
  2. app.py +3 -327
  3. dots_ocr/utils/pptx_generator.py +354 -0
  4. local_test.py +48 -0
  5. test.json +51 -0
PPT_DATA_SCHEMA.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PPT Conversion Data Schema
2
+
3
+ This document outlines the data schema required by the `build_pptx_from_results` function in `app.py` to generate a PowerPoint presentation.
4
+
5
+ ## Overview
6
+
7
+ The input data (`parse_results`) is a list of dictionaries, where each dictionary represents a **single page**.
8
+
9
+ ## Data Structure
10
+
11
+ ```json
12
+ [
13
+ {
14
+ "layout_result": [
15
+ {
16
+ "bbox": [109, 235, 450, 280],
17
+ "category": "Title",
18
+ "text": "Document Title"
19
+ },
20
+ {
21
+ "bbox": [109, 300, 800, 500],
22
+ "category": "Text",
23
+ "text": "Content goes here..."
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "layout_result": [ ... next page data ... ]
29
+ }
30
+ ]
31
+ ```
32
+
33
+ ## Field Definitions
34
+
35
+ Each item in the `layout_result` list must be a dictionary with the following fields:
36
+
37
+ | Field | Type | Required | Description |
38
+ | :--- | :--- | :--- | :--- |
39
+ | **bbox** | `List[int]` | **Yes** | `[x1, y1, x2, y2]` coordinates (Top-Left, Bottom-Right). Absolute pixel coordinates relative to the image size. |
40
+ | **category** | `str` | **Yes** | The layout element category (see Allowed Categories below). |
41
+ | **text** | `str` | Conditional | The text content to display. Required for text-based categories. |
42
+
43
+ ## Allowed Categories
44
+
45
+ The `category` field determines styling (font size, bolding) and whether the element is included in the PPT.
46
+
47
+ ### Text Elements (Rendered)
48
+ These categories are rendered as text boxes in the PowerPoint slide.
49
+
50
+ * `Title`: Rendered in **Bold**. Minimum font size 24pt.
51
+ * `Section-header`: Rendered in **Bold**. Minimum font size 18pt.
52
+ * `Caption`: Maximum font size 12pt.
53
+ * `Footnote`: Maximum font size 12pt.
54
+ * `Text`: Standard text body.
55
+ * `List-item`: Standard text body.
56
+ * `Page-header`
57
+ * `Page-footer`
58
+ * `Formula`
59
+
60
+ ### Non-Text Elements (Skipped)
61
+ These categories are **excluded** from text box generation in the current implementation (they are assumed to be part of the background image or handled separately).
62
+
63
+ * `Picture`
64
+ * `Table`
65
+
app.py CHANGED
@@ -38,15 +38,13 @@ from qwen_vl_utils import process_vision_info
38
  # pptx imports
39
  try:
40
  from pptx import Presentation
41
- from pptx.util import Inches, Pt
42
- from pptx.dml.color import RGBColor
43
- from pptx.enum.text import MSO_AUTO_SIZE, MSO_ANCHOR, PP_ALIGN
44
  except ImportError as exc:
45
  raise ImportError("python-pptx is required.") from exc
46
 
47
  # dots_ocr utils (PDF ๋กœ๋“œ์šฉ)
48
  from dots_ocr.utils.doc_utils import load_images_from_pdf
49
  from dots_ocr.utils.consts import MIN_PIXELS, MAX_PIXELS, IMAGE_FACTOR
 
50
 
51
 
52
  # ============================================================
@@ -107,144 +105,7 @@ CATEGORY_COLORS = {
107
  # ํฐํŠธ ํฌ๊ธฐ ๊ด€๋ จ ์ƒ์ˆ˜ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ
108
  # ============================================================
109
 
110
- # PPT ํ‘œ์ค€ ํฐํŠธ ํฌ๊ธฐ ํ”„๋ฆฌ์…‹
111
- FONT_PRESETS = [
112
- 8, 9, 10, 11, 12, 14, 16, 18, 20, 24,
113
- 28, 32, 36, 40, 44, 48, 54, 60, 66, 72,
114
- 80, 88, 96
115
- ]
116
-
117
- def _calculate_font_size(
118
- width: int,
119
- height: int,
120
- text: str,
121
- category: str = "",
122
- is_bold: bool = False
123
- ) -> Pt:
124
- """
125
- ๋ฐ•์Šค ํฌ๊ธฐ์™€ ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„์„ ๋ถ„์„ํ•˜์—ฌ ์ตœ์ ์˜ ํฐํŠธ ํฌ๊ธฐ ๊ณ„์‚ฐ
126
-
127
- Args:
128
- width: ๋ฐ•์Šค ๋„ˆ๋น„ (EMU ๋‹จ์œ„)
129
- height: ๋ฐ•์Šค ๋†’์ด (EMU ๋‹จ์œ„)
130
- text: ํ…์ŠคํŠธ ๋‚ด์šฉ
131
- category: ๋ ˆ์ด์•„์›ƒ ์นดํ…Œ๊ณ ๋ฆฌ
132
- is_bold: ๋ณผ๋“œ์ฒด ์—ฌ๋ถ€
133
-
134
- Returns:
135
- Pt ๊ฐ์ฒด (๊ณ„์‚ฐ๋œ ํฐํŠธ ํฌ๊ธฐ)
136
- """
137
- if not text:
138
- return Pt(12)
139
-
140
- # EMU to Pt conversion
141
- w_pt = width / 12700
142
- h_pt = height / 12700
143
-
144
- # 1. ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„ ๋ถ„์„ (Character Composition Analysis)
145
- total_weight = 0.0
146
- max_word_weight = 0.0
147
- current_word_weight = 0.0
148
-
149
- # ๊ฐ€์ค‘์น˜ ์„ค์ •
150
- # ๋ณผ๋“œ์ฒด์ผ ๊ฒฝ์šฐ ๊ฐ€์ค‘์น˜ ์ƒํ–ฅ
151
- w_hangul = 1.0
152
- w_upper = 0.8 if is_bold else 0.7
153
- w_lower = 0.6 if is_bold else 0.55
154
- w_other = 0.4 if is_bold else 0.3
155
-
156
- for char in text:
157
- if '๊ฐ€' <= char <= 'ํžฃ':
158
- weight = w_hangul
159
- elif 'A' <= char <= 'Z':
160
- weight = w_upper
161
- elif 'a' <= char <= 'z' or '0' <= char <= '9':
162
- weight = w_lower
163
- else:
164
- weight = w_other
165
-
166
- # ๊ณต๋ฐฑ์ด๋ฉด ๋‹จ์–ด ๊ตฌ๋ถ„
167
- if char.isspace():
168
- max_word_weight = max(max_word_weight, current_word_weight)
169
- current_word_weight = 0.0
170
- # ๊ณต๋ฐฑ ์ž์ฒด๋Š” total_weight์— ํฌํ•จ (์ค„๋ฐ”๊ฟˆ ๊ณ„์‚ฐ์šฉ)
171
- total_weight += weight
172
- else:
173
- current_word_weight += weight
174
- total_weight += weight
175
-
176
- # ๋งˆ์ง€๋ง‰ ๋‹จ์–ด ์ฒ˜๋ฆฌ
177
- max_word_weight = max(max_word_weight, current_word_weight)
178
-
179
- # ์•ˆ์ „ ์žฅ์น˜: ๊ฐ€์ค‘์น˜๊ฐ€ 0์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’
180
- if total_weight <= 0:
181
- total_weight = len(text) * 0.5
182
- if max_word_weight <= 0:
183
- max_word_weight = total_weight
184
-
185
- # 2. Case A: ๋„ˆ๋น„ ์ œ์•ฝ (๋‹จ์–ด ๋‹จ์œ„)
186
- # ๊ฐ€์žฅ ๊ธด ๋‹จ์–ด๊ฐ€ ๋ฐ•์Šค ๋„ˆ๋น„๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ํ•จ
187
- # Font Size * Max Word Weight <= Width
188
- # Font Size <= Width / Max Word Weight
189
- # ์•ˆ์ „ ๋งˆ์ง„ 90% ์ ์šฉ
190
- width_limit_size = (w_pt / max_word_weight) * 0.9
191
-
192
- # 3. Case B: ๋†’์ด ์ œ์•ฝ (์ค„๋ฐ”๊ฟˆ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
193
- # ํฐ ํ”„๋ฆฌ์…‹๋ถ€ํ„ฐ ๋‚ด๋ ค์˜ค๋ฉด์„œ ๋†’์ด ์กฐ๊ฑด ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธ
194
- height_limit_size = FONT_PRESETS[0] # ๊ธฐ๋ณธ๊ฐ’
195
-
196
- for preset in reversed(FONT_PRESETS):
197
- # ์˜ˆ์ƒ ์ค„ ์ˆ˜ = ์˜ฌ๋ฆผ(์ด ํ…์ŠคํŠธ ๋„ˆ๋น„ / ๋ฐ•์Šค ๋„ˆ๋น„)
198
- # ์ด ํ…์ŠคํŠธ ๋„ˆ๋น„ = total_weight * preset
199
- # ์—ฌ์œ  ์žˆ๊ฒŒ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•ด ์ค„๋ฐ”๊ฟˆ ์‹œ ๋‚ญ๋น„๋˜๋Š” ๊ณต๊ฐ„ ๊ณ ๋ ค (1.0 -> 1.1 ๋“ฑ ์กฐ์ • ๊ฐ€๋Šฅ)
200
- text_virtual_width = total_weight * preset
201
- expected_lines = math.ceil(text_virtual_width / w_pt)
202
-
203
- # ์ตœ์†Œ 1์ค„
204
- if expected_lines < 1:
205
- expected_lines = 1
206
-
207
- # ํ•„์š” ๋†’์ด = ์ค„ ์ˆ˜ * ํฐํŠธํฌ๊ธฐ * ์ค„๊ฐ„๊ฒฉ(1.2)
208
- required_height = expected_lines * preset * 1.2
209
-
210
- # ๋ฐ•์Šค ๋†’์ด์˜ 95% ์ด๋‚ด์— ๋“ค์–ด์˜ค๋ฉด ์ฑ„ํƒ
211
- if required_height <= h_pt * 0.95:
212
- height_limit_size = preset
213
- break
214
-
215
- # ๊ฐ€์žฅ ์ž‘์€ ํ”„๋ฆฌ์…‹๊นŒ์ง€ ์™”๋Š”๋ฐ๋„ ์•ˆ๋˜๋ฉด ๊ฐ€์žฅ ์ž‘์€๊ฑฐ ์„ ํƒ
216
- if preset == FONT_PRESETS[0]:
217
- height_limit_size = preset
218
-
219
- # 4. ์ตœ์ข… ํฌ๊ธฐ ๊ฒฐ์ • ๋ฐ ๋ณด์ •
220
- target_size = min(width_limit_size, height_limit_size)
221
-
222
- # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ตœ์†Œ/์ตœ๋Œ€ ๋ณด์ •
223
- min_size = 9
224
- max_size = 80
225
-
226
- if category == 'Title':
227
- min_size = 24
228
- # ์ œ๋ชฉ์€ ํ•œ ์ค„์— ๊ฝ‰ ์ฐจ๊ฒŒ ๋ณด์ด๋Š”๊ฒŒ ์ข‹์œผ๋ฏ€๋กœ width_limit ๋น„์ค‘์„ ๋†’๊ฒŒ ๋ด„
229
- target_size = max(target_size, 24)
230
- elif category == 'Section-header':
231
- min_size = 18
232
- target_size = max(target_size, 18)
233
- elif category in ('Caption', 'Footnote'):
234
- max_size = 12
235
-
236
- # ๋ฒ”์œ„ ์ œํ•œ
237
- target_size = max(min_size, min(target_size, max_size))
238
-
239
- # ํ”„๋ฆฌ์…‹ ๋งคํ•‘ (๊ณ„์‚ฐ๋œ ๊ฐ’๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์€ ๊ฐ€์žฅ ํฐ ํ”„๋ฆฌ์…‹)
240
- final_size = FONT_PRESETS[0]
241
- for preset in FONT_PRESETS:
242
- if preset <= target_size:
243
- final_size = preset
244
- else:
245
- break
246
-
247
- return Pt(final_size)
248
 
249
 
250
  # ============================================================
@@ -712,193 +573,8 @@ def process_single_image(
712
  # ============================================================
713
  # PPTX ์ƒ์„ฑ ํ•จ์ˆ˜๋“ค
714
  # ============================================================
715
- def _get_page_size(
716
- page_image: Optional[Image.Image], layout_data: Optional[List[Dict]]
717
- ) -> Tuple[int, int]:
718
- if page_image:
719
- return page_image.width, page_image.height
720
- return 1200, 1600
721
-
722
-
723
- def _add_background(slide, slide_width, slide_height, page_image: Optional[Image.Image]) -> None:
724
- if not page_image:
725
- return
726
- try:
727
- buf = io.BytesIO()
728
- page_image.save(buf, format="PNG")
729
- buf.seek(0)
730
- slide.shapes.add_picture(buf, 0, 0, width=slide_width, height=slide_height)
731
- except Exception as e:
732
- print(f"Background add failed: {e}")
733
-
734
-
735
- def _clean_text_for_pptx(text: str) -> str:
736
- """
737
- PPTX์šฉ ํ…์ŠคํŠธ ์ •๋ฆฌ - ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
738
-
739
- ์ œ๊ฑฐ ๋Œ€์ƒ: #, **, *, `, ^, ~~ ๋“ฑ
740
- """
741
- import re
742
-
743
- if not text:
744
- return ""
745
-
746
- # ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
747
- cleaned = text
748
-
749
- # ํ—ค๋”ฉ ๊ธฐํ˜ธ ์ œ๊ฑฐ: # ## ### ๋“ฑ
750
- cleaned = re.sub(r'^#{1,6}\s*', '', cleaned, flags=re.MULTILINE)
751
-
752
- # ๋ณผ๋“œ/์ดํƒค๋ฆญ ์ œ๊ฑฐ: **text** โ†’ text, *text* โ†’ text, __text__ โ†’ text, _text_ โ†’ text
753
- cleaned = re.sub(r'\*\*(.+?)\*\*', r'\1', cleaned)
754
- cleaned = re.sub(r'__(.+?)__', r'\1', cleaned)
755
- cleaned = re.sub(r'\*(.+?)\*', r'\1', cleaned)
756
- cleaned = re.sub(r'_(.+?)_', r'\1', cleaned)
757
-
758
- # ์ธ๋ผ์ธ ์ฝ”๋“œ ์ œ๊ฑฐ: `code` โ†’ code
759
- cleaned = re.sub(r'`(.+?)`', r'\1', cleaned)
760
-
761
- # ์ทจ์†Œ์„  ์ œ๊ฑฐ: ~~text~~ โ†’ text
762
- cleaned = re.sub(r'~~(.+?)~~', r'\1', cleaned)
763
-
764
- # ๊ฐ์ฃผ ๊ธฐํ˜ธ ์ œ๊ฑฐ: ^text^ โ†’ text
765
- cleaned = re.sub(r'\^(.+?)\^', r'\1', cleaned)
766
-
767
- # ๋งํฌ ์ œ๊ฑฐ: [text](url) โ†’ text
768
- cleaned = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', cleaned)
769
-
770
- # ๋‚จ์€ ํŠน์ˆ˜ ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
771
- cleaned = cleaned.replace('**', '').replace('__', '')
772
-
773
- return cleaned.strip()
774
-
775
-
776
- def _add_textbox(
777
- slide,
778
- bbox,
779
- text,
780
- scale_x,
781
- scale_y,
782
- category: str = "",
783
- page_height: int = 0,
784
- use_dark_bg: bool = False
785
- ) -> None:
786
- """
787
- ํ…์ŠคํŠธ ๋ฐ•์Šค ์ถ”๊ฐ€ (AutoSize ๊ฐ•์ œ ์ ์šฉ - ์ˆœ์„œ ์ˆ˜์ • ์ตœ์ข… ๋ฒ„์ „)
788
- """
789
- try:
790
- left = int(bbox[0] * scale_x)
791
- top = int(bbox[1] * scale_y)
792
- width = int((bbox[2] - bbox[0]) * scale_x)
793
- height = int((bbox[3] - bbox[1]) * scale_y)
794
-
795
- if width <= 0 or height <= 0:
796
- return
797
-
798
- textbox = slide.shapes.add_textbox(left, top, width, height)
799
-
800
- # 1. ํ…์ŠคํŠธ ํ”„๋ ˆ์ž„ ์„ค์ •
801
- text_frame = textbox.text_frame
802
- text_frame.clear()
803
- text_frame.word_wrap = True
804
-
805
- # ์ •๋ ฌ ๋ฐ ์—ฌ๋ฐฑ: ๋ฐ•์Šค ์ค‘์•™์— ์˜ค๋„๋ก, ์—ฌ๋ฐฑ์€ ์ œ๊ฑฐ
806
- text_frame.vertical_anchor = MSO_ANCHOR.MIDDLE
807
- text_frame.margin_left = 0
808
- text_frame.margin_right = 0
809
- text_frame.margin_top = 0
810
- text_frame.margin_bottom = 0
811
-
812
- # 2. ํ…์ŠคํŠธ ์ž…๋ ฅ (๊ฐ€์žฅ ๋จผ์ €!)
813
- if len(text_frame.paragraphs) == 0:
814
- p = text_frame.paragraphs.add_paragraph()
815
- else:
816
- p = text_frame.paragraphs[0]
817
-
818
- run = p.add_run()
819
- cleaned_text = _clean_text_for_pptx(text)
820
- run.text = cleaned_text
821
-
822
- # 3. ํฐํŠธ ์„ค์ •
823
- is_bold = category in ('Title', 'Section-header')
824
- if is_bold:
825
- run.font.bold = True
826
-
827
- # ํฐํŠธ ํฌ๊ธฐ ์ „๋žต: ๋ฐ•์Šค ํฌ๊ธฐ์™€ ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„ ๊ธฐ๋ฐ˜ ์ •๊ตํ•œ ๊ณ„์‚ฐ
828
- run.font.size = _calculate_font_size(width, height, cleaned_text, category, is_bold=is_bold)
829
-
830
- # 4. ์ƒ‰์ƒ ๋ฐ ๋ฐฐ๊ฒฝ
831
- if use_dark_bg:
832
- run.font.color.rgb = RGBColor(255, 255, 255)
833
- textbox.fill.solid()
834
- textbox.fill.fore_color.rgb = RGBColor(0, 0, 0)
835
- else:
836
- run.font.color.rgb = RGBColor(0, 0, 0)
837
- textbox.fill.solid()
838
- textbox.fill.fore_color.rgb = RGBColor(255, 255, 255)
839
-
840
- # 5. AutoSize ์„ค์ • (๋งˆ์ง€๋ง‰์—!)
841
- # MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE: ํ…์ŠคํŠธ๊ฐ€ ๋„˜์น˜๋ฉด ํฐํŠธ๋ฅผ ์ค„์—ฌ์„œ ๋งž์ถค
842
- text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
843
-
844
- except Exception as e:
845
- print(f"Textbox add failed: {e}")
846
-
847
-
848
- def build_pptx_from_results(
849
- parse_results: List[Dict],
850
- background_images: List[Image.Image],
851
- output_path: Path,
852
- ) -> Tuple[int, int]:
853
- """ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ PPTX ์ƒ์„ฑ"""
854
- prs = Presentation()
855
-
856
- first_image = background_images[0] if background_images else None
857
- sample_width, sample_height = 1200, 1600
858
- if first_image:
859
- sample_width, sample_height = first_image.width, first_image.height
860
-
861
- slide_width = Inches(10)
862
- slide_height = Inches(10 * sample_height / sample_width)
863
- prs.slide_width = slide_width
864
- prs.slide_height = slide_height
865
-
866
- total_boxes = 0
867
-
868
- for idx, result in enumerate(parse_results):
869
- slide = prs.slides.add_slide(prs.slide_layouts[6])
870
- bg_image = background_images[idx] if idx < len(background_images) else None
871
-
872
- page_width, page_height = _get_page_size(bg_image, result.get('layout_result'))
873
- if page_width == 0: page_width = sample_width
874
- if page_height == 0: page_height = sample_height
875
-
876
- _add_background(slide, slide_width, slide_height, bg_image)
877
-
878
- scale_x = slide_width / float(page_width)
879
- scale_y = slide_height / float(page_height)
880
-
881
- layout_data = result.get('layout_result') or [] # None ๋ฐ ๋นˆ ๊ฐ’ ๋ฐฉ์–ด
882
- if layout_data and isinstance(layout_data, list): # ํƒ€์ž… ์ฒดํฌ ์ถ”๊ฐ€
883
- for cell in layout_data:
884
- bbox = cell.get("bbox")
885
- if not bbox or len(bbox) != 4:
886
- continue
887
- category = cell.get("category", "")
888
- text = cell.get("text", "") or ""
889
- if category in ("Picture", "Table"):
890
- continue
891
- if not text.strip():
892
- continue
893
- _add_textbox(
894
- slide, bbox, text, scale_x, scale_y,
895
- category=category,
896
- page_height=page_height
897
- )
898
- total_boxes += 1
899
 
900
- prs.save(output_path)
901
- return len(parse_results), total_boxes
902
 
903
 
904
  # ============================================================
 
38
  # pptx imports
39
  try:
40
  from pptx import Presentation
 
 
 
41
  except ImportError as exc:
42
  raise ImportError("python-pptx is required.") from exc
43
 
44
  # dots_ocr utils (PDF ๋กœ๋“œ์šฉ)
45
  from dots_ocr.utils.doc_utils import load_images_from_pdf
46
  from dots_ocr.utils.consts import MIN_PIXELS, MAX_PIXELS, IMAGE_FACTOR
47
+ from dots_ocr.utils.pptx_generator import build_pptx_from_results
48
 
49
 
50
  # ============================================================
 
105
  # ํฐํŠธ ํฌ๊ธฐ ๊ด€๋ จ ์ƒ์ˆ˜ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ
106
  # ============================================================
107
 
108
+ # (pptx_generator๋กœ ์ด๋™๋จ)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
 
111
  # ============================================================
 
573
  # ============================================================
574
  # PPTX ์ƒ์„ฑ ํ•จ์ˆ˜๋“ค
575
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
+ # (pptx_generator๋กœ ์ด๋™๋จ)
 
578
 
579
 
580
  # ============================================================
dots_ocr/utils/pptx_generator.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import math
3
+ from typing import List, Optional, Tuple, Dict
4
+ from pathlib import Path
5
+ from PIL import Image
6
+
7
+ try:
8
+ from pptx import Presentation
9
+ from pptx.util import Inches, Pt
10
+ from pptx.dml.color import RGBColor
11
+ from pptx.enum.text import MSO_AUTO_SIZE, MSO_ANCHOR
12
+ except ImportError as exc:
13
+ raise ImportError("python-pptx is required.") from exc
14
+
15
+ # ============================================================
16
+ # ํฐํŠธ ํฌ๊ธฐ ๊ด€๋ จ ์ƒ์ˆ˜ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ
17
+ # ============================================================
18
+
19
+ # PPT ํ‘œ์ค€ ํฐํŠธ ํฌ๊ธฐ ํ”„๋ฆฌ์…‹
20
+ FONT_PRESETS = [
21
+ 8, 9, 10, 11, 12, 14, 16, 18, 20, 24,
22
+ 28, 32, 36, 40, 44, 48, 54, 60, 66, 72,
23
+ 80, 88, 96
24
+ ]
25
+
26
+ def _calculate_font_size(
27
+ width: int,
28
+ height: int,
29
+ text: str,
30
+ category: str = "",
31
+ is_bold: bool = False
32
+ ) -> Pt:
33
+ """
34
+ ๋ฐ•์Šค ํฌ๊ธฐ์™€ ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„์„ ๋ถ„์„ํ•˜์—ฌ ์ตœ์ ์˜ ํฐํŠธ ํฌ๊ธฐ ๊ณ„์‚ฐ
35
+
36
+ Args:
37
+ width: ๋ฐ•์Šค ๋„ˆ๋น„ (EMU ๋‹จ์œ„)
38
+ height: ๋ฐ•์Šค ๋†’์ด (EMU ๋‹จ์œ„)
39
+ text: ํ…์ŠคํŠธ ๋‚ด์šฉ
40
+ category: ๋ ˆ์ด์•„์›ƒ ์นดํ…Œ๊ณ ๋ฆฌ
41
+ is_bold: ๋ณผ๋“œ์ฒด ์—ฌ๋ถ€
42
+
43
+ Returns:
44
+ Pt ๊ฐ์ฒด (๊ณ„์‚ฐ๋œ ํฐํŠธ ํฌ๊ธฐ)
45
+ """
46
+ if not text:
47
+ return Pt(12)
48
+
49
+ # EMU to Pt conversion
50
+ w_pt = width / 12700
51
+ h_pt = height / 12700
52
+
53
+ # 1. ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„ ๋ถ„์„ (Character Composition Analysis)
54
+ total_weight = 0.0
55
+ max_word_weight = 0.0
56
+ current_word_weight = 0.0
57
+
58
+ # ๊ฐ€์ค‘์น˜ ์„ค์ •
59
+ # ๋ณผ๋“œ์ฒด์ผ ๊ฒฝ์šฐ ๊ฐ€์ค‘์น˜ ์ƒํ–ฅ
60
+ w_hangul = 1.0
61
+ w_upper = 0.8 if is_bold else 0.7
62
+ w_lower = 0.6 if is_bold else 0.55
63
+ w_other = 0.4 if is_bold else 0.3
64
+
65
+ for char in text:
66
+ if '๊ฐ€' <= char <= 'ํžฃ':
67
+ weight = w_hangul
68
+ elif 'A' <= char <= 'Z':
69
+ weight = w_upper
70
+ elif 'a' <= char <= 'z' or '0' <= char <= '9':
71
+ weight = w_lower
72
+ else:
73
+ weight = w_other
74
+
75
+ # ๊ณต๋ฐฑ์ด๋ฉด ๋‹จ์–ด ๊ตฌ๋ถ„
76
+ if char.isspace():
77
+ max_word_weight = max(max_word_weight, current_word_weight)
78
+ current_word_weight = 0.0
79
+ # ๊ณต๋ฐฑ ์ž์ฒด๋Š” total_weight์— ํฌํ•จ (์ค„๋ฐ”๊ฟˆ ๊ณ„์‚ฐ์šฉ)
80
+ total_weight += weight
81
+ else:
82
+ current_word_weight += weight
83
+ total_weight += weight
84
+
85
+ # ๋งˆ์ง€๋ง‰ ๋‹จ์–ด ์ฒ˜๋ฆฌ
86
+ max_word_weight = max(max_word_weight, current_word_weight)
87
+
88
+ # ์•ˆ์ „ ์žฅ์น˜: ๊ฐ€์ค‘์น˜๊ฐ€ 0์ด๋ฉด ๊ธฐ๋ณธ๊ฐ’
89
+ if total_weight <= 0:
90
+ total_weight = len(text) * 0.5
91
+ if max_word_weight <= 0:
92
+ max_word_weight = total_weight
93
+
94
+ # 2. Case A: ๋„ˆ๋น„ ์ œ์•ฝ (๋‹จ์–ด ๋‹จ์œ„)
95
+ # ๊ฐ€์žฅ ๊ธด ๋‹จ์–ด๊ฐ€ ๋ฐ•์Šค ๋„ˆ๋น„๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก ํ•จ
96
+ # Font Size * Max Word Weight <= Width
97
+ # Font Size <= Width / Max Word Weight
98
+ # ์•ˆ์ „ ๋งˆ์ง„ 90% ์ ์šฉ
99
+ width_limit_size = (w_pt / max_word_weight) * 0.9
100
+
101
+ # 3. Case B: ๋†’์ด ์ œ์•ฝ (์ค„๋ฐ”๊ฟˆ ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
102
+ # ํฐ ํ”„๋ฆฌ์…‹๋ถ€ํ„ฐ ๋‚ด๋ ค์˜ค๋ฉด์„œ ๋†’์ด ์กฐ๊ฑด ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธ
103
+ height_limit_size = FONT_PRESETS[0] # ๊ธฐ๋ณธ๊ฐ’
104
+
105
+ for preset in reversed(FONT_PRESETS):
106
+ # ์˜ˆ์ƒ ์ค„ ์ˆ˜ = ์˜ฌ๋ฆผ(์ด ํ…์ŠคํŠธ ๋„ˆ๋น„ / ๋ฐ•์Šค ๋„ˆ๋น„)
107
+ # ์ด ํ…์ŠคํŠธ ๋„ˆ๋น„ = total_weight * preset
108
+ # ์—ฌ์œ  ์žˆ๊ฒŒ ๊ณ„์‚ฐํ•˜๊ธฐ ์œ„ํ•ด ์ค„๋ฐ”๊ฟˆ ์‹œ ๋‚ญ๋น„๋˜๋Š” ๊ณต๊ฐ„ ๊ณ ๋ ค (1.0 -> 1.1 ๋“ฑ ์กฐ์ • ๊ฐ€๋Šฅ)
109
+ text_virtual_width = total_weight * preset
110
+ expected_lines = math.ceil(text_virtual_width / w_pt)
111
+
112
+ # ์ตœ์†Œ 1์ค„
113
+ if expected_lines < 1:
114
+ expected_lines = 1
115
+
116
+ # ํ•„์š” ๋†’์ด = ์ค„ ์ˆ˜ * ํฐํŠธํฌ๊ธฐ * ์ค„๊ฐ„๊ฒฉ(1.2)
117
+ required_height = expected_lines * preset * 1.2
118
+
119
+ # ๋ฐ•์Šค ๋†’์ด์˜ 95% ์ด๋‚ด์— ๋“ค์–ด์˜ค๋ฉด ์ฑ„ํƒ
120
+ if required_height <= h_pt * 0.95:
121
+ height_limit_size = preset
122
+ break
123
+
124
+ # ๊ฐ€์žฅ ์ž‘์€ ํ”„๋ฆฌ์…‹๊นŒ์ง€ ์™”๋Š”๋ฐ๋„ ์•ˆ๋˜๋ฉด ๊ฐ€์žฅ ์ž‘์€๊ฑฐ ์„ ํƒ
125
+ if preset == FONT_PRESETS[0]:
126
+ height_limit_size = preset
127
+
128
+ # 4. ์ตœ์ข… ํฌ๊ธฐ ๊ฒฐ์ • ๋ฐ ๋ณด์ •
129
+ target_size = min(width_limit_size, height_limit_size)
130
+
131
+ # ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ตœ์†Œ/์ตœ๋Œ€ ๋ณด์ •
132
+ min_size = 9
133
+ max_size = 80
134
+
135
+ if category == 'Title':
136
+ min_size = 24
137
+ # ์ œ๋ชฉ์€ ํ•œ ์ค„์— ๊ฝ‰ ์ฐจ๊ฒŒ ๋ณด์ด๋Š”๊ฒŒ ์ข‹์œผ๋ฏ€๋กœ width_limit ๋น„์ค‘์„ ๋†’๊ฒŒ ๋ด„
138
+ target_size = max(target_size, 24)
139
+ elif category == 'Section-header':
140
+ min_size = 18
141
+ target_size = max(target_size, 18)
142
+ elif category in ('Caption', 'Footnote'):
143
+ max_size = 12
144
+
145
+ # ๋ฒ”์œ„ ์ œํ•œ
146
+ target_size = max(min_size, min(target_size, max_size))
147
+
148
+ # ํ”„๋ฆฌ์…‹ ๋งคํ•‘ (๊ณ„์‚ฐ๋œ ๊ฐ’๋ณด๋‹ค ์ž‘๊ฑฐ๋‚˜ ๊ฐ™์€ ๊ฐ€์žฅ ํฐ ํ”„๋ฆฌ์…‹)
149
+ final_size = FONT_PRESETS[0]
150
+ for preset in FONT_PRESETS:
151
+ if preset <= target_size:
152
+ final_size = preset
153
+ else:
154
+ break
155
+
156
+ return Pt(final_size)
157
+
158
+
159
+ # ============================================================
160
+ # PPTX ์ƒ์„ฑ ํ•จ์ˆ˜๋“ค
161
+ # ============================================================
162
+ def _get_page_size(
163
+ page_image: Optional[Image.Image], layout_data: Optional[List[Dict]]
164
+ ) -> Tuple[int, int]:
165
+ if page_image:
166
+ return page_image.width, page_image.height
167
+ return 1200, 1600
168
+
169
+
170
+ def _add_background(slide, slide_width, slide_height, page_image: Optional[Image.Image]) -> None:
171
+ if not page_image:
172
+ return
173
+ try:
174
+ buf = io.BytesIO()
175
+ page_image.save(buf, format="PNG")
176
+ buf.seek(0)
177
+ slide.shapes.add_picture(buf, 0, 0, width=slide_width, height=slide_height)
178
+ except Exception as e:
179
+ print(f"Background add failed: {e}")
180
+
181
+
182
+ def _clean_text_for_pptx(text: str) -> str:
183
+ """
184
+ PPTX์šฉ ํ…์ŠคํŠธ ์ •๋ฆฌ - ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
185
+
186
+ ์ œ๊ฑฐ ๋Œ€์ƒ: #, **, *, `, ^, ~~ ๋“ฑ
187
+ """
188
+ import re
189
+
190
+ if not text:
191
+ return ""
192
+
193
+ # ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
194
+ cleaned = text
195
+
196
+ # ํ—ค๋”ฉ ๊ธฐํ˜ธ ์ œ๊ฑฐ: # ## ### ๋“ฑ
197
+ cleaned = re.sub(r'^#{1,6}\s*', '', cleaned, flags=re.MULTILINE)
198
+
199
+ # ๋ณผ๋“œ/์ดํƒค๋ฆญ ์ œ๊ฑฐ: **text** โ†’ text, *text* โ†’ text, __text__ โ†’ text, _text_ โ†’ text
200
+ cleaned = re.sub(r'\*\*(.+?)\*\*', r'\1', cleaned)
201
+ cleaned = re.sub(r'__(.+?)__', r'\1', cleaned)
202
+ cleaned = re.sub(r'\*(.+?)\*', r'\1', cleaned)
203
+ cleaned = re.sub(r'_(.+?)_', r'\1', cleaned)
204
+
205
+ # ์ธ๋ผ์ธ ์ฝ”๋“œ ์ œ๊ฑฐ: `code` โ†’ code
206
+ cleaned = re.sub(r'`(.+?)`', r'\1', cleaned)
207
+
208
+ # ์ทจ์†Œ์„  ์ œ๊ฑฐ: ~~text~~ โ†’ text
209
+ cleaned = re.sub(r'~~(.+?)~~', r'\1', cleaned)
210
+
211
+ # ๊ฐ์ฃผ ๊ธฐํ˜ธ ์ œ๊ฑฐ: ^text^ โ†’ text
212
+ cleaned = re.sub(r'\^(.+?)\^', r'\1', cleaned)
213
+
214
+ # ๋งํฌ ์ œ๊ฑฐ: [text](url) โ†’ text
215
+ cleaned = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', cleaned)
216
+
217
+ # ๋‚จ์€ ํŠน์ˆ˜ ๋งˆํฌ๋‹ค์šด ๊ธฐํ˜ธ ์ œ๊ฑฐ
218
+ cleaned = cleaned.replace('**', '').replace('__', '')
219
+
220
+ return cleaned.strip()
221
+
222
+
223
+ def _add_textbox(
224
+ slide,
225
+ bbox,
226
+ text,
227
+ scale_x,
228
+ scale_y,
229
+ category: str = "",
230
+ page_height: int = 0,
231
+ use_dark_bg: bool = False
232
+ ) -> None:
233
+ """
234
+ ํ…์ŠคํŠธ ๋ฐ•์Šค ์ถ”๊ฐ€ (AutoSize ๊ฐ•์ œ ์ ์šฉ - ์ˆœ์„œ ์ˆ˜์ • ์ตœ์ข… ๋ฒ„์ „)
235
+ """
236
+ try:
237
+ left = int(bbox[0] * scale_x)
238
+ top = int(bbox[1] * scale_y)
239
+ width = int((bbox[2] - bbox[0]) * scale_x)
240
+ height = int((bbox[3] - bbox[1]) * scale_y)
241
+
242
+ if width <= 0 or height <= 0:
243
+ return
244
+
245
+ textbox = slide.shapes.add_textbox(left, top, width, height)
246
+
247
+ # 1. ํ…์ŠคํŠธ ํ”„๋ ˆ์ž„ ์„ค์ •
248
+ text_frame = textbox.text_frame
249
+ text_frame.clear()
250
+ text_frame.word_wrap = True
251
+
252
+ # ์ •๋ ฌ ๋ฐ ์—ฌ๋ฐฑ: ๋ฐ•์Šค ์ค‘์•™์— ์˜ค๋„๋ก, ์—ฌ๋ฐฑ์€ ์ œ๊ฑฐ
253
+ text_frame.vertical_anchor = MSO_ANCHOR.MIDDLE
254
+ text_frame.margin_left = 0
255
+ text_frame.margin_right = 0
256
+ text_frame.margin_top = 0
257
+ text_frame.margin_bottom = 0
258
+
259
+ # 2. ํ…์ŠคํŠธ ์ž…๋ ฅ (๊ฐ€์žฅ ๋จผ์ €!)
260
+ if len(text_frame.paragraphs) == 0:
261
+ p = text_frame.paragraphs.add_paragraph()
262
+ else:
263
+ p = text_frame.paragraphs[0]
264
+
265
+ run = p.add_run()
266
+ cleaned_text = _clean_text_for_pptx(text)
267
+ run.text = cleaned_text
268
+
269
+ # 3. ํฐํŠธ ์„ค์ •
270
+ is_bold = category in ('Title', 'Section-header')
271
+ if is_bold:
272
+ run.font.bold = True
273
+
274
+ # ํฐํŠธ ํฌ๊ธฐ ์ „๋žต: ๋ฐ•์Šค ํฌ๊ธฐ์™€ ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์„ฑ๋ถ„ ๊ธฐ๋ฐ˜ ์ •๊ตํ•œ ๊ณ„์‚ฐ
275
+ run.font.size = _calculate_font_size(width, height, cleaned_text, category, is_bold=is_bold)
276
+
277
+ # 4. ์ƒ‰์ƒ ๋ฐ ๋ฐฐ๊ฒฝ
278
+ if use_dark_bg:
279
+ run.font.color.rgb = RGBColor(255, 255, 255)
280
+ textbox.fill.solid()
281
+ textbox.fill.fore_color.rgb = RGBColor(0, 0, 0)
282
+ else:
283
+ run.font.color.rgb = RGBColor(0, 0, 0)
284
+ textbox.fill.solid()
285
+ textbox.fill.fore_color.rgb = RGBColor(255, 255, 255)
286
+
287
+ # 5. AutoSize ์„ค์ • (๋งˆ์ง€๋ง‰์—!)
288
+ # MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE: ํ…์ŠคํŠธ๊ฐ€ ๋„˜์น˜๋ฉด ํฐํŠธ๋ฅผ ์ค„์—ฌ์„œ ๋งž์ถค
289
+ text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
290
+
291
+ except Exception as e:
292
+ print(f"Textbox add failed: {e}")
293
+
294
+
295
+ def build_pptx_from_results(
296
+ parse_results: List[Dict],
297
+ background_images: List[Image.Image],
298
+ output_path: Path,
299
+ ) -> Tuple[int, int]:
300
+ """ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋กœ๋ถ€ํ„ฐ PPTX ์ƒ์„ฑ"""
301
+ prs = Presentation()
302
+
303
+ first_image = background_images[0] if background_images else None
304
+ sample_width, sample_height = 1200, 1600
305
+ if first_image:
306
+ sample_width, sample_height = first_image.width, first_image.height
307
+
308
+ slide_width = Inches(10)
309
+ slide_height = Inches(10 * sample_height / sample_width)
310
+ prs.slide_width = slide_width
311
+ prs.slide_height = slide_height
312
+
313
+ total_boxes = 0
314
+
315
+ for idx, result in enumerate(parse_results):
316
+ slide = prs.slides.add_slide(prs.slide_layouts[6])
317
+ bg_image = background_images[idx] if idx < len(background_images) else None
318
+
319
+ page_width, page_height = _get_page_size(bg_image, result.get('layout_result'))
320
+ if page_width == 0: page_width = sample_width
321
+ if page_height == 0: page_height = sample_height
322
+
323
+ _add_background(slide, slide_width, slide_height, bg_image)
324
+
325
+ scale_x = slide_width / float(page_width)
326
+ scale_y = slide_height / float(page_height)
327
+
328
+ layout_data = result.get('layout_result') or [] # None ๋ฐ ๋นˆ ๊ฐ’ ๋ฐฉ์–ด
329
+ if layout_data and isinstance(layout_data, list): # ํƒ€์ž… ์ฒดํฌ ์ถ”๊ฐ€
330
+ for cell in layout_data:
331
+ bbox = cell.get("bbox")
332
+ if not bbox or len(bbox) != 4:
333
+ continue
334
+ category = cell.get("category", "")
335
+ text = cell.get("text", "") or ""
336
+ if category in ("Picture", "Table"):
337
+ continue
338
+ if not text.strip():
339
+ continue
340
+ _add_textbox(
341
+ slide, bbox, text, scale_x, scale_y,
342
+ category=category,
343
+ page_height=page_height
344
+ )
345
+ total_boxes += 1
346
+
347
+ prs.save(output_path)
348
+ return len(parse_results), total_boxes
349
+
350
+ if __name__ == "__main__":
351
+ # ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ
352
+ print("PPTX Generator Test")
353
+ # ๋”๋ฏธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ ๋ฐ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
354
+
local_test.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ from dots_ocr.utils.pptx_generator import build_pptx_from_results
5
+
6
+ def main():
7
+ # ๊ฒฝ๋กœ ์„ค์ •
8
+ base_dir = Path(__file__).parent
9
+ input_json_path = base_dir / "test.json"
10
+ output_dir = base_dir / "output"
11
+ output_pptx_path = output_dir / "test_result.pptx"
12
+
13
+ # 1. Output ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ
14
+ output_dir.mkdir(exist_ok=True)
15
+
16
+ # 2. JSON ๋ฐ์ดํ„ฐ ๋กœ๋“œ
17
+ if not input_json_path.exists():
18
+ print(f"Error: {input_json_path} not found.")
19
+ return
20
+
21
+ print(f"Reading data from {input_json_path}...")
22
+ with open(input_json_path, "r", encoding="utf-8") as f:
23
+ parse_results = json.load(f)
24
+
25
+ # 3. PPT ์ƒ์„ฑ
26
+ # ๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋นˆ ๋ฆฌ์ŠคํŠธ ์ „๋‹ฌ (ํฐ ๋ฐฐ๊ฒฝ ์ƒ์„ฑ๋จ)
27
+ # build_pptx_from_results๋Š” background_images ๋ฆฌ์ŠคํŠธ ๊ธธ์ด๋ฅผ ์ฒดํฌํ•˜์—ฌ ์ฒ˜๋ฆฌํ•จ
28
+ background_images = []
29
+
30
+ print("Generating PPTX...")
31
+ try:
32
+ page_count, box_count = build_pptx_from_results(
33
+ parse_results=parse_results,
34
+ background_images=background_images,
35
+ output_path=output_pptx_path
36
+ )
37
+ print(f"โœ… Success! Generated {output_pptx_path}")
38
+ print(f" Pages: {page_count}")
39
+ print(f" Text Boxes: {box_count}")
40
+
41
+ except Exception as e:
42
+ print(f"โŒ Failed to generate PPTX: {e}")
43
+ import traceback
44
+ traceback.print_exc()
45
+
46
+ if __name__ == "__main__":
47
+ main()
48
+
test.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "layout_result": [
4
+ {
5
+ "bbox": [100, 100, 800, 200],
6
+ "category": "Title",
7
+ "text": "PPT ์ƒ์„ฑ ํ…Œ์ŠคํŠธ: ์ œ๋ชฉ"
8
+ },
9
+ {
10
+ "bbox": [100, 250, 400, 300],
11
+ "category": "Section-header",
12
+ "text": "์„น์…˜ 1: ๊ฐœ์š”"
13
+ },
14
+ {
15
+ "bbox": [100, 320, 500, 600],
16
+ "category": "Text",
17
+ "text": "์ด๊ฒƒ์€ ๋กœ์ปฌ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.\nJSON ํŒŒ์ผ์—์„œ ์ฝ์–ด์˜จ ์ขŒํ‘œ์™€ ํ…์ŠคํŠธ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ PPT๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค."
18
+ },
19
+ {
20
+ "bbox": [600, 320, 1000, 600],
21
+ "category": "List-item",
22
+ "text": "- ์ฒซ ๋ฒˆ์งธ ํ•ญ๋ชฉ\n- ๋‘ ๋ฒˆ์งธ ํ•ญ๋ชฉ\n- ์„ธ ๋ฒˆ์งธ ํ•ญ๋ชฉ"
23
+ },
24
+ {
25
+ "bbox": [100, 800, 500, 850],
26
+ "category": "Caption",
27
+ "text": "๊ทธ๋ฆผ 1: ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ"
28
+ },
29
+ {
30
+ "bbox": [100, 1400, 500, 1450],
31
+ "category": "Page-footer",
32
+ "text": "Page 1"
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "layout_result": [
38
+ {
39
+ "bbox": [100, 100, 800, 200],
40
+ "category": "Title",
41
+ "text": "๋‘ ๋ฒˆ์งธ ํŽ˜์ด์ง€"
42
+ },
43
+ {
44
+ "bbox": [100, 300, 800, 500],
45
+ "category": "Text",
46
+ "text": "ํŽ˜์ด์ง€ ๋„˜๊น€์ด ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค."
47
+ }
48
+ ]
49
+ }
50
+ ]
51
+