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.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""찾아보기(색인) 페이지에서 용어와 인쇄 페이지 번호를 추출."""
|
||||
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
|
||||
Reference in New Issue
Block a user