Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions fastcdm/core.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from fastcdm.render.render_worker import RenderWorker
from fastcdm.render.render_worker import RenderResult, RenderWorker
from fastcdm.matcher import update_inliers, HungarianMatcher, SimpleAffineTransform
from fastcdm.clean import (
clean,
Expand Down Expand Up @@ -291,17 +291,23 @@ def render(self, latex_list: list) -> list:
latex_strings = [
f"$${s}$$" if not s.startswith("$$") else s for s in latex_list
]
imgs = self.render_worker.render(latex_strings)
results = self.render_worker.render(latex_strings)
except Exception as e:
print("Rendering failed:")
print("=" * 30)
print(traceback.format_exc())
return []

assert len(imgs) == len(
assert len(results) == len(
latex_strings
), "Number of rendered images must match number of input strings"
return imgs
return [result.image for result in results]

def render_results(self, latex_list: list) -> List[RenderResult]:
latex_strings = [
f"$${s}$$" if not s.startswith("$$") else s for s in latex_list
]
return self.render_worker.render(latex_strings)

def compute(self, gt: str, pred: str, visualize: bool = False) -> tuple:
"""
Expand Down
23 changes: 17 additions & 6 deletions fastcdm/render/render_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import cv2
import random
import numpy as np
from typing import List
from dataclasses import dataclass
from typing import List, Optional

from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
Expand All @@ -13,6 +14,16 @@
from webdriver_manager.chrome import ChromeDriverManager


@dataclass
class RenderResult:
image: Optional[np.ndarray]
error: bool
error_text: Optional[str]
width: int
height: int
error_type: Optional[str] = None


class RenderWorker:
"""
一个使用 Selenium Headless Chrome 渲染HTML内容的工具类。
Expand Down Expand Up @@ -76,7 +87,7 @@ def __init__(self, template_file: str, timeout: int = 15, driver_path: str = Non
EC.presence_of_all_elements_located((By.ID, "container"))
)

def render(self, contents: List[str]) -> List[np.ndarray]:
def render(self, contents: List[str]) -> List[RenderResult]:
"""
渲染一组内容并返回每个元素的截图。
"""
Expand Down Expand Up @@ -113,13 +124,13 @@ def render(self, contents: List[str]) -> List[np.ndarray]:

# 获取每个渲染元素的边界框
rects = self.get_rects()
cropped_imgs = []
results = []
img_h, img_w = fullpage_img.shape[:2]

# 根据边界框裁剪出每个元素的图像
for rect in rects:
if rect is None:
cropped_imgs.append(None)
results.append(RenderResult(None, True, "Invalid capture rectangle", 0, 0, "invalid_capture"))
else:
x, y, w, h = rect
# 计算一个小的随机边距,让截图更自然
Expand All @@ -132,9 +143,9 @@ def render(self, contents: List[str]) -> List[np.ndarray]:
y2 = min(img_h, y + h + border_size)

cropped = fullpage_img[y1:y2, x1:x2]
cropped_imgs.append(cropped)
results.append(RenderResult(cropped, cropped.size == 0, "Empty cropped image" if cropped.size == 0 else None, w, h, "empty_image" if cropped.size == 0 else None))

return cropped_imgs
return results

def get_rects(self) -> list:
"""
Expand Down
50 changes: 40 additions & 10 deletions fastcdm/tokenize.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import subprocess
from pathlib import Path
from typing import Tuple
import sys


class CDMPreprocessingError(RuntimeError):
"""Raised when formula normalization fails or produces unusable output."""


IMPLICIT_MULTIPLICATION_TARGETS = [
Expand Down Expand Up @@ -124,6 +127,7 @@
"partial",
"nabla",
"int",
"limits",
"oint",
"sum",
"prod",
Expand Down Expand Up @@ -198,9 +202,12 @@
PATTERN_LATEX_CMD_CONCAT_CMD = re.compile(
r"\\(" + TARGETS_PATTERN + r")" + r"(\\[a-zA-Z])"
)
PATTERN_LATEX_CMD_CONCAT_TEXT = re.compile(r"\\(" + TARGETS_PATTERN + r")([a-zA-Z])")
PATTERN_LATEX_CMD_CONCAT_TEXT = re.compile(
r"\\(?!(?:" + TARGETS_PATTERN + r")(?![a-zA-Z]))"
r"(" + TARGETS_PATTERN + r")([a-zA-Z])"
)
PATTERN_NON_CMD_IMPLICIT_MULT = re.compile(
r"\b(" + TARGETS_PATTERN + r")([a-zA-Z][a-zA-Z0-9]*)\b"
r"(?<!\\)\b(" + TARGETS_PATTERN + r")([a-zA-Z][a-zA-Z0-9]*)\b"
)

OPERATORS = "\s?".join(
Expand Down Expand Up @@ -247,7 +254,27 @@



def tokenize(latex_code: str) -> Tuple[bool, str]:
def _validate_normalized(source: str, normalized: str) -> None:
if source.strip() and not normalized.strip():
raise CDMPreprocessingError("Tokenizer returned empty output for non-empty input")
if "[PROCESSING FAILED]" in normalized:
raise CDMPreprocessingError("Tokenizer returned a processing failure marker")
environments = []
for match in re.finditer(r"\\(begin|end)\s*\{\s*([^{}]*?)\s*\}", normalized):
# A backslash escaped by another backslash is not a command start.
preceding = len(normalized[:match.start()]) - len(normalized[:match.start()].rstrip("\\"))
if preceding % 2:
continue
command, environment = match.groups()
if command == "begin":
environments.append(environment)
elif not environments or environments.pop() != environment:
raise CDMPreprocessingError("Tokenizer returned mismatched begin/end environments")
if environments:
raise CDMPreprocessingError("Tokenizer returned mismatched begin/end environments")


def tokenize(latex_code: str, timeout: float = 30.0) -> Tuple[bool, str]:

if not latex_code:
return True, ""
Expand Down Expand Up @@ -280,13 +307,14 @@ def tokenize(latex_code: str) -> Tuple[bool, str]:
text=True,
check=True,
encoding="utf-8",
timeout=timeout,
)
normalized_latex = proc.stdout
except (subprocess.CalledProcessError, FileNotFoundError) as e:
print(f"执行 Node.js 脚本(公式)时出错:{e}", file=sys.stderr)
if hasattr(e, "stderr"):
print(f"Node.js stderr:{e.stderr}", file=sys.stderr)
return False, latex_code
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
detail = getattr(e, "stderr", None) or str(e)
raise CDMPreprocessingError(f"Node.js tokenizer failed: {detail}") from e

_validate_normalized(latex_code, normalized_latex)

names = [
"\\" + x.replace(" ", "")
Expand All @@ -295,4 +323,6 @@ def tokenize(latex_code: str) -> Tuple[bool, str]:
post = PATTERN_OPERATOR_NAME.sub(
lambda match: str(names.pop(0)), normalized_latex
).replace(r"\\ \end{array}", r"\end{array}")
return True, post.strip()
post = post.strip()
_validate_normalized(latex_code, post)
return True, post
8 changes: 6 additions & 2 deletions fastcdm/tokenize_latex/preprocess_formula.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ rl.on('line', function(line){
console.error(line);
console.error(norm_str);
console.error(e);
console.log();
process.exitCode = 1;
}
global_str = ""
norm_str = ""
Expand Down Expand Up @@ -256,7 +256,7 @@ groupTypes.spacing = function(group) {
groupTypes.op = function(group) {
var node;

if (group.value.symbol) {
if (group.value.symbol || group.value.alwaysHandleSupSub) {
// 直接输出符号
norm_str = norm_str + group.value.body + " ";

Expand All @@ -271,6 +271,10 @@ groupTypes.op = function(group) {
}
norm_str = norm_str + "} ";
}
// The parser sets this flag only for an explicit limit control.
if (group.value.alwaysHandleSupSub) {
norm_str += group.value.limits ? "\\limits " : "\\nolimits ";
}
};

groupTypes.katex = function(group) {
Expand Down