curlkun commited on
Commit
ef2b6af
·
verified ·
1 Parent(s): 8746487

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -366
app.py CHANGED
@@ -356,372 +356,7 @@ def count_stats(text: str) -> dict:
356
  'words': words,
357
  }
358
 
359
- # ============================================================
360
- # PDF生成
361
- # ============================================================
362
-
363
- def generate_pdf(src_orig: str, dst_orig: str, settings: dict,
364
- src_html_body: str, dst_html_body: str, has_diff: bool) -> str:
365
-
366
- FONT = "NotoSansJP"
367
- PAGE_W, PAGE_H = A4
368
- MARGIN = 15 * mm
369
- COL_GAP = 4 * mm
370
- COL_W = (PAGE_W - MARGIN * 2 - COL_GAP) / 2
371
- FONT_SIZE = 8.5
372
- LINE_HEIGHT= 13
373
- COL_L = MARGIN
374
- COL_R = MARGIN + COL_W + COL_GAP
375
- DIV_X = MARGIN + COL_W + COL_GAP / 2
376
-
377
- color_mode = settings.get('color_mode', 'color1')
378
- if color_mode == 'color1':
379
- HL_COLOR_RL = (1.0, 0.6, 0.6)
380
- HL_FG_RL = (0.0, 0.0, 0.0)
381
- HL_COLOR_MU = (1.0, 0.6, 0.6)
382
- elif color_mode == 'color2':
383
- HL_COLOR_RL = (0.6, 0.8, 1.0)
384
- HL_FG_RL = (0.0, 0.0, 0.0)
385
- HL_COLOR_MU = (0.6, 0.8, 1.0)
386
- else: # monochrome
387
- HL_COLOR_RL = (0.0, 0.0, 0.0)
388
- HL_FG_RL = (1.0, 1.0, 1.0)
389
- HL_COLOR_MU = (0.5, 0.5, 0.5)
390
-
391
- # ----------------------------------------------------------
392
- # HTMLから差分範囲を復元
393
- # ----------------------------------------------------------
394
- def extract_diff_ranges(text: str, html_body: str) -> list:
395
- """
396
- html_bodyのspanタグからハイライト箇所の文字インデックスリストを復元する。
397
- 戻り値: [(start, end), ...] ※textのインデックス
398
- """
399
- ranges = []
400
- # ハイライトspanを検索
401
- pattern = re.compile(
402
- r'<span style="[^"]*background-color[^"]*">(.*?)</span>',
403
- re.DOTALL
404
- )
405
- # タグを除去してプレーンテキストを再構築しながら位置を追跡
406
- plain = ''
407
- pos = 0
408
- for m in re.finditer(
409
- r'<span(?:\s+style="([^"]*)")?>(.*?)</span>',
410
- html_body, re.DOTALL
411
- ):
412
- style = m.group(1) or ''
413
- content = m.group(2)
414
- # <br>を改行に戻す
415
- content = content.replace('<br>', '\n')
416
- # HTMLエスケープを戻す
417
- content = html.unescape(content)
418
- is_hl = 'background-color' in style
419
- start = len(plain)
420
- plain += content
421
- end = len(plain)
422
- if is_hl:
423
- ranges.append((start, end))
424
- return ranges
425
-
426
- src_diff_ranges = extract_diff_ranges(src_orig, src_html_body)
427
- dst_diff_ranges = extract_diff_ranges(dst_orig, dst_html_body)
428
-
429
- # ----------------------------------------------------------
430
- # テキストを折り返し済み描画単位に変換
431
- # ----------------------------------------------------------
432
- def to_sublines(text: str, col_w: float, font_size: float):
433
- """
434
- 戻り値: [(char_offset, line_text), ...]
435
- char_offset: テキスト先頭からの文字インデックス
436
- """
437
- result = []
438
- lines = text.split('\n')
439
- offset = 0
440
- for line in lines:
441
- cur = ''
442
- cur_start = offset
443
- for ci, ch in enumerate(line):
444
- test = cur + ch
445
- if pdfmetrics.stringWidth(test, FONT, font_size) > col_w - 2 and cur:
446
- result.append((cur_start, cur))
447
- cur = ch
448
- cur_start = offset + ci
449
- else:
450
- cur = test
451
- result.append((cur_start, cur))
452
- offset += len(line) + 1 # +1 for '\n'
453
- return result
454
-
455
- # ----------------------------------------------------------
456
- # ハイライト判定
457
- # ----------------------------------------------------------
458
- def is_highlighted(char_idx: int, diff_ranges: list) -> bool:
459
- for s, e in diff_ranges:
460
- if s <= char_idx < e:
461
- return True
462
- return False
463
-
464
- # ----------------------------------------------------------
465
- # 1行描画(背景矩形 → 文字)
466
- # 戻り値: ハイライトした文字の座標リスト [(x0,y0,x1,y1), ...]
467
- # ※ PyMuPDF注釈付与用
468
- # ----------------------------------------------------------
469
- def draw_subline(c, sub_text: str, sub_offset: int, diff_ranges: list,
470
- x_start: float, y_rl: float, font_size: float) -> list:
471
- """
472
- y_rl: reportlabのy座標(下原点)
473
- 戻り値: ハイライト文字のrect座標リスト (reportlab座標系)
474
- """
475
- if not sub_text:
476
- return []
477
-
478
- hl_rects = []
479
-
480
- # パス1: 背景矩形
481
- dx = x_start
482
- for ci, ch in enumerate(sub_text):
483
- cw = pdfmetrics.stringWidth(ch, FONT, font_size)
484
- if is_highlighted(sub_offset + ci, diff_ranges):
485
- c.saveState()
486
- c.setFillColorRGB(*HL_COLOR_RL)
487
- c.rect(dx, y_rl - 2, cw, LINE_HEIGHT - 1, fill=1, stroke=0)
488
- c.restoreState()
489
- hl_rects.append((dx, y_rl - 2, dx + cw, y_rl + LINE_HEIGHT - 1))
490
- dx += cw
491
-
492
- # パス2: 文字描画
493
- dx = x_start
494
- seg = ''
495
- seg_hl = None
496
- seg_x = x_start
497
-
498
- for ci, ch in enumerate(sub_text):
499
- hl = is_highlighted(sub_offset + ci, diff_ranges)
500
- cw = pdfmetrics.stringWidth(ch, FONT, font_size)
501
- if seg_hl is None:
502
- seg_hl, seg, seg_x = hl, ch, dx
503
- elif hl == seg_hl:
504
- seg += ch
505
- else:
506
- c.saveState()
507
- c.setFont(FONT, font_size)
508
- c.setFillColorRGB(*(HL_FG_RL if seg_hl else (0.0, 0.0, 0.0)))
509
- c.drawString(seg_x, y_rl, seg)
510
- c.restoreState()
511
- seg_x += pdfmetrics.stringWidth(seg, FONT, font_size)
512
- seg_hl, seg = hl, ch
513
- dx += cw
514
-
515
- if seg:
516
- c.saveState()
517
- c.setFont(FONT, font_size)
518
- c.setFillColorRGB(*(HL_FG_RL if seg_hl else (0.0, 0.0, 0.0)))
519
- c.drawString(seg_x, y_rl, seg)
520
- c.restoreState()
521
-
522
- return hl_rects
523
-
524
- # ----------------------------------------------------------
525
- # reportlabでベースPDF生成
526
- # ----------------------------------------------------------
527
- def build_base_pdf() -> tuple:
528
- """
529
- 戻り値: (pdf_bytes, all_hl_rects)
530
- all_hl_rects: {page_no: {'src': [(x0,y0_rl,x1,y1_rl),...], 'dst': [...]}}
531
- ※ y座標はreportlab座標系(下原点)
532
- """
533
- buffer = io.BytesIO()
534
- c = rl_canvas.Canvas(buffer, pagesize=A4)
535
- all_hl = {} # page_no -> {'src':[], 'dst':[]}
536
- page_no = 0
537
-
538
- def hline(y, lw=0.5):
539
- c.saveState()
540
- c.setStrokeColorRGB(0.5, 0.5, 0.5)
541
- c.setLineWidth(lw)
542
- c.line(MARGIN, y, PAGE_W - MARGIN, y)
543
- c.restoreState()
544
-
545
- def new_page():
546
- nonlocal page_no
547
- c.showPage()
548
- page_no += 1
549
-
550
- def ensure_page_space(y, needed=LINE_HEIGHT):
551
- if y < MARGIN + needed:
552
- new_page()
553
- return PAGE_H - MARGIN
554
- return y
555
-
556
- y = PAGE_H - MARGIN
557
-
558
- # タイトル
559
- c.saveState()
560
- c.setFont(FONT, 18)
561
- c.setFillColorRGB(0, 0, 0)
562
- title = "テキスト比較ツール"
563
- c.drawString((PAGE_W - pdfmetrics.stringWidth(title, FONT, 18)) / 2, y, title)
564
- c.restoreState()
565
- y -= 24
566
-
567
- hline(y)
568
- y -= 10
569
-
570
- # 比較設定
571
- c.saveState()
572
- c.setFont(FONT, 10)
573
- c.setFillColorRGB(0, 0, 0)
574
- c.drawString(MARGIN, y, "■ 比較設定")
575
- c.restoreState()
576
- y -= 14
577
-
578
- ignore_labels = {
579
- 'ignore_alpha': '英字(全半角)無視',
580
- 'ignore_digit': '数字(全半角)無視',
581
- 'ignore_kana': 'カナ(全半角)無視',
582
- 'ignore_yakumono': '約物(全半角)無視',
583
- 'ignore_ligature': 'リガチャ(合字)無視',
584
- 'ignore_kangxi': '康煕部首(CJK部首補助)無視',
585
- 'ignore_brackets_with_content': '括弧付き文字無視',
586
- 'ignore_kanjinum': '漢数字(算用数字)無視',
587
- 'ignore_hyphen': 'ハイフン無視',
588
- 'ignore_kumimoji': '組み文字無視',
589
- 'ignore_space': 'スペース(空白)削除',
590
- 'ignore_newline': '改行削除',
591
- }
592
- checked = [v for k, v in ignore_labels.items() if settings.get(k)]
593
- c.saveState()
594
- c.setFont(FONT, 8)
595
- c.setFillColorRGB(0, 0, 0)
596
- c.drawString(MARGIN, y, "有効な無視設定: " + ("、".join(checked) if checked else "なし"))
597
- c.restoreState()
598
- y -= 12
599
-
600
- hline(y)
601
- y -= 14
602
-
603
- # 結果ヘッダー
604
- c.saveState()
605
- c.setFont(FONT, 14)
606
- c.setFillColorRGB(0.85, 0, 0)
607
- c.drawString(MARGIN, y, "相違が見つかりました" if has_diff else "相違はありません")
608
- c.restoreState()
609
- y -= 20
610
-
611
- # カラムヘッダー
612
- c.saveState()
613
- c.setFont(FONT, 9)
614
- c.setFillColorRGB(0, 0, 0)
615
- c.drawString(COL_L, y, "【比較元】")
616
- c.drawString(COL_R, y, "【比較先】")
617
- c.restoreState()
618
- y -= 14
619
-
620
- # 本文描画
621
- sublines_src = to_sublines(src_orig, COL_W, FONT_SIZE)
622
- sublines_dst = to_sublines(dst_orig, COL_W, FONT_SIZE)
623
- max_lines = max(len(sublines_src), len(sublines_dst))
624
- while len(sublines_src) < max_lines:
625
- sublines_src.append((len(src_orig), ''))
626
- while len(sublines_dst) < max_lines:
627
- sublines_dst.append((len(dst_orig), ''))
628
-
629
- seg_y_top = y
630
- all_hl[page_no] = {'src': [], 'dst': []}
631
-
632
- for (off_s, txt_s), (off_d, txt_d) in zip(sublines_src, sublines_dst):
633
- y = ensure_page_space(y)
634
- if y == PAGE_H - MARGIN:
635
- # ページ送り発生 → 縦区切り線を前ページに描画済み想定
636
- # 新ページの縦線開始位置を更新
637
- seg_y_top = y
638
- all_hl[page_no] = {'src': [], 'dst': []}
639
-
640
- rects_s = draw_subline(c, txt_s, off_s, src_diff_ranges, COL_L, y, FONT_SIZE)
641
- rects_d = draw_subline(c, txt_d, off_d, dst_diff_ranges, COL_R, y, FONT_SIZE)
642
- all_hl[page_no]['src'].extend(rects_s)
643
- all_hl[page_no]['dst'].extend(rects_d)
644
- y -= LINE_HEIGHT
645
-
646
- # 縦区切り線
647
- c.saveState()
648
- c.setStrokeColorRGB(0.5, 0.5, 0.5)
649
- c.setLineWidth(0.5)
650
- c.line(DIV_X, seg_y_top, DIV_X, y + LINE_HEIGHT)
651
- c.restoreState()
652
-
653
- y -= 6
654
-
655
- # 統計
656
- hline(y, lw=0.3)
657
- y -= 10
658
-
659
- src_stats = count_stats(src_orig)
660
- dst_stats = count_stats(dst_orig)
661
-
662
- def draw_stats(x, stats):
663
- nonlocal y
664
- for line in [
665
- f"文字数: {stats['char']}",
666
- f"空白数: {stats['space']} 空白込み文字数: {stats['space_incl']}",
667
- f"改行数: {stats['newline']} 改行込み文字数: {stats['newline_incl']}",
668
- f"単語数: {stats['words']}",
669
- ]:
670
- c.saveState()
671
- c.setFont(FONT, 7.5)
672
- c.setFillColorRGB(0.2, 0.2, 0.2)
673
- c.drawString(x, y, line)
674
- c.restoreState()
675
- y -= 10
676
-
677
- y_stats = y
678
- draw_stats(COL_L, src_stats)
679
- y = y_stats
680
- draw_stats(COL_R, dst_stats)
681
-
682
- c.save()
683
- return buffer.getvalue(), all_hl
684
-
685
- # ----------------------------------------------------------
686
- # PyMuPDFでHighlight Annotation付与
687
- # ----------------------------------------------------------
688
- def add_highlight_annots(pdf_bytes: bytes, all_hl: dict) -> bytes:
689
- doc = fitz.open(stream=pdf_bytes, filetype="pdf")
690
-
691
- for page_no, hl_data in all_hl.items():
692
- if page_no >= len(doc):
693
- continue
694
- page = doc[page_no]
695
- page_h = page.rect.height # PyMuPDF座標系の高さ(上原点)
696
-
697
- rects_all = hl_data.get('src', []) + hl_data.get('dst', [])
698
- for (x0, y0_rl, x1, y1_rl) in rects_all:
699
- # reportlab座標(下原点) → PyMuPDF座標(上原点)変換
700
- # reportlab: y0_rl=下端, y1_rl=上端
701
- # PyMuPDF: y_top = page_h - y1_rl, y_bottom = page_h - y0_rl
702
- mu_y_top = page_h - y1_rl
703
- mu_y_bottom = page_h - y0_rl
704
- rect = fitz.Rect(x0, mu_y_top, x1, mu_y_bottom)
705
- annot = page.add_highlight_annot(rect)
706
- annot.set_colors(stroke=HL_COLOR_MU)
707
- annot.update()
708
-
709
- result = io.BytesIO()
710
- doc.save(result)
711
- doc.close()
712
- return result.getvalue()
713
-
714
- # ----------------------------------------------------------
715
- # 実行
716
- # ----------------------------------------------------------
717
- base_pdf_bytes, all_hl_rects = build_base_pdf()
718
- final_pdf_bytes = add_highlight_annots(base_pdf_bytes, all_hl_rects)
719
-
720
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
721
- tmp.write(final_pdf_bytes)
722
- tmp.close()
723
- return tmp.name
724
-
725
  # ============================================================
726
  # Gradio メイン処理
727
  # ============================================================
 
356
  'words': words,
357
  }
358
 
359
+ undefined
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  # ============================================================
361
  # Gradio メイン処理
362
  # ============================================================