Update app.py
Browse files
app.py
CHANGED
|
@@ -1,50 +1,42 @@
|
|
| 1 |
# verify_highlight_annotation.py
|
| 2 |
-
#
|
| 3 |
-
#
|
| 4 |
-
# 確認: Adobe Acrobat等で開き、ハイライトが「注釈」として認識されるか確認
|
| 5 |
|
| 6 |
import io
|
| 7 |
-
import
|
| 8 |
from reportlab.lib.pagesizes import A4
|
| 9 |
from reportlab.pdfgen import canvas as rl_canvas
|
| 10 |
from reportlab.pdfbase import pdfmetrics
|
| 11 |
from reportlab.pdfbase.ttfonts import TTFont
|
| 12 |
import fitz # PyMuPDF
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
# =====================
|
| 15 |
-
# 設定
|
| 16 |
-
# =====================
|
| 17 |
FONT_NAME = "NotoSansJP"
|
| 18 |
-
FONT_FILE = "NotoSansJP-Regular.ttf"
|
| 19 |
-
OUTPUT_FILE = "output_highlighted.pdf"
|
| 20 |
|
| 21 |
-
# ハイライト色 (RGB 0.0-1.0)
|
| 22 |
COLORS = {
|
| 23 |
-
"color1": (1.0, 0.6, 0.6),
|
| 24 |
-
"color2": (0.6, 0.8, 1.0),
|
| 25 |
-
"mono": (0.5, 0.5, 0.5),
|
| 26 |
}
|
| 27 |
|
| 28 |
# =====================
|
| 29 |
# Step1: reportlabでベースPDF生成
|
| 30 |
# =====================
|
| 31 |
-
def build_base_pdf(
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
pdfmetrics.registerFont(TTFont(FONT_NAME, font_file))
|
| 35 |
-
|
| 36 |
buffer = io.BytesIO()
|
| 37 |
PAGE_W, PAGE_H = A4
|
| 38 |
MARGIN = 20
|
| 39 |
c = rl_canvas.Canvas(buffer, pagesize=A4)
|
| 40 |
c.setFont(FONT_NAME, 12)
|
| 41 |
|
| 42 |
-
# テスト文字列(差分あり想定)
|
| 43 |
lines = [
|
| 44 |
-
"吾輩は猫である。名前はまだない。",
|
| 45 |
-
"吾輩は犬である。名前はもうある。",
|
| 46 |
-
"abcXYZdef",
|
| 47 |
-
"テスト スペース あり",
|
| 48 |
]
|
| 49 |
|
| 50 |
y = PAGE_H - MARGIN - 20
|
|
@@ -55,120 +47,72 @@ def build_base_pdf(font_file: str) -> bytes:
|
|
| 55 |
c.save()
|
| 56 |
return buffer.getvalue()
|
| 57 |
|
| 58 |
-
|
| 59 |
# =====================
|
| 60 |
-
# Step2:
|
| 61 |
# =====================
|
| 62 |
def add_highlight_annotations(pdf_bytes: bytes, color_mode: str = "color1") -> bytes:
|
| 63 |
-
"""
|
| 64 |
-
PyMuPDFを使い、指定テキストにHighlight Annotationを付与する。
|
| 65 |
-
テキスト検索 → quads取得 → add_highlight_annot の流れを検証。
|
| 66 |
-
"""
|
| 67 |
hl_color = COLORS.get(color_mode, COLORS["color1"])
|
| 68 |
-
|
| 69 |
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
highlight_targets = [
|
| 73 |
-
"犬",
|
| 74 |
-
"もうある",
|
| 75 |
-
"XYZ",
|
| 76 |
-
]
|
| 77 |
-
|
| 78 |
-
page = doc[0] # 1ページ目
|
| 79 |
|
| 80 |
for target in highlight_targets:
|
| 81 |
-
# テキスト検索でquadsを取得
|
| 82 |
areas = page.search_for(target, quads=True)
|
| 83 |
if not areas:
|
| 84 |
-
print(f"[WARNING] テキストが見つかりません: '{target}'")
|
| 85 |
continue
|
| 86 |
-
|
| 87 |
for quad in areas:
|
| 88 |
annot = page.add_highlight_annot(quad)
|
| 89 |
annot.set_colors(stroke=hl_color)
|
| 90 |
annot.update()
|
| 91 |
-
print(f"[OK] ハイライト注釈追加: '{target}' at {quad}")
|
| 92 |
|
| 93 |
result = io.BytesIO()
|
| 94 |
doc.save(result)
|
| 95 |
doc.close()
|
| 96 |
return result.getvalue()
|
| 97 |
|
| 98 |
-
|
| 99 |
# =====================
|
| 100 |
-
#
|
| 101 |
# =====================
|
| 102 |
-
def
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
#
|
| 119 |
-
|
| 120 |
-
# Highlight用rect: x0, y0(上端), x1, y1(下端)
|
| 121 |
-
|
| 122 |
-
# 行インデックス1(0始まり)= 「吾輩は犬である...」行
|
| 123 |
-
line_idx = 1
|
| 124 |
-
y_rl = PAGE_H - MARGIN - 20 - LINE_HEIGHT * line_idx # reportlab y(ベースライン)
|
| 125 |
-
y_mupdf_base = PAGE_H - y_rl # PyMuPDF y(上原点換算)
|
| 126 |
-
|
| 127 |
-
# 「犬」は「吾輩は」(3文字)の後 → x方向はフォント幅が必要だが近似で指定
|
| 128 |
-
# 実際はreportlab側でstringWidthを使って正確に計算する
|
| 129 |
-
x0 = MARGIN + 3 * FONT_SIZE * 0.9 # 近似(全角文字)
|
| 130 |
-
x1 = x0 + FONT_SIZE
|
| 131 |
-
|
| 132 |
-
rect = fitz.Rect(x0, y_mupdf_base - FONT_SIZE, x1, y_mupdf_base + 2)
|
| 133 |
-
quad = rect.quad
|
| 134 |
-
|
| 135 |
-
annot = page.add_highlight_annot(quad)
|
| 136 |
-
annot.set_colors(stroke=hl_color)
|
| 137 |
-
annot.update()
|
| 138 |
-
print(f"[OK] 座標指定ハイライト注釈追加: rect={rect}")
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
return result.getvalue()
|
| 144 |
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
# メイン
|
| 148 |
-
# =====================
|
| 149 |
-
if __name__ == "__main__":
|
| 150 |
-
# フォントファイル確認
|
| 151 |
-
if not os.path.isfile(FONT_FILE):
|
| 152 |
-
print(f"[ERROR] フォントファイルが見つかりません: {FONT_FILE}")
|
| 153 |
-
print("カレントディレクトリに NotoSansJP-Regular.ttf を配置してください。")
|
| 154 |
-
exit(1)
|
| 155 |
-
|
| 156 |
-
print("=== Step1: reportlabでベースPDF生成 ===")
|
| 157 |
-
base_pdf = build_base_pdf(FONT_FILE)
|
| 158 |
-
print(f"[OK] ベースPDF生成完了 ({len(base_pdf)} bytes)")
|
| 159 |
-
|
| 160 |
-
print("\n=== Step2: テキスト検索によるHighlight Annotation ===")
|
| 161 |
-
highlighted_pdf = add_highlight_annotations(base_pdf, color_mode="color1")
|
| 162 |
-
print(f"[OK] Annotation付与完了 ({len(highlighted_pdf)} bytes)")
|
| 163 |
-
|
| 164 |
-
print("\n=== Step3: 座標指定によるHighlight Annotation ===")
|
| 165 |
-
highlighted_pdf2 = add_highlight_by_position(highlighted_pdf, color_mode="color1")
|
| 166 |
-
print(f"[OK] 座標指定Annotation付与完了 ({len(highlighted_pdf2)} bytes)")
|
| 167 |
-
|
| 168 |
-
with open(OUTPUT_FILE, "wb") as f:
|
| 169 |
-
f.write(highlighted_pdf2)
|
| 170 |
-
print(f"\n=== 出力完了: {OUTPUT_FILE} ===")
|
| 171 |
-
print("Adobe Acrobat等で開き、以下を確認してください:")
|
| 172 |
-
print(" 1. 「犬」「もうある」「XYZ」がハイライト注釈として表示される")
|
| 173 |
-
print(" 2. 注釈パネルでHighlight Annotationとして認識されている")
|
| 174 |
-
print(" 3. 座標指定のハイライトも正しい位置に表示される")
|
|
|
|
| 1 |
# verify_highlight_annotation.py
|
| 2 |
+
# Hugging Face Spaces上で実行する検証用アプリ
|
| 3 |
+
# フォントファイル NotoSansJP-Regular.ttf を同じフォルダに配置すること
|
|
|
|
| 4 |
|
| 5 |
import io
|
| 6 |
+
import gradio as gr
|
| 7 |
from reportlab.lib.pagesizes import A4
|
| 8 |
from reportlab.pdfgen import canvas as rl_canvas
|
| 9 |
from reportlab.pdfbase import pdfmetrics
|
| 10 |
from reportlab.pdfbase.ttfonts import TTFont
|
| 11 |
import fitz # PyMuPDF
|
| 12 |
+
import tempfile
|
| 13 |
+
import os
|
| 14 |
|
|
|
|
|
|
|
|
|
|
| 15 |
FONT_NAME = "NotoSansJP"
|
| 16 |
+
FONT_FILE = "NotoSansJP-Regular.ttf"
|
|
|
|
| 17 |
|
|
|
|
| 18 |
COLORS = {
|
| 19 |
+
"color1": (1.0, 0.6, 0.6),
|
| 20 |
+
"color2": (0.6, 0.8, 1.0),
|
| 21 |
+
"mono": (0.5, 0.5, 0.5),
|
| 22 |
}
|
| 23 |
|
| 24 |
# =====================
|
| 25 |
# Step1: reportlabでベースPDF生成
|
| 26 |
# =====================
|
| 27 |
+
def build_base_pdf() -> bytes:
|
| 28 |
+
pdfmetrics.registerFont(TTFont(FONT_NAME, FONT_FILE))
|
|
|
|
|
|
|
|
|
|
| 29 |
buffer = io.BytesIO()
|
| 30 |
PAGE_W, PAGE_H = A4
|
| 31 |
MARGIN = 20
|
| 32 |
c = rl_canvas.Canvas(buffer, pagesize=A4)
|
| 33 |
c.setFont(FONT_NAME, 12)
|
| 34 |
|
|
|
|
| 35 |
lines = [
|
| 36 |
+
"吾輩は猫である。名前はまだない。",
|
| 37 |
+
"吾輩は犬である。名前はもうある。",
|
| 38 |
+
"abcXYZdef",
|
| 39 |
+
"テスト スペース あり",
|
| 40 |
]
|
| 41 |
|
| 42 |
y = PAGE_H - MARGIN - 20
|
|
|
|
| 47 |
c.save()
|
| 48 |
return buffer.getvalue()
|
| 49 |
|
|
|
|
| 50 |
# =====================
|
| 51 |
+
# Step2: テキスト検索によるHighlight Annotation
|
| 52 |
# =====================
|
| 53 |
def add_highlight_annotations(pdf_bytes: bytes, color_mode: str = "color1") -> bytes:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
hl_color = COLORS.get(color_mode, COLORS["color1"])
|
|
|
|
| 55 |
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
| 56 |
+
page = doc[0]
|
| 57 |
|
| 58 |
+
highlight_targets = ["犬", "もうある", "XYZ"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
for target in highlight_targets:
|
|
|
|
| 61 |
areas = page.search_for(target, quads=True)
|
| 62 |
if not areas:
|
|
|
|
| 63 |
continue
|
|
|
|
| 64 |
for quad in areas:
|
| 65 |
annot = page.add_highlight_annot(quad)
|
| 66 |
annot.set_colors(stroke=hl_color)
|
| 67 |
annot.update()
|
|
|
|
| 68 |
|
| 69 |
result = io.BytesIO()
|
| 70 |
doc.save(result)
|
| 71 |
doc.close()
|
| 72 |
return result.getvalue()
|
| 73 |
|
|
|
|
| 74 |
# =====================
|
| 75 |
+
# メイン処理
|
| 76 |
# =====================
|
| 77 |
+
def run_verify():
|
| 78 |
+
log = []
|
| 79 |
+
|
| 80 |
+
try:
|
| 81 |
+
# Step1
|
| 82 |
+
base_pdf = build_base_pdf()
|
| 83 |
+
log.append(f"[OK] Step1: ベースPDF生成完了 ({len(base_pdf)} bytes)")
|
| 84 |
+
except Exception as e:
|
| 85 |
+
return f"[ERROR] Step1失敗: {e}", None
|
| 86 |
+
|
| 87 |
+
try:
|
| 88 |
+
# Step2
|
| 89 |
+
highlighted_pdf = add_highlight_annotations(base_pdf, color_mode="color1")
|
| 90 |
+
log.append(f"[OK] Step2: Highlight Annotation付与完了 ({len(highlighted_pdf)} bytes)")
|
| 91 |
+
except Exception as e:
|
| 92 |
+
return "\n".join(log) + f"\n[ERROR] Step2失敗: {e}", None
|
| 93 |
+
|
| 94 |
+
try:
|
| 95 |
+
# 一時ファイルに保存してGradioのFileで返す
|
| 96 |
+
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")
|
| 97 |
+
tmp.write(highlighted_pdf)
|
| 98 |
+
tmp.close()
|
| 99 |
+
log.append(f"[OK] PDF出力完了: {tmp.name}")
|
| 100 |
+
except Exception as e:
|
| 101 |
+
return "\n".join(log) + f"\n[ERROR] ファイル出力失敗: {e}", None
|
| 102 |
+
|
| 103 |
+
return "\n".join(log), tmp.name
|
| 104 |
|
| 105 |
+
# =====================
|
| 106 |
+
# Gradio UI
|
| 107 |
+
# =====================
|
| 108 |
+
with gr.Blocks(title="Highlight Annotation 検証") as demo:
|
| 109 |
+
gr.Markdown("## Highlight Annotation 検証ツール")
|
| 110 |
+
gr.Markdown("ボタンを押すとPDFを生成します。ダウンロードして注釈が認識されるか確認してください。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
+
run_btn = gr.Button("検証実行", variant="primary")
|
| 113 |
+
log_out = gr.Textbox(label="実行ログ", lines=8)
|
| 114 |
+
pdf_out = gr.File(label="PDFダウンロード")
|
|
|
|
| 115 |
|
| 116 |
+
run_btn.click(fn=run_verify, inputs=[], outputs=[log_out, pdf_out])
|
| 117 |
|
| 118 |
+
demo.launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|