curlkun commited on
Commit
e5daf1a
·
verified ·
1 Parent(s): 40edb0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +30 -1509
app.py CHANGED
@@ -1,1520 +1,41 @@
1
  import gradio as gr
2
- import fitz # PyMuPDF
3
- import difflib
4
  import tempfile
5
  import os
6
- import pandas as pd
7
 
8
- try:
9
- from bs4 import BeautifulSoup
10
- BS4_AVAILABLE = True
11
- except ImportError:
12
- BS4_AVAILABLE = False
13
-
14
- try:
15
- from striprtf.striprtf import rtf_to_text
16
- STRIPRTF_AVAILABLE = True
17
- except ImportError:
18
- STRIPRTF_AVAILABLE = False
19
-
20
- try:
21
- from docx import Document as DocxDocument
22
- DOCX_AVAILABLE = True
23
- except ImportError:
24
- DOCX_AVAILABLE = False
25
-
26
- try:
27
- import openpyxl
28
- OPENPYXL_AVAILABLE = True
29
- except ImportError:
30
- OPENPYXL_AVAILABLE = False
31
-
32
- try:
33
- from pptx import Presentation
34
- PPTX_AVAILABLE = True
35
- except ImportError:
36
- PPTX_AVAILABLE = False
37
-
38
- # --- 定数・設定 ---
39
-
40
- COLOR_PRESETS = {
41
- "赤 / 青 (標準)": {"del": (1, 0.5, 0.5), "ins": (0.5, 0.5, 1), "alpha": 0.3},
42
- "赤 / 青 (濃いめ)": {"del": (1, 0.5, 0.5), "ins": (0.5, 0.5, 1), "alpha": 0.6},
43
- "黄 / 緑 (標準)": {"del": (1, 1, 0.5), "ins": (0.5, 1, 0.5), "alpha": 0.3},
44
- "黄 / 緑 (濃いめ)": {"del": (1, 1, 0.5), "ins": (0.5, 1, 0.5), "alpha": 0.6},
45
- "ピンク / 水色 (標準)": {"del": (1, 0.7, 0.8), "ins": (0.5, 0.9, 1), "alpha": 0.3},
46
- "グレー / オレンジ (標準)": {"del": (0.8, 0.8, 0.8), "ins": (1, 0.7, 0.4), "alpha": 0.3},
47
- }
48
-
49
- PAGE_W = 595
50
- PAGE_H = 842
51
- MARGIN_X = 40
52
- MARGIN_TOP = 40
53
- LINE_HEIGHT = 18
54
- FONT_SIZE = 10
55
- FONT_PATH = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
56
- FONT_NAME = "notocjk"
57
-
58
- # ------------------------------------------------------------------ #
59
- # ページ範囲パース
60
- # ------------------------------------------------------------------ #
61
- def parse_page_range(page_range_str, max_pages):
62
- page_range_str = page_range_str.strip()
63
- if not page_range_str or page_range_str.lower() == "all":
64
- return list(range(max_pages))
65
- pages = set()
66
- for part in page_range_str.split(","):
67
- part = part.strip()
68
- if "-" in part:
69
- bounds = part.split("-")
70
- if len(bounds) == 2:
71
- try:
72
- start = int(bounds[0].strip()) - 1
73
- end = int(bounds[1].strip()) - 1
74
- for p in range(start, end + 1):
75
- if 0 <= p < max_pages:
76
- pages.add(p)
77
- except ValueError:
78
- pass
79
- else:
80
- try:
81
- p = int(part) - 1
82
- if 0 <= p < max_pages:
83
- pages.add(p)
84
- except ValueError:
85
- pass
86
- return sorted(list(pages))
87
-
88
- # ------------------------------------------------------------------ #
89
- # シート名取得
90
- # ------------------------------------------------------------------ #
91
- def get_sheet_names(file_obj):
92
  if file_obj is None:
93
- return []
94
- ext = os.path.splitext(file_obj.name)[1].lower()
95
- if ext != ".xlsx":
96
- return []
97
- try:
98
- wb = openpyxl.load_workbook(file_obj.name, read_only=True, data_only=True)
99
- names = wb.sheetnames
100
- wb.close()
101
- return names
102
- except Exception:
103
- return []
104
-
105
- # ------------------------------------------------------------------ #
106
- # テキスト抽出(PDF以外・テキスト比較用)
107
- # ------------------------------------------------------------------ #
108
- def extract_text_from_file(path, page_range_str="", selected_sheets=None):
109
- ext = os.path.splitext(path)[1].lower()
110
- try:
111
- if ext == ".txt":
112
- with open(path, "r", encoding="utf-8") as f:
113
- return f.read(), None, False
114
- elif ext in [".html", ".htm"]:
115
- if not BS4_AVAILABLE:
116
- return None, "beautifulsoup4 がインストールされていません", False
117
- with open(path, "r", encoding="utf-8") as f:
118
- soup = BeautifulSoup(f.read(), "html.parser")
119
- return soup.get_text(), None, False
120
- elif ext == ".rtf":
121
- if not STRIPRTF_AVAILABLE:
122
- return None, "striprtf がインストールされていません", False
123
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
124
- raw = f.read()
125
- return rtf_to_text(raw, errors="ignore"), None, False
126
- elif ext == ".docx":
127
- if not DOCX_AVAILABLE:
128
- return None, "python-docx がインストールされていません", False
129
- doc = DocxDocument(path)
130
- return "\n".join([p.text for p in doc.paragraphs]), None, False
131
- elif ext == ".xlsx":
132
- if not OPENPYXL_AVAILABLE:
133
- return None, "openpyxl がインストールされていません", False
134
- wb = openpyxl.load_workbook(path, data_only=True)
135
- if selected_sheets:
136
- target_sheets = [ws for ws in wb.worksheets if ws.title in selected_sheets]
137
- else:
138
- target_sheets = wb.worksheets
139
- pages = []
140
- for ws in target_sheets:
141
- lines = [f"[シート: {ws.title}]"]
142
- for row in ws.iter_rows(values_only=True):
143
- line = "\t".join([str(c) if c is not None else "" for c in row])
144
- if line.strip():
145
- lines.append(line)
146
- pages.append("\n".join(lines))
147
- return pages, None, True
148
- elif ext == ".pptx":
149
- if not PPTX_AVAILABLE:
150
- return None, "python-pptx がインストールされていません", False
151
- prs = Presentation(path)
152
- max_slides = len(prs.slides)
153
- target_slides = parse_page_range(page_range_str, max_slides)
154
- pages = []
155
- for i in target_slides:
156
- slide = prs.slides[i]
157
- lines = [f"[スライド {i + 1}]"]
158
- for shape in slide.shapes:
159
- if shape.has_text_frame:
160
- for para in shape.text_frame.paragraphs:
161
- t = para.text.strip()
162
- if t:
163
- lines.append(t)
164
- pages.append("\n".join(lines))
165
- return pages, None, True
166
- else:
167
- return None, f"非対応の形式です: {ext}", False
168
- except Exception as e:
169
- return None, f"抽出エラー: {str(e)}", False
170
-
171
- # ------------------------------------------------------------------ #
172
- # PDF → テキスト+座標+フォント情報抽出
173
- # ------------------------------------------------------------------ #
174
- def get_text_and_rects(doc, page_range_str="", trim=False, trim_margin=20):
175
- target_pages = parse_page_range(page_range_str, len(doc))
176
- new_doc = fitz.open()
177
- full_text = ""
178
- char_map = []
179
- for new_page_idx, page_num in enumerate(target_pages):
180
- new_doc.insert_pdf(doc, from_page=page_num, to_page=page_num)
181
- page = doc[page_num]
182
- if trim:
183
- cb = page.cropbox
184
- mb = page.mediabox
185
- if abs(cb.width - mb.width) < 1 and abs(cb.height - mb.height) < 1:
186
- clip = fitz.Rect(
187
- mb.x0 + trim_margin, mb.y0 + trim_margin,
188
- mb.x1 - trim_margin, mb.y1 - trim_margin,
189
- )
190
- else:
191
- clip = cb
192
- else:
193
- clip = page.rect
194
- blocks = page.get_text("rawdict", clip=clip)["blocks"]
195
- for block in blocks:
196
- if "lines" in block:
197
- for line in block["lines"]:
198
- for span in line["spans"]:
199
- font_name = span.get("font", "")
200
- font_size = round(span.get("size", 0), 2)
201
- flags = span.get("flags", 0)
202
- is_bold = bool(flags & 16)
203
- is_italic = bool(flags & 1)
204
- for char in span["chars"]:
205
- c = char["c"]
206
- full_text += c
207
- char_map.append({
208
- "page": new_page_idx,
209
- "rect": char["bbox"],
210
- "char": c,
211
- "font": font_name,
212
- "font_size": font_size,
213
- "is_bold": is_bold,
214
- "is_italic": is_italic,
215
- })
216
- return full_text, char_map, new_doc
217
-
218
- # ------------------------------------------------------------------ #
219
- # フォント情報抽出(PDF以外・フォント比較用)
220
- # ------------------------------------------------------------------ #
221
- def extract_font_map_from_file(path, page_range_str="", selected_sheets=None):
222
- ext = os.path.splitext(path)[1].lower()
223
- try:
224
- if ext == ".docx":
225
- if not DOCX_AVAILABLE:
226
- return None, None, "python-docx がインストールされていません"
227
- doc_obj = DocxDocument(path)
228
- char_map = []
229
- doc = fitz.open()
230
- page = doc.new_page(width=PAGE_W, height=PAGE_H)
231
- page_idx = 0
232
- y = MARGIN_TOP
233
- CHAR_W_FULL = FONT_SIZE
234
- CHAR_W_HALF = FONT_SIZE * 0.6
235
- for para in doc_obj.paragraphs:
236
- for run in para.runs:
237
- fname = run.font.name or ""
238
- fsize = round(run.font.size.pt, 2) if run.font.size else 0
239
- is_bold = bool(run.bold)
240
- is_italic = bool(run.italic)
241
- x = MARGIN_X
242
- for c in run.text:
243
- if c in ("\n", "\r"):
244
- continue
245
- cw = CHAR_W_HALF if c.isascii() else CHAR_W_FULL
246
- char_map.append({
247
- "page": page_idx,
248
- "rect": (x, y, x + cw, y + LINE_HEIGHT),
249
- "char": c,
250
- "font": fname,
251
- "font_size": fsize,
252
- "is_bold": is_bold,
253
- "is_italic": is_italic,
254
- })
255
- x += cw
256
- return char_map, doc, None
257
-
258
- elif ext == ".xlsx":
259
- if not OPENPYXL_AVAILABLE:
260
- return None, None, "openpyxl がインストールされていません"
261
- wb = openpyxl.load_workbook(path, data_only=True)
262
- if selected_sheets:
263
- target_sheets = [ws for ws in wb.worksheets if ws.title in selected_sheets]
264
- else:
265
- target_sheets = wb.worksheets
266
- char_map = []
267
- doc = fitz.open()
268
- for page_idx, ws in enumerate(target_sheets):
269
- page = doc.new_page(width=PAGE_W, height=PAGE_H)
270
- y = MARGIN_TOP
271
- CHAR_W_FULL = FONT_SIZE
272
- CHAR_W_HALF = FONT_SIZE * 0.6
273
- for row in ws.iter_rows():
274
- for cell in row:
275
- if cell.value is None:
276
- continue
277
- fname = cell.font.name if cell.font else ""
278
- fsize = round(cell.font.size, 2) if (cell.font and cell.font.size) else 0
279
- is_bold = bool(cell.font.bold) if cell.font else False
280
- is_italic = bool(cell.font.italic) if cell.font else False
281
- x = MARGIN_X
282
- for c in str(cell.value):
283
- cw = CHAR_W_HALF if c.isascii() else CHAR_W_FULL
284
- char_map.append({
285
- "page": page_idx,
286
- "rect": (x, y, x + cw, y + LINE_HEIGHT),
287
- "char": c,
288
- "font": fname,
289
- "font_size": fsize,
290
- "is_bold": is_bold,
291
- "is_italic": is_italic,
292
- })
293
- x += cw
294
- return char_map, doc, None
295
-
296
- elif ext == ".pptx":
297
- if not PPTX_AVAILABLE:
298
- return None, None, "python-pptx がインストールされていません"
299
- prs = Presentation(path)
300
- max_slides = len(prs.slides)
301
- target_slides = parse_page_range(page_range_str, max_slides)
302
- char_map = []
303
- doc = fitz.open()
304
- CHAR_W_FULL = FONT_SIZE
305
- CHAR_W_HALF = FONT_SIZE * 0.6
306
- for page_idx, i in enumerate(target_slides):
307
- slide = prs.slides[i]
308
- page = doc.new_page(width=PAGE_W, height=PAGE_H)
309
- y = MARGIN_TOP
310
- for shape in slide.shapes:
311
- if shape.has_text_frame:
312
- for para in shape.text_frame.paragraphs:
313
- for run in para.runs:
314
- fname = run.font.name or ""
315
- fsize = round(run.font.size.pt, 2) if run.font.size else 0
316
- is_bold = bool(run.bold)
317
- is_italic = bool(run.italic)
318
- x = MARGIN_X
319
- for c in run.text:
320
- if c in ("\n", "\r"):
321
- continue
322
- cw = CHAR_W_HALF if c.isascii() else CHAR_W_FULL
323
- char_map.append({
324
- "page": page_idx,
325
- "rect": (x, y, x + cw, y + LINE_HEIGHT),
326
- "char": c,
327
- "font": fname,
328
- "font_size": fsize,
329
- "is_bold": is_bold,
330
- "is_italic": is_italic,
331
- })
332
- x += cw
333
- return char_map, doc, None
334
-
335
- else:
336
- return None, None, f"フォント比較非対応の形式です: {ext}"
337
-
338
- except Exception as e:
339
- return None, None, f"フォント情報抽出エラー: {str(e)}"
340
-
341
- # ------------------------------------------------------------------ #
342
- # テキスト → 座標付きPDF(txt/html/rtf/docx用)
343
- # ------------------------------------------------------------------ #
344
- def text_to_pdf_with_rects(text):
345
- lines = text.splitlines()
346
- doc = fitz.open()
347
- char_rects = []
348
- page = None
349
- y = MARGIN_TOP
350
- page_idx = -1
351
- CHAR_W_FULL = FONT_SIZE
352
- CHAR_W_HALF = FONT_SIZE * 0.6
353
-
354
- def get_char_width(c):
355
- return CHAR_W_HALF if c.isascii() else CHAR_W_FULL
356
-
357
- def new_page():
358
- nonlocal page, y, page_idx
359
- page = doc.new_page(width=PAGE_W, height=PAGE_H)
360
- y = MARGIN_TOP
361
- page_idx += 1
362
-
363
- new_page()
364
- for line in lines:
365
- if y + LINE_HEIGHT > PAGE_H - MARGIN_TOP:
366
- new_page()
367
- display = line if line.strip() != "" else " "
368
- try:
369
- page.insert_text(
370
- (MARGIN_X, y + FONT_SIZE), display,
371
- fontfile=FONT_PATH, fontname=FONT_NAME,
372
- fontsize=FONT_SIZE, color=(0, 0, 0),
373
- )
374
- except Exception:
375
- page.insert_text(
376
- (MARGIN_X, y + FONT_SIZE), display,
377
- fontsize=FONT_SIZE, color=(0, 0, 0),
378
- )
379
- x = MARGIN_X
380
- for c in line:
381
- cw = get_char_width(c)
382
- char_rects.append({
383
- "page": page_idx, "rect": (x, y, x + cw, y + LINE_HEIGHT),
384
- "char": c, "font": "", "font_size": 0,
385
- "is_bold": False, "is_italic": False,
386
- })
387
- x += cw
388
- char_rects.append({
389
- "page": page_idx, "rect": (x, y, x, y + LINE_HEIGHT),
390
- "char": "\n", "font": "", "font_size": 0,
391
- "is_bold": False, "is_italic": False,
392
- })
393
- y += LINE_HEIGHT
394
- return doc, char_rects
395
-
396
- # ------------------------------------------------------------------ #
397
- # ページリスト → 座標付きPDF(pptx/xlsx用)
398
- # ------------------------------------------------------------------ #
399
- def pages_to_pdf_with_rects(page_texts):
400
- doc = fitz.open()
401
- char_rects = []
402
- CHAR_W_FULL = FONT_SIZE
403
- CHAR_W_HALF = FONT_SIZE * 0.6
404
-
405
- def get_char_width(c):
406
- return CHAR_W_HALF if c.isascii() else CHAR_W_FULL
407
-
408
- for page_idx, page_text in enumerate(page_texts):
409
- page = doc.new_page(width=PAGE_W, height=PAGE_H)
410
- y = MARGIN_TOP
411
- lines = page_text.splitlines()
412
- for line in lines:
413
- if y + LINE_HEIGHT > PAGE_H - MARGIN_TOP:
414
- try:
415
- page.insert_text(
416
- (MARGIN_X, y + FONT_SIZE), "...",
417
- fontfile=FONT_PATH, fontname=FONT_NAME,
418
- fontsize=FONT_SIZE, color=(0.5, 0.5, 0.5),
419
- )
420
- except Exception:
421
- pass
422
- break
423
- display = line if line.strip() != "" else " "
424
- try:
425
- page.insert_text(
426
- (MARGIN_X, y + FONT_SIZE), display,
427
- fontfile=FONT_PATH, fontname=FONT_NAME,
428
- fontsize=FONT_SIZE, color=(0, 0, 0),
429
- )
430
- except Exception:
431
- page.insert_text(
432
- (MARGIN_X, y + FONT_SIZE), display,
433
- fontsize=FONT_SIZE, color=(0, 0, 0),
434
- )
435
- x = MARGIN_X
436
- for c in line:
437
- cw = get_char_width(c)
438
- char_rects.append({
439
- "page": page_idx, "rect": (x, y, x + cw, y + LINE_HEIGHT),
440
- "char": c, "font": "", "font_size": 0,
441
- "is_bold": False, "is_italic": False,
442
- })
443
- x += cw
444
- char_rects.append({
445
- "page": page_idx, "rect": (x, y, x, y + LINE_HEIGHT),
446
- "char": "\n", "font": "", "font_size": 0,
447
- "is_bold": False, "is_italic": False,
448
- })
449
- y += LINE_HEIGHT
450
- return doc, char_rects
451
-
452
- # ------------------------------------------------------------------ #
453
- # 空白処理
454
- # ------------------------------------------------------------------ #
455
- def preprocess_text(text, mode):
456
- if mode == "そのまま (Strict)":
457
- return text, list(range(len(text)))
458
- processed_text = ""
459
- index_map = []
460
- if mode == "無視 (Ignore)":
461
- for i, char in enumerate(text):
462
- if not char.isspace():
463
- processed_text += char
464
- index_map.append(i)
465
- elif mode == "正規化 (Normalize)":
466
- last_was_space = False
467
- for i, char in enumerate(text):
468
- if char.isspace():
469
- if not last_was_space:
470
- processed_text += " "
471
- index_map.append(i)
472
- last_was_space = True
473
- else:
474
- processed_text += char
475
- index_map.append(i)
476
- last_was_space = False
477
- return processed_text, index_map
478
-
479
- def get_page_from_index(char_map, index):
480
- if not char_map: return 0
481
- if index < 0: return char_map[0]["page"]
482
- if index >= len(char_map): return char_map[-1]["page"]
483
- return char_map[index]["page"]
484
-
485
- # ------------------------------------------------------------------ #
486
- # ハイライト注釈追加
487
- # ------------------------------------------------------------------ #
488
- def _add_highlight_annot(page, rect, color):
489
- if rect.width <= 0 or rect.height <= 0:
490
- return
491
- try:
492
- quad = rect.quad
493
- annot = page.add_highlight_annot(quad)
494
- annot.set_colors(stroke=color)
495
- annot.set_opacity(0.4)
496
- annot.update()
497
- except Exception:
498
- page.draw_rect(rect, color=None, fill=color, fill_opacity=0.3)
499
-
500
- # ------------------------------------------------------------------ #
501
- # 差異なしページにスタンプ
502
- # ------------------------------------------------------------------ #
503
- def add_no_diff_stamp(page):
504
- rect_x, rect_y, rect_w, rect_h = 20, 20, 140, 30
505
- rect = fitz.Rect(rect_x, rect_y, rect_x + rect_w, rect_y + rect_h)
506
- page.draw_rect(rect, color=(1, 0, 0), width=1.5)
507
  try:
508
- page.insert_text(
509
- (rect_x + 5, rect_y + 20), "テキスト差異無し",
510
- fontfile=FONT_PATH, fontname=FONT_NAME,
511
- fontsize=12, color=(1, 0, 0),
512
- )
513
- except Exception:
514
- page.insert_text((rect_x + 5, rect_y + 20), "NO TEXT DIFF", fontsize=12, color=(1, 0, 0))
515
-
516
- # ------------------------------------------------------------------ #
517
- # グレースケール変換
518
- # ------------------------------------------------------------------ #
519
- def convert_to_grayscale(doc):
520
- out = fitz.open()
521
- for i in range(len(doc)):
522
- page = doc[i]
523
- pix = page.get_pixmap(matrix=fitz.Matrix(150/72, 150/72), colorspace=fitz.csGRAY, annots=False)
524
- new_page = out.new_page(width=page.rect.width, height=page.rect.height)
525
- new_page.insert_image(page.rect, pixmap=pix)
526
- for annot in page.annots():
527
- if annot.type[0] != fitz.PDF_ANNOT_HIGHLIGHT:
528
- continue
529
- colors = annot.colors
530
- opacity = annot.opacity
531
- vertices = annot.vertices
532
- if not vertices:
533
  continue
534
- quads = [fitz.Quad(vertices[j], vertices[j+1], vertices[j+2], vertices[j+3])
535
- for j in range(0, len(vertices), 4)]
536
- new_annot = new_page.add_highlight_annot(quads)
537
- new_annot.set_colors(stroke=colors.get("stroke"))
538
- new_annot.set_opacity(opacity if opacity is not None else 0.4)
539
- new_annot.update()
540
- return out
541
-
542
- # ------------------------------------------------------------------ #
543
- # 左右見開きPDF生成(OCGレイヤー保持版)
544
- # ------------------------------------------------------------------ #
545
- def create_side_by_side_pdf_raster(path1, path2, output_path, dpi=150):
546
- doc1 = fitz.open(path1)
547
- doc2 = fitz.open(path2)
548
- len1 = len(doc1)
549
- len2 = len(doc2)
550
- max_pages = max(len1, len2)
551
- out_doc = fitz.open()
552
-
553
- for i in range(max_pages):
554
- w1 = doc1[i].rect.width if i < len1 else PAGE_W
555
- h1 = doc1[i].rect.height if i < len1 else PAGE_H
556
- w2 = doc2[i].rect.width if i < len2 else PAGE_W
557
- h2 = doc2[i].rect.height if i < len2 else PAGE_H
558
-
559
- new_page = out_doc.new_page(width=w1 + w2, height=max(h1, h2))
560
-
561
- if i < len1:
562
- new_page.show_pdf_page(
563
- fitz.Rect(0, 0, w1, h1), doc1, i, keep_proportion=False,
564
- )
565
- if i < len2:
566
- new_page.show_pdf_page(
567
- fitz.Rect(w1, 0, w1 + w2, h2), doc2, i, keep_proportion=False,
568
- )
569
-
570
- out_doc.save(output_path, garbage=4, deflate=True)
571
- out_doc.close()
572
- doc1.close()
573
- doc2.close()
574
-
575
- # ------------------------------------------------------------------ #
576
- # シート選択UI更新
577
- # ------------------------------------------------------------------ #
578
- def update_sheets1(file_obj):
579
- names = get_sheet_names(file_obj)
580
- if not names:
581
- return gr.CheckboxGroup.update(choices=[], value=[], visible=False)
582
- return gr.CheckboxGroup.update(choices=names, value=names, visible=True)
583
-
584
- def update_sheets2(file_obj):
585
- names = get_sheet_names(file_obj)
586
- if not names:
587
- return gr.CheckboxGroup.update(choices=[], value=[], visible=False)
588
- return gr.CheckboxGroup.update(choices=names, value=names, visible=True)
589
-
590
- # ------------------------------------------------------------------ #
591
- # 四角注釈テキスト抽出
592
- # ------------------------------------------------------------------ #
593
- def get_annotation_texts(doc, trim=False, trim_margin=20):
594
- annotations = []
595
- for page_num in range(len(doc)):
596
- page = doc[page_num]
597
- if trim:
598
- cb = page.cropbox
599
- mb = page.mediabox
600
- if abs(cb.width - mb.width) < 1 and abs(cb.height - mb.height) < 1:
601
- clip = fitz.Rect(
602
- mb.x0 + trim_margin, mb.y0 + trim_margin,
603
- mb.x1 - trim_margin, mb.y1 - trim_margin,
604
- )
605
- else:
606
- clip = cb
607
- else:
608
- clip = page.rect
609
- for annot in page.annots():
610
- if annot.type[0] == fitz.PDF_ANNOT_SQUARE:
611
- rect = annot.rect
612
- if trim and not clip.contains(rect):
613
- continue
614
- text = page.get_text("text", clip=rect).strip()
615
- annotations.append({
616
- "page": page_num, "rect": rect, "text": text,
617
- })
618
- annotations.sort(key=lambda a: (a["page"], a["rect"].y0, a["rect"].x0))
619
- return annotations
620
-
621
- # ------------------------------------------------------------------ #
622
- # 注釈内 char_map 取得
623
- # ------------------------------------------------------------------ #
624
- def get_char_map_in_rect(doc, page_num, rect):
625
- page = doc[page_num]
626
- raw = page.get_text("rawdict", clip=rect)
627
- char_map = []
628
- text = ""
629
- for block in raw["blocks"]:
630
- if "lines" not in block:
631
- continue
632
- for line in block["lines"]:
633
- for span in line["spans"]:
634
- font_name = span.get("font", "")
635
- font_size = round(span.get("size", 0), 2)
636
- for char in span["chars"]:
637
- c = char["c"]
638
- text += c
639
- char_map.append({
640
- "page": page_num,
641
- "rect": char["bbox"],
642
- "char": c,
643
- "font": font_name,
644
- "font_size": font_size,
645
- })
646
- return text, char_map
647
-
648
- # ------------------------------------------------------------------ #
649
- # テキスト比較メイン処理
650
- # ------------------------------------------------------------------ #
651
- def process_files(
652
- file1_obj, file2_obj,
653
- page_range_str, whitespace_mode, color_mode,
654
- sheets1, sheets2,
655
- trim, trim_margin,
656
- grayscale,
657
- ):
658
- if file1_obj is None or file2_obj is None:
659
- return "ファイルをアップロードしてください", None, None, None, None
660
-
661
- file1_path = file1_obj.name
662
- file2_path = file2_obj.name
663
- ext1 = os.path.splitext(file1_path)[1].lower()
664
- ext2 = os.path.splitext(file2_path)[1].lower()
665
- colors = COLOR_PRESETS.get(color_mode, COLOR_PRESETS["赤 / 青 (標準)"])
666
- del_color = colors["del"]
667
- ins_color = colors["ins"]
668
- temp_dir = tempfile.mkdtemp()
669
- trim_margin = int(trim_margin)
670
-
671
- if ext1 == ".pdf":
672
- try:
673
- src_doc1 = fitz.open(file1_path)
674
- try:
675
- full_text1, map1, doc1 = get_text_and_rects(src_doc1, page_range_str, trim, trim_margin)
676
- finally:
677
- src_doc1.close()
678
- except Exception as e:
679
- return f"エラー(ファイル1): {str(e)}", None, None, None, None
680
- is_pdf1 = True
681
- else:
682
- selected1 = sheets1 if ext1 == ".xlsx" else None
683
- result1, err1, is_paged1 = extract_text_from_file(file1_path, page_range_str, selected1)
684
- if err1:
685
- return f"ファイル1 エラー: {err1}", None, None, None, None
686
- if is_paged1:
687
- doc1, map1 = pages_to_pdf_with_rects(result1)
688
- full_text1 = "\n".join(result1)
689
- else:
690
- doc1, map1 = text_to_pdf_with_rects(result1)
691
- full_text1 = result1
692
- is_pdf1 = False
693
-
694
- if ext2 == ".pdf":
695
- try:
696
- src_doc2 = fitz.open(file2_path)
697
- try:
698
- full_text2, map2, doc2 = get_text_and_rects(src_doc2, page_range_str, trim, trim_margin)
699
- finally:
700
- src_doc2.close()
701
- except Exception as e:
702
- return f"エラー(ファイル2): {str(e)}", None, None, None, None
703
- is_pdf2 = True
704
- else:
705
- selected2 = sheets2 if ext2 == ".xlsx" else None
706
- result2, err2, is_paged2 = extract_text_from_file(file2_path, page_range_str, selected2)
707
- if err2:
708
- return f"ファイル2 エラー: {err2}", None, None, None, None
709
- if is_paged2:
710
- doc2, map2 = pages_to_pdf_with_rects(result2)
711
- full_text2 = "\n".join(result2)
712
- else:
713
- doc2, map2 = text_to_pdf_with_rects(result2)
714
- full_text2 = result2
715
- is_pdf2 = False
716
-
717
- if is_pdf1 and is_pdf2:
718
- cmp_text1, idx_map1 = preprocess_text(full_text1, whitespace_mode)
719
- cmp_text2, idx_map2 = preprocess_text(full_text2, whitespace_mode)
720
- else:
721
- cmp_text1, idx_map1 = preprocess_text(full_text1, "無視 (Ignore)")
722
- cmp_text2, idx_map2 = preprocess_text(full_text2, "無視 (Ignore)")
723
-
724
- matcher = difflib.SequenceMatcher(None, cmp_text1, cmp_text2)
725
- changes = []
726
- summary_counts = {"delete": 0, "insert": 0}
727
- changed_pages_doc1 = set()
728
- changed_pages_doc2 = set()
729
-
730
- for tag, i1, i2, j1, j2 in matcher.get_opcodes():
731
- if tag == "equal":
732
- continue
733
- if tag in ["delete", "replace"]:
734
- diff_str = cmp_text1[i1:i2]
735
- summary_counts["delete"] += len(diff_str)
736
- for c_idx in range(i1, i2):
737
- if c_idx < len(idx_map1):
738
- orig_idx = idx_map1[c_idx]
739
- if orig_idx < len(map1):
740
- info = map1[orig_idx]
741
- if info.get("char") == "\n":
742
- continue
743
- p_idx = info["page"]
744
- changed_pages_doc1.add(p_idx)
745
- _add_highlight_annot(doc1[p_idx], fitz.Rect(info["rect"]), del_color)
746
- display_page = get_page_from_index(map1, idx_map1[min(i1, len(idx_map1) - 1)]) + 1 if idx_map1 else 1
747
- changes.append({"Type": "削除/変更(旧)", "Page": display_page, "Text": diff_str})
748
- if tag in ["insert", "replace"]:
749
- diff_str = cmp_text2[j1:j2]
750
- summary_counts["insert"] += len(diff_str)
751
- for c_idx in range(j1, j2):
752
- if c_idx < len(idx_map2):
753
- orig_idx = idx_map2[c_idx]
754
- if orig_idx < len(map2):
755
- info = map2[orig_idx]
756
- if info.get("char") == "\n":
757
- continue
758
- p_idx = info["page"]
759
- changed_pages_doc2.add(p_idx)
760
- _add_highlight_annot(doc2[p_idx], fitz.Rect(info["rect"]), ins_color)
761
- display_page = get_page_from_index(map2, idx_map2[min(j1, len(idx_map2) - 1)]) + 1 if idx_map2 else 1
762
- changes.append({"Type": "追加/変更(新)", "Page": display_page, "Text": diff_str})
763
-
764
- out1_path = os.path.join(temp_dir, "result_1_Original.pdf")
765
- out2_path = os.path.join(temp_dir, "result_2_Modified.pdf")
766
- combined_path = os.path.join(temp_dir, "result_3_Combined.pdf")
767
- csv_path = os.path.join(temp_dir, "diff_report.csv")
768
-
769
- all_changed_pages = changed_pages_doc1 | changed_pages_doc2
770
-
771
- if grayscale:
772
- gs1 = convert_to_grayscale(doc1)
773
- gs2 = convert_to_grayscale(doc2)
774
- for p_idx in range(len(gs1)):
775
- if p_idx not in all_changed_pages:
776
- add_no_diff_stamp(gs1[p_idx])
777
- for p_idx in range(len(gs2)):
778
- if p_idx not in all_changed_pages:
779
- add_no_diff_stamp(gs2[p_idx])
780
- gs1.save(out1_path, garbage=4, deflate=True)
781
- gs2.save(out2_path, garbage=4, deflate=True)
782
- gs1.close()
783
- gs2.close()
784
- else:
785
- for p_idx in range(len(doc1)):
786
- if p_idx not in all_changed_pages:
787
- add_no_diff_stamp(doc1[p_idx])
788
- for p_idx in range(len(doc2)):
789
- if p_idx not in all_changed_pages:
790
- add_no_diff_stamp(doc2[p_idx])
791
- doc1.save(out1_path, garbage=4, deflate=True)
792
- doc2.save(out2_path, garbage=4, deflate=True)
793
- doc1.close()
794
- doc2.close()
795
-
796
- create_side_by_side_pdf_raster(out1_path, out2_path, combined_path)
797
- pd.DataFrame(changes).to_csv(csv_path, index=False, encoding="utf-8-sig")
798
-
799
- applied_mode = whitespace_mode if (is_pdf1 and is_pdf2) else "無視 (Ignore) ※PDF以外は固定"
800
- summary_text = (
801
- f"### テキスト比較完了\n"
802
- f"**ファイル1形式**: {ext1} \n"
803
- f"**ファイル2形式**: {ext2} \n"
804
- f"**空白処理**: {applied_mode} \n"
805
- f"**配色**: {color_mode} \n"
806
- f"**トンボ除外**: {'ON' if trim else 'OFF'} \n"
807
- f"**グレースケール**: {'ON' if grayscale else 'OFF'} \n"
808
- f"- **削除対象文字数**: {summary_counts['delete']}\n"
809
- f"- **追加対象文字数**: {summary_counts['insert']}\n"
810
- f"- **検出ブロック数**: {len(changes)}"
811
- )
812
- return summary_text, out1_path, out2_path, combined_path, csv_path
813
-
814
- # ------------------------------------------------------------------ #
815
- # フォント比較メイン処理
816
- # ------------------------------------------------------------------ #
817
- def process_font_compare(
818
- file1_obj, file2_obj,
819
- page_range_str, color_mode,
820
- sheets1, sheets2,
821
- grayscale,
822
- cmp_font_name, cmp_font_size, cmp_font_color,
823
- cmp_bold, cmp_italic,
824
- tolerance,
825
- ):
826
- if file1_obj is None or file2_obj is None:
827
- return "ファイルをアップロードしてください", None, None, None, None
828
- if not any([cmp_font_name, cmp_font_size, cmp_font_color, cmp_bold, cmp_italic]):
829
- return "比較対象を1つ以上選択してください", None, None, None, None
830
-
831
- file1_path = file1_obj.name
832
- file2_path = file2_obj.name
833
- ext1 = os.path.splitext(file1_path)[1].lower()
834
- ext2 = os.path.splitext(file2_path)[1].lower()
835
- temp_dir = tempfile.mkdtemp()
836
- tolerance = float(tolerance)
837
-
838
- SUPPORTED = [".pdf", ".docx", ".xlsx", ".pptx"]
839
- if ext1 not in SUPPORTED:
840
- return f"ファイル1はフォン��比較非対応の形式です: {ext1}", None, None, None, None
841
- if ext2 not in SUPPORTED:
842
- return f"ファイル2はフォント比較非対応の形式です: {ext2}", None, None, None, None
843
-
844
- if ext1 == ".pdf":
845
- try:
846
- src_doc1 = fitz.open(file1_path)
847
- try:
848
- _, map1, doc1 = get_text_and_rects(src_doc1, page_range_str, False, 20)
849
- finally:
850
- src_doc1.close()
851
- except Exception as e:
852
- return f"エラー(ファイル1): {str(e)}", None, None, None, None
853
- else:
854
- selected1 = sheets1 if ext1 == ".xlsx" else None
855
- map1, doc1, err1 = extract_font_map_from_file(file1_path, page_range_str, selected1)
856
- if err1:
857
- return f"ファイル1 エラー: {err1}", None, None, None, None
858
-
859
- if ext2 == ".pdf":
860
- try:
861
- src_doc2 = fitz.open(file2_path)
862
- try:
863
- _, map2, doc2 = get_text_and_rects(src_doc2, page_range_str, False, 20)
864
- finally:
865
- src_doc2.close()
866
- except Exception as e:
867
- return f"エラー(ファイル2): {str(e)}", None, None, None, None
868
- else:
869
- selected2 = sheets2 if ext2 == ".xlsx" else None
870
- map2, doc2, err2 = extract_font_map_from_file(file2_path, page_range_str, selected2)
871
- if err2:
872
- return f"ファイル2 エラー: {err2}", None, None, None, None
873
-
874
- def enrich_color(map_list, path):
875
- ext = os.path.splitext(path)[1].lower()
876
- if ext != ".pdf":
877
- for m in map_list:
878
- m["color_r"] = 0.0
879
- m["color_g"] = 0.0
880
- m["color_b"] = 0.0
881
- return
882
- doc_tmp = fitz.open(path)
883
- color_lookup = {}
884
- for page_num in range(len(doc_tmp)):
885
- page = doc_tmp[page_num]
886
- color_lookup[page_num] = {}
887
- for block in page.get_text("rawdict")["blocks"]:
888
- if "lines" not in block:
889
- continue
890
- for line in block["lines"]:
891
- for span in line["spans"]:
892
- cv = span.get("color", 0)
893
- r = ((cv >> 16) & 0xFF) / 255.0
894
- g = ((cv >> 8) & 0xFF) / 255.0
895
- b = ((cv ) & 0xFF) / 255.0
896
- for char in span["chars"]:
897
- bbox = char["bbox"]
898
- cx = round((bbox[0] + bbox[2]) / 2, 2)
899
- cy = round((bbox[1] + bbox[3]) / 2, 2)
900
- color_lookup[page_num][(cx, cy)] = (r, g, b)
901
- doc_tmp.close()
902
- for m in map_list:
903
- bbox = m["rect"]
904
- cx = round((bbox[0] + bbox[2]) / 2, 2)
905
- cy = round((bbox[1] + bbox[3]) / 2, 2)
906
- rgb = color_lookup.get(m["page"], {}).get((cx, cy), (0.0, 0.0, 0.0))
907
- m["color_r"] = rgb[0]
908
- m["color_g"] = rgb[1]
909
- m["color_b"] = rgb[2]
910
-
911
- enrich_color(map1, file1_path)
912
- enrich_color(map2, file2_path)
913
-
914
- for m in map1:
915
- m.setdefault("is_bold", False)
916
- m.setdefault("is_italic", False)
917
- for m in map2:
918
- m.setdefault("is_bold", False)
919
- m.setdefault("is_italic", False)
920
-
921
- ocg_cache1 = {}
922
- ocg_cache2 = {}
923
-
924
- def get_ocg1(label):
925
- if label not in ocg_cache1:
926
- ocg_cache1[label] = doc1.add_ocg(label, on=True)
927
- return ocg_cache1[label]
928
-
929
- def get_ocg2(label):
930
- if label not in ocg_cache2:
931
- ocg_cache2[label] = doc2.add_ocg(label, on=True)
932
- return ocg_cache2[label]
933
-
934
- def color_label(r, g, b):
935
- return f"rgb({int(r*255)},{int(g*255)},{int(b*255)})"
936
-
937
- from collections import defaultdict
938
-
939
- def group_by_line(char_list, line_tol):
940
- if not char_list:
941
- return []
942
- sorted_chars = sorted(char_list, key=lambda m: (m["page"], m["rect"][1]))
943
- lines = []
944
- current_line = [sorted_chars[0]]
945
- for m in sorted_chars[1:]:
946
- prev = current_line[-1]
947
- cy_prev = (prev["rect"][1] + prev["rect"][3]) / 2
948
- cy_curr = (m["rect"][1] + m["rect"][3]) / 2
949
- if m["page"] != prev["page"] or abs(cy_curr - cy_prev) > line_tol:
950
- lines.append(sorted(current_line, key=lambda m: (m["rect"][0] + m["rect"][2]) / 2))
951
- current_line = [m]
952
- else:
953
- current_line.append(m)
954
- lines.append(sorted(current_line, key=lambda m: (m["rect"][0] + m["rect"][2]) / 2))
955
- return lines
956
-
957
- nospace1 = [m for m in map1 if not m["char"].isspace()]
958
- nospace2 = [m for m in map2 if not m["char"].isspace()]
959
-
960
- page_chars1 = defaultdict(list)
961
- page_chars2 = defaultdict(list)
962
- for m in nospace1:
963
- page_chars1[m["page"]].append(m)
964
- for m in nospace2:
965
- page_chars2[m["page"]].append(m)
966
-
967
- matched_pairs = []
968
-
969
- for page_num in sorted(page_chars1.keys()):
970
- lines1 = group_by_line(page_chars1[page_num], tolerance)
971
- lines2 = group_by_line(page_chars2.get(page_num, []), tolerance)
972
- if not lines2:
973
- continue
974
- used_lines2 = set()
975
- line_pairs = []
976
- for li1, line1 in enumerate(lines1):
977
- cy1 = sum((m["rect"][1]+m["rect"][3])/2 for m in line1) / len(line1)
978
- best_li2 = None
979
- best_dist = float("inf")
980
- for li2, line2 in enumerate(lines2):
981
- if li2 in used_lines2:
982
- continue
983
- cy2 = sum((m["rect"][1]+m["rect"][3])/2 for m in line2) / len(line2)
984
- dist = abs(cy2 - cy1)
985
- if dist <= tolerance and dist < best_dist:
986
- best_dist = dist
987
- best_li2 = li2
988
- if best_li2 is not None:
989
- used_lines2.add(best_li2)
990
- line_pairs.append((line1, lines2[best_li2]))
991
- for line1, line2 in line_pairs:
992
- for idx in range(min(len(line1), len(line2))):
993
- matched_pairs.append((line1[idx], line2[idx]))
994
-
995
- changes = []
996
- summary_count = 0
997
- changed_pages_doc1 = set()
998
- changed_pages_doc2 = set()
999
-
1000
- for info1, info2 in matched_pairs:
1001
- font_diff = cmp_font_name and (info1["font"] != info2["font"])
1002
- size_diff = cmp_font_size and (abs(info1["font_size"] - info2["font_size"]) > 0.1)
1003
- color_diff = cmp_font_color and (
1004
- abs(info1["color_r"] - info2["color_r"]) > 0.01
1005
- or abs(info1["color_g"] - info2["color_g"]) > 0.01
1006
- or abs(info1["color_b"] - info2["color_b"]) > 0.01
1007
- )
1008
- bold_diff = cmp_bold and (info1["is_bold"] != info2["is_bold"])
1009
- italic_diff = cmp_italic and (info1["is_italic"] != info2["is_italic"])
1010
-
1011
- if not any([font_diff, size_diff, color_diff, bold_diff, italic_diff]):
1012
- continue
1013
-
1014
- p1 = info1["page"]
1015
- rect1 = fitz.Rect(info1["rect"])
1016
- changed_pages_doc1.add(p1)
1017
-
1018
- if font_diff:
1019
- xref = get_ocg1(f"元ファイル_{info1['font']}")
1020
- shape = doc1[p1].new_shape()
1021
- shape.draw_rect(rect1)
1022
- shape.finish(fill=(1, 0.5, 0.5), fill_opacity=0.4, color=None, oc=xref)
1023
- shape.commit()
1024
- if size_diff:
1025
- xref = get_ocg1(f"元ファイル_{info1['font_size']}pt")
1026
- shape = doc1[p1].new_shape()
1027
- shape.draw_rect(rect1)
1028
- shape.finish(fill=(1, 0.5, 0.5), fill_opacity=0.4, color=None, oc=xref)
1029
- shape.commit()
1030
- if color_diff:
1031
- xref = get_ocg1(f"元ファイル_{color_label(info1['color_r'], info1['color_g'], info1['color_b'])}")
1032
- shape = doc1[p1].new_shape()
1033
- shape.draw_rect(rect1)
1034
- shape.finish(fill=(1, 0.5, 0.5), fill_opacity=0.4, color=None, oc=xref)
1035
- shape.commit()
1036
- if bold_diff:
1037
- lbl = "元ファイル_Bold" if info1["is_bold"] else "元ファイル_NotBold"
1038
- xref = get_ocg1(lbl)
1039
- shape = doc1[p1].new_shape()
1040
- shape.draw_rect(rect1)
1041
- shape.finish(fill=(1, 0.5, 0.5), fill_opacity=0.4, color=None, oc=xref)
1042
- shape.commit()
1043
- if italic_diff:
1044
- lbl = "元ファイル_Italic" if info1["is_italic"] else "元ファイル_NotItalic"
1045
- xref = get_ocg1(lbl)
1046
- shape = doc1[p1].new_shape()
1047
- shape.draw_rect(rect1)
1048
- shape.finish(fill=(1, 0.5, 0.5), fill_opacity=0.4, color=None, oc=xref)
1049
- shape.commit()
1050
-
1051
- p2 = info2["page"]
1052
- rect2 = fitz.Rect(info2["rect"])
1053
- changed_pages_doc2.add(p2)
1054
-
1055
- if font_diff:
1056
- xref = get_ocg2(f"修正ファイル_{info2['font']}")
1057
- shape = doc2[p2].new_shape()
1058
- shape.draw_rect(rect2)
1059
- shape.finish(fill=(0.5, 0.5, 1), fill_opacity=0.4, color=None, oc=xref)
1060
- shape.commit()
1061
- if size_diff:
1062
- xref = get_ocg2(f"修正ファイル_{info2['font_size']}pt")
1063
- shape = doc2[p2].new_shape()
1064
- shape.draw_rect(rect2)
1065
- shape.finish(fill=(0.5, 0.5, 1), fill_opacity=0.4, color=None, oc=xref)
1066
- shape.commit()
1067
- if color_diff:
1068
- xref = get_ocg2(f"修正ファイル_{color_label(info2['color_r'], info2['color_g'], info2['color_b'])}")
1069
- shape = doc2[p2].new_shape()
1070
- shape.draw_rect(rect2)
1071
- shape.finish(fill=(0.5, 0.5, 1), fill_opacity=0.4, color=None, oc=xref)
1072
- shape.commit()
1073
- if bold_diff:
1074
- lbl = "修正ファイル_Bold" if info2["is_bold"] else "修正ファイル_NotBold"
1075
- xref = get_ocg2(lbl)
1076
- shape = doc2[p2].new_shape()
1077
- shape.draw_rect(rect2)
1078
- shape.finish(fill=(0.5, 0.5, 1), fill_opacity=0.4, color=None, oc=xref)
1079
- shape.commit()
1080
- if italic_diff:
1081
- lbl = "修正ファイル_Italic" if info2["is_italic"] else "修正ファイル_NotItalic"
1082
- xref = get_ocg2(lbl)
1083
- shape = doc2[p2].new_shape()
1084
- shape.draw_rect(rect2)
1085
- shape.finish(fill=(0.5, 0.5, 1), fill_opacity=0.4, color=None, oc=xref)
1086
- shape.commit()
1087
-
1088
- summary_count += 1
1089
- changes.append({
1090
- "Char": info1["char"],
1091
- "Page(旧)": p1 + 1,
1092
- "Font(旧)": info1["font"],
1093
- "Size(旧)": info1["font_size"],
1094
- "Color(旧)": color_label(info1["color_r"], info1["color_g"], info1["color_b"]),
1095
- "Bold(旧)": info1["is_bold"],
1096
- "Italic(旧)": info1["is_italic"],
1097
- "Page(新)": p2 + 1,
1098
- "Font(新)": info2["font"],
1099
- "Size(新)": info2["font_size"],
1100
- "Color(新)": color_label(info2["color_r"], info2["color_g"], info2["color_b"]),
1101
- "Bold(新)": info2["is_bold"],
1102
- "Italic(新)": info2["is_italic"],
1103
- "差異種別": "/".join(
1104
- (["フォント名"] if font_diff else [])
1105
- + (["サイズ"] if size_diff else [])
1106
- + (["色"] if color_diff else [])
1107
- + (["Bold"] if bold_diff else [])
1108
- + (["Italic"] if italic_diff else [])
1109
- ),
1110
- })
1111
-
1112
- all_changed = changed_pages_doc1 | changed_pages_doc2
1113
- for p_idx in range(len(doc1)):
1114
- if p_idx not in all_changed:
1115
- add_no_diff_stamp(doc1[p_idx])
1116
- for p_idx in range(len(doc2)):
1117
- if p_idx not in all_changed:
1118
- add_no_diff_stamp(doc2[p_idx])
1119
-
1120
- out1_path = os.path.join(temp_dir, "result_1_Original.pdf")
1121
- out2_path = os.path.join(temp_dir, "result_2_Modified.pdf")
1122
- combined_path = os.path.join(temp_dir, "result_3_Combined.pdf")
1123
- csv_path = os.path.join(temp_dir, "diff_report.csv")
1124
-
1125
- if grayscale:
1126
- gs1 = convert_to_grayscale(doc1)
1127
- gs2 = convert_to_grayscale(doc2)
1128
- gs1.save(out1_path, garbage=4, deflate=True)
1129
- gs2.save(out2_path, garbage=4, deflate=True)
1130
- gs1.close()
1131
- gs2.close()
1132
- else:
1133
- doc1.save(out1_path, garbage=4, deflate=True)
1134
- doc2.save(out2_path, garbage=4, deflate=True)
1135
- doc1.close()
1136
- doc2.close()
1137
-
1138
- create_side_by_side_pdf_raster(out1_path, out2_path, combined_path)
1139
- pd.DataFrame(changes).to_csv(csv_path, index=False, encoding="utf-8-sig")
1140
-
1141
- cmp_targets = []
1142
- if cmp_font_name: cmp_targets.append("フォント名")
1143
- if cmp_font_size: cmp_targets.append("フォントサイズ")
1144
- if cmp_font_color: cmp_targets.append("フォント色")
1145
- if cmp_bold: cmp_targets.append("Bold")
1146
- if cmp_italic: cmp_targets.append("Italic")
1147
-
1148
- summary_text = (
1149
- f"### フォント比較完了\n"
1150
- f"**ファイル1形式**: {ext1} \n"
1151
- f"**ファイル2形式**: {ext2} \n"
1152
- f"**比較対象**: {' / '.join(cmp_targets)} \n"
1153
- f"**許容距離**: {tolerance}pt \n"
1154
- f"**グレースケール**: {'ON' if grayscale else 'OFF'} \n"
1155
- f"- **フォント差異箇所数**: {summary_count}\n"
1156
- f"- **生成レイヤー数(元)**: {len(ocg_cache1)}\n"
1157
- f"- **生成レイヤー数(修正)**: {len(ocg_cache2)}"
1158
- )
1159
- return summary_text, out1_path, out2_path, combined_path, csv_path
1160
-
1161
- # ------------------------------------------------------------------ #
1162
- # 注釈比較メイン処理
1163
- # ------------------------------------------------------------------ #
1164
- def process_annotations(
1165
- file1_obj, file2_obj,
1166
- page_range_str, whitespace_mode, color_mode,
1167
- trim, trim_margin,
1168
- grayscale,
1169
- ):
1170
- if file1_obj is None or file2_obj is None:
1171
- return "ファイルをアップロードしてください", None, None, None, None
1172
-
1173
- file1_path = file1_obj.name
1174
- file2_path = file2_obj.name
1175
- ext1 = os.path.splitext(file1_path)[1].lower()
1176
- ext2 = os.path.splitext(file2_path)[1].lower()
1177
-
1178
- if ext1 != ".pdf" or ext2 != ".pdf":
1179
- return "注釈比較モードはPDFのみ対応しています", None, None, None, None
1180
-
1181
- colors = COLOR_PRESETS.get(color_mode, COLOR_PRESETS["赤 / 青 (標準)"])
1182
- del_color = colors["del"]
1183
- ins_color = colors["ins"]
1184
- temp_dir = tempfile.mkdtemp()
1185
- trim_margin = int(trim_margin)
1186
-
1187
- try:
1188
- doc1 = fitz.open(file1_path)
1189
- doc2 = fitz.open(file2_path)
1190
  except Exception as e:
1191
- return f"エラー: {str(e)}", None, None, None, None
1192
-
1193
- annots1 = get_annotation_texts(doc1, trim=trim, trim_margin=trim_margin)
1194
- annots2 = get_annotation_texts(doc2, trim=trim, trim_margin=trim_margin)
1195
-
1196
- if len(annots1) == 0 and len(annots2) == 0:
1197
- return "四角注釈が見つかりませんでした", None, None, None, None
1198
-
1199
- n = min(len(annots1), len(annots2))
1200
- skipped = abs(len(annots1) - len(annots2))
1201
-
1202
- changes = []
1203
- summary_counts = {"delete": 0, "insert": 0}
1204
- changed_pages_doc1 = set()
1205
- changed_pages_doc2 = set()
1206
-
1207
- for i in range(n):
1208
- a1 = annots1[i]
1209
- a2 = annots2[i]
1210
- text1, char_map1 = get_char_map_in_rect(doc1, a1["page"], a1["rect"])
1211
- text2, char_map2 = get_char_map_in_rect(doc2, a2["page"], a2["rect"])
1212
- cmp_text1, idx_map1 = preprocess_text(text1, whitespace_mode)
1213
- cmp_text2, idx_map2 = preprocess_text(text2, whitespace_mode)
1214
- matcher = difflib.SequenceMatcher(None, cmp_text1, cmp_text2)
1215
- has_diff = False
1216
- for tag, i1, i2, j1, j2 in matcher.get_opcodes():
1217
- if tag == "equal":
1218
- continue
1219
- has_diff = True
1220
- if tag in ["delete", "replace"]:
1221
- summary_counts["delete"] += (i2 - i1)
1222
- for ci in range(i1, i2):
1223
- if ci < len(idx_map1):
1224
- orig_idx = idx_map1[ci]
1225
- if orig_idx < len(char_map1):
1226
- info = char_map1[orig_idx]
1227
- p_idx = info["page"]
1228
- changed_pages_doc1.add(p_idx)
1229
- _add_highlight_annot(doc1[p_idx], fitz.Rect(info["rect"]), del_color)
1230
- if tag in ["insert", "replace"]:
1231
- summary_counts["insert"] += (j2 - j1)
1232
- for cj in range(j1, j2):
1233
- if cj < len(idx_map2):
1234
- orig_idx = idx_map2[cj]
1235
- if orig_idx < len(char_map2):
1236
- info = char_map2[orig_idx]
1237
- p_idx = info["page"]
1238
- changed_pages_doc2.add(p_idx)
1239
- _add_highlight_annot(doc2[p_idx], fitz.Rect(info["rect"]), ins_color)
1240
- if has_diff:
1241
- changes.append({"Type": "変更(旧)", "Page": a1["page"] + 1, "AnnotNo": i + 1, "Text": text1})
1242
- changes.append({"Type": "変更(新)", "Page": a2["page"] + 1, "AnnotNo": i + 1, "Text": text2})
1243
-
1244
- out1_path = os.path.join(temp_dir, "result_1_Original.pdf")
1245
- out2_path = os.path.join(temp_dir, "result_2_Modified.pdf")
1246
- combined_path = os.path.join(temp_dir, "result_3_Combined.pdf")
1247
- csv_path = os.path.join(temp_dir, "diff_report.csv")
1248
-
1249
- all_changed_pages = changed_pages_doc1 | changed_pages_doc2
1250
-
1251
- if grayscale:
1252
- gs1 = convert_to_grayscale(doc1)
1253
- gs2 = convert_to_grayscale(doc2)
1254
- for p_idx in range(len(gs1)):
1255
- if p_idx not in all_changed_pages:
1256
- add_no_diff_stamp(gs1[p_idx])
1257
- for p_idx in range(len(gs2)):
1258
- if p_idx not in all_changed_pages:
1259
- add_no_diff_stamp(gs2[p_idx])
1260
- gs1.save(out1_path, garbage=4, deflate=True)
1261
- gs2.save(out2_path, garbage=4, deflate=True)
1262
- gs1.close()
1263
- gs2.close()
1264
- else:
1265
- for p_idx in range(len(doc1)):
1266
- if p_idx not in all_changed_pages:
1267
- add_no_diff_stamp(doc1[p_idx])
1268
- for p_idx in range(len(doc2)):
1269
- if p_idx not in all_changed_pages:
1270
- add_no_diff_stamp(doc2[p_idx])
1271
- doc1.save(out1_path, garbage=4, deflate=True)
1272
- doc2.save(out2_path, garbage=4, deflate=True)
1273
- doc1.close()
1274
- doc2.close()
1275
-
1276
- create_side_by_side_pdf_raster(out1_path, out2_path, combined_path)
1277
- pd.DataFrame(changes).to_csv(csv_path, index=False, encoding="utf-8-sig")
1278
-
1279
- summary_text = (
1280
- f"### 注釈比較完了\n"
1281
- f"**空白処理**: {whitespace_mode} \n"
1282
- f"**配色**: {color_mode} \n"
1283
- f"**トンボ除外**: {'ON' if trim else 'OFF'} \n"
1284
- f"**グレースケール**: {'ON' if grayscale else 'OFF'} \n"
1285
- f"- **比較注釈数**: {n}\n"
1286
- f"- **削除対象文字数**: {summary_counts['delete']}\n"
1287
- f"- **追加対象文字数**: {summary_counts['insert']}\n"
1288
- f"- **差分注釈数**: {len(set(c['AnnotNo'] for c in changes)) if changes else 0}\n"
1289
- )
1290
- if skipped > 0:
1291
- summary_text += f"- ⚠️ 注釈数が異なるため {skipped} 件をスキップしました\n"
1292
-
1293
- return summary_text, out1_path, out2_path, combined_path, csv_path
1294
-
1295
- # ------------------------------------------------------------------ #
1296
- # クリア
1297
- # ------------------------------------------------------------------ #
1298
- def clear_all():
1299
- return (
1300
- # テキスト比較
1301
- None, None, "", "無視 (Ignore)", "赤 / 青 (標準)",
1302
- gr.CheckboxGroup.update(choices=[], value=[], visible=False),
1303
- gr.CheckboxGroup.update(choices=[], value=[], visible=False),
1304
- False, 20, False,
1305
- # フォント比較
1306
- None, None, "", "赤 / 青 (標準)",
1307
- gr.CheckboxGroup.update(choices=[], value=[], visible=False),
1308
- gr.CheckboxGroup.update(choices=[], value=[], visible=False),
1309
- False, True, False, False, False, False, 5,
1310
- # 注釈比較
1311
- None, None, "", "無視 (Ignore)", "赤 / 青 (標準)",
1312
- False, 20, False,
1313
- # 結果
1314
- "", None, None, None, None,
1315
- )
1316
-
1317
- # ------------------------------------------------------------------ #
1318
- # UI
1319
- # ------------------------------------------------------------------ #
1320
- with gr.Blocks(title="PDF Diff Tool") as demo:
1321
- gr.Markdown("## PDF / テキスト 差分比較ツール")
1322
- gr.Markdown("対応形式: `.pdf` `.txt` `.html` `.htm` `.rtf` `.docx` `.xlsx` `.pptx`")
1323
-
1324
- with gr.Row():
1325
- with gr.Column(scale=1):
1326
- with gr.Tabs():
1327
-
1328
- with gr.TabItem("テキスト比較"):
1329
- gr.Markdown("### 設定 & ファイル入力")
1330
- color_select = gr.Dropdown(
1331
- choices=list(COLOR_PRESETS.keys()), value="赤 / 青 (標準)",
1332
- label="ハイライト配色", interactive=True,
1333
- )
1334
- mode_select = gr.Radio(
1335
- choices=["そのまま (Strict)", "正規化 (Normalize)", "無視 (Ignore)"],
1336
- value="無視 (Ignore)",
1337
- label="空白文字の扱い (PDF同士のみ有効)",
1338
- info="PDF以外のファイルは常に「無視」で処理されます。",
1339
- )
1340
- page_range_input = gr.Textbox(
1341
- label="ページ / スライド指定(PDF・pptx のみ有効)",
1342
- placeholder="例: 1,3-5,7 空欄または all で全ページ",
1343
- value="",
1344
- )
1345
- with gr.Row():
1346
- trim_toggle = gr.Checkbox(label="トンボ領域を除外する", value=False)
1347
- trim_margin_input = gr.Number(label="除外マージン (pt)", value=20, precision=0)
1348
- grayscale_toggle = gr.Checkbox(label="グレースケールで出力", value=False)
1349
- input_file1 = gr.File(
1350
- label="元ファイル",
1351
- file_types=[".pdf", ".txt", ".html", ".htm", ".rtf", ".docx", ".xlsx", ".pptx"],
1352
- type="file",
1353
- )
1354
- sheets1_select = gr.CheckboxGroup(
1355
- choices=[], value=[],
1356
- label="元ファイル シート選択(xlsx のみ)", visible=False,
1357
- )
1358
- input_file2 = gr.File(
1359
- label="修正ファイル",
1360
- file_types=[".pdf", ".txt", ".html", ".htm", ".rtf", ".docx", ".xlsx", ".pptx"],
1361
- type="file",
1362
- )
1363
- sheets2_select = gr.CheckboxGroup(
1364
- choices=[], value=[],
1365
- label="修正ファイル シート選択(xlsx のみ)", visible=False,
1366
- )
1367
- with gr.Row():
1368
- clear_btn = gr.Button("クリア", variant="secondary")
1369
- btn = gr.Button("比較を実行", variant="primary")
1370
-
1371
- with gr.TabItem("フォント比較"):
1372
- gr.Markdown("### 設定 & ファイル入力")
1373
- gr.Markdown("対応形式: `.pdf` `.docx` `.xlsx` `.pptx` \n※ 同じ座標位置にある文字同士のフォントを比較します。空白文字は比較対象外です。")
1374
- font_color_select = gr.Dropdown(
1375
- choices=list(COLOR_PRESETS.keys()), value="赤 / 青 (標準)",
1376
- label="ハイライト配色", interactive=True,
1377
- )
1378
- font_page_range_input = gr.Textbox(
1379
- label="ページ / スライド指定(PDF・pptx のみ有効)",
1380
- placeholder="例: 1,3-5,7 空欄または all で全ページ",
1381
- value="",
1382
- )
1383
- font_tolerance_input = gr.Slider(
1384
- minimum=1, maximum=20, value=5, step=1,
1385
- label="座標マッチング許容距離 (pt)",
1386
- info="この距離以内にある文字同士を同じ位置とみなして比較します",
1387
- )
1388
- font_grayscale_toggle = gr.Checkbox(label="グレースケールで出力", value=False)
1389
- gr.Markdown("**比較対象**")
1390
- with gr.Row():
1391
- cmp_font_name = gr.Checkbox(label="フォント名", value=True)
1392
- cmp_font_size = gr.Checkbox(label="フォントサイズ", value=False)
1393
- cmp_font_color = gr.Checkbox(label="フォント色", value=False)
1394
- cmp_bold = gr.Checkbox(label="Bold", value=False)
1395
- cmp_italic = gr.Checkbox(label="Italic", value=False)
1396
- font_file1 = gr.File(
1397
- label="元ファイル",
1398
- file_types=[".pdf", ".docx", ".xlsx", ".pptx"],
1399
- type="file",
1400
- )
1401
- font_sheets1_select = gr.CheckboxGroup(
1402
- choices=[], value=[],
1403
- label="元ファイル シート選択(xlsx のみ)", visible=False,
1404
- )
1405
- font_file2 = gr.File(
1406
- label="修正ファイル",
1407
- file_types=[".pdf", ".docx", ".xlsx", ".pptx"],
1408
- type="file",
1409
- )
1410
- font_sheets2_select = gr.CheckboxGroup(
1411
- choices=[], value=[],
1412
- label="修正ファイル シート選択(xlsx のみ)", visible=False,
1413
- )
1414
- with gr.Row():
1415
- font_clear_btn = gr.Button("クリア", variant="secondary")
1416
- font_btn = gr.Button("比較を実行", variant="primary")
1417
-
1418
- with gr.TabItem("注釈比較(四角)"):
1419
- gr.Markdown("### 設定 & ファイル入力")
1420
- gr.Markdown("PDFの四角注釈内テキストを上から順に比較します。")
1421
- annot_color_select = gr.Dropdown(
1422
- choices=list(COLOR_PRESETS.keys()), value="赤 / 青 (標準)",
1423
- label="ハイライト配色", interactive=True,
1424
- )
1425
- annot_mode_select = gr.Radio(
1426
- choices=["そのまま (Strict)", "正規化 (Normalize)", "無視 (Ignore)"],
1427
- value="無視 (Ignore)", label="空白文字の扱い",
1428
- )
1429
- annot_page_range_input = gr.Textbox(
1430
- label="ページ指定",
1431
- placeholder="例: 1,3-5,7 空欄または all で全ページ",
1432
- value="",
1433
- )
1434
- with gr.Row():
1435
- annot_trim_toggle = gr.Checkbox(label="トンボ領域を除外する", value=False)
1436
- annot_trim_margin_input = gr.Number(label="除外マージン (pt)", value=20, precision=0)
1437
- annot_grayscale_toggle = gr.Checkbox(label="グレースケールで出力", value=False)
1438
- annot_file1 = gr.File(
1439
- label="元ファイル (PDF)", file_types=[".pdf"], type="file",
1440
- )
1441
- annot_file2 = gr.File(
1442
- label="修正ファイル (PDF)", file_types=[".pdf"], type="file",
1443
- )
1444
- with gr.Row():
1445
- annot_clear_btn = gr.Button("クリア", variant="secondary")
1446
- annot_btn = gr.Button("比較を実行", variant="primary")
1447
-
1448
- with gr.Column(scale=1):
1449
- gr.Markdown("### 結果サマリ")
1450
- summary_box = gr.Markdown("ここに結果が表示されます...")
1451
- gr.Markdown("### ファイルダウンロード")
1452
- with gr.Tabs():
1453
- with gr.TabItem("個別PDF"):
1454
- output_pdf1 = gr.File(label="結果1: Original (元ファイル)")
1455
- output_pdf2 = gr.File(label="結果2: Modified (修正ファイル)")
1456
- with gr.TabItem("統合PDF"):
1457
- output_combined = gr.File(label="結果3: Combined (左右統合)")
1458
- with gr.TabItem("レポート"):
1459
- output_csv = gr.File(label="差分リスト (CSV)")
1460
-
1461
- # --- イベント ---
1462
- input_file1.change(fn=update_sheets1, inputs=[input_file1], outputs=[sheets1_select])
1463
- input_file2.change(fn=update_sheets2, inputs=[input_file2], outputs=[sheets2_select])
1464
- font_file1.change(fn=update_sheets1, inputs=[font_file1], outputs=[font_sheets1_select])
1465
- font_file2.change(fn=update_sheets2, inputs=[font_file2], outputs=[font_sheets2_select])
1466
-
1467
- btn.click(
1468
- fn=process_files,
1469
- inputs=[
1470
- input_file1, input_file2, page_range_input, mode_select, color_select,
1471
- sheets1_select, sheets2_select,
1472
- trim_toggle, trim_margin_input, grayscale_toggle,
1473
- ],
1474
- outputs=[summary_box, output_pdf1, output_pdf2, output_combined, output_csv],
1475
- )
1476
- font_btn.click(
1477
- fn=process_font_compare,
1478
- inputs=[
1479
- font_file1, font_file2, font_page_range_input, font_color_select,
1480
- font_sheets1_select, font_sheets2_select,
1481
- font_grayscale_toggle,
1482
- cmp_font_name, cmp_font_size, cmp_font_color,
1483
- cmp_bold, cmp_italic,
1484
- font_tolerance_input,
1485
- ],
1486
- outputs=[summary_box, output_pdf1, output_pdf2, output_combined, output_csv],
1487
- )
1488
- annot_btn.click(
1489
- fn=process_annotations,
1490
- inputs=[
1491
- annot_file1, annot_file2,
1492
- annot_page_range_input, annot_mode_select, annot_color_select,
1493
- annot_trim_toggle, annot_trim_margin_input, annot_grayscale_toggle,
1494
- ],
1495
- outputs=[summary_box, output_pdf1, output_pdf2, output_combined, output_csv],
1496
- )
1497
-
1498
- outputs_all = [
1499
- # テキスト比較
1500
- input_file1, input_file2, page_range_input, mode_select, color_select,
1501
- sheets1_select, sheets2_select,
1502
- trim_toggle, trim_margin_input, grayscale_toggle,
1503
- # フォント比較
1504
- font_file1, font_file2, font_page_range_input, font_color_select,
1505
- font_sheets1_select, font_sheets2_select,
1506
- font_grayscale_toggle,
1507
- cmp_font_name, cmp_font_size, cmp_font_color, cmp_bold, cmp_italic,
1508
- font_tolerance_input,
1509
- # 注釈比較
1510
- annot_file1, annot_file2, annot_page_range_input, annot_mode_select, annot_color_select,
1511
- annot_trim_toggle, annot_trim_margin_input, annot_grayscale_toggle,
1512
- # 結果
1513
- summary_box, output_pdf1, output_pdf2, output_combined, output_csv,
1514
- ]
1515
- clear_btn.click(fn=clear_all, inputs=[], outputs=outputs_all)
1516
- font_clear_btn.click(fn=clear_all, inputs=[], outputs=outputs_all)
1517
- annot_clear_btn.click(fn=clear_all, inputs=[], outputs=outputs_all)
1518
 
1519
  if __name__ == "__main__":
1520
  demo.launch()
 
1
  import gradio as gr
2
+ import fitz
 
3
  import tempfile
4
  import os
 
5
 
6
+ def check_italic(file_obj):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  if file_obj is None:
8
+ return "PDFをアップロードしてください"
9
+ log = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  try:
11
+ doc = fitz.open(file_obj.name)
12
+ page = doc[0]
13
+ for block in page.get_text("rawdict")["blocks"]:
14
+ if "lines" not in block:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  continue
16
+ for line in block["lines"]:
17
+ for span in line["spans"]:
18
+ flags = span.get("flags", 0)
19
+ font = span.get("font", "")
20
+ text = span.get("text", "")[:20]
21
+ is_italic_flag = bool(flags & 1)
22
+ is_italic_name = any(k in font.lower() for k in ["italic", "oblique", "it-", "-it"])
23
+ log.append(
24
+ f"font={font} flags={flags}(bin={bin(flags)}) "
25
+ f"flag_italic={is_italic_flag} name_italic={is_italic_name} "
26
+ f"text='{text}'"
27
+ )
28
+ doc.close()
29
+ return "\n".join(log) if log else "テキストなし"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  except Exception as e:
31
+ return f"エラー: {e}"
32
+
33
+ with gr.Blocks() as demo:
34
+ gr.Markdown("## Italic検出デバッグ")
35
+ f = gr.File(label="PDF", file_types=[".pdf"], type="file")
36
+ b = gr.Button("実行")
37
+ o = gr.Textbox(label="結果", lines=30)
38
+ b.click(fn=check_italic, inputs=[f], outputs=[o])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  if __name__ == "__main__":
41
  demo.launch()