Files
jsahn 4e943700fc feat: add common utilities and feedback handling scripts
- Introduced common.js for shared functions including API calls, error reporting, and UI helpers.
- Implemented feedback.js for the feedback reporting page, including form handling and displaying feedback status.
- Added layout.js for managing galaxy and section layouts in the visualization.
- Created login.js for user authentication with session management.
- Developed search.js for term searching based on user input and synonyms.
- Implemented signup.js for user registration with validation and immediate login.
2026-10-06 23:14:45 +09:00

151 lines
6.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""찾아보기(색인) 페이지에서 용어와 인쇄 페이지 번호를 추출."""
import re
import pymupdf
from .pdfutil import Line, page_lines
PAGES_ONLY = re.compile(r"^\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*$")
ENTRY = re.compile(r"^(.+?)\s*[\s_]\s*(\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*)$")
XREF = re.compile(r"^(.+?)\s*(?:→|☞|⇒|=>|참조|see)\s*(.+)$")
HEADINGS = {"찾아보기", "한글 찾아보기", "영문 찾아보기", "인물 찾아보기", "기타", "INDEX", "Index"}
JAMO = set("ㄱㄴㄷㄹㅁㅂㅅㅇㅈㅊㅋㅌㅍㅎㄲㄸㅃㅆㅉ가나다라마바사아자차카타파하")
INDEX_PAT = re.compile(r"^(.+?)\s+(\d{1,3}(?:\s*[,~\-–]\s*\d{1,3})*)$")
def find_index_pages(doc: pymupdf.Document) -> list[int]:
"""책 끝 60쪽 가운데 대부분의 줄이 '용어 페이지' 꼴인 쪽."""
pages = []
for i in range(max(0, len(doc) - 60), len(doc)):
lines = page_lines(doc, i)
merged = _merge_numbers(lines)
if len(merged) < 15:
continue
hits = sum(bool(INDEX_PAT.match(l.text)) for l in merged)
if hits / len(merged) > 0.6:
pages.append(i)
# 연속 구간만 사용 (본문 중간의 표 등 배제)
if not pages:
return []
run = [pages[-1]]
for p in reversed(pages[:-1]):
if run[0] - p <= 2:
run.insert(0, p)
else:
break
return run
def _merge_numbers(lines: list[Line]) -> list[Line]:
"""'용어'와 '페이지 번호'가 별개 줄로 나뉜 경우(같은 높이) 하나로 합침."""
out: list[Line] = []
used = set()
nums = [k for k, l in enumerate(lines) if PAGES_ONLY.match(l.text)]
for k in nums:
L = lines[k]
best = None
for j, T in enumerate(lines):
if j == k or j in nums or abs(T.y - L.y) > 3.5:
continue
gap = L.x - T.x1
if -2 <= gap < 160 and (best is None or gap < best[0]):
best = (gap, j)
if best is not None:
used.add(k)
j = best[1]
T = lines[j]
lines[j] = Line(T.page, T.x, T.y, L.x1, T.size, T.font, f"{T.text} {L.text}")
for k, l in enumerate(lines):
if k not in used:
out.append(l)
return out
def _pages(s: str) -> list[int]:
out = []
for part in re.split(r"\s*,\s*", s):
m = re.match(r"(\d+)(?:\s*[~\-–]\s*(\d+))?", part)
if m:
out.append(int(m.group(1)))
return out
def _columns(lines: list[Line]) -> list[list[Line]]:
"""x 시작점으로 단을 나눈 뒤 각 단을 위→아래로 정렬."""
xs = sorted({round(l.x) for l in lines})
cols: list[list[Line]] = []
starts: list[float] = []
for l in sorted(lines, key=lambda l: l.x):
for ci, s in enumerate(starts):
if abs(l.x - s) < 40:
cols[ci].append(l)
break
else:
starts.append(l.x)
cols.append([l])
order = sorted(range(len(cols)), key=lambda c: starts[c])
return [sorted(cols[c], key=lambda l: l.y) for c in order]
def extract_index(doc: pymupdf.Document, book: dict) -> list[dict]:
pages = list(book["index_pages"]) if book.get("index_pages") else find_index_pages(doc)
entries: list[dict] = []
section = "ko"
for pi in pages:
h = doc[pi].rect.height
lines = [l for l in page_lines(doc, pi) if 40 < l.y < h - 40 and l.size < 20]
lines = _merge_numbers(lines)
for col in _columns(lines):
pending: Line | None = None
for l in col:
t = l.text.strip()
if t in HEADINGS or t.endswith("찾아보기"):
if "인물" in t:
section = "person"
elif "영문" in t:
section = "en"
elif "한글" in t:
section = "ko"
pending = None
continue
if len(t) == 1 and (t in JAMO or t.isascii() and t.isalpha()):
if section != "person":
section = "en" if t.isascii() else "ko"
pending = None
continue
if t == "●" or t.isdigit() and int(t) > 400:
continue
m = ENTRY.match(t)
if not m:
x = XREF.match(t)
if x:
entries.append({"term": x.group(1).strip(), "see": x.group(2).strip(),
"pages": [], "section": section, "index_page": pi})
pending = None
continue
# 다음 줄로 이어지는 긴 용어
if pending is not None and abs(l.y - pending.y) < 20:
pending = Line(l.page, pending.x, l.y, l.x1, l.size, l.font, pending.text + " " + t)
else:
pending = l
continue
term = m.group(1).strip()
if pending is not None and abs(l.y - pending.y) < 20:
term = pending.text + " " + term
pending = None
term = re.sub(r"\s*●\s*", "", term).strip(" ,")
if not term or term.isdigit():
continue
# 영문 색인에서 두 항목이 한 줄로 붙은 경우: "a 20, 101 b" → a / b
split = re.match(r"^(.+?) (\d{1,3}(?:, ?\d{1,3})*) ([a-z].+)$", term) if section == "en" else None
if split:
entries.append({"term": split.group(1), "pages": _pages(split.group(2)),
"section": section, "index_page": pi})
term = split.group(3)
sec = section
if sec == "person" and not re.search(r"[가-힣]", term):
sec = "en" # 인물 찾아보기 뒤에 이어진 영문 색인
entries.append({"term": term, "pages": _pages(m.group(2)), "section": sec,
"index_page": pi})
return entries