init: WordFileParse docx→HTML 解析服务(含 Word EQ 域公式修复)

服务:Spring Boot 3.2 + docx4j 8.3.9,POST /api/v3/server_office_file/file/parse
      接受 .docx,返回 HTML(公式 → LaTeX,图片 → RustFS 外链)

公式支持三类来源:
- OMML(Word 2007+ 公式)      → Python omml_mathml2latex
- MathType OLE 对象            → Ruby mathtype2mathml → MathML → LaTeX
- Word EQ 域(老教辅主力)      → EqToLatexUtil(本次新增)

本次改动:
1. 新增 java-svc/.../core/EqToLatexUtil.java:
   移植自 jby backend-services office-core,EQ 语法 → LaTeX
   (\f→\dfrac、\r→\sqrt、\i→\int/\sum/\prod、\s→^{}/_{}、\b→\left\right、
     \a→array、\x→\boxed/\overline、\o\al 堆叠、圈码、化学符号、极限、连等式)
   品牌标记 JBYU* → WPU*,与采集端一致
2. CtoMathFinder:接上 EQ 分支(原为 not-ported 桩),域代码经
   EqToLatexUtil.convert 转 LaTeX 写入 eqLatexMap(WPEQNUM 占位符)
3. WordParseUtil.dealPlaceholder:EQ 的 LaTeX 同样加 \( \) 包裹
   (原先因 WPEQNUM 例外不加包裹,前端/MathJax 不渲染)

修前症状:教辅 Word 里分式/方程组/上下标整段丢失(如 {−1/3, 1} 变成 {-,1})
修后实测:46 份教辅 2908 处 EQ 域全部还原,公式总数 2954 处、零未包裹残留

安全:application.yml(含 RustFS 密钥)不入库,提供 application.yml.example;
      OssService 的 @Value 默认密钥已清空
This commit is contained in:
2026-09-29 09:43:13 +08:00
commit 3271fee5cc
360 changed files with 86082 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
# Node.js
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Ruby
.bundle
vendor/bundle
# 开发工具
.git
.gitignore
*.md
!README.md
!SETUP_GUIDE.md
# 临时文件和缓存
uploads/*
!uploads/.gitkeep
*.tmp
*.temp
.DS_Store
Thumbs.db
# 日志文件
*.log
logs
# 编辑器文件
.vscode
.idea
*.swp
*.swo
*~
# 测试文件
test_*
spec/
.rspec
# 环境文件
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# 构建文件
dist/
build/
# 操作系统文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@@ -0,0 +1,116 @@
# 🐳 MathType to MathML Docker 化完成
## 📁 新增的 Docker 文件
| 文件名 | 用途 | 描述 |
|--------|------|------|
| `Dockerfile` | 容器构建配置 | 定义如何构建包含 Ruby + Node.js 的容器镜像 |
| `docker-compose.yml` | 服务编排 | 简化容器启动、端口映射和卷挂载 |
| `.dockerignore` | 构建排除 | 指定哪些文件不需要复制到容器中 |
| `entrypoint.sh` | 启动脚本 | 容器启动时的环境检查和初始化 |
| `docker-build.sh` | 构建脚本 | 自动化构建和启动流程 |
| `README.Docker.md` | 详细文档 | 完整的 Docker 部署和使用指南 |
| `uploads/.gitkeep` | 目录占位 | 确保上传目录存在 |
## 🚀 快速启动(3种方式)
### 方式1:一键脚本(最简单)
```bash
./docker-build.sh
```
### 方式2:Docker Compose(推荐)
```bash
docker-compose up -d
```
### 方式3:原生 Docker
```bash
docker build -t mathtype-converter .
docker run -d --name mathtype-to-mathml -p 3000:3000 -v $(pwd)/uploads:/app/uploads mathtype-converter
```
## 🌐 访问服务
启动后通过以下地址访问:
- **Web 界面**: http://localhost:3000/mathtype_converter_demo.html
- **健康检查**: http://localhost:3000/health
- **API 转换**: `POST http://localhost:3000/convert-mathtype`
## 📋 常用管理命令
```bash
# 查看运行状态
docker-compose ps
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
# 重启服务
docker-compose restart
# 重建镜像
docker-compose build --no-cache
```
## 🔧 技术栈
- **基础镜像**: Ruby 3.1 (Debian Bullseye)
- **运行时**: Ruby + Node.js LTS
- **端口**: 3000
- **依赖**: mathtype_to_mathml gem + Express.js
- **持久化**: uploads 目录挂载
## 📊 资源使用
- **镜像大小**: ~500-800MB
- **运行内存**: ~100-200MB
- **文件上传限制**: 10MB
- **并发处理**: 支持多文件并发转换
## 🚀 部署到生产环境
1. **修改端口映射**:编辑 `docker-compose.yml` 中的端口配置
2. **添加反向代理**:使用 Nginx/Apache 代理到容器端口
3. **配置域名**:更新 Traefik 标签或反向代理配置
4. **监控日志**:设置日志轮转和监控告警
5. **备份数据**:定期备份 uploads 目录
## 🐛 故障排除
### 常见问题
1. **端口冲突**: 修改 `docker-compose.yml` 中的端口映射
2. **Ruby Gem 错误**: 重建镜像 `docker-compose build --no-cache`
3. **文件权限**: 检查 uploads 目录权限
4. **内存不足**: 增加 Docker 内存限制
### 调试命令
```bash
# 进入容器调试
docker-compose exec mathtype-converter bash
# 查看详细日志
docker-compose logs --tail=100 mathtype-converter
# 检查容器内文件
docker exec mathtype-to-mathml ls -la /app
```
## ✅ 验证部署
部署完成后,验证以下功能:
1. ✅ 健康检查响应正常
2. ✅ Web 界面可访问
3. ✅ 文件上传功能正常
4. ✅ MathML 转换功能正常
5. ✅ 容器重启后服务恢复
---
🎉 **Docker 化完成!** 现在您可以轻松地将此服务部署到任何支持 Docker 的平台上。

View File

@@ -0,0 +1,71 @@
# 使用官方 Ruby 镜像作为基础镜像
FROM ruby:3.1-bullseye
# 设置镜像源(提高国内网络访问速度)
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
# 安装系统依赖
RUN apt-get update && apt-get install -y \
curl \
gnupg \
build-essential \
libxml2-dev \
libxslt1-dev \
pkg-config \
wget \
&& rm -rf /var/lib/apt/lists/*
# 安装 Node.js 16 (LTS) - 使用官方二进制包,增加重试机制
RUN for i in 1 2 3; do \
curl -fsSL --connect-timeout 10 --max-time 300 \
https://nodejs.org/dist/v16.20.2/node-v16.20.2-linux-x64.tar.xz -o node.tar.xz \
&& break || sleep 5; \
done \
&& tar -xJf node.tar.xz -C /usr/local --strip-components=1 \
&& rm node.tar.xz \
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs
# 设置工作目录
WORKDIR /app
# 复制 vendor 目录(包含本地gems)
COPY vendor ./vendor
# 复制 Gemfile 和 package.json 先安装依赖(利用 Docker 缓存)
COPY Gemfile Gemfile.lock ./
COPY package.json package-lock.json ./
# 配置npm使用国内镜像源(提高网络访问速度)
RUN npm config set registry https://registry.npmmirror.com/ \
&& npm config set timeout 60000 \
&& npm config set network-timeout 300000
# 配置bundle使用国内gem源
RUN bundle config mirror.https://rubygems.org https://gems.ruby-china.com
# 安装 Ruby 依赖(不使用 deployment 模式避免版本冲突)
RUN bundle config set --local without 'development test' \
&& bundle install --retry=3
# 安装 Node.js 依赖(使用重试机制)
RUN npm install --production --retry=3 || \
(npm config set registry https://registry.npmjs.org/ && npm install --production --retry=3)
# 复制项目文件
COPY . .
# 创建必要的目录
RUN mkdir -p uploads public
# 设置文件权限
RUN chmod +x entrypoint.sh
# 暴露端口
EXPOSE 3000
# 设置启动脚本
ENTRYPOINT ["./entrypoint.sh"]
# 默认命令
CMD ["npm", "start"]

View File

@@ -0,0 +1,21 @@
source 'https://rubygems.org'
# Use local, vendored gems
gem 'mathtype_to_mathml', path: 'vendor/gems/mathtype_to_mathml'
gem 'mathtype', path: 'vendor/gems/mathtype'
# Required dependency for XML processing
gem 'nokogiri', '~> 1.13'
# Required dependency for mathtype_to_mathml gem
gem 'pry'
# Concurrency support for batch processing
gem 'concurrent-ruby', '~> 1.1'
# Multi-process parallel processing
gem 'parallel'
# Optional: Add these for better development experience
gem 'rake', '~> 13.0'
gem 'rspec', '~> 3.0' if ENV['RAILS_ENV'] == 'test'

View File

@@ -0,0 +1,67 @@
PATH
remote: vendor/gems/mathtype_to_mathml
specs:
mathtype_to_mathml (0.0.7)
mathtype (~> 0.1.0)
nokogiri (~> 1.6)
PATH
remote: vendor/gems/mathtype
specs:
mathtype (0.1.0)
bindata (~> 2.1)
nokogiri (~> 1.6)
ruby-ole (~> 1.2)
GEM
remote: https://rubygems.org/
specs:
bindata (2.5.1)
coderay (1.1.3)
concurrent-ruby (1.3.7)
io-console (0.8.2)
method_source (1.1.0)
nokogiri (1.19.4-x64-mingw-ucrt)
racc (~> 1.4)
parallel (2.1.0)
pry (0.16.0)
coderay (~> 1.1)
method_source (~> 1.0)
reline (>= 0.6.0)
racc (1.8.1)
rake (13.4.2)
reline (0.6.3)
io-console (~> 0.5)
ruby-ole (1.2.13.1)
PLATFORMS
x64-mingw-ucrt
DEPENDENCIES
concurrent-ruby (~> 1.1)
mathtype!
mathtype_to_mathml!
nokogiri (~> 1.13)
parallel
pry
rake (~> 13.0)
CHECKSUMS
bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58
bundler (4.0.15) sha256=a4ceb882fe94a0e0ac63cd0813932bbfd631a14e5ac0b7975189b19a4d28d9e7
coderay (1.1.3) sha256=dc530018a4684512f8f38143cd2a096c9f02a1fc2459edcfe534787a7fc77d4b
concurrent-ruby (1.3.7) sha256=4412caec3a5ea2e5fdc52076724c071a81f2c0593d83b2ac8cbb8ca63b3151b0
io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
mathtype (0.1.0)
mathtype_to_mathml (0.0.7)
method_source (1.1.0) sha256=181301c9c45b731b4769bc81e8860e72f9161ad7d66dd99103c9ab84f560f5c5
nokogiri (1.19.4-x64-mingw-ucrt) sha256=051da97b8eccfdb5444fed40246a35e10d7298b9efe759b4cd25455ea04c587e
parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
pry (0.16.0) sha256=d76c69065698ed1f85e717bd33d7942c38a50868f6b0673c636192b3d1b6054e
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
ruby-ole (1.2.13.1) sha256=578d10dd2a797a2b35a1286c6fb2c9525f67c24791346fc8015d39f0ffa3cb72
BUNDLED WITH
4.0.15

View File

@@ -0,0 +1,182 @@
# MathType to MathML Docker 部署指南
这个 Docker 化版本让您可以轻松在任何支持 Docker 的平台上运行 MathType 到 MathML 转换服务。
## 🚀 快速开始
### 方法一:使用 Docker Compose(推荐)
```bash
# 构建并启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
### 方法二:直接使用 Docker
```bash
# 构建镜像
docker build -t mathtype-converter .
# 运行容器
docker run -d \
--name mathtype-to-mathml \
-p 3000:3000 \
-v $(pwd)/uploads:/app/uploads \
mathtype-converter
# 查看日志
docker logs -f mathtype-to-mathml
# 停止容器
docker stop mathtype-to-mathml
docker rm mathtype-to-mathml
```
## 📱 使用服务
服务启动后,可以通过以下方式使用:
### Web 界面
打开浏览器访问:`http://localhost:3000/mathtype_converter_demo.html`
### API 接口
#### 健康检查
```bash
curl http://localhost:3000/health
```
#### 转换 MathType 文件
```bash
curl -X POST \
-F "file=@your_mathtype_file.bin" \
http://localhost:3000/convert-mathtype
```
## 🔧 配置说明
### 环境变量
- `NODE_ENV`: 运行环境(默认:production)
- `PORT`: 服务端口(默认:3000)
### 数据卷
- `/app/uploads`: 上传文件存储目录
- `/app/config`: 配置文件目录(可选)
### 端口映射
- `3000`: Web 服务端口
## 📁 目录结构
```
/app/
├── server_converter.js # Node.js 主服务器
├── server/
│ ├── converter.rb # Ruby 转换脚本
│ └── setup.js # 设置脚本
├── public/ # 静态文件
├── uploads/ # 上传文件目录
├── vendor/ # Ruby gems
└── Gemfile # Ruby 依赖
```
## 🐛 故障排除
### 查看容器日志
```bash
# Docker Compose
docker-compose logs mathtype-converter
# 直接 Docker
docker logs mathtype-to-mathml
```
### 进入容器调试
```bash
# Docker Compose
docker-compose exec mathtype-converter bash
# 直接 Docker
docker exec -it mathtype-to-mathml bash
```
### 重建镜像
```bash
# 清理并重建
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
## 🚀 部署到生产环境
### 修改 docker-compose.yml
```yaml
services:
mathtype-converter:
# ... 其他配置
environment:
- NODE_ENV=production
restart: always
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 256M
```
### 使用反向代理(Nginx 示例)
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 文件上传大小限制
client_max_body_size 10M;
}
```
## 📊 监控和维护
### 健康检查
服务内置健康检查端点:`/health`
### 日志管理
```bash
# 设置日志轮转
docker-compose up -d --log-opt max-size=10m --log-opt max-file=3
```
### 备份上传文件
```bash
# 备份上传目录
docker run --rm -v mathtype_uploads:/data -v $(pwd):/backup alpine tar czf /backup/uploads-backup.tar.gz -C /data .
```
## 🔄 更新服务
```bash
# 停止服务
docker-compose down
# 拉取最新代码(如果有)
git pull
# 重建并启动
docker-compose up -d --build
```

View File

@@ -0,0 +1,115 @@
# MathType to MathML Converter
一个基于 Ruby gem 的 MathType 二进制文件转 MathML 的 Node.js 服务器。
## 功能特点
- 支持上传 MathType 二进制文件 (.bin)
- 转换为标准 MathML 格式
- RESTful API 接口
- Web 界面演示
- Docker 容器化部署
- 网络优化和重试机制
- 支持开发和生产环境配置
## 快速开始
### 方式一:Docker 部署(推荐)
#### Windows 环境
```powershell
# 开发环境构建并启动
.\docker-build.ps1
# 生产环境构建并启动
.\docker-build.ps1 -Production
# 查看帮助
.\docker-build.ps1 -Help
```
#### Linux/macOS 环境
```bash
# 增强构建脚本(推荐)
./docker-build-enhanced.sh
# 标准构建脚本
./docker-build.sh
# 或使用 docker-compose
docker-compose up -d
# 生产环境
docker-compose -f docker-compose.production.yml up -d
```
### 方式二:本地部署
1. 确保安装了 Ruby 和 Node.js
2. 安装依赖:
```bash
# 安装 Ruby 依赖
bundle install
# 安装 Node.js 依赖
npm install
```
3. 启动服务:
```bash
npm start
```
## 配置说明
### 环境配置
- **开发环境**: 使用 `docker-compose.yml`,包含代码热重载
- **生产环境**: 使用 `docker-compose.production.yml`,优化性能和安全性
### 配置文件
- `config/app.json`: 应用基础配置
- `uploads/`: 文件上传目录
- `vendor/gems/`: 本地 Ruby gems
## API 使用
### 转换 MathType 文件
```bash
curl -X POST http://localhost:3000/convert-mathtype \
-F "file=@path/to/mathtype.bin"
```
### 健康检查
```bash
curl http://localhost:3000/health
```
## Web 界面
启动服务后访问:http://localhost:3000/mathtype_converter_demo.html
## 故障排除
### 网络问题
如果遇到 Docker Hub 连接问题:
1. 使用增强构建脚本:`./docker-build-enhanced.sh` 或 `.\docker-build.ps1`
2. 脚本会自动检测网络状况并使用国内镜像源
### 常见问题
- **端口冲突**: 修改 docker-compose.yml 中的端口映射
- **构建失败**: 清理 Docker 缓存 `docker system prune`
- **权限问题**: 确保 uploads 目录权限正确
## 技术栈
- **后端**: Node.js + Express
- **Ruby Gem**: mathtype_to_mathml
- **容器化**: Docker + Docker Compose
- **前端**: 原生 HTML/JavaScript
- **网络优化**: 国内镜像源支持

View File

@@ -0,0 +1,217 @@
# 🔧 Setup Guide: Converting Real MathType .bin Files
## Why You Need This Setup
The JavaScript client-side converter only shows a sample equation (x²) because **parsing MathType's proprietary binary format requires the Ruby gem**. To convert real `.bin` files, you need the server setup.
## 📋 Prerequisites
### 1. Install Ruby and Gems
```bash
# On Windows (using RubyInstaller)
# Download from: https://rubyinstaller.org/
# On macOS (using Homebrew)
brew install ruby
# On Ubuntu/Debian
sudo apt-get install ruby-full
# Install bundler and the required gems from the project root
gem install bundler
bundle install
```
### 2. Install Node.js
```bash
# Download from: https://nodejs.org/
# Or using package managers:
# macOS
brew install node
# Ubuntu/Debian
sudo apt-get install nodejs npm
# Windows
# Download installer from nodejs.org
```
## 🚀 Quick Setup
### Step 1: Install Server Dependencies
```bash
# In your project's server directory
cd server
npm install
cd ..
```
### Step 2: Start the Server
```bash
# From the project root
npm start
```
You should see:
```
MathType converter server running at http://localhost:3000
Make sure you have the mathtype_to_mathml Ruby gem installed.
```
### Step 3: Open the Demo
1. Open `http://localhost:3000/index.html`
2. Upload a real MathType `.bin` file
3. Click "Convert Real File (Server Required)"
## 📁 Finding MathType .bin Files
MathType equations in Word documents are typically stored as:
- `oleObject1.bin`, `oleObject2.bin`, etc.
- In the `word/embeddings/` folder when you unzip a `.docx` file
### Extract from Word Document:
```bash
# Rename .docx to .zip and extract
cp document.docx document.zip
unzip document.zip
ls word/embeddings/ # Look for .bin files
```
## 🧪 Testing the Setup
### Test 1: Server Health Check
```bash
curl http://localhost:3000/health
# Should return: {"status":"ok","message":"MathType converter server is running"}
```
### Test 2: Convert a File via API
```bash
# Make sure to be in the directory with oleObject1.bin
curl -X POST -F "file=@oleObject1.bin" http://localhost:3000/convert-mathtype
# Should return JSON with MathML
```
### Test 3: Web Interface
1. Open browser to `http://localhost:3000/index.html`
2. Upload a `.bin` file
3. Click "Convert Real File"
4. Should see actual MathML output (not just x²)
## 🐛 Troubleshooting
### Error: "Ruby conversion failed"
```bash
# Check if Ruby gems are installed via bundle from the project root
bundle check
# Reinstall if missing
bundle install
```
### Error: "Failed to spawn Ruby process"
```bash
# Check if Ruby is in PATH
ruby --version
# The script now runs from the project root, so bundle should find the correct ruby executable.
# If you still have issues, ensure 'bundle' command is in your system's PATH.
```
### Error: "Cannot find module 'express'"
```bash
cd server
npm install express multer cors
cd ..
```
### Error: "File upload failed"
- Check file size (limit is 10MB)
- Ensure file has `.bin` extension
- Verify file is actually a MathType binary
## 📊 Expected Behavior
### ✅ Success Case:
1. Upload `oleObject1.bin` containing equation `E = mc²`
2. Server processes file with Ruby gem
3. Returns: `<math xmlns="http://www.w3.org/1998/Math/MathML"><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></math>`
4. Browser displays rendered equation
### ❌ Fallback Case:
1. Server unavailable or fails
2. Client falls back to demo mode
3. Always shows `x²` sample regardless of uploaded file
4. Warning message: "Client-side demo completed (shows sample x² only)"
## 🔄 Development Workflow
### For Development:
```bash
# Use nodemon for auto-restart (run from project root)
npm run dev
```
### For Production:
```bash
# Use PM2 or similar process manager (run from project root)
npm install -g pm2
pm2 start server/server.js --name mathtype-converter
```
## 📈 Performance Notes
- **Ruby Process**: Each conversion spawns a Ruby process (~100-500ms overhead)
- **File Size**: Typical `.bin` files are 1-50KB
- **Concurrent Requests**: Server handles multiple files simultaneously
- **Memory**: Each Ruby process uses ~10-50MB temporarily
## 🔒 Security Considerations
For production deployment:
- Add file type validation
- Implement rate limiting
- Add authentication if needed
- Sanitize file names
- Use temporary directories with cleanup
## 📦 Alternative Deployment
### Docker Option:
```dockerfile
FROM node:18
RUN apt-get update && apt-get install -y ruby-full
RUN gem install bundler
# Set work directory
WORKDIR /app
# Copy gem files and install gems
COPY Gemfile Gemfile.lock ./
COPY vendor ./vendor
RUN bundle install
# Copy server files and install node modules
COPY server ./server
WORKDIR /app/server
RUN npm install
# Go back to root to run the server
WORKDIR /app
EXPOSE 3000
CMD ["npm", "start"]
```
### Cloud Function Option:
Deploy to AWS Lambda, Google Cloud Functions, or Vercel with Ruby runtime support.
## 📞 Support
If you encounter issues:
1. Check Ruby gem installation from root: `bundle check`
2. Verify Node.js setup in server folder: `cd server && npm install && cd ..`
3. Test with sample `.bin` files first
4. Check server logs for detailed error messages
The key is having both Ruby (for binary parsing) and Node.js (for web interface) working together!

View File

@@ -0,0 +1,211 @@
require_relative 'vendor/gems/mathtype_to_mathml/lib/mathtype_to_mathml'
require 'json'
require 'concurrent-ruby'
require 'pathname'
require 'parallel'
class BatchConverter
def initialize(max_threads: 4)
@max_threads = max_threads
@thread_pool = Concurrent::ThreadPoolExecutor.new(
min_threads: 1,
max_threads: @max_threads,
max_queue: 100,
fallback_policy: :caller_runs
)
end
# 新增多进程处理方法
def process_files_multi_process(file_paths)
# 确保传入的是可枚举的哈希结构
file_paths_hash = file_paths.is_a?(Hash) ? file_paths : {}
# 根据平台自动选择并行模式:
# Linux/macOS 支持 Process.fork,使用多进程实现真正的 CPU 并行
# Windows 不支持 Process.fork,降级为多线程
parallel_opts = Process.respond_to?(:fork) ? { in_processes: @max_threads } : { in_threads: @max_threads }
result_array = Parallel.map(file_paths_hash.to_a, parallel_opts) do |key, value|
begin
result = process_single_file(value)
[key, result]
rescue => e
[key, {
success: false,
error: e.message,
mathml: nil
}]
end
end
# 将数组结果转换为哈希
result_array.to_h
end
def process_files(file_paths)
results = {}
futures = []
# 遍历json入参开始转换
file_paths.each do |key, value|
future = Concurrent::Future.execute(executor: @thread_pool) do
process_single_file(value)
end
futures << [key, future]
end
# Wait for all conversions to complete
futures.each do |file_path, future|
begin
result = future.value(30) # 30 second timeout per file
results[file_path] = result
rescue Concurrent::TimeoutError
results[file_path] = {
success: false,
error: 'Conversion timeout (30 seconds)',
mathml: nil
}
rescue => e
results[file_path] = {
success: false,
error: e.message,
mathml: nil
}
end
end
@thread_pool.shutdown
@thread_pool.wait_for_termination(60)
results
end
private
def process_single_file(file_path)
begin
# Validate file
unless File.exist?(file_path)
return {
success: false,
error: "File does not exist: #{file_path}",
mathml: nil
}
end
unless File.readable?(file_path)
return {
success: false,
error: "File is not readable: #{file_path}",
mathml: nil
}
end
file_size = File.size(file_path)
if file_size == 0
return {
success: false,
error: "File is empty: #{file_path}",
mathml: nil
}
end
# Convert the file
converter = MathTypeToMathML::Converter.new(file_path)
mathml = converter.convert
# Validate output
if mathml.nil? || mathml.strip.empty?
return {
success: false,
error: "Conversion resulted in empty MathML",
mathml: nil
}
end
{
success: true,
error: nil,
mathml: mathml.strip,
file_size: file_size
}
rescue NoMethodError => e
{
success: false,
error: "Ruby parsing error - #{e.message}. This usually indicates an incompatible or corrupted MathType file",
mathml: nil
}
rescue ArgumentError => e
{
success: false,
error: "Invalid file format - #{e.message}",
mathml: nil
}
rescue StandardError => e
{
success: false,
error: "Unexpected error - #{e.class}: #{e.message}",
mathml: nil
}
end
end
end
# Command line interface
if __FILE__ == $0
begin
if ARGV.empty?
STDERR.puts "Usage: #{$0} file1.bin [file2.bin ...]"
exit 1
end
# Validate all files exist before processing
# missing_files = ARGV.reject { |file| File.exist?(file) }
# unless missing_files.empty?
# STDERR.puts "Error: Files not found: #{missing_files.join(', ')}"
# exit 1
# end
# 入参可以是文件路径或JSON字符串
arg = ARGV[0] || '{}'
if File.exist?(arg) && File.file?(arg)
json_data = File.read(arg, mode: 'rb:utf-8').gsub(/\A\xEF\xBB\xBF/, '')
else
json_data = arg.gsub(/\A'|'\z/, '')
end
begin
data = JSON.parse(json_data)
rescue JSON::ParserError => e
STDERR.puts "ERROR: Invalid JSON input: #{e.message}"
exit 1
end
# Process files
# batch_converter = BatchConverter.new(max_threads: [ARGV.size, 8].min)
# results = batch_converter.process_files(data)
batch_converter = BatchConverter.new(max_threads: [data.size, Parallel.processor_count].min)
results = batch_converter.process_files_multi_process(data)
# Output results as JSON
output = {
total_files: data.size,
successful: results.count { |_, result| result[:success] },
failed: results.count { |_, result| !result[:success] },
results: results
}
puts "WP_RUBY_".concat(output.to_json)
# puts JSON.pretty_generate(output)
# Exit with error code if any conversions failed
exit_code = results.any? { |_, result| !result[:success] } ? 1 : 0
# exit exit_code
exit 0
rescue Interrupt
STDERR.puts "\nBatch conversion interrupted by user"
exit 130
rescue => e
STDERR.puts "Fatal error: #{e.message}"
exit 1
end
end

View File

@@ -0,0 +1 @@
# Keep this directory in git

View File

@@ -0,0 +1,19 @@
{
"name": "MathType to MathML Converter",
"version": "1.0.0",
"description": "Server for converting MathType binary files to MathML",
"port": 3000,
"environment": "production",
"limits": {
"maxFileSize": "10MB",
"maxFiles": 10
},
"logging": {
"level": "info",
"format": "combined"
},
"cors": {
"enabled": true,
"origin": "*"
}
}

View File

@@ -0,0 +1,65 @@
require 'mathtype_to_mathml'
def safe_convert(file_path)
begin
# Check if file exists and is readable
unless File.exist?(file_path)
STDERR.puts "Error: File does not exist: #{file_path}"
exit 1
end
unless File.readable?(file_path)
STDERR.puts "Error: File is not readable: #{file_path}"
exit 1
end
# Check file size
file_size = File.size(file_path)
if file_size == 0
STDERR.puts "Error: File is empty: #{file_path}"
exit 1
end
STDERR.puts "Processing file: #{file_path} (#{file_size} bytes)"
# Try to convert the file
converter = MathTypeToMathML::Converter.new(file_path)
mathml = converter.convert
# Check if we got valid output
if mathml.nil? || mathml.strip.empty?
STDERR.puts "Error: Conversion resulted in empty MathML"
exit 1
end
puts mathml
rescue NoMethodError => e
STDERR.puts "Error: Ruby parsing error - #{e.message}"
STDERR.puts "This usually indicates an incompatible or corrupted MathType file"
STDERR.puts "Backtrace: #{e.backtrace.first(3).join(', ')}"
exit 1
rescue ArgumentError => e
STDERR.puts "Error: Invalid file format - #{e.message}"
exit 1
rescue StandardError => e
STDERR.puts "Error: Unexpected error - #{e.class}: #{e.message}"
STDERR.puts "Backtrace: #{e.backtrace.first(5).join(', ')}"
exit 1
end
end
begin
file_path = ARGV[0]
if file_path.nil? || file_path.empty?
STDERR.puts "Error: No file path provided"
exit 1
end
safe_convert(file_path)
rescue => e
STDERR.puts "Error: #{e.message}"
exit 1
end

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# Enhanced Docker 构建脚本 - 网络优化版本
set -e
echo "🐳 Enhanced MathType to MathML Docker 构建脚本"
echo "================================================="
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null; then
echo "❌ Docker 未安装,请先安装 Docker"
exit 1
fi
# 检查 docker-compose 是否安装
if ! command -v docker-compose &> /dev/null; then
echo "⚠️ docker-compose 未安装,将使用 docker build"
USE_COMPOSE=false
else
USE_COMPOSE=true
fi
# 设置镜像名称
IMAGE_NAME="mathtype-converter"
CONTAINER_NAME="mathtype-to-mathml"
# 检查网络连接并选择最佳构建策略
echo "🌐 检查网络连接..."
# 测试Docker Hub连接
if curl -s --connect-timeout 5 https://registry.hub.docker.com/ > /dev/null; then
echo "✅ Docker Hub 连接正常"
NETWORK_MODE="standard"
else
echo "⚠️ Docker Hub 连接异常,使用网络优化模式"
NETWORK_MODE="optimized"
fi
# 根据网络状况选择构建参数
if [ "$NETWORK_MODE" = "optimized" ]; then
echo "📦 使用网络优化构建..."
BUILD_ARGS="--build-arg USE_MIRROR=true --network=host"
else
echo "📦 使用标准构建..."
BUILD_ARGS=""
fi
echo "🔨 开始构建 Docker 镜像..."
# 构建函数
build_image() {
local attempt=$1
echo "📝 构建尝试 $attempt/3..."
if [ "$USE_COMPOSE" = true ]; then
if docker-compose build $BUILD_ARGS; then
return 0
else
return 1
fi
else
if docker build -t $IMAGE_NAME $BUILD_ARGS .; then
return 0
else
return 1
fi
fi
}
# 重试构建
for attempt in 1 2 3; do
if build_image $attempt; then
echo "✅ Docker 镜像构建成功!"
BUILD_SUCCESS=true
break
else
echo "❌ 构建尝试 $attempt 失败"
if [ $attempt -lt 3 ]; then
echo "⏳ 等待 10 秒后重试..."
sleep 10
fi
BUILD_SUCCESS=false
fi
done
if [ "$BUILD_SUCCESS" != true ]; then
echo "❌ 所有构建尝试都失败了"
echo "💡 建议:"
echo " 1. 检查网络连接"
echo " 2. 确认 Docker 服务运行正常"
echo " 3. 尝试手动清理 Docker 缓存: docker system prune"
exit 1
fi
# 验证镜像构建
echo "🔍 验证镜像..."
if docker images | grep -q $IMAGE_NAME; then
echo "✅ 镜像验证成功!"
else
echo "❌ 镜像验证失败"
exit 1
fi
# 提供启动选项
echo ""
echo "🚀 启动选项:"
echo "1. 使用 docker-compose(推荐):"
echo " docker-compose up -d"
echo ""
echo "2. 直接使用 Docker:"
echo " docker run -d --name $CONTAINER_NAME -p 3000:3000 -v \$(pwd)/uploads:/app/uploads $IMAGE_NAME"
echo ""
echo "📱 启动后访问:"
echo " Web界面: http://localhost:3000/mathtype_converter_demo.html"
echo " 健康检查: http://localhost:3000/health"
echo ""
echo "📋 管理命令:"
echo " 查看日志: docker logs -f $CONTAINER_NAME"
echo " 停止容器: docker stop $CONTAINER_NAME"
echo " 删除容器: docker rm $CONTAINER_NAME"
# 询问是否立即启动
read -p "是否立即启动容器? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🚀 启动容器..."
if [ "$USE_COMPOSE" = true ]; then
if docker-compose up -d; then
echo "✅ 容器已启动!"
echo "📊 查看状态: docker-compose ps"
echo "📋 查看日志: docker-compose logs -f"
else
echo "❌ 容器启动失败"
exit 1
fi
else
# 停止并删除已存在的容器
docker stop $CONTAINER_NAME 2>/dev/null || true
docker rm $CONTAINER_NAME 2>/dev/null || true
# 启动新容器
if docker run -d \
--name $CONTAINER_NAME \
-p 3000:3000 \
-v "$(pwd)/uploads:/app/uploads" \
-v "$(pwd)/config:/app/config:ro" \
$IMAGE_NAME; then
echo "✅ 容器已启动!"
echo "📊 查看状态: docker ps"
echo "📋 查看日志: docker logs -f $CONTAINER_NAME"
else
echo "❌ 容器启动失败"
exit 1
fi
fi
echo ""
echo "⏳ 等待服务启动..."
sleep 10
# 健康检查
echo "🏥 执行健康检查..."
for i in {1..6}; do
if curl -s http://localhost:3000/health > /dev/null; then
echo "✅ 服务启动成功!"
echo "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html"
break
else
if [ $i -eq 6 ]; then
echo "⚠️ 健康检查超时,但服务可能仍在启动中"
echo "💡 请手动检查: curl http://localhost:3000/health"
else
echo "⏳ 等待服务启动... ($i/6)"
sleep 5
fi
fi
done
fi
echo ""
echo "🎉 构建完成!"

View File

@@ -0,0 +1,216 @@
# Enhanced Docker 构建脚本 - Windows PowerShell 版本
param(
[switch]$Production,
[switch]$NoBuild,
[switch]$Help
)
if ($Help) {
Write-Host "🐳 MathType to MathML Docker 构建脚本 - Windows版本" -ForegroundColor Cyan
Write-Host "用法: .\docker-build.ps1 [参数]" -ForegroundColor Yellow
Write-Host ""
Write-Host "参数:" -ForegroundColor Yellow
Write-Host " -Production 使用生产环境配置 (docker-compose.production.yml)"
Write-Host " -NoBuild 跳过构建,直接启动已有镜像"
Write-Host " -Help 显示此帮助信息"
Write-Host ""
Write-Host "示例:" -ForegroundColor Yellow
Write-Host " .\docker-build.ps1 # 开发环境构建和启动"
Write-Host " .\docker-build.ps1 -Production # 生产环境构建和启动"
Write-Host " .\docker-build.ps1 -NoBuild # 跳过构建,直接启动"
exit 0
}
$ErrorActionPreference = "Stop"
Write-Host "🐳 Enhanced MathType to MathML Docker 构建脚本" -ForegroundColor Cyan
Write-Host "=================================================" -ForegroundColor Cyan
# 检查 Docker 是否安装
if (!(Get-Command docker -ErrorAction SilentlyContinue)) {
Write-Host "❌ Docker 未安装,请先安装 Docker Desktop" -ForegroundColor Red
exit 1
}
# 检查 docker-compose 是否安装
if (!(Get-Command docker-compose -ErrorAction SilentlyContinue)) {
Write-Host "⚠️ docker-compose 未安装,将使用 docker build" -ForegroundColor Yellow
$UseCompose = $false
} else {
$UseCompose = $true
}
# 设置镜像名称和配置文件
$ImageName = "mathtype-converter"
$ContainerName = "mathtype-to-mathml"
if ($Production) {
$ComposeFile = "docker-compose.production.yml"
$ContainerName += "-prod"
Write-Host "📦 使用生产环境配置" -ForegroundColor Green
} else {
$ComposeFile = "docker-compose.yml"
Write-Host "📦 使用开发环境配置" -ForegroundColor Green
}
# 检查网络连接
Write-Host "🌐 检查网络连接..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "https://registry.hub.docker.com/" -TimeoutSec 5 -UseBasicParsing
Write-Host "✅ Docker Hub 连接正常" -ForegroundColor Green
$NetworkMode = "standard"
} catch {
Write-Host "⚠️ Docker Hub 连接异常,使用网络优化模式" -ForegroundColor Yellow
$NetworkMode = "optimized"
}
if (!$NoBuild) {
Write-Host "🔨 开始构建 Docker 镜像..." -ForegroundColor Yellow
# 构建函数
function Build-DockerImage {
param([int]$Attempt)
Write-Host "📝 构建尝试 $Attempt/3..." -ForegroundColor Cyan
try {
if ($UseCompose) {
if ($NetworkMode -eq "optimized") {
docker-compose -f $ComposeFile build --build-arg USE_MIRROR=true
} else {
docker-compose -f $ComposeFile build
}
} else {
if ($NetworkMode -eq "optimized") {
docker build -t $ImageName --build-arg USE_MIRROR=true .
} else {
docker build -t $ImageName .
}
}
return $true
} catch {
Write-Host "❌ 构建失败: $($_.Exception.Message)" -ForegroundColor Red
return $false
}
}
# 重试构建
$BuildSuccess = $false
for ($attempt = 1; $attempt -le 3; $attempt++) {
if (Build-DockerImage -Attempt $attempt) {
Write-Host "✅ Docker 镜像构建成功!" -ForegroundColor Green
$BuildSuccess = $true
break
} else {
if ($attempt -lt 3) {
Write-Host "⏳ 等待 10 秒后重试..." -ForegroundColor Yellow
Start-Sleep -Seconds 10
}
}
}
if (!$BuildSuccess) {
Write-Host "❌ 所有构建尝试都失败了" -ForegroundColor Red
Write-Host "💡 建议:" -ForegroundColor Yellow
Write-Host " 1. 检查网络连接"
Write-Host " 2. 确认 Docker Desktop 运行正常"
Write-Host " 3. 尝试手动清理 Docker 缓存: docker system prune"
exit 1
}
# 验证镜像构建
Write-Host "🔍 验证镜像..." -ForegroundColor Yellow
$images = docker images --format "{{.Repository}}" | Where-Object { $_ -eq $ImageName }
if ($images) {
Write-Host "✅ 镜像验证成功!" -ForegroundColor Green
} else {
Write-Host "❌ 镜像验证失败" -ForegroundColor Red
exit 1
}
} else {
Write-Host "⏭️ 跳过构建步骤" -ForegroundColor Yellow
}
# 提供启动选项
Write-Host ""
Write-Host "🚀 启动选项:" -ForegroundColor Cyan
if ($UseCompose) {
Write-Host "1. 使用 docker-compose(推荐):" -ForegroundColor White
Write-Host " docker-compose -f $ComposeFile up -d" -ForegroundColor Gray
} else {
Write-Host "1. 直接使用 Docker:" -ForegroundColor White
Write-Host " docker run -d --name $ContainerName -p 3000:3000 -v `$(pwd)/uploads:/app/uploads -v `$(pwd)/config:/app/config:ro $ImageName" -ForegroundColor Gray
}
Write-Host ""
Write-Host "📱 启动后访问:" -ForegroundColor Cyan
Write-Host " Web界面: http://localhost:3000/mathtype_converter_demo.html" -ForegroundColor White
Write-Host " 健康检查: http://localhost:3000/health" -ForegroundColor White
Write-Host ""
Write-Host "📋 管理命令:" -ForegroundColor Cyan
Write-Host " 查看日志: docker logs -f $ContainerName" -ForegroundColor Gray
Write-Host " 停止容器: docker stop $ContainerName" -ForegroundColor Gray
Write-Host " 删除容器: docker rm $ContainerName" -ForegroundColor Gray
# 询问是否立即启动
$reply = Read-Host "`n是否立即启动容器? (y/N)"
if ($reply -match "^[Yy]$") {
Write-Host "🚀 启动容器..." -ForegroundColor Yellow
try {
if ($UseCompose) {
docker-compose -f $ComposeFile up -d
Write-Host "✅ 容器已启动!" -ForegroundColor Green
Write-Host "📊 查看状态: docker-compose -f $ComposeFile ps" -ForegroundColor Gray
Write-Host "📋 查看日志: docker-compose -f $ComposeFile logs -f" -ForegroundColor Gray
} else {
# 停止并删除已存在的容器
try { docker stop $ContainerName 2>$null } catch {}
try { docker rm $ContainerName 2>$null } catch {}
# 启动新容器
$currentDir = Get-Location
docker run -d `
--name $ContainerName `
-p 3000:3000 `
-v "${currentDir}/uploads:/app/uploads" `
-v "${currentDir}/config:/app/config:ro" `
$ImageName
Write-Host "✅ 容器已启动!" -ForegroundColor Green
Write-Host "📊 查看状态: docker ps" -ForegroundColor Gray
Write-Host "📋 查看日志: docker logs -f $ContainerName" -ForegroundColor Gray
}
Write-Host ""
Write-Host "⏳ 等待服务启动..." -ForegroundColor Yellow
Start-Sleep -Seconds 10
# 健康检查
Write-Host "🏥 执行健康检查..." -ForegroundColor Yellow
$healthCheckSuccess = $false
for ($i = 1; $i -le 6; $i++) {
try {
$response = Invoke-WebRequest -Uri "http://localhost:3000/health" -TimeoutSec 5 -UseBasicParsing
Write-Host "✅ 服务启动成功!" -ForegroundColor Green
Write-Host "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html" -ForegroundColor Cyan
$healthCheckSuccess = $true
break
} catch {
if ($i -eq 6) {
Write-Host "⚠️ 健康检查超时,但服务可能仍在启动中" -ForegroundColor Yellow
Write-Host "💡 请手动检查: Invoke-WebRequest http://localhost:3000/health" -ForegroundColor Gray
} else {
Write-Host "⏳ 等待服务启动... ($i/6)" -ForegroundColor Yellow
Start-Sleep -Seconds 5
}
}
}
} catch {
Write-Host "❌ 容器启动失败: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
Write-Host ""
Write-Host "🎉 构建完成!" -ForegroundColor Green

View File

@@ -0,0 +1,96 @@
#!/bin/bash
# Docker 构建和测试脚本
set -e
echo "🐳 MathType to MathML Docker 构建脚本"
echo "=================================="
# 检查 Docker 是否安装
if ! command -v docker &> /dev/null; then
echo "❌ Docker 未安装,请先安装 Docker"
exit 1
fi
# 检查 docker-compose 是否安装
if ! command -v docker-compose &> /dev/null; then
echo "⚠️ docker-compose 未安装,将使用 docker build"
USE_COMPOSE=false
else
USE_COMPOSE=true
fi
# 设置镜像名称
IMAGE_NAME="mathtype-converter"
CONTAINER_NAME="mathtype-to-mathml"
echo "📦 开始构建 Docker 镜像..."
if [ "$USE_COMPOSE" = true ]; then
echo "使用 docker-compose 构建..."
docker-compose build
else
echo "使用 docker build 构建..."
docker build -t $IMAGE_NAME .
fi
echo "✅ Docker 镜像构建完成!"
# 提供启动选项
echo ""
echo "🚀 启动选项:"
echo "1. 使用 docker-compose(推荐):"
echo " docker-compose up -d"
echo ""
echo "2. 直接使用 Docker:"
echo " docker run -d --name $CONTAINER_NAME -p 3000:3000 -v \$(pwd)/uploads:/app/uploads $IMAGE_NAME"
echo ""
echo "📱 启动后访问:"
echo " Web界面: http://localhost:3000/mathtype_converter_demo.html"
echo " 健康检查: http://localhost:3000/health"
echo ""
echo "📋 管理命令:"
echo " 查看日志: docker logs -f $CONTAINER_NAME"
echo " 停止容器: docker stop $CONTAINER_NAME"
echo " 删除容器: docker rm $CONTAINER_NAME"
# 询问是否立即启动
read -p "是否立即启动容器? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🚀 启动容器..."
if [ "$USE_COMPOSE" = true ]; then
docker-compose up -d
echo "✅ 容器已启动!"
echo "📊 查看状态: docker-compose ps"
echo "📋 查看日志: docker-compose logs -f"
else
# 停止并删除已存在的容器
docker stop $CONTAINER_NAME 2>/dev/null || true
docker rm $CONTAINER_NAME 2>/dev/null || true
# 启动新容器
docker run -d \
--name $CONTAINER_NAME \
-p 3000:3000 \
-v "$(pwd)/uploads:/app/uploads" \
$IMAGE_NAME
echo "✅ 容器已启动!"
echo "📊 查看状态: docker ps"
echo "📋 查看日志: docker logs -f $CONTAINER_NAME"
fi
echo ""
echo "⏳ 等待服务启动..."
sleep 5
# 健康检查
echo "🏥 执行健康检查..."
if curl -s http://localhost:3000/health > /dev/null; then
echo "✅ 服务启动成功!"
echo "🌐 请访问: http://localhost:3000/mathtype_converter_demo.html"
else
echo "⚠️ 服务可能还在启动中,请稍等片刻后再次访问"
fi
fi

View File

@@ -0,0 +1,29 @@
services:
mathtype-converter:
build: .
container_name: mathtype-to-mathml-prod
ports:
- "3000:3000"
volumes:
# 持久化上传文件目录
- ./uploads:/app/uploads
# 挂载配置文件
- ./config:/app/config:ro
environment:
- NODE_ENV=production
- PORT=3000
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
- "traefik.enable=true"
- "traefik.http.routers.mathtype-prod.rule=Host(`mathtype.local`)"
- "traefik.http.services.mathtype-prod.loadbalancer.server.port=3000"
networks:
default:
name: mathtype-network-prod

View File

@@ -0,0 +1,37 @@
services:
mathtype-converter:
build: .
container_name: mathtype-to-mathml
ports:
- "3000:3000"
volumes:
# 持久化上传文件目录
- ./uploads:/app/uploads
# 挂载配置文件
- ./config:/app/config:ro
# 开发模式:实时同步源代码
- ./server_converter.js:/app/server_converter.js
- ./server:/app/server
- ./public:/app/public
- ./vendor:/app/vendor
- ./package.json:/app/package.json
- ./package-lock.json:/app/package-lock.json
- ./entrypoint.sh:/app/entrypoint.sh
environment:
- NODE_ENV=production
- PORT=3000
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
labels:
- "traefik.enable=true"
- "traefik.http.routers.mathtype.rule=Host(`mathtype.local`)"
- "traefik.http.services.mathtype.loadbalancer.server.port=3000"
networks:
default:
name: mathtype-network

View File

@@ -0,0 +1,48 @@
#!/bin/bash
set -e
echo "🚀 Starting MathType to MathML Converter..."
# 检查必要的目录
echo "📁 Checking directories..."
mkdir -p uploads
mkdir -p public
mkdir -p vendor
# 确保上传目录权限正确
chmod 755 uploads
# 检查 Ruby 和 gem 是否可用
echo "💎 Checking Ruby environment..."
ruby --version
bundle --version
# 检查关键 gem 是否安装
echo "🔍 Verifying gem installation..."
if ! bundle exec ruby -e "require 'mathtype_to_mathml'; puts 'mathtype_to_mathml gem loaded successfully'"; then
echo "❌ mathtype_to_mathml gem not found!"
exit 1
fi
# 检查 Node.js 环境
echo "📦 Checking Node.js environment..."
node --version
npm --version
# 检查关键文件是否存在
echo "📄 Checking critical files..."
for file in "server_converter.js" "server/converter.rb" "public/mathtype_converter_demo.html"; do
if [ ! -f "$file" ]; then
echo "❌ Critical file missing: $file"
exit 1
fi
done
echo "✅ Environment check completed successfully!"
# 创建健康检查端点(如果 server_converter.js 没有的话)
echo "🏥 Setting up health check..."
# 启动应用
echo "🎯 Starting application..."
exec "$@"

View File

@@ -0,0 +1,43 @@
# 本地开发环境配置
1. 安装ruby
2. 安装依赖包
```
进入到jby-office-formula目录下
bundle install
```
3. 测试
```
ruby batch_converter.rb '{"testkey":"C:\\embeddings\\oleObject1.bin"}'
ruby batch_converter.rb '{"testkey":"/opt/embeddings/oleObject1.bin"}'
ruby batch_converter.rb '{"testkey":"install/example.bin"}'
```
# 线上环境配置
1. ruby环境基础镜像(bjjby-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com/base/image:ruby-v1)核心命令
安装包: install/ruby-2.6.0.tar.gz、install/gems/*
```
cd /opt
&& tar -zxf ruby-2.6.0.tar.gz
&& cd /opt/ruby-2.6.0
&& ./configure --prefix=/usr/local/ruby
&& make -j$(nproc)
&& make install
&& export PATH=/usr/local/ruby/bin:$PATH
&& gem --version
&& unzip /opt/gems.zip -d /opt/
&& gem install --local /opt/gems/one/*
&& gem install --local /opt/gems/two/*
&& make clean
&& rm -rf /opt/ruby/ruby-2.6.0
&& rm -rf /opt/gems
&& rm -rf /var/cache/*
```
2. 业务容器镜像Dockerfile
```
FROM bjjby-registry-vpc.cn-zhangjiakou.cr.aliyuncs.com/base/image:ruby-v1
LABEL maintainer="chen 15662138562@163.com"
RUN mkdir /opt/servers
COPY jby-office-formula /opt/servers/jby-office-formula
ADD jby-office-file-server/target/jby-office-file-server-3.0.jar /opt/servers/office-file.jar
CMD ["java","-jar", "/opt/servers/office-file.jar"]
```

Binary file not shown.

Binary file not shown.

1397
scripts/mathtype2mathml/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
{
"name": "mathtype-to-mathml-server",
"version": "1.0.0",
"description": "Server for converting MathType binary files to MathML using Ruby gem",
"main": "server_converter.js",
"scripts": {
"setup": "node server/setup.js",
"start": "node server_converter.js",
"dev": "nodemon server_converter.js"
},
"keywords": [
"mathtype",
"mathml",
"equation",
"converter",
"ruby",
"gem"
],
"author": "Your Name",
"license": "MIT",
"dependencies": {
"express": "^4.18.2",
"multer": "^1.4.5-lts.1",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"engines": {
"node": ">=14.0.0"
}
}

View File

@@ -0,0 +1,891 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MathType to MathML Converter Demo</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
h1 {
color: #333;
text-align: center;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #666;
margin-bottom: 30px;
}
.upload-section {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 30px;
text-align: center;
margin-bottom: 30px;
background-color: #fafafa;
}
.upload-section:hover {
border-color: #007bff;
background-color: #f0f8ff;
}
input[type="file"] {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
margin: 10px;
}
button:hover {
background-color: #0056b3;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.demo-section {
margin: 20px 0;
}
.output-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.output-panel {
border: 1px solid #ddd;
border-radius: 6px;
padding: 15px;
background-color: #f9f9f9;
}
.output-panel h3 {
margin-top: 0;
color: #333;
border-bottom: 1px solid #ddd;
padding-bottom: 10px;
}
.mathml-output {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 15px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 300px;
overflow-y: auto;
font-size: 12px;
}
.mathml-preview {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 20px;
text-align: center;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.status {
padding: 10px;
border-radius: 4px;
margin: 10px 0;
font-weight: bold;
}
.status.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.debug-info {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 15px;
margin-top: 15px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
.architecture-diagram {
text-align: center;
margin: 20px 0;
padding: 20px;
background-color: #f8f9fa;
border-radius: 6px;
font-family: monospace;
line-height: 1.8;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 6px;
border-left: 4px solid #007bff;
}
.feature-card h4 {
margin-top: 0;
color: #007bff;
}
.batch-upload-zone {
border: 2px dashed #007bff;
border-radius: 8px;
padding: 30px;
text-align: center;
background-color: #f8f9fa;
margin: 20px 0;
transition: all 0.3s ease;
cursor: pointer;
}
.batch-upload-zone:hover {
border-color: #0056b3;
background-color: #e3f2fd;
}
.batch-upload-zone.dragover {
border-color: #28a745;
background-color: #d4edda;
}
.file-list {
margin-top: 15px;
text-align: left;
}
.file-item {
background: white;
padding: 8px 12px;
margin: 5px 0;
border: 1px solid #dee2e6;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
.file-item .remove-file {
color: #dc3545;
cursor: pointer;
font-weight: bold;
}
.progress-container {
margin: 20px 0;
display: none;
}
.progress-bar {
width: 100%;
height: 20px;
background-color: #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #007bff;
transition: width 0.3s ease;
width: 0%;
}
.batch-results {
margin-top: 20px;
display: none;
}
.result-item {
background: white;
border: 1px solid #dee2e6;
border-radius: 4px;
margin: 10px 0;
padding: 15px;
}
.result-item.success {
border-left: 4px solid #28a745;
}
.result-item.error {
border-left: 4px solid #dc3545;
}
.result-header {
font-weight: bold;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.result-mathml {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 150px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
@media (max-width: 768px) {
.output-section {
grid-template-columns: 1fr;
}
.container {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🧮 MathType to MathML Converter</h1>
<p class="subtitle">JavaScript implementation for converting MathType binary files to MathML format</p>
<div class="architecture-diagram">
<strong>Conversion Pipeline:</strong><br>
Binary MTEF → XML Structure → Preprocessing → MathML Transform → Final Output
</div>
<div class="upload-section">
<h3>📁 Upload Single MathType File</h3>
<p>Select a MathType binary file (.bin) to convert to MathML</p>
<div class="status info" style="margin: 10px 0; font-size: 14px;">
<strong>💡 For Real Conversion:</strong> Start the server with <code>npm start</code> to convert actual .bin files.<br>
<strong>🎮 Demo Mode:</strong> Works without server but only shows sample equation (x²).
</div>
<input type="file" id="fileInput" accept=".bin" />
<br>
<button id="convertBtn" onclick="convertFile()">Convert Real File (Server Required)</button>
<button id="demoBtn" onclick="runDemo()">Run Client Demo (x² Sample)</button>
</div>
<div class="upload-section">
<h3>📁 Batch Upload MathType Files</h3>
<p>Upload multiple MathType binary files (.bin) for batch conversion</p>
<div class="status info" style="margin: 10px 0; font-size: 14px;">
<strong>🚀 High Performance:</strong> Process multiple files in parallel using optimized batch processing.<br>
<strong>📊 Progress Tracking:</strong> Real-time progress updates for each file in the batch.
</div>
<div class="batch-upload-zone" id="batchUploadZone" onclick="document.getElementById('batchFileInput').click()">
<div>
<h4>🎯 Drop files here or click to select</h4>
<p>Drag and drop multiple .bin files, or click to browse</p>
<p style="font-size: 14px; color: #6c757d;">Supports up to 50 files per batch</p>
</div>
</div>
<input type="file" id="batchFileInput" accept=".bin" multiple style="display: none;" />
<div class="file-list" id="fileList" style="display: none;">
<h4>Selected Files:</h4>
<div id="fileItems"></div>
</div>
<div style="margin-top: 15px;">
<button id="batchConvertBtn" onclick="convertBatchFiles()" style="display: none;">
🚀 Convert Batch (Server Required)
</button>
<button id="clearBatchBtn" onclick="clearBatchFiles()" style="display: none; margin-left: 10px;">
🗑️ Clear All
</button>
</div>
<div class="progress-container" id="progressContainer">
<h4>Conversion Progress:</h4>
<div class="progress-bar">
<div class="progress-bar-fill" id="progressBarFill"></div>
</div>
<p id="progressText">0 / 0 files processed</p>
</div>
<div class="batch-results" id="batchResults">
<h4>Conversion Results:</h4>
<div id="resultItems"></div>
</div>
</div>
<div id="status"></div>
<div class="output-section">
<div class="output-panel">
<h3>📄 Generated MathML</h3>
<div id="mathmlOutput" class="mathml-output">
<!-- Generated MathML will appear here -->
</div>
</div>
<div class="output-panel">
<h3>🔍 MathML Preview</h3>
<div id="mathmlPreview" class="mathml-preview">
<!-- Rendered math will appear here -->
</div>
</div>
</div>
<div id="debugInfo" class="debug-info" style="display: none;">
<!-- Debug information will appear here -->
</div>
</div>
<div class="container">
<h2>🚀 Features</h2>
<div class="feature-grid">
<div class="feature-card">
<h4>Multi-Stage Pipeline</h4>
<p>Follows the same architecture as the Ruby mathtype_to_mathml gem with binary parsing, preprocessing, and transformation stages.</p>
</div>
<div class="feature-card">
<h4>Character Mapping</h4>
<p>Comprehensive Unicode character support including Greek letters, mathematical operators, and special symbols.</p>
</div>
<div class="feature-card">
<h4>Mathematical Structures</h4>
<p>Supports fractions, subscripts, superscripts, roots, matrices, integrals, summations, and more.</p>
</div>
<div class="feature-card">
<h4>Web Compatible</h4>
<p>Pure JavaScript implementation that works in browsers without external dependencies.</p>
</div>
</div>
</div>
<div class="container">
<h2>⚠️ Important Notes</h2>
<div class="status info">
<strong>Binary Parsing Limitation:</strong> This demo uses a simplified MTEF parser. For production use, you would need:
<ul style="margin: 10px 0; padding-left: 20px;">
<li>A complete MTEF binary format parser</li>
<li>WebAssembly port of the Ruby mathtype gem</li>
<li>Server-side conversion with the Ruby gem</li>
</ul>
</div>
</div>
<!-- Include the converter script -->
<script src="mathtype_to_mathml_converter.js"></script>
<script>
// Global converter instance
let converter;
// Initialize converter when page loads
document.addEventListener('DOMContentLoaded', function() {
try {
converter = new MathTypeToMathMLConverter();
showStatus('Converter initialized successfully', 'success');
} catch (error) {
showStatus('Failed to initialize converter: ' + error.message, 'error');
}
});
// File conversion function - uses server for real binary parsing
async function convertFile() {
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
if (!file) {
showStatus('Please select a file first', 'error');
return;
}
try {
showStatus('Converting file using server...', 'info');
setButtonsEnabled(false);
// Show debug info about the binary data
showDebugInfo(`Uploading file: ${file.name} (${file.size} bytes)`);
// Send file to server for conversion
const mathml = await convertFileOnServer(file);
// Display results
displayResults(mathml);
showStatus('Server conversion completed successfully!', 'success');
} catch (error) {
showStatus('Server conversion failed: ' + error.message, 'error');
console.error('Conversion error:', error);
// Fallback to client-side demo
showStatus('Falling back to client-side demo...', 'info');
try {
const binaryData = await loadBinaryFile(fileInput);
const mathml = await converter.convert(binaryData);
displayResults(mathml);
showStatus('Client-side demo completed (shows sample x² only)', 'success');
} catch (fallbackError) {
showStatus('Both server and client conversion failed', 'error');
}
} finally {
setButtonsEnabled(true);
}
}
// Server-side conversion function
async function convertFileOnServer(file) {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/convert-mathtype', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `Server error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Conversion failed');
}
return result.mathml;
}
// Demo function with sample data
async function runDemo() {
if (!converter) {
showStatus('Converter not initialized', 'error');
return;
}
try {
showStatus('Running demo with sample equation (x²)...', 'info');
setButtonsEnabled(false);
// Create sample binary data (this would normally be read from a .bin file)
const sampleData = new Uint8Array([0x01, 0x02, 0x03, 0x04]); // Placeholder binary data
showDebugInfo('Demo using simulated binary data for equation: x²');
// Convert using the sample data
const mathml = await converter.convert(sampleData);
// Display results
displayResults(mathml);
showStatus('Demo completed successfully!', 'success');
} catch (error) {
showStatus('Demo failed: ' + error.message, 'error');
console.error('Demo error:', error);
} finally {
setButtonsEnabled(true);
}
}
// Display conversion results
function displayResults(mathml) {
// Show raw MathML
document.getElementById('mathmlOutput').textContent = mathml;
// Try to render MathML preview
const previewDiv = document.getElementById('mathmlPreview');
try {
previewDiv.innerHTML = mathml;
// If MathJax is available, re-render
if (window.MathJax && window.MathJax.typesetPromise) {
MathJax.typesetPromise([previewDiv]).catch((err) => {
console.warn('MathJax rendering failed:', err);
});
}
} catch (error) {
previewDiv.innerHTML = '<em>Preview not available (MathML not supported in this browser)</em>';
}
}
// Show status message
function showStatus(message, type = 'info') {
const statusDiv = document.getElementById('status');
statusDiv.innerHTML = `<div class="status ${type}">${message}</div>`;
// Auto-hide success/info messages after 5 seconds
if (type === 'success' || type === 'info') {
setTimeout(() => {
statusDiv.innerHTML = '';
}, 5000);
}
}
// Show debug information
function showDebugInfo(info) {
const debugDiv = document.getElementById('debugInfo');
debugDiv.style.display = 'block';
debugDiv.textContent = info;
}
// Enable/disable buttons
function setButtonsEnabled(enabled) {
document.getElementById('convertBtn').disabled = !enabled;
document.getElementById('demoBtn').disabled = !enabled;
}
// Batch processing variables
let batchFiles = [];
// Initialize batch upload functionality
document.addEventListener('DOMContentLoaded', function() {
const batchUploadZone = document.getElementById('batchUploadZone');
const batchFileInput = document.getElementById('batchFileInput');
// File input change handler
batchFileInput.addEventListener('change', function(e) {
handleBatchFiles(Array.from(e.target.files));
});
// Drag and drop handlers for batch upload
batchUploadZone.addEventListener('dragover', function(e) {
e.preventDefault();
batchUploadZone.classList.add('dragover');
});
batchUploadZone.addEventListener('dragleave', function(e) {
e.preventDefault();
batchUploadZone.classList.remove('dragover');
});
batchUploadZone.addEventListener('drop', function(e) {
e.preventDefault();
batchUploadZone.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(file =>
file.name.toLowerCase().endsWith('.bin')
);
if (files.length > 0) {
handleBatchFiles(files);
} else {
showStatus('No valid .bin files found in the dropped files', 'error');
}
});
});
// Handle batch file selection
function handleBatchFiles(files) {
// Filter for .bin files and check for duplicates
const validFiles = files.filter(file => {
if (!file.name.toLowerCase().endsWith('.bin')) {
return false;
}
// Check for duplicates
return !batchFiles.some(existing =>
existing.name === file.name && existing.size === file.size
);
});
if (validFiles.length === 0) {
showStatus('No new valid .bin files to add', 'error');
return;
}
// Check total file limit
if (batchFiles.length + validFiles.length > 50) {
showStatus('Maximum 50 files allowed per batch', 'error');
return;
}
// Add files to batch
batchFiles.push(...validFiles);
updateFileList();
showStatus(`Added ${validFiles.length} file(s) to batch (${batchFiles.length} total)`, 'success');
}
// Update file list display
function updateFileList() {
const fileList = document.getElementById('fileList');
const fileItems = document.getElementById('fileItems');
const batchConvertBtn = document.getElementById('batchConvertBtn');
const clearBatchBtn = document.getElementById('clearBatchBtn');
if (batchFiles.length === 0) {
fileList.style.display = 'none';
batchConvertBtn.style.display = 'none';
clearBatchBtn.style.display = 'none';
return;
}
fileList.style.display = 'block';
batchConvertBtn.style.display = 'inline-block';
clearBatchBtn.style.display = 'inline-block';
fileItems.innerHTML = batchFiles.map((file, index) => `
<div class="file-item">
<span>${file.name} (${formatFileSize(file.size)})</span>
<span class="remove-file" onclick="removeFile(${index})">✕</span>
</div>
`).join('');
}
// Remove file from batch
function removeFile(index) {
batchFiles.splice(index, 1);
updateFileList();
showStatus('File removed from batch', 'info');
}
// Clear all batch files
function clearBatchFiles() {
batchFiles = [];
updateFileList();
document.getElementById('progressContainer').style.display = 'none';
document.getElementById('batchResults').style.display = 'none';
showStatus('Batch cleared', 'info');
}
// Convert batch files
async function convertBatchFiles() {
if (batchFiles.length === 0) {
showStatus('No files selected for batch conversion', 'error');
return;
}
try {
showStatus(`Starting batch conversion of ${batchFiles.length} files...`, 'info');
// Show progress container
const progressContainer = document.getElementById('progressContainer');
const progressBarFill = document.getElementById('progressBarFill');
const progressText = document.getElementById('progressText');
const batchResults = document.getElementById('batchResults');
const resultItems = document.getElementById('resultItems');
progressContainer.style.display = 'block';
batchResults.style.display = 'block';
resultItems.innerHTML = '';
// Update progress
progressBarFill.style.width = '0%';
progressText.textContent = `0 / ${batchFiles.length} files processed`;
// Disable buttons
document.getElementById('batchConvertBtn').disabled = true;
document.getElementById('clearBatchBtn').disabled = true;
// Send files to server for batch conversion
const results = await convertBatchOnServer(batchFiles);
// Update progress to 100%
progressBarFill.style.width = '100%';
progressText.textContent = `${batchFiles.length} / ${batchFiles.length} files processed`;
// Display results
displayBatchResults(results);
const successCount = results.results.filter(r => r.success).length;
const failCount = results.results.length - successCount;
showStatus(
`Batch conversion completed! ${successCount} successful, ${failCount} failed`,
failCount === 0 ? 'success' : 'info'
);
} catch (error) {
showStatus('Batch conversion failed: ' + error.message, 'error');
console.error('Batch conversion error:', error);
} finally {
// Re-enable buttons
document.getElementById('batchConvertBtn').disabled = false;
document.getElementById('clearBatchBtn').disabled = false;
}
}
// Server-side batch conversion function
async function convertBatchOnServer(files) {
const formData = new FormData();
// Append all files
files.forEach(file => {
formData.append('files', file);
});
const response = await fetch('/convert-mathtype-batch', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || `Server error: ${response.status}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Batch conversion failed');
}
return result;
}
// Display batch conversion results
function displayBatchResults(results) {
const resultItems = document.getElementById('resultItems');
resultItems.innerHTML = results.results.map((result, index) => {
const statusClass = result.success ? 'success' : 'error';
const statusIcon = result.success ? '✅' : '❌';
return `
<div class="result-item ${statusClass}">
<div class="result-header">
<span>${statusIcon} ${result.originalName}</span>
<span>${result.success ? 'Success' : 'Failed'}</span>
</div>
${result.error ? `<div style="color: #dc3545; margin-bottom: 10px;">Error: ${result.error}</div>` : ''}
${result.success && result.mathml ? `
<div class="result-mathml">${result.mathml}</div>
<button onclick="copyToClipboard('${escapeHtml(result.mathml)}')" style="margin-top: 10px;">
📋 Copy MathML
</button>
` : ''}
</div>
`;
}).join('');
}
// Helper functions
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML.replace(/'/g, '&apos;');
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text.replace(/&apos;/g, "'")).then(() => {
showStatus('MathML copied to clipboard', 'success');
}).catch(err => {
console.error('Failed to copy: ', err);
showStatus('Failed to copy to clipboard', 'error');
});
}
// File drop functionality
const uploadSection = document.querySelector('.upload-section');
uploadSection.addEventListener('dragover', (e) => {
e.preventDefault();
uploadSection.style.borderColor = '#007bff';
uploadSection.style.backgroundColor = '#f0f8ff';
});
uploadSection.addEventListener('dragleave', (e) => {
e.preventDefault();
uploadSection.style.borderColor = '#ddd';
uploadSection.style.backgroundColor = '#fafafa';
});
uploadSection.addEventListener('drop', (e) => {
e.preventDefault();
uploadSection.style.borderColor = '#ddd';
uploadSection.style.backgroundColor = '#fafafa';
const files = e.dataTransfer.files;
if (files.length > 0) {
document.getElementById('fileInput').files = files;
showStatus('File dropped successfully: ' + files[0].name, 'success');
}
});
</script>
<!-- Optional: Include MathJax for better MathML rendering -->
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js"></script>
<script>
window.MathJax = {
startup: {
ready: () => {
console.log('MathJax is loaded and ready');
MathJax.startup.defaultReady();
}
}
};
</script>
</body>
</html>

View File

@@ -0,0 +1,766 @@
/**
* MathType to MathML Converter (JavaScript Implementation)
*
* This JavaScript implementation follows the same architectural principles
* as the Ruby mathtype_to_mathml gem, providing a multi-stage conversion
* pipeline from MathType binary format to MathML.
*
* Architecture:
* Binary MTEF → XML Structure → Preprocessing → XSLT-like Transform → MathML
*/
class MathTypeToMathMLConverter {
constructor() {
this.characterReplacer = new CharacterReplacer();
this.mover = new ElementMover();
this.transformer = new MathMLTransformer();
}
/**
* Convert MathType binary data to MathML
* @param {ArrayBuffer|Uint8Array} binaryData - MathType binary data
* @returns {Promise<string>} MathML string
*/
async convert(binaryData) {
try {
// Stage 1: Parse binary MTEF to XML structure
const xmlStructure = await this.parseBinaryMTEF(binaryData);
// Stage 2: Preprocessing
this.mover.restructureElements(xmlStructure);
this.characterReplacer.replaceCharacters(xmlStructure);
// Stage 3: Transform to MathML
const mathml = this.transformer.transformToMathML(xmlStructure);
// Stage 4: Cleanup and namespace handling
return this.finalizeMathML(mathml);
} catch (error) {
throw new Error(`MathType conversion failed: ${error.message}`);
}
}
/**
* Parse binary MTEF data to XML-like structure
* Note: This is a simplified implementation. In practice, you would need
* a full MTEF parser or use a WebAssembly port of the mathtype gem.
*/
async parseBinaryMTEF(binaryData) {
// For demo purposes, we'll simulate the XML structure
// In a real implementation, this would parse the actual MTEF binary format
if (binaryData instanceof ArrayBuffer) {
binaryData = new Uint8Array(binaryData);
}
// Placeholder: In reality, this would parse the binary MTEF format
// You could use a WebAssembly port of the Ruby mathtype gem
// or implement a native JavaScript MTEF parser
return this.createSampleXMLStructure();
}
/**
* Create a sample XML structure for demonstration
* This would be replaced by actual MTEF parsing
*/
createSampleXMLStructure() {
return {
type: 'mtef',
equation_options: 'inline',
children: [
{
type: 'slot',
children: [
{
type: 'char',
mt_code_value: '0x0078', // 'x'
typeface: '1'
},
{
type: 'tmpl',
selector: 'tmSUP',
children: [
{
type: 'slot',
children: [
{
type: 'char',
mt_code_value: '0x0032', // '2'
typeface: '2'
}
]
}
]
}
]
}
]
};
}
/**
* Finalize MathML output with proper namespace
*/
finalizeMathML(mathmlContent) {
return `<math xmlns="http://www.w3.org/1998/Math/MathML">${mathmlContent}</math>`;
}
}
/**
* Character Replacement Module
* Handles Unicode character conversion and mathematical symbol mapping
*/
class CharacterReplacer {
constructor() {
this.replacements = this.initializeReplacements();
}
/**
* Initialize character replacement mappings
* Based on the Ruby CharReplacer class
*/
initializeReplacements() {
return {
// Basic Latin digits
'0x0030': { mathmode: '<mn>0</mn>', textmode: '0' },
'0x0031': { mathmode: '<mn>1</mn>', textmode: '1' },
'0x0032': { mathmode: '<mn>2</mn>', textmode: '2' },
'0x0033': { mathmode: '<mn>3</mn>', textmode: '3' },
'0x0034': { mathmode: '<mn>4</mn>', textmode: '4' },
'0x0035': { mathmode: '<mn>5</mn>', textmode: '5' },
'0x0036': { mathmode: '<mn>6</mn>', textmode: '6' },
'0x0037': { mathmode: '<mn>7</mn>', textmode: '7' },
'0x0038': { mathmode: '<mn>8</mn>', textmode: '8' },
'0x0039': { mathmode: '<mn>9</mn>', textmode: '9' },
// Basic Latin letters (uppercase)
'0x0041': { mathmode: '<mi>A</mi>', textmode: 'A' },
'0x0042': { mathmode: '<mi>B</mi>', textmode: 'B' },
'0x0043': { mathmode: '<mi>C</mi>', textmode: 'C' },
'0x0044': { mathmode: '<mi>D</mi>', textmode: 'D' },
'0x0045': { mathmode: '<mi>E</mi>', textmode: 'E' },
'0x0046': { mathmode: '<mi>F</mi>', textmode: 'F' },
// Basic Latin letters (lowercase)
'0x0061': { mathmode: '<mi>a</mi>', textmode: 'a' },
'0x0062': { mathmode: '<mi>b</mi>', textmode: 'b' },
'0x0063': { mathmode: '<mi>c</mi>', textmode: 'c' },
'0x0064': { mathmode: '<mi>d</mi>', textmode: 'd' },
'0x0065': { mathmode: '<mi>e</mi>', textmode: 'e' },
'0x0066': { mathmode: '<mi>f</mi>', textmode: 'f' },
'0x0067': { mathmode: '<mi>g</mi>', textmode: 'g' },
'0x0068': { mathmode: '<mi>h</mi>', textmode: 'h' },
'0x0069': { mathmode: '<mi>i</mi>', textmode: 'i' },
'0x006A': { mathmode: '<mi>j</mi>', textmode: 'j' },
'0x006B': { mathmode: '<mi>k</mi>', textmode: 'k' },
'0x006C': { mathmode: '<mi>l</mi>', textmode: 'l' },
'0x006D': { mathmode: '<mi>m</mi>', textmode: 'm' },
'0x006E': { mathmode: '<mi>n</mi>', textmode: 'n' },
'0x006F': { mathmode: '<mi>o</mi>', textmode: 'o' },
'0x0070': { mathmode: '<mi>p</mi>', textmode: 'p' },
'0x0071': { mathmode: '<mi>q</mi>', textmode: 'q' },
'0x0072': { mathmode: '<mi>r</mi>', textmode: 'r' },
'0x0073': { mathmode: '<mi>s</mi>', textmode: 's' },
'0x0074': { mathmode: '<mi>t</mi>', textmode: 't' },
'0x0075': { mathmode: '<mi>u</mi>', textmode: 'u' },
'0x0076': { mathmode: '<mi>v</mi>', textmode: 'v' },
'0x0077': { mathmode: '<mi>w</mi>', textmode: 'w' },
'0x0078': { mathmode: '<mi>x</mi>', textmode: 'x' },
'0x0079': { mathmode: '<mi>y</mi>', textmode: 'y' },
'0x007A': { mathmode: '<mi>z</mi>', textmode: 'z' },
// Mathematical operators
'0x002B': { mathmode: '<mo>+</mo>', textmode: '+' },
'0x002D': { mathmode: '<mo>-</mo>', textmode: '-' },
'0x002A': { mathmode: '<mo>*</mo>', textmode: '*' },
'0x002F': { mathmode: '<mo>/</mo>', textmode: '/' },
'0x003D': { mathmode: '<mo>=</mo>', textmode: '=' },
'0x003C': { mathmode: '<mo>&lt;</mo>', textmode: '<' },
'0x003E': { mathmode: '<mo>&gt;</mo>', textmode: '>' },
// Parentheses and brackets
'0x0028': { mathmode: '<mo stretchy="false">(</mo>', textmode: '(' },
'0x0029': { mathmode: '<mo stretchy="false">)</mo>', textmode: ')' },
'0x005B': { mathmode: '<mo stretchy="false">[</mo>', textmode: '[' },
'0x005D': { mathmode: '<mo stretchy="false">]</mo>', textmode: ']' },
'0x007B': { mathmode: '<mo>{</mo>', textmode: '{' },
'0x007D': { mathmode: '<mo>}</mo>', textmode: '}' },
// Greek letters (common mathematical symbols)
'0x03B1': { mathmode: '<mi>α</mi>', textmode: 'α' }, // alpha
'0x03B2': { mathmode: '<mi>β</mi>', textmode: 'β' }, // beta
'0x03B3': { mathmode: '<mi>γ</mi>', textmode: 'γ' }, // gamma
'0x03B4': { mathmode: '<mi>δ</mi>', textmode: 'δ' }, // delta
'0x03B5': { mathmode: '<mi>ε</mi>', textmode: 'ε' }, // epsilon
'0x03B8': { mathmode: '<mi>θ</mi>', textmode: 'θ' }, // theta
'0x03BB': { mathmode: '<mi>λ</mi>', textmode: 'λ' }, // lambda
'0x03BC': { mathmode: '<mi>μ</mi>', textmode: 'μ' }, // mu
'0x03C0': { mathmode: '<mi>π</mi>', textmode: 'π' }, // pi
'0x03C3': { mathmode: '<mi>σ</mi>', textmode: 'σ' }, // sigma
'0x03C4': { mathmode: '<mi>τ</mi>', textmode: 'τ' }, // tau
'0x03C6': { mathmode: '<mi>φ</mi>', textmode: 'φ' }, // phi
'0x03C7': { mathmode: '<mi>χ</mi>', textmode: 'χ' }, // chi
'0x03C8': { mathmode: '<mi>ψ</mi>', textmode: 'ψ' }, // psi
'0x03C9': { mathmode: '<mi>ω</mi>', textmode: 'ω' }, // omega
// Mathematical symbols
'0x221E': { mathmode: '<mo>∞</mo>', textmode: '∞' }, // infinity
'0x2211': { mathmode: '<mo>∑</mo>', textmode: '∑' }, // summation
'0x222B': { mathmode: '<mo>∫</mo>', textmode: '∫' }, // integral
'0x2206': { mathmode: '<mo>∆</mo>', textmode: '∆' }, // delta
'0x2207': { mathmode: '<mo>∇</mo>', textmode: '∇' }, // nabla
'0x2208': { mathmode: '<mo>∈</mo>', textmode: '∈' }, // element of
'0x2209': { mathmode: '<mo>∉</mo>', textmode: '∉' }, // not element of
'0x220B': { mathmode: '<mo>∋</mo>', textmode: '∋' }, // contains
'0x2260': { mathmode: '<mo>≠</mo>', textmode: '≠' }, // not equal
'0x2264': { mathmode: '<mo>≤</mo>', textmode: '≤' }, // less than or equal
'0x2265': { mathmode: '<mo>≥</mo>', textmode: '≥' }, // greater than or equal
'0x00B1': { mathmode: '<mo>±</mo>', textmode: '±' }, // plus-minus
'0x00D7': { mathmode: '<mo>×</mo>', textmode: '×' }, // multiplication
'0x00F7': { mathmode: '<mo>÷</mo>', textmode: '÷' }, // division
};
}
/**
* Replace characters in the XML structure
*/
replaceCharacters(xmlStructure) {
this.traverseAndReplace(xmlStructure);
}
/**
* Recursively traverse and replace characters
*/
traverseAndReplace(node) {
if (node.type === 'char') {
const replacement = this.getCharacterReplacement(node);
if (replacement) {
node.mathml = replacement;
}
}
if (node.children) {
node.children.forEach(child => this.traverseAndReplace(child));
}
}
/**
* Get character replacement based on mt_code_value
*/
getCharacterReplacement(charNode) {
const codeValue = charNode.mt_code_value;
const isTextMode = charNode.variation === 'textmode';
const replacement = this.replacements[codeValue];
if (!replacement) {
// Handle character ranges
return this.handleCharacterRange(codeValue, isTextMode);
}
return isTextMode ? replacement.textmode : replacement.mathmode;
}
/**
* Handle character ranges for undefined individual characters
*/
handleCharacterRange(codeValue, isTextMode) {
const codeNum = parseInt(codeValue, 16);
// Numbers (0-9)
if (codeNum >= 0x0030 && codeNum <= 0x0039) {
const char = String.fromCharCode(codeNum);
return isTextMode ? char : `<mn>${char}</mn>`;
}
// Uppercase letters (A-Z)
if (codeNum >= 0x0041 && codeNum <= 0x005A) {
const char = String.fromCharCode(codeNum);
return isTextMode ? char : `<mi>${char}</mi>`;
}
// Lowercase letters (a-z)
if (codeNum >= 0x0061 && codeNum <= 0x007A) {
const char = String.fromCharCode(codeNum);
return isTextMode ? char : `<mi>${char}</mi>`;
}
// Mathematical operators range
if (codeNum >= 0x2200 && codeNum <= 0x22FF) {
const char = String.fromCharCode(codeNum);
return isTextMode ? char : `<mo>${char}</mo>`;
}
// Default fallback
try {
const char = String.fromCharCode(codeNum);
return isTextMode ? char : `<mi>${char}</mi>`;
} catch (e) {
return 'Unsupported character';
}
}
}
/**
* Element Mover Module
* Handles structural reorganization for subscripts/superscripts
*/
class ElementMover {
constructor() {
this.subscriptSelectors = ['tmSUP', 'tmSUB', 'tmSUBSUP'];
this.parenthesesPairs = {
'0x0028': '0x0029', // ( )
'0x005B': '0x005D', // [ ]
'0x007B': '0x007D', // { }
};
}
/**
* Restructure elements to handle MathType vs MathML differences
*/
restructureElements(xmlStructure) {
this.moveSubscriptsAndSuperscripts(xmlStructure);
this.invertEmbellishments(xmlStructure);
}
/**
* Move base elements into subscript/superscript containers
*/
moveSubscriptsAndSuperscripts(node) {
if (node.children) {
for (let i = 0; i < node.children.length; i++) {
const child = node.children[i];
if (child.type === 'tmpl' && this.subscriptSelectors.includes(child.selector)) {
// Find preceding element(s) to use as base
const baseElements = this.findBaseElements(node.children, i);
if (baseElements.length > 0) {
// Create new slot for base elements
const baseSlot = {
type: 'slot',
children: baseElements
};
// Insert base slot as first child of template
child.children = child.children || [];
child.children.unshift(baseSlot);
// Remove base elements from original position
baseElements.forEach(elem => {
const index = node.children.indexOf(elem);
if (index > -1) {
node.children.splice(index, 1);
if (index < i) i--; // Adjust loop index
}
});
}
}
// Recursively process children
this.moveSubscriptsAndSuperscripts(child);
}
}
}
/**
* Find base elements for subscript/superscript
*/
findBaseElements(siblings, templateIndex) {
const baseElements = [];
// Look for preceding elements
for (let i = templateIndex - 1; i >= 0; i--) {
const sibling = siblings[i];
// For simple cases, take the immediately preceding element
if (sibling.type === 'char' ||
(sibling.type === 'tmpl' && !this.subscriptSelectors.includes(sibling.selector))) {
baseElements.unshift(sibling);
break;
}
}
return baseElements;
}
/**
* Invert embellishment structures: char[embell] -> embell[char]
*/
invertEmbellishments(node) {
if (node.type === 'char' && node.embellishment) {
// Move char content into embellishment
const embellishment = node.embellishment;
embellishment.children = embellishment.children || [];
embellishment.children.push({
type: 'char',
mt_code_value: node.mt_code_value,
typeface: node.typeface
});
// Replace char node with embellishment
Object.keys(node).forEach(key => delete node[key]);
Object.assign(node, embellishment);
}
if (node.children) {
node.children.forEach(child => this.invertEmbellishments(child));
}
}
}
/**
* MathML Transformer Module
* Converts preprocessed XML structure to MathML
*/
class MathMLTransformer {
constructor() {
this.templateHandlers = this.initializeTemplateHandlers();
}
/**
* Initialize template transformation handlers
*/
initializeTemplateHandlers() {
return {
// Fractions
'tmFRACT': (node) => this.handleFraction(node),
// Subscripts and superscripts
'tmSUP': (node) => this.handleSuperscript(node),
'tmSUB': (node) => this.handleSubscript(node),
'tmSUBSUP': (node) => this.handleSubSuperscript(node),
// Roots
'tmROOT': (node) => this.handleRoot(node),
'tmSQRT': (node) => this.handleSquareRoot(node),
// Large operators
'tmSUM': (node) => this.handleSummation(node),
'tmINT': (node) => this.handleIntegral(node),
// Matrices
'tmMATRIX': (node) => this.handleMatrix(node),
// Fences (parentheses, brackets)
'tmPARENS': (node) => this.handleParentheses(node),
'tmBRACK': (node) => this.handleBrackets(node),
'tmBRACE': (node) => this.handleBraces(node),
};
}
/**
* Transform XML structure to MathML
*/
transformToMathML(xmlStructure) {
return this.processNode(xmlStructure);
}
/**
* Process a single node
*/
processNode(node) {
switch (node.type) {
case 'mtef':
return this.processMTEF(node);
case 'slot':
return this.processSlot(node);
case 'char':
return this.processCharacter(node);
case 'tmpl':
return this.processTemplate(node);
default:
return this.processChildren(node);
}
}
/**
* Process MTEF root node
*/
processMTEF(node) {
const content = this.processChildren(node);
const displayMode = node.equation_options === 'block' ? ' display="block"' : '';
return content; // Math wrapper added in finalizeMathML
}
/**
* Process slot (grouping) node
*/
processSlot(node) {
const content = this.processChildren(node);
return content ? `<mrow>${content}</mrow>` : '';
}
/**
* Process character node
*/
processCharacter(node) {
return node.mathml || `<mi>${String.fromCharCode(parseInt(node.mt_code_value, 16))}</mi>`;
}
/**
* Process template node
*/
processTemplate(node) {
const handler = this.templateHandlers[node.selector];
if (handler) {
return handler(node);
}
// Default processing for unknown templates
return this.processChildren(node);
}
/**
* Process children nodes
*/
processChildren(node) {
if (!node.children || node.children.length === 0) {
return '';
}
return node.children
.map(child => this.processNode(child))
.filter(result => result !== '')
.join('');
}
// Template handlers
/**
* Handle fraction templates
*/
handleFraction(node) {
const children = node.children || [];
const numerator = children[0] ? this.processNode(children[0]) : '';
const denominator = children[1] ? this.processNode(children[1]) : '';
if (node.variation === 'tvFR_SLASH') {
return `<mfrac bevelled="true">${numerator}${denominator}</mfrac>`;
} else if (node.variation === 'tvFR_SMALL') {
return `<mstyle scriptlevel="+1"><mfrac>${numerator}${denominator}</mfrac></mstyle>`;
} else {
return `<mfrac>${numerator}${denominator}</mfrac>`;
}
}
/**
* Handle superscript templates
*/
handleSuperscript(node) {
const children = node.children || [];
const base = children[0] ? this.processNode(children[0]) : '';
const superscript = children[1] ? this.processNode(children[1]) : '';
if (node.variation === 'tvSU_PRECEDES') {
return `<mmultiscripts>${base}<mprescripts/><none/>${superscript}</mmultiscripts>`;
} else {
return `<msup>${base}${superscript}</msup>`;
}
}
/**
* Handle subscript templates
*/
handleSubscript(node) {
const children = node.children || [];
const base = children[0] ? this.processNode(children[0]) : '';
const subscript = children[1] ? this.processNode(children[1]) : '';
if (node.variation === 'tvSU_PRECEDES') {
return `<mmultiscripts>${base}<mprescripts/>${subscript}<none/></mmultiscripts>`;
} else {
return `<msub>${base}${subscript}</msub>`;
}
}
/**
* Handle combined subscript-superscript templates
*/
handleSubSuperscript(node) {
const children = node.children || [];
const base = children[0] ? this.processNode(children[0]) : '';
const subscript = children[1] ? this.processNode(children[1]) : '';
const superscript = children[2] ? this.processNode(children[2]) : '';
if (node.variation === 'tvSU_PRECEDES') {
return `<mmultiscripts>${base}<mprescripts/>${subscript}${superscript}</mmultiscripts>`;
} else {
return `<msubsup>${base}${subscript}${superscript}</msubsup>`;
}
}
/**
* Handle square root templates
*/
handleSquareRoot(node) {
const children = node.children || [];
const content = children[0] ? this.processNode(children[0]) : '';
return `<msqrt>${content}</msqrt>`;
}
/**
* Handle nth root templates
*/
handleRoot(node) {
const children = node.children || [];
const radicand = children[0] ? this.processNode(children[0]) : '';
const index = children[1] ? this.processNode(children[1]) : '';
return `<mroot>${radicand}${index}</mroot>`;
}
/**
* Handle summation templates
*/
handleSummation(node) {
const children = node.children || [];
const base = '<mo>∑</mo>';
if (children.length === 1) {
return `<mrow>${base}${this.processNode(children[0])}</mrow>`;
} else if (children.length === 2) {
return `<munder>${base}${this.processNode(children[0])}</munder>`;
} else if (children.length === 3) {
return `<munderover>${base}${this.processNode(children[1])}${this.processNode(children[2])}</munderover>`;
}
return base;
}
/**
* Handle integral templates
*/
handleIntegral(node) {
const children = node.children || [];
const base = '<mo>∫</mo>';
if (children.length === 1) {
return `<mrow>${base}${this.processNode(children[0])}</mrow>`;
} else if (children.length === 2) {
return `<munder>${base}${this.processNode(children[0])}</munder>`;
} else if (children.length === 3) {
return `<munderover>${base}${this.processNode(children[1])}${this.processNode(children[2])}</munderover>`;
}
return base;
}
/**
* Handle matrix templates
*/
handleMatrix(node) {
const children = node.children || [];
let tableContent = '';
children.forEach(row => {
if (row.type === 'mtr') {
tableContent += '<mtr>';
if (row.children) {
row.children.forEach(cell => {
if (cell.type === 'mtd') {
tableContent += `<mtd>${this.processNode(cell)}</mtd>`;
}
});
}
tableContent += '</mtr>';
}
});
return `<mtable>${tableContent}</mtable>`;
}
/**
* Handle parentheses templates
*/
handleParentheses(node) {
const content = this.processChildren(node);
return `<mrow><mo>(</mo>${content}<mo>)</mo></mrow>`;
}
/**
* Handle bracket templates
*/
handleBrackets(node) {
const content = this.processChildren(node);
return `<mrow><mo>[</mo>${content}<mo>]</mo></mrow>`;
}
/**
* Handle brace templates
*/
handleBraces(node) {
const content = this.processChildren(node);
return `<mrow><mo>{</mo>${content}<mo>}</mo></mrow>`;
}
}
// Utility functions for working with binary data
/**
* Load binary file from input element
*/
function loadBinaryFile(fileInput) {
return new Promise((resolve, reject) => {
const file = fileInput.files[0];
if (!file) {
reject(new Error('No file selected'));
return;
}
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = (e) => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file);
});
}
/**
* Convert ArrayBuffer to hex string for debugging
*/
function arrayBufferToHex(buffer) {
const uint8Array = new Uint8Array(buffer);
return Array.from(uint8Array)
.map(byte => byte.toString(16).padStart(2, '0'))
.join(' ');
}
// Export for Node.js or browser usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
MathTypeToMathMLConverter,
CharacterReplacer,
ElementMover,
MathMLTransformer,
loadBinaryFile,
arrayBufferToHex
};
} else if (typeof window !== 'undefined') {
window.MathTypeToMathMLConverter = MathTypeToMathMLConverter;
window.CharacterReplacer = CharacterReplacer;
window.ElementMover = ElementMover;
window.MathMLTransformer = MathMLTransformer;
window.loadBinaryFile = loadBinaryFile;
window.arrayBufferToHex = arrayBufferToHex;
}
// Example usage:
/*
// In a web page:
const converter = new MathTypeToMathMLConverter();
// From file input
document.getElementById('fileInput').addEventListener('change', async (e) => {
try {
const binaryData = await loadBinaryFile(e.target);
const mathml = await converter.convert(binaryData);
document.getElementById('output').innerHTML = mathml;
} catch (error) {
console.error('Conversion failed:', error);
}
});
// From existing binary data
const binaryData = new Uint8Array([...]); // Your binary MTEF data
converter.convert(binaryData).then(mathml => {
console.log('Generated MathML:', mathml);
});
*/

View File

@@ -0,0 +1,387 @@
// Node.js server for MathType to MathML conversion
// This uses the actual Ruby gem to parse binary files
const express = require('express');
const multer = require('multer');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const crypto = require('crypto'); // Added for filename generation
const app = express();
const port = 3000;
// Define the correct paths relative to this file location
const MATHTYPE_DIR = __dirname; // Current directory is the mathtype_to_mathml folder
const CONVERTER_SCRIPT = path.join(MATHTYPE_DIR, 'server', 'converter.rb');
const BATCH_CONVERTER_SCRIPT = path.join(MATHTYPE_DIR, 'server', 'batch_converter.rb');
const PUBLIC_DIR = path.join(MATHTYPE_DIR, 'public');
// Enable CORS for cross-origin requests
app.use(cors());
// Configure multer for file uploads with proper extension preservation
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
// Preserve the original file extension for proper format detection
const ext = path.extname(file.originalname);
const name = crypto.randomBytes(16).toString('hex');
cb(null, name + ext);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});
// Serve static files from the correct public directory
app.use(express.static(PUBLIC_DIR));
// Convert MathType binary to MathML using Ruby gem
app.post('/convert-mathtype', upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const filePath = req.file.path;
console.log(`File uploaded: ${req.file.originalname}`);
console.log(`Temp file path: ${filePath}`);
console.log(`File exists: ${fs.existsSync(filePath)}`);
try {
// Call Ruby script to convert the file
const mathml = await convertWithRubyGem(filePath);
// Clean up uploaded file
fs.unlinkSync(filePath);
res.json({
success: true,
mathml: mathml,
originalName: req.file.originalname
});
} catch (error) {
// Clean up uploaded file
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
console.error('Conversion error:', error);
res.status(500).json({
error: 'Conversion failed',
message: error.message
});
}
});
// Function to call Ruby gem for conversion
function convertWithRubyGem(filePath) {
return new Promise((resolve, reject) => {
// Check if converter script exists
if (!fs.existsSync(CONVERTER_SCRIPT)) {
reject(new Error(`Converter script not found at: ${CONVERTER_SCRIPT}`));
return;
}
// Convert relative path to absolute path since Ruby script runs from different directory
const absoluteFilePath = path.resolve(filePath);
console.log(`Converting relative path: ${filePath}`);
console.log(`To absolute path: ${absoluteFilePath}`);
console.log(`Absolute file exists: ${fs.existsSync(absoluteFilePath)}`);
// Add debugging for working directory
console.log(`Working directory (MATHTYPE_DIR): ${MATHTYPE_DIR}`);
console.log(`Working directory exists: ${fs.existsSync(MATHTYPE_DIR)}`);
console.log(`Converter script path: ${CONVERTER_SCRIPT}`);
console.log(`Converter script exists: ${fs.existsSync(CONVERTER_SCRIPT)}`);
// Check if the file actually exists
if (!fs.existsSync(absoluteFilePath)) {
reject(new Error(`Upload file not found at: ${absoluteFilePath}`));
return;
}
// Validate working directory exists
if (!fs.existsSync(MATHTYPE_DIR)) {
reject(new Error(`Working directory not found at: ${MATHTYPE_DIR}`));
return;
}
// Execute Ruby script with bundle exec from the correct directory
// Removed shell: true to fix path issues with spaces
const ruby = spawn('bundle', ['exec', 'ruby', CONVERTER_SCRIPT, absoluteFilePath], {
cwd: MATHTYPE_DIR,
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
ruby.stdout.on('data', (data) => {
output += data.toString();
});
ruby.stderr.on('data', (data) => {
errorOutput += data.toString();
});
ruby.on('close', (code) => {
console.log(`Ruby process exited with code: ${code}`);
if (errorOutput) {
console.log(`Ruby stderr output: ${errorOutput}`);
}
if (code !== 0) {
console.error(`Ruby script exited with code ${code}: ${errorOutput}`);
// Provide more specific error messages based on the error content
let errorMessage = 'Ruby conversion failed';
if (errorOutput.includes('undefined method')) {
errorMessage = 'MathType file parsing error - this may be due to an incompatible or corrupted MathType file format';
} else if (errorOutput.includes('No such file')) {
errorMessage = 'File not found during conversion';
} else if (errorOutput.includes('File is empty')) {
errorMessage = 'The uploaded file appears to be empty';
} else if (errorOutput.includes('Invalid file format')) {
errorMessage = 'The file does not appear to be a valid MathType binary file';
} else if (errorOutput.includes('Permission denied')) {
errorMessage = 'Permission error accessing the file';
} else if (errorOutput.includes('command not found') || errorOutput.includes('bundle: not found')) {
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed';
} else if (errorOutput.includes('Could not locate Gemfile')) {
errorMessage = 'Gemfile not found - please run bundle install from the project directory';
}
reject(new Error(`${errorMessage}: ${errorOutput || 'Unknown error'}`));
} else {
const mathML = output.trim();
if (!mathML) {
reject(new Error('Conversion resulted in empty MathML.'));
} else {
console.log('Ruby conversion successful, MathML length:', mathML.length);
resolve(mathML);
}
}
});
ruby.on('error', (err) => {
console.error('Failed to start Ruby process:', err);
console.error('Error details:', {
code: err.code,
errno: err.errno,
syscall: err.syscall,
path: err.path
});
let errorMessage = `Failed to start Ruby process: ${err.message}`;
if (err.code === 'ENOENT') {
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed and in your PATH';
}
reject(new Error(errorMessage));
});
});
}
// Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
message: 'MathType converter server is running',
paths: {
mathtypeDir: MATHTYPE_DIR,
converterScript: CONVERTER_SCRIPT,
publicDir: PUBLIC_DIR
}
});
});
// Batch convert multiple MathType files to MathML
app.post('/convert-mathtype-batch', upload.array('files', 50), async (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ error: 'No files uploaded' });
}
console.log(`Batch conversion requested for ${req.files.length} files`);
try {
// Prepare file paths for batch processing
const filePaths = req.files.map(file => path.resolve(file.path));
const fileInfo = req.files.map(file => ({
originalName: file.originalname,
tempPath: file.path,
size: file.size
}));
console.log('Files to process:', fileInfo.map(f => f.originalName));
// Call Ruby batch converter
const results = await convertBatchWithRubyGem(filePaths);
// Clean up uploaded files
req.files.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
// Combine results with file info
const response = {
success: true,
totalFiles: req.files.length,
successful: results.successful,
failed: results.failed,
results: Object.entries(results.results).map(([filePath, result], index) => ({
originalName: fileInfo[index].originalName,
success: result.success,
mathml: result.mathml,
error: result.error,
fileSize: result.file_size || fileInfo[index].size
}))
};
res.json(response);
} catch (error) {
// Clean up uploaded files in case of error
req.files.forEach(file => {
if (fs.existsSync(file.path)) {
fs.unlinkSync(file.path);
}
});
console.error('Batch conversion error:', error);
res.status(500).json({
error: 'Batch conversion failed',
message: error.message
});
}
});
// Function to call Ruby batch converter
function convertBatchWithRubyGem(filePaths) {
return new Promise((resolve, reject) => {
// Check if batch converter script exists
if (!fs.existsSync(BATCH_CONVERTER_SCRIPT)) {
reject(new Error(`Batch converter script not found at: ${BATCH_CONVERTER_SCRIPT}`));
return;
}
// Validate working directory exists
if (!fs.existsSync(MATHTYPE_DIR)) {
reject(new Error(`Working directory not found at: ${MATHTYPE_DIR}`));
return;
}
console.log(`Starting batch conversion of ${filePaths.length} files`);
console.log(`Using batch converter: ${BATCH_CONVERTER_SCRIPT}`);
// Execute Ruby batch script
const args = ['exec', 'ruby', BATCH_CONVERTER_SCRIPT, ...filePaths];
const ruby = spawn('bundle', args, {
cwd: MATHTYPE_DIR,
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
ruby.stdout.on('data', (data) => {
output += data.toString();
});
ruby.stderr.on('data', (data) => {
errorOutput += data.toString();
});
ruby.on('close', (code) => {
console.log(`Ruby batch process exited with code: ${code}`);
if (errorOutput) {
console.log(`Ruby stderr output: ${errorOutput}`);
}
if (code !== 0) {
console.error(`Ruby batch script exited with code ${code}: ${errorOutput}`);
reject(new Error(`Batch conversion failed: ${errorOutput || 'Unknown error'}`));
} else {
try {
const results = JSON.parse(output.trim());
console.log(`Batch conversion completed: ${results.successful}/${results.total_files} successful`);
resolve(results);
} catch (parseError) {
console.error('Failed to parse batch conversion results:', parseError);
reject(new Error(`Failed to parse batch conversion results: ${parseError.message}`));
}
}
});
ruby.on('error', (err) => {
console.error('Failed to start Ruby batch process:', err);
let errorMessage = `Failed to start Ruby batch process: ${err.message}`;
if (err.code === 'ENOENT') {
errorMessage = 'Bundle command not found - please ensure Ruby bundler is installed and in your PATH';
}
reject(new Error(errorMessage));
});
// Set timeout for the entire batch (5 minutes)
const timeout = setTimeout(() => {
ruby.kill('SIGTERM');
reject(new Error('Batch conversion timeout (5 minutes)'));
}, 5 * 60 * 1000);
ruby.on('close', () => {
clearTimeout(timeout);
});
});
}
// Diagnostic endpoint for testing file uploads
app.post('/test-upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const filePath = req.file.path;
const absoluteFilePath = path.resolve(filePath);
// Get file info
const stats = fs.statSync(absoluteFilePath);
// Clean up uploaded file immediately after getting info
fs.unlinkSync(filePath);
res.json({
success: true,
fileInfo: {
originalName: req.file.originalname,
size: stats.size,
mimetype: req.file.mimetype,
encoding: req.file.encoding,
tempPath: filePath,
absolutePath: absoluteFilePath,
existed: true
},
message: 'File upload test successful - file was received and cleaned up'
});
});
// Start server
app.listen(port, () => {
console.log(`MathType converter server running at http://localhost:${port}`);
console.log(`Using MathType directory: ${MATHTYPE_DIR}`);
console.log(`Converter script: ${CONVERTER_SCRIPT}`);
console.log(`Public directory: ${PUBLIC_DIR}`);
console.log('Make sure you have the mathtype_to_mathml Ruby gem installed:');
console.log(` cd "${MATHTYPE_DIR}" && bundle install`);
console.log(`Demo page available at: http://localhost:${port}/mathtype_converter_demo.html`);
});
module.exports = app;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
# Keep this directory in git

View File

@@ -0,0 +1,2 @@
--format documentation
--color

View File

@@ -0,0 +1 @@
2.2.2

View File

@@ -0,0 +1,3 @@
language: ruby
rvm:
- 2.2.2

View File

@@ -0,0 +1,2 @@
--color
--require spec_helper

View File

@@ -0,0 +1,28 @@
language: ruby
matrix:
include:
- rvm: 2.1
before_script: mv gemspecs/mathtype.ruby-2-2.gemspec mathtype.gemspec
- rvm: 2.2
before_script: mv gemspecs/mathtype.ruby-2-2.gemspec mathtype.gemspec
- rvm: 2.3
before_install:
- gem update --system
- gem install bundler
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
- rvm: 2.4
before_install:
- gem update --system
- gem install bundler
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
- rvm: 2.5
before_install:
- gem update --system
- gem install bundler
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec
- rvm: 2.6
before_install:
- gem update --system
- gem install bundler
before_script: mv gemspecs/mathtype.ruby-2-6.gemspec mathtype.gemspec

View File

@@ -0,0 +1,4 @@
source 'https://rubygems.org'
# Specify your gem's dependencies in mathtype.gemspec
gemspec

View File

@@ -0,0 +1,22 @@
Copyright (c) 2015 Jure Triglav
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,43 @@
# What does the `mathtype` gem do?
This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'mathtype'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install mathtype
## Usage
To convert a MathType binary equation (e.g. extracted from a Word document) to an XML form:
```
xml = Mathtype::Converter.new("equation1.bin") # to get the Nokogiri XML object
puts xml.to_xml # to get the string XML representation
```
# Testing
Run `bundle exec rspec` to run specs.
# Maintainers
- Sebastian Bulka ([sbulka](https://github.com/sbulka))
- Jure Triglav ([jure](https://github.com/jure))
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

View File

@@ -0,0 +1,10 @@
require "bundler/gem_tasks"
task :default => :spec
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
rescue LoadError
end

View File

@@ -0,0 +1,28 @@
# coding: utf-8
lib = File.expand_path('../../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mathtype/version'
Gem::Specification.new do |spec|
spec.name = "mathtype"
spec.version = Mathtype::VERSION
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
spec.homepage = ""
spec.license = "MIT"
spec.files = Dir.glob("{lib,spec}/**/*")
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "> 1.7"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.3"
spec.add_development_dependency "pry", "~> 0.10"
spec.add_dependency "bindata", "~> 2.1"
spec.add_dependency "nokogiri", "~> 1.6"
spec.add_dependency "ruby-ole", "~> 1.2"
end

View File

@@ -0,0 +1,28 @@
# coding: utf-8
lib = File.expand_path('../../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mathtype/version'
Gem::Specification.new do |spec|
spec.name = "mathtype"
spec.version = Mathtype::VERSION
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
spec.homepage = ""
spec.license = "MIT"
spec.files = Dir.glob("{lib,spec}/**/*")
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.3"
spec.add_development_dependency "pry", "~> 0.10"
spec.add_dependency "bindata", "~> 2.1"
spec.add_dependency "nokogiri", "~> 1.6"
spec.add_dependency "ruby-ole", "~> 1.2"
end

View File

@@ -0,0 +1,30 @@
module Mathtype
class EpsFileParser < FileParser
def initialize(path)
super(path)
extract_mtef_from_epscomment(@raw)
end
def extract_mtef_from_epscomment(comment)
string64 = @raw.gsub /\r/, ""
# d for delimiter
d = string64[(/MathType(.)MTEF\1/ =~ string64) + 8]
matches = string64.match /.*MathType#{d}MTEF#{d}(.)#{d}(.)#{d}(.)(.)(.*)#{d}([0-9A-F]{4})#{d}.*/m
leading_chrs = matches[1].to_i
trailing_chrs = matches[2].to_i - 1
a64_chr1 = matches[3]
a64_chr2 = matches[4]
A64[a64_chr1] = 62
A64[a64_chr2] = 63
A64[62] = a64_chr1
A64[63] = a64_chr2
string64 = matches[5]
mtchecksum = matches[6]
string64.gsub! /.{#{trailing_chrs}}(\n).{#{leading_chrs}}/m, ""
@equation = decode string64
unless checksum == mtchecksum
raise ArgumentError, "Checksums do not match, expected #{mtchecksum} but got #{checksum}"
end
end
end
end

View File

@@ -0,0 +1,15 @@
require "bindata"
require "ole/storage"
module Mathtype
class OleFileParser < FileParser
def initialize(path)
read_from_file(path)
end
def read_from_file(path)
ole = Ole::Storage.open(path, "rb+")
@equation = ole.file.read("Equation Native")[28..-1]
ole.close
end
end
end

View File

@@ -0,0 +1,102 @@
module Mathtype
class FileParser
attr_reader :raw
attr_reader :equation
def initialize(path)
read_from_file(path)
end
def checksum
(@equation.bytes.reduce(:+) & 0xFFFF).to_s(16).upcase
end
def read_from_file(path)
f = File.open(path, "rb")
@raw = f.read
f.close
end
A64 =
{
"a" => 0, "b" => 1, "c" => 2, "d" => 3, "e" => 4,
"f" => 5, "g" => 6, "h" => 7, "i" => 8, "j" => 9,
"k" => 10, "l" => 11, "m" => 12, "n" => 13, "o" => 14, "p" => 15,
"q" => 16, "r" => 17, "s" => 18, "t" => 19, "u" => 20, "v" => 21,
"w" => 22, "x" => 23, "y" => 24, "z" => 25, "A" => 26, "B" => 27,
"C" => 28, "D" => 29, "E" => 30, "F" => 31, "G" => 32, "H" => 33,
"I" => 34, "J" => 35, "K" => 36, "L" => 37, "M" => 38, "N" => 39,
"O" => 40, "P" => 41, "Q" => 42, "R" => 43, "S" => 44, "T" => 45,
"U" => 46, "V" => 47, "W" => 48, "X" => 49, "Y" => 50, "Z" => 51,
"0" => 52, "1" => 53, "2" => 54, "3" => 55, "4" => 56, "5" => 57,
"6" => 58, "7" => 59, "8" => 60, "9" => 61, "+" => 62, "-" => 63,
0 => "a", 1 => "b", 2 => "c", 3 => "d", 4 => "e",
5 => "f", 6 => "g", 7 => "h", 8 => "i", 9 => "j",
10 => "k", 11 => "l", 12 => "m", 13 => "n", 14 => "o", 15 => "p",
16 => "q", 17 => "r", 18 => "s", 19 => "t", 20 => "u", 21 => "v",
22 => "w", 23 => "x", 24 => "y", 25 => "z", 26 => "A", 27 => "B",
28 => "C", 29 => "D", 30 => "E", 31 => "F", 32 => "G", 33 => "H",
34 => "I", 35 => "J", 36 => "K", 37 => "L", 38 => "M", 39 => "N",
40 => "O", 41 => "P", 42 => "Q", 43 => "R", 44 => "S", 45 => "T",
46 => "U", 47 => "V", 48 => "W", 49 => "X", 50 => "Y", 51 => "Z",
52 => "0", 53 => "1", 54 => "2", 55 => "3", 56 => "4", 57 => "5",
58 => "6", 59 => "7", 60 => "8", 61 => "9", 62 => "+", 63 => "-"
}
def decode(string64)
#decode from base64 mtef to binary mtef
l = string64.length
i = b0 = b1 = carry = 0
out = []
while i < l
chr = A64[string64[i]]
case i % 4
when 0
b0 = chr
b1 = 0
when 1
b0 += ((chr & 3) << 6)
b1 = ((chr & 0xfc) >> 2)
out << b0
when 2
b0 = ((chr & 0x30) >> 4)
b1 += ((chr & 0xf) << 4)
when 3
b0 += chr << 2
out << b1 << b0
b0=b1=0
end
i += 1
end
out << b0 if i % 2 == 1
return out.pack("C*")
end
def encode(mtef)
#encode from binary mtef to base64 mtef
out = ""
bytes = mtef.bytes
i = shft = 0
l = bytes.length
carry = 0
while i < l
chr = bytes[i] << shft
chr = chr | carry
out << A64[chr & 0x3f]
carry = chr >> 6
i += 1
shft = (i % 3) * 2
if shft == 0
out << A64[carry]
carry = 0
end
end
return out
end
end
end
require_relative "ole.rb"
require_relative "wmf.rb"
require_relative "eps.rb"

View File

@@ -0,0 +1,28 @@
module Mathtype
class WmfFileParser < FileParser
def initialize(path)
super(path)
extract_mtef_from_mfcomment(@raw)
end
def extract_mtef_from_mfcomment(comment)
match = comment.match("MathTypeUU")
if match
lenpos = match.end(0)
len = comment[lenpos..lenpos+1].unpack("H*")[0].to_i 16
start = lenpos + 2
else
match = comment.match("AppsMFCC\x01")
raise ::NotImplementedError, "No MathType Equation found in wmf" unless match
len_start = match.end(0)
totallen = comment[len_start..len_start + 3].reverse.chars.rotate(-1).join.unpack("H*")[0].to_i 16
datalen = comment[len_start + 4 .. len_start + 7].reverse.chars.rotate(-1).join.unpack("H*")[0].to_i 16
signature = comment[len_start + 8.. -1].match(/.+?\x00/)
raise ::NotImplementedError, "Equation split over multiple comments" unless totallen == datalen
start = len_start + 8 + signature.end(0)
len = datalen
end
@equation = comment[start..(start + len)]
end
end
end

View File

@@ -0,0 +1,86 @@
require_relative "mathtype/version"
require "nokogiri"
require_relative "file_parser/parser.rb"
require_relative "records/mtef.rb"
require_relative "records3/mtef.rb"
require_relative "records5/mtef.rb"
module Mathtype
class Converter
attr_reader :parser
attr_reader :xml
attr_reader :builder
attr_reader :version
def initialize(equation)
set_parser(equation)
raise ::NotImplementedError, "Equation format not supported. Only .eps, .wmf and .bin (OLE-Object) currently supported, supplied was: #{equation}" unless @parser.equation
@version = @parser.equation[0].unpack('C')[0].to_i
raise ::NotImplementedError, "Only MTEF Version 3 and 5 currently supported, version is #{version}" unless (version==3 or version==5)
case @version
when 3
data = Mathtype3::Equation.read(@parser.equation).snapshot
when 5
data = Mathtype5::Equation.read(@parser.equation).snapshot
end
@builder = Nokogiri::XML::Builder.new do |xml|
@xml = xml
xml.root do
process(object: data)
end
end
end
def set_parser(equation)
if equation.end_with?(".bin")
@parser = Mathtype::OleFileParser.new equation
elsif equation.end_with?(".wmf")
@parser = Mathtype::WmfFileParser.new equation
else
@parser = Mathtype::EpsFileParser.new equation
end
end
def to_xml
@builder.to_xml
end
def process(element: "mtef", object:)
if object.is_a? Hash
case @version
when 3
name = Mathtype3::RECORD_NAMES[object[:record_type]]
else
name = Mathtype5::RECORD_NAMES[object[:record_type]]
end
if name
xml.send(name) do
(object[:payload] || {}).each do |k, v|
process(element: k, object: v)
end
end
else
xml.send(element) do
object.each do |k, v|
process(element: k, object: v)
end
end
end
elsif object.is_a? Array
object.each do |a|
process(element: element, object: a)
end
else
process_final_element(element, object)
end
end
def process_final_element(element, object)
if object.is_a? Hash
xml.send(element, object)
else
xml.send(element) { xml.text object }
end
end
end
end

View File

@@ -0,0 +1,3 @@
module Mathtype
VERSION = "0.1.0"
end

View File

@@ -0,0 +1,37 @@
module Mathtype
class Mtef16 < BinData::Primitive
uint8 :low, :initial_value => 0
uint8 :high, :initial_value => 0
def get; (high << 8) + low end
def getlow
self.low
end
def gethigh
self.high
end
def set(v)
self.low = v & 0xFF
self.high = (v & 0xFF00) >> 8
end
end
class MatrixLineByte < BinData::Primitive
# row/columlines are represented as 2-bit values in a full byte, in reverse
# see MATRIX RECORD (5)
# a 3x3 matrix with rows "solid none dashed solid" would be represented as
# 01 10 00 01 = reverse 01 00 10 01
# a 4x4 matrix with "solid none dashed solid solid " accordingly does
# 01100001 00000001 = 01 00 10 01 01 00 00 00
uint8 :_byte
def get
[
(self._byte & 0x03),
(self._byte & 0x0C) >> 2,
(self._byte & 0x30) >> 4,
(self._byte & 0xC0) >> 6
]
end
end
end

View File

@@ -0,0 +1,8 @@
module Mathtype
class RecordEnd < BinData::Primitive
def get; ""; end
def set(v); end
def to_formatted_s(indent = 0); to_s; end
end
end

View File

@@ -0,0 +1,36 @@
require_relative 'snapshot'
require_relative 'bintypes'
require_relative 'end'
require_relative 'nudge'
require_relative 'ruler'
require_relative 'typesizes'
module Mathtype
HALIGN = {
1 => "left",
2 => "center",
3 => "right",
4 => "al", # relational
5 => "dec" # decimal
}
VALIGN = {
0 => "top_baseline",
1 => "center_baseline",
2 => "bottom_baseline",
3 => "center", # vertical centering
4 => "axis" # math axis (center of +,-, brace points, etc.)
}
class Equation < BinData::Record
endian :little
uint8 :mtef_version
uint8 :platform
uint8 :product
uint8 :product_version
uint8 :product_subversion
include Snapshot
end
end

View File

@@ -0,0 +1,38 @@
# Nudge values:
# LINE, CHAR, TMPL, PILE, MATRIX, and EMBELL records may store the result of
# nudging (small offsets applied by the user). A nudged record has the
# mtefOPT_NUDGE option (0x8) and the option byte is followed immediately by
# the nudge offset. The nudge offset consists of either two bytes or six,
# depending on the amount of offset. If -128 <= dx < +128 and
# -128 <= dy < +128, then the offsets are stored as two bytes, dx followed
# by dy, where each value has 128 added to it before it is written. Otherwise,
# two bytes of 128 are stored, followed by the offsets, dx and dy, stored as
# 16-bit values, low byte followed by high byte.
require_relative "snapshot"
module Mathtype
class RecordNudge < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(dx dy)
endian :little
int8 :_small_dx
int8 :_small_dy
mtef16 :_large_dx, :onlyif => lambda { has_large_offset }
mtef16 :_large_dy, :onlyif => lambda { has_large_offset }
def dx
has_large_offset ? _large_dx : (_small_dx - 128)
end
def dy
has_large_offset ? _large_dy : (_small_dy - 128)
end
def has_large_offset
_small_dx == -128 and _small_dy == -128
end
end
end

View File

@@ -0,0 +1,22 @@
# RULER record (7):
# Consists of:
# record type (7)
# [n_stops] number of tab-stops
# [tab-stop list] tab-stops in order from left-to-right
# Each tab stop is described by a tab-stop type
# (0 for left, 1 for center, 2 for right, 3 for equal, 4 for decimal),
# followed by a 16-bit integer offset from the left end of the slot or pile
# with which it is associated.
module Mathtype
class RecordRuler < BinData::Record
endian :little
int8 :n_stops
array :tab_stops, initial_length: :n_stops do
int8 :tab_stop_type
mtef16 :tab_stop
end
end
end

View File

@@ -0,0 +1,19 @@
require "bindata"
module Mathtype
module Snapshot
def snapshot
snapshot = BinData::Record::Snapshot.new
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
exposed.each do |name|
obj = find_obj_for_name(name)
if obj
snapshot[name] = obj.snapshot if include_obj?(obj)
else
snapshot[name] = self.send(name)
end
end
snapshot
end
end
end

View File

@@ -0,0 +1,19 @@
# Typesize records (10-14):
# Consists of:
# record type (10-14)
# These records are just short ways of specifying a simple typesize where dsize
# is zero. The tag value represents an lsize + 10. So if the tag value is 10,
# it means equation content following it will be Full size (szFULL), tag value
# 11 means szSUB, and so on. See typesize.
module Mathtype
class RecordFull < BinData::Record; end
class RecordSub < BinData::Record; end
class RecordSub2 < BinData::Record; end
class RecordSym < BinData::Record; end
class RecordSubsym < BinData::Record; end
end

View File

@@ -0,0 +1,53 @@
# CHAR record (2):
# Consists of:
# record type (2)
# options
# [nudge] if mtefOPT_NUDGE is set
# [typeface] typeface value (signed integer; see FONT_STYLE_DEF record below)
# [character] character value (see below)
# [embellishment list] if mtefOPT_CHAR_EMBELL is set (embellishments)
# The character value itself is represented by one or more values. The presence or absence of these value is indicated by options and appear in this order:
# 16-bit integer MTCode value present unless the mtefOPT_CHAR_ENC_NO_MTCODE option is set
# 8-bit font position present if the mtefOPT_CHAR_ENC_CHAR_8 option is set
# 16-bit integer font position present if the mtefOPT_CHAR_ENC_CHAR_16 option is set
# The MTCode value defines the character independent of its font. MTCode is a superset of Unicode and is described in MTCode Encoding Tables. The 8-bit and 16-bit font positions are mutually exclusive but may both be absent. This is the position of the character within its font. Some of the common font encodings are given in Font Encoding Tables.
require_relative "snapshot"
module Mathtype3
class RecordEmbell < BinData::Record; end
class RecordChar < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge typeface mt_code_value embellishment_list)
endian :little
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
int8 :_typeface
mtef16 :_mt_code_value
array :embellishment_list,
onlyif: lambda { _options & OPTIONS["xfEMBELL"] > 0 },
read_until: lambda { element.record_type == 0 } do
named_record
end
def mt_code_value
sprintf("0x%04X", _mt_code_value)
end
def typeface
_typeface + 128
end
def options
_tag_options
end
end
end

View File

@@ -0,0 +1,109 @@
require_relative "snapshot"
# EMBELL record (6):
# Consists of:
# record type (6)
# options
# [nudge] if mtefOPT_NUDGE is set
# [embell] embellishment type
# The embellishment types are:
# value symbol description
# 2 emb1DOT over single dot
# 3 emb2DOT over double dot
# 4 emb3DOT over triple dot
# 5 emb1PRIME single prime
# 6 emb2PRIME double prime
# 7 embBPRIME backwards prime (left of character)
# 8 embTILDE tilde
# 9 embHAT hat (circumflex)
# 10 embNOT diagonal slash through character
# 11 embRARROW over right arrow
# 12 embLARROW over left arrow
# 13 embBARROW over both arrow (left and right)
# 14 embR1ARROW over right single-barbed arrow
# 15 embL1ARROW over left single-barbed arrow
# 16 embMBAR mid-height horizontal bar
# 17 embOBAR over-bar
# 18 emb3PRIME triple prime
# 19 embFROWN over-arc, concave downward
# 20 embSMILE over-arc, concave upward
# 21 embX_BARS double diagonal bars
# 22 embUP_BAR bottom-left to top-right diagonal bar
# 23 embDOWN_BAR top-left to bottom-right diagonal bar
# 24 emb4DOT over quad dot
# 25 embU_1DOT under single dot
# 26 embU_2DOT under double dot
# 27 embU_3DOT under triple dot
# 28 embU_4DOT under quad dot
# 29 embU_BAR under bar
# 30 embU_TILDE under tilde (~)
# 31 embU_FROWN under arc (ends point down)
# 32 embU_SMILE under arc (ends point up)
# 33 embU_RARROW under right arrow
# 34 embU_LARROW under left arrow
# 35 embU_BARROW under both arrow (left and right)
# 36 embU_R1ARROW under right arrow (1 barb)
# 37 embU_L1ARROW under left arrow (1 barb)
module Mathtype3
class RecordEmbell < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge embell)
EMBELL = {
2 => "emb1DOT", # over single dot
3 => "emb2DOT", # over double dot
4 => "emb3DOT", # over triple dot
5 => "emb1PRIME", # single prime
6 => "emb2PRIME", # double prime
7 => "embBPRIME", # backwards prime (left of character)
8 => "embTILDE", # tilde
9 => "embHAT", # hat (circumflex)
10 => "embNOT", # diagonal slash through character
11 => "embRARROW", # over right arrow
12 => "embLARROW", # over left arrow
13 => "embBARROW", # over both arrow (left and right)
14 => "embR1ARROW", # over right single-barbed arrow
15 => "embL1ARROW", # over left single-barbed arrow
16 => "embMBAR", # mid-height horizontal bar
17 => "embOBAR", # over-bar
18 => "emb3PRIME", # triple prime
19 => "embFROWN", # over-arc, concave downward
20 => "embSMILE", # over-arc, concave upward
21 => "embX_BARS", # double diagonal bars
22 => "embUP_BAR", # bottom-left to top-right diagonal bar
23 => "embDOWN_BAR", # top-left to bottom-right diagonal bar
24 => "emb4DOT", # over quad dot
25 => "embU_1DOT", # under single dot
26 => "embU_2DOT", # under double dot
27 => "embU_3DOT", # under triple dot
28 => "embU_4DOT", # under quad dot
29 => "embU_BAR", # under bar
30 => "embU_TILDE", # under tilde (~)
31 => "embU_FROWN", # under arc (ends point down)
32 => "embU_SMILE", # under arc (ends point up)
33 => "embU_RARROW", # under right arrow
34 => "embU_LARROW", # under left arrow
35 => "embU_BARROW", # under both arrow (left and right)
36 => "embU_R1ARROW", # under right arrow (1 barb)
37 => "embU_L1ARROW", # under left arrow (1 barb)
}
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
int8 :_embell
int8 :_none
def embell
EMBELL[_embell]
end
def options
_tag_options
end
end
end

View File

@@ -0,0 +1,27 @@
require_relative "snapshot"
module Mathtype3
class RecordFont < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options name typeface style)
endian :little
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
int8 :_typeface
int8 :style
stringz :name
def typeface
_typeface + 128
end
def options
_tag_options
end
end
end

View File

@@ -0,0 +1,47 @@
# LINE record (1):
# Consists of:
# record type (1)
# options
# [nudge] if mtefOPT_NUDGE is set
# [line spacing] if mtefOPT_LINE_LSPACE is set (16-bit integer)
# [RULER record] if mtefOPT_LP_RULER is set
# object list of line (single pile, characters and templates, or nothing)
# The line spacing value, if present, is the distance between the baseline of
# this line and the line above it.
require_relative "snapshot"
module Mathtype3
class NamedRecord < BinData::Record; end
class RecordLine < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge line_spacing ruler object_list)
endian :little
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
mtef16 :line_spacing, onlyif: (lambda do
_options & OPTIONS["xfLSPACE"] > 0
end)
record_ruler :ruler, onlyif: (lambda do
_options & OPTIONS["xfRULER"] > 0
end)
array :object_list,
onlyif: lambda { _options & OPTIONS["xfNULL"] == 0 },
read_until: lambda { element.record_type == 0 } do
named_record
end
def to_formatted_s(indent = 0); to_s; end
def options
_tag_options
end
end
end

View File

@@ -0,0 +1,80 @@
require_relative "snapshot"
# MATRIX record (5):
# record type (5)
# options
# [nudge] if mtefOPT_NUDGE is set
# [valign] vertical alignment of matrix within container
# [h_just] horizontal alignment within columns
# [v_just] vertical alignment within columns
# [rows] number of rows
# [cols] number of columns
# [row_parts] row partition line types (see below)
# [col_parts] column partition line types (see below)
# [object list] list of lines, one for each element of the matrix, in order from
# left-to-right and top-to-bottom
# The values for valign, h_just, and v_just are described in PILE above.
# The row partition line type list consists of two-bit values for each possible
# partition line (one more than the number of rows), rounded out to the nearest
# byte. Each value determines the line style of the corresponding partition line
# (0 for none, 1 for solid, 2 for dashed, or 3 for dotted). Similarly for the
# column partition lines.
module Mathtype3
class RecordMatrix < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge valign h_just v_just rows cols
row_parts col_parts object_list)
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
int8 :_valign
int8 :_h_just
int8 :_v_just
int8 :rows
int8 :cols
array :row_parts, initial_length: lambda { rows + 1 } do
bit nbits: 2
end
bit :_realign_rows, nbits: lambda { realign(rows) }
array :col_parts, initial_length: lambda { cols + 1 } do
bit nbits: 2
end
bit :_realign_cols, nbits: lambda { realign(cols) }
array :object_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def valign
VALIGN[_valign]
end
def h_just
HALIGN[_h_just]
end
def v_just
VALIGN[_v_just]
end
def options
_tag_options
end
def realign (nparts)
offset = (((nparts + 1) * 2) % 8)
return offset == 0 ? 0 : (8 - offset)
end
end
end

View File

@@ -0,0 +1,97 @@
require_relative "line"
require_relative "embell"
require_relative "char"
require_relative "pile"
require_relative "tmpl"
require_relative "font"
require_relative "matrix"
require_relative "size"
module Mathtype3
class RecordEnd < Mathtype::RecordEnd; end
class RecordNudge < Mathtype::RecordNudge; end
class RecordRuler < Mathtype::RecordRuler; end
class RecordNudge < Mathtype::RecordNudge; end
class RecordFull < Mathtype::RecordFull; end
class RecordSub < Mathtype::RecordSub; end
class RecordSub2 < Mathtype::RecordSub2; end
class RecordSym < Mathtype::RecordSym; end
class RecordSubsym < Mathtype::RecordSubsym; end
HALIGN = Mathtype::HALIGN
VALIGN = Mathtype::VALIGN
RECORD_NAMES = {
0 => "end",
1 => "slot",
2 => "char",
3 => "tmpl",
4 => "pile",
5 => "matrix",
6 => "embell",
7 => "ruler",
8 => "font",
9 => "size",
10 => "full",
11 => "sub",
12 => "sub2",
13 => "sym",
14 => "subsym",
}
OPTIONS = {
# value => symbol # description
# Option flag values for all equation structure records:
"xfLMOVE" => 0x08, # nudge values follow tag
# Option flag values for CHAR records:
"xfLAUTO" => 0x01, # character is followed by an embellishment list
"xfEMBELL" => 0x02, # character starts a function (sin, cos, etc.)
# Option flag values for LINE records:
"xfNULL" => 0x01, # line is a placeholder only (i.e. not displayed)
"xfLSPACE" => 0x04, # line spacing value follows tag
# Option flag values for LINE and PILE records:
"xfRULER" => 0x02, # RULER record follows LINE or PILE record
}
class Payload < BinData::Choice
opt = {:_options => :options}
record_end 0 # end is a reserved keyword
record_line 1, opt
record_char 2, opt
record_tmpl 3, opt
record_pile 4, opt
record_matrix 5, opt
record_embell 6, opt
record_ruler 7, opt
record_font 8, opt
record_size 9, opt
record_full 10, opt
record_sub 11, opt
record_sub2 12, opt
record_sym 13, opt
record_subsym 14, opt
end
## Payload is the most important class to understand.
## This abstraction allows recursive formats.
## eg. lists can contain lists can contain lists.
class NamedRecord < BinData::Record
bit4 :record_options
bit4 :record_type
payload :payload, :onlyif => :not_end_tag?, :selection => :record_type, :options => :record_options
def not_end_tag?
record_type != 0
end
end
class Equation < Mathtype::Equation
EXPOSED_IN_SNAPSHOT = %i(mtef_version platform product product_version
product_subversion equation)
array :equation, read_until: lambda { element.record_type == 0 } do
named_record
end
end
end

View File

@@ -0,0 +1,5 @@
module Mathtype3
class mtef_options < BinData::Record
end
end

View File

@@ -0,0 +1,43 @@
require_relative "snapshot"
# PILE record (4):
# Consists of:
# record type (4)
# options
# [nudge] if mtefOPT_NUDGE is set
# [halign] horizontal alignment
# [valign] vertical alignment
# [[RULER record]] if mtefOPT_LP_RULER is set
# [object list] list of lines contained by the pile
module Mathtype3
class RecordPile < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options halign valign object_list)
mandatory_parameter :_options
virtual :options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
int8 :_halign
int8 :_valign
record_ruler :ruler, onlyif: lambda { _options & OPTIONS["xfRULER"] > 0 }
array :object_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def halign
HALIGN[_halign]
end
def valign
VALIGN[_valign]
end
end
end

View File

@@ -0,0 +1,75 @@
require_relative "snapshot"
# SIZE record (9):
# Consists of one of the following cases:
# if lsize < 0 (explicit point size):
# record type (9)
# 101
# -point size (16 bit integer)
# else if -128 < dsize < +128:
# record type (9)
# lsize (typesize)
# dsize + 128
# else: (large delta)
# record type (9)
# 100
# lsize (typesize)
# dsize (16 bit integer)
# Sizes in MathType are represented as a pair of values, lsize and dsize. Lsize
# stands for "logical size", dsize for "delta size". If it is negative, it is an
# explicit point size (in 32nds of a point) negated and dsize is ignored.
# Otherwise, lsize is a typesize value and dsize is a delta from that size:
# Simple typesizes, without a delta value, are written using the records
# described in the next section.
module Mathtype3
class RecordSize < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options lsize dsize point_size)
endian :little
mandatory_parameter :_options
virtual :_tag_options, :value => lambda{ _options }
int8 :_size_select
mtef16 :_point_size, onlyif: lambda { _size_select == 101 }
uint8 :_lsize_large_delta, onlyif: lambda { _size_select == 100 }
uint8 :_dsize, onlyif: lambda { _size_select != 100 && _size_select != 101 }
mtef16 :_dsize_large, onlyif: lambda { _size_select == 100 }
def dsize
case _size_select
when 100
_dsize_large / 32
when 101
nil
else
(_dsize - 128) / 32 # in 32nds of a point
end
end
def lsize
case _size_select
when 100
_lsize_large_delta
when 101
nil
else
_size_select
end
end
def point_size
-_point_size if _point_size != 0
end
def options
_tag_options
end
end
end

View File

@@ -0,0 +1,19 @@
require "bindata"
module Mathtype3
module Snapshot
def snapshot
snapshot = BinData::Record::Snapshot.new
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
exposed.each do |name|
obj = find_obj_for_name(name)
if obj
snapshot[name] = obj.snapshot if include_obj?(obj)
else
snapshot[name] = self.send(name)
end
end
snapshot
end
end
end

View File

@@ -0,0 +1,751 @@
require_relative "snapshot"
# TMPL record (3):
# Consists of:
# record type (3)
# options
# [nudge] if mtefOPT_NUDGE is set
# [selector] template selector code
# [variation] template variation code (1 or 2 bytes; see below)
# [options] template-specific options
# [subobject list] either a single character (e.g. sigma in a summation) and/or lines
# The template selector and variation codes determine the class of the
# template and various properties of the template, such as which subobjects
# can be deleted by the user (see Templates). The class of a template
# determines the order and meaning of each of its subobjects (see Template
# subobject order).
# The variation code may be 1 or 2 bytes long. If the first byte value has the
# high bit set (0x80), the next byte is read and combined with the first
# according to this formula:
# variation code = (byte1 & 0x7F) | (byte2 << 8)
# Template-specific options field only used for integrals & fence templates:
# Fence template option field values (fence alignment):
# 0 center fence on math axis, place math axis of contents on math axis of
# containing line (default);
# 1 center fence on contents, place math axis of contents on math axis of
# containing line;
# 2 center fence on contents, center contents on math axis of containing line.
# Warning: the expanding integral property is duplicated in the integral
# templates variation codes (see Limit variations). On reading, MathType only
# looks at the variation code.
# Integral template option field values:
# 0 fixed-size integral;
# 1 the integral expands vertically to fit its contents.
# Limit variations:
# The following variation codes apply to all templates whose class is BigOpBoxClass or LimBoxClass:
# variation bits symbol description
# 0x0001 tvBO_LOWER lower limit is present
# 0x0002 tvBO_UPPER upper limit is present
# 0x0040 tvBO_SUM summation-style limit positions, else integral-style
# Template selectors and variations:
# Fences (parentheses, etc.):
# selector symbol description class
# 0 tmANGLE angle brackets ParBoxClass
# 1 tmPAREN parentheses ParBoxClass
# 2 tmBRACE braces (curly brackets) ParBoxClass
# 3 tmBRACK square brackets ParBoxClass
# 4 tmBAR vertical bars ParBoxClass
# 5 tmDBAR double vertical bars ParBoxClass
# 6 tmFLOOR floor brackets ParBoxClass
# 7 tmCEILING ceiling brackets ParBoxClass
# 8 tmOBRACK open (white) brackets ParBoxClass
# variations variation bits symbol description
# 0x0001 tvFENCE_L left fence is present
# 0x0002 tvFENCE_R right fence is present
# Intervals:
# selector symbol description class
# 9 tmINTERVAL unmatched brackets and parentheses ParBoxClass
# variations variation bits symbol description
# 0x0000 tvINTV_LEFT_LP left fence is left parenthesis
# 0x0001 tvINTV_LEFT_RP left fence is right parenthesis
# 0x0002 tvINTV_LEFT_LB left fence is left bracket
# 0x0003 tvINTV_LEFT_RB left fence is right bracket
# 0x0000 tvINTV_RIGHT_LP right fence is left parenthesis
# 0x0010 tvINTV_RIGHT_RP right fence is right parenthesis
# 0x0020 tvINTV_RIGHT_LB right fence is left bracket
# 0x0030 tvINTV_RIGHT_RB right fence is right bracket
# Radicals (square and nth roots):
# selector symbol description class
# 10 tmROOT radical RootBoxClass
# variations variation symbol description
# 0 tvROOT_SQ square root
# 1 tvROOT_NTH nth root
# Fractions:
# selector symbol description class
# 11 tmFRACT fractions FracBoxClass
# variations variation bits symbol description
# 0x0001 tvFR_SMALL subscript-size slots (piece fraction)
# 0x0002 tvFR_SLASH fraction bar is a slash
# 0x0004 tvFR_BASE num. and denom. are baseline aligned
# Over and Underbars:
# selector symbol description class
# 12 tmUBAR underbar BarBoxClass
# 13 tmOBAR overbar BarBoxClass
# variations variation bits symbol description
# 0x0001 tvBAR_DOUBLE bar is doubled, else single
# Arrows:
# selector symbol description class
# 14 tmARROW arrow ArroBoxClass
# variations variation symbol description
# 0x0000 tvAR_SINGLE single arrow
# 0x0001 tvAR_DOUBLE double arrow
# 0x0002 tvAR_HARPOON harpoon
# 0x0004 tvAR_TOP top slot is present
# 0x0008 tvAR_BOTTOM bottom slot is present
# 0x0010 tvAR_LEFT if single, arrow points left
# 0x0020 tvAR_RIGHT if single, arrow points right
# 0x0010 tvAR_LOS if double or harpoon, large over small
# 0x0020 tvAR_SOL if double or harpoon, small over large
# Integrals (see Limit Variations):
# selector symbol description class
# 15 tmINTEG integral BigOpBoxClass
# variations variation symbol description
# 0x0001 tvINT_1 single integral sign
# 0x0002 tvINT_2 double integral sign
# 0x0003 tvINT_3 triple integral sign
# 0x0004 tvINT_LOOP has loop w/o arrows
# 0x0008 tvINT_CW_LOOP has clockwise loop
# 0x000C tvINT_CCW_LOOP has counter-clockwise loop
# 0x0100 tvINT_EXPAND integral signs expand
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
# selector symbol description class
# 16 tmSUM sum BigOpBoxClass
# 17 tmPROD product BigOpBoxClass
# 18 tmCOPROD coproduct BigOpBoxClass
# 19 tmUNION union BigOpBoxClass
# 20 tmINTER intersection BigOpBoxClass
# 21 tmINTOP integral-style big operator BigOpBoxClass
# 22 tmSUMOP summation-style big operator BigOpBoxClass
# Limits (see Limit Variations):
# selector symbol description class
# 23 tmLIM limits LimBoxClass
# variations variation symbol description
# 0 tvSUBAR single underbar
# 1 tvDUBAR double underbar
# Horizontal braces and brackets:
# selector symbol description class
# 24 tmHBRACE horizontal brace HFenceBoxClass
# 25 tmHBRACK horizontal bracket HFenceBoxClass
# variation symbol description
# 0x0001 tvHB_TOP slot is on the top, else on the bottom
# Long division:
# selector symbol description class
# 26 tmLDIV long division LDivBoxClass
# variation symbol description
# 0x0001 tvLD_UPPER upper slot is present
# Subscripts and superscripts:
# selector symbol description class
# 27 tmSUB subscript ScrBoxClass
# 28 tmSUP superscript ScrBoxClass
# 29 tmSUBSUP subscript and superscript ScrBoxClass
# variation symbol description
# 0x0001 tvSU_PRECEDES script precedes scripted item,
# else follows
# Dirac bra-ket notation:
# selector symbol description class
# 30 tmDIRAC bra-ket notation DiracBoxClass
# variation symbol description
# 0x0001 tvDI_LEFT left part is present
# 0x0002 tvDI_RIGHT right part is present
# Vectors:
# selector symbol description class
# 31 tmVEC vector HatBoxClass
# variation symbol description
# 0x0001 tvVE_LEFT arrow points left
# 0x0002 tvVE_RIGHT arrow points right
# 0x0004 tvVE_UNDER arrow under slot, else over slot
# 0x0008 tvVE_HARPOON harpoon
# Hats, arcs, tilde, joint status:
# selector symbol description class
# 32 tmTILDE tilde over characters HatBoxClass
# 33 tmHAT hat over characters HatBoxClass
# 34 tmARC arc over characters HatBoxClass
# 35 tmJSTATUS joint status construct HatBoxClass
# Overstrikes (cross-outs):
# selector symbol description class
# 36 tmSTRIKE overstrike (cross-out) StrikeBoxClass
# variation symbol description
# 0x0001 tvST_HORIZ line is horizontal, else slashes
# 0x0002 tvST_UP if slashes, slash from lower-left to upper-right is present
# 0x0004 tvST_DOWN if slashes, slash from upper-left to lower-right is present
# Boxes:
# selector symbol description class
# 37 tmBOX box TBoxBoxClass
# variation symbol description
# 0x0001 tvBX_ROUND corners are round, else square
# 0x0002 tvBX_LEFT left side is present
# 0x0004 tvBX_RIGHT right side is present
# 0x0008 tvBX_TOP top side is present
# 0x0010 tvBX_BOTTOM bottom side is present
module Mathtype3
class RecordTmpl < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options selector variation template_specific_options nudge
subobject_list)
SCRIPT_SELECTOR = {
0 => "tmSUP",
1 => "tmSUB",
2 => "tmSUBSUP"
}
SELECTORS = {
0 => "tmANGLE",
1 => "tmPAREN",
2 => "tmBRACE",
3 => "tmBRACK",
4 => "tmBAR",
5 => "tmDBAR",
6 => "tmFLOOR",
7 => "tmCEILING",
8 => "tmINTERVAL",
9 => "tmINTERVAL",
10 => "tmINTERVAL",
11 => "tmINTERVAL",
12 => "tmINTERVAL",
13 => "tmROOT",
14 => "tmFRACT",
15 => "tmSCRIPT",
16 => "tmUBAR",
17 => "tmOBAR",
18 => "tmARROW",
19 => "tmARROW",
20 => "tmARROW",
21 => "tmINTEG",
22 => "tmINTEG",
23 => "tmINTEG",
24 => "tmINTEG",
25 => "tmINTEG",
26 => "tmINTEG",
27 => "tmHBRACE",
28 => "tmHBRACE",
29 => "tmSUM",
30 => "tmSUM",
31 => "tmPROD",
32 => "tmPROD",
33 => "tmCOPROD",
34 => "tmCOPROD",
35 => "tmUNION",
36 => "tmUNION",
37 => "tmINTER",
38 => "tmINTER",
39 => "tmLIM",
40 => "tmLDIV",
41 => "tmFRACT",
42 => "tmINTOP",
43 => "tmSUMOP",
44 => "tmSCRIPT",
45 => "tmDIRAC",
46 => "tmVEC",
47 => "tmVEC",
48 => "tmBOX"
}
FENCE_BOTH = [
"tvFENCE_L",
"tvFENCE_R"
]
FENCE_VARIATIONS = {
0 => FENCE_BOTH,
1 => ["tvFENCE_L"], # left fence is present
2 => ["tvFENCE_R"] # right fence is present
}
BAR_VARIATIONS = {
0 => ["tvBAR_SINGLE"],
1 => ["tvBAR_DOUBLE"] # bar is doubled, else single
}
VARIATIONS = {
# Fences (parentheses, etc.):
0 => FENCE_VARIATIONS,
1 => FENCE_VARIATIONS,
2 => FENCE_VARIATIONS,
3 => FENCE_VARIATIONS,
4 => FENCE_VARIATIONS,
5 => FENCE_VARIATIONS,
6 => {
0 => FENCE_BOTH
},
7 => {
0 => FENCE_BOTH
},
# Intervals:
# 0x0000 => "tvINTV_LEFT_LP", # left fence is left parenthesis
# 0x0001 => "tvINTV_LEFT_RP", # left fence is right parenthesis
# 0x0002 => "tvINTV_LEFT_LB", # left fence is left bracket
# 0x0003 => "tvINTV_LEFT_RB", # left fence is right bracket
# 0x0004 => "tvINTV_RIGHT_LP", # right fence is left parenthesis # WARNING: DOCUMENTATION SAYS 0x0000?
# 0x0010 => "tvINTV_RIGHT_RP", # right fence is right parenthesis
# 0x0020 => "tvINTV_RIGHT_LB", # right fence is left bracket
# 0x0030 => "tvINTV_RIGHT_RB", # right fence is right bracket
# Replaced above to match MathML translator
8 => {
0 => FENCE_BOTH
},
9 => {
0 => FENCE_BOTH
},
10 => {
0 => FENCE_BOTH
},
11 => {
0 => FENCE_BOTH
},
12 => {
0 => FENCE_BOTH
},
# Radicals (square and nth roots):
13 => {
0 => ["tvROOT_SQ"], # square root
1 => ["tvROOT_NTH"] # nth root
},
# Fractions:
14 => {
#TODO: full-size slots?
0 => ["tvFR_FULL"],
1 => ["tvFR_SMALL"] # subscript-size slots (piece fraction)
},
# Over and Underbars:
16 => BAR_VARIATIONS,
17 => BAR_VARIATIONS,
# Arrows:
18 => {
0 => [
"tvAR_SINGLE",
"tvAR_LEFT",
"tvAR_TOP",
], # single arrow
1 => [
"tvAR_SINGLE",
"tvAR_LEFT",
"tvAR_BOTTOM"
]
},
19 => {
0 => [
"tvAR_SINGLE",
"tvAR_RIGHT",
"tvAR_TOP"
],
1 => [
"tvAR_SINGLE",
"tvAR_RIGHT",
"tvAR_TOP"
]
},
20 => {
0 => [
"tvAR_SINGLE",
"tvAR_RIGHT",
"tvAR_LEFT",
"tvAR_TOP"
],
1 => [
"tvAR_SINGLE",
"tvAR_RIGHT",
"tvAR_LEFT",
"tvAR_TOP"
]
},
# Integrals (see Limit Variations):
21 => {
0 => [
"tvINT_1" # single integral sign
],
1 => [
"tvINT_1",
"tvBO_LOWER"
],
2 => [
"tvINT_1",
"tvBO_LOWER",
"tvBO_UPPER"
],
3 => [
"tvINT_1",
],
4 => [
"tvINT_1",
"tvBO_LOWER"
]
},
22 => {
0 => [
"tvINT_2" # double integral sign
],
1 => [
"tvINT_2",
"tvBO_LOWER"
],
2 => [
"tvINT_2",
"tvBO_LOWER",
"tvBO_UPPER"
],
3 => [
"tvINT_2",
],
4 => [
"tvINT_2",
"tvBO_LOWER"
]
},
23 => {
0 => [
"tvINT_3" # triple integral sign
],
1 => [
"tvINT_3",
"tvBO_LOWER"
],
2 => [
"tvINT_3",
"tvBO_LOWER",
"tvBO_UPPER"
],
3 => ["tvINT_3"],
4 => [
"tvINT_3",
"tvBO_LOWER"
]
},
24 => {
0 => [
"tvINT_1", # single integral sign
"tvBO_SUM",
"tvBO_LOWER",
"tvBO_UPPER"
],
1 => [
"tvINT_1",
"tvBO_SUM",
"tvBO_LOWER"
],
2 => [
"tvINT_1",
"tvBO_SUM",
"tvBO_LOWER",
]
},
25 => {
0 => [
"tvINT_2", # double integral sign
"tvBO_SUM",
"tvBO_LOWER",
],
1 => [
"tvINT_2",
"tvBO_SUM",
"tvBO_LOWER"
]
},
26 => {
0 => [
"tvINT_3", # triple integral sign
"tvBO_SUM",
"tvBO_LOWER"
],
1 => [
"tvINT_3",
"tvBO_SUM",
"tvBO_LOWER"
]
},
# Horizontal braces and brackets:
27 => {
0 => ["tvHB_TOP"] # slot is on the top
},
28 => {
# default on the bottom
0 => ["tvHB_BOT"]
},
# Sums, products, coproducts, unions, intersections, etc.:
29 => {
0 => [
"tvBO_LOWER", # lower limit is present
"tvBO_SUM" # summation-style limit positions,
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER", # upper limit is present
"tvBO_SUM"
],
2 => ["tvBO_SUM"]
},
30 => {
0 => [
"tvBO_LOWER" # lower limit is present
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
31 => {
0 => [
"tvBO_LOWER", # lower limit is present
"tvBO_SUM" # summation-style limit positions,
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER", # upper limit is present
"tvBO_SUM"
],
2 => ["tvBO_SUM"]
},
32 => {
0 => [
"tvBO_LOWER" # lower limit is present
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
33 => {
0 => [
"tvBO_LOWER", # lower limit is present
"tvBO_SUM" # summation-style limit positions,
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER", # upper limit is present
"tvBO_SUM"
],
2 => ["tvBO_SUM"]
},
34 => {
0 => [
"tvBO_LOWER" # lower limit is present
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
35 => {
0 => [
"tvBO_LOWER", # lower limit is present
"tvBO_SUM" # summation-style limit positions,
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER", # upper limit is present
"tvBO_SUM"
],
2 => ["tvBO_SUM"]
},
36 => {
0 => [
"tvBO_LOWER" # lower limit is present
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
37 => {
0 => [
"tvBO_LOWER", # lower limit is present
"tvBO_SUM" # summation-style limit positions,
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER", # upper limit is present
"tvBO_SUM"
],
2 => ["tvBO_SUM"]
},
38 => {
0 => [
"tvBO_LOWER" # lower limit is present
],
1 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
39 => {
0 => [
"tvBO_UPPER" # upper limit is present
],
1 => [
"tvBO_LOWER" # lower limit is present
],
2 => [
"tvBO_LOWER", # lower limit is present
"tvBO_UPPER" # upper limit is present
]
},
# Long division:
40 => {
0 => ["tvLD_UPPER"]
},
# Frac with Slash:
41 => {
0 => ["tvFR_SLASH"],
1 => [
"tvFR_SLASH",
"tvFR_BASE"
],
2 => [
"tvFR_SLASH",
"tvFR_SMALL"
]
},
# Big integral-style operators:
42 => {
0 => ["tvBO_LOWER"],
1 => ["tvBO_UPPER"],
2 => [
"tvBO_LOWER",
"tvBO_UPPER"
]
},
# Big summation-style operators:
43 => {
0 => [
"tvBO_SUM",
"tvBO_LOWER"
],
1 => [
"tvBO_SUM",
"tvBO_UPPER"
],
2 => [
"tvBO_SUM",
"tvBO_LOWER",
"tvBO_UPPER"
]
},
44 => {
0 => ["tvSU_PRECEDES"],
1 => ["tvSU_PRECEDES"],
2 => ["tvSU_PRECEDES"],
3 => ["tvSU_PRECEDES"]
},
45 => {
0 => [
"tvDI_LEFT",
"tvDI_RIGHT"
],
1 => ["tvDI_LEFT"],
2 => ["tvDI_RIGHT"]
},
46 => {
0 => [
"tvVE_UNDER",
"tvVE_LEFT"
],
1 => [
"tvVE_UNDER",
"tvVE_RIGHT"
],
2 => [
"tvVE_UNDER",
"tvVE_LEFT",
"tvVE_RIGHT"
],
},
47 => {
0 => ["tvVE_LEFT"],
1 => ["tvVE_RIGHT"],
2 => [
"tvVE_LEFT",
"tvVE_RIGHT"
],
},
# Selectors with empty variation:
15 => {0=>nil,1=>nil,2=>nil,3=>nil,4=>nil},
48 => {0=>nil,1=>nil,2=>nil,3=>nil,4=>nil}
}
# When options overlap in the binary space, ordinary bitmasks
# are not the correct tool to use for detection. We use digit
# position and presence instead.
DIGIT_MODE_VARIATIONS = [9]
# Top-level keys are template identifiers, defined in TEMPLATES.
# Second-level keys are bits for certain variations, negative keys mean
# that the variation is present if the bit is absent.
mandatory_parameter :_options
virtual :options, :value => lambda{ _options }
record_nudge :nudge, onlyif: lambda { _options & OPTIONS["xfLMOVE"] > 0 }
int8 :_selector
int8 :_variation
int8 :template_specific_options
array :subobject_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def selector
if is_script _selector
#SCRIPT has to be handled separate
SCRIPT_SELECTOR[_variation]
else
SELECTORS[_selector]
end
end
def is_script(sel)
return sel == 44 || sel == 15
end
def variation
VARIATIONS[_selector][_variation]
end
end
end

View File

@@ -0,0 +1,130 @@
# CHAR record (2):
# Consists of:
# record type (2)
# options
# [nudge] if mtefOPT_NUDGE is set
# [typeface] typeface value (signed integer; see FONT_STYLE_DEF record below)
# [character] character value (see below)
# [embellishment list] if mtefOPT_CHAR_EMBELL is set (embellishments)
# The character value itself is represented by one or more values. The presence or absence of these value is indicated by options and appear in this order:
# 16-bit integer MTCode value present unless the mtefOPT_CHAR_ENC_NO_MTCODE option is set
# 8-bit font position present if the mtefOPT_CHAR_ENC_CHAR_8 option is set
# 16-bit integer font position present if the mtefOPT_CHAR_ENC_CHAR_16 option is set
# The MTCode value defines the character independent of its font. MTCode is a superset of Unicode and is described in MTCode Encoding Tables. The 8-bit and 16-bit font positions are mutually exclusive but may both be absent. This is the position of the character within its font. Some of the common font encodings are given in Font Encoding Tables.
require_relative "snapshot"
module Mathtype5
class RecordEmbell < BinData::Record; end
class RecordChar < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(nudge typeface mt_code_value options font_position
variation embellishment_list)
endian :little
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
int8 :_typeface
mtef16 :_mt_code_value, onlyif: (lambda do
options & OPTIONS["mtefOPT_CHAR_ENC_NO_MTCODE"] == 0
end)
font_position_choice = lambda do
char_enc_char_8 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_8"] > 0
char_enc_char_16 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_16"] > 0
if char_enc_char_8
8
elsif char_enc_char_16
16
end
end
font_position_present = lambda do
char_enc_char_8 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_8"] > 0
char_enc_char_16 = options & OPTIONS["mtefOPT_CHAR_ENC_CHAR_16"] > 0
char_enc_char_8 || char_enc_char_16
end
choice :font_position,
selection: font_position_choice,
onlyif: font_position_present do
uint8 8
mtef16 16
end
array :embellishment_list,
onlyif: lambda { options & OPTIONS["mtefOPT_CHAR_EMBELL"] > 0 },
read_until: lambda { element.record_type == 0 } do
named_record
end
def mt_code_value
sprintf("0x%04X", _mt_code_value)
end
def typeface
_typeface + 128
end
def variation
# Check for overrides from eqn_prefs
# typeface < 128 implies it's a style index
if typeface < 128
prefs = find_eqn_prefs
# Check if styles exist and index is valid
# Note: styles is a BinData::Array, access via [] works
if prefs && prefs.respond_to?(:styles)
# BinData arrays might be lazy or behave differently, but generally index access works
# We need to ensure we don't access out of bounds
if typeface < prefs.styles.length
style = prefs.styles[typeface]
# Check italic bit (bit 1, value 2). If 0, it is upright (textmode)
if (style.font_style & 2) == 0
return "textmode"
end
end
end
end
case typeface
when 1, 9, 10
"textmode"
else
"mathmode"
end
end
private
def find_eqn_prefs
# Traverse up parent chain to find the equation array
# Hierarchy: RecordChar -> Payload (Choice) -> NamedRecord -> Array -> Equation
node = parent
while node
# BinData::Array usually behaves like an Array
if node.respond_to?(:find) && node.respond_to?(:each)
# We suspect this is the record list.
# Verify if it contains records with record_type
# We can check the first element or just try to find
# Note: We need to use safe navigation if elements aren't populated yet?
# But parsing is done sequentially. If we are in Char, EqnPrefs (usually earlier) should be readable.
# Search for record_type 18 (eqn_prefs)
# Element is NamedRecord
prefs_record = node.find { |r| r.respond_to?(:record_type) && r.record_type == 18 }
if prefs_record
return prefs_record.payload
end
end
node = node.parent
end
nil
end
end
end

View File

@@ -0,0 +1,13 @@
# COLOR records (15):
# Consists of:
# record type (15)
# [color_def_index] index of corresponding COLOR_DEF record (unsigned integer)
# The appearance of this record in the stream indicates that all following
# equation records (until the next COLOR record) have the color defined by the
# indicated COLOR_DEF record.
module Mathtype5
class RecordColor < BinData::Record
mt_uint :color_def_index
end
end

View File

@@ -0,0 +1,37 @@
# COLOR_DEF records (16):
# Consists of:
# record type (16)
# [options] model is RGB unless mtefCOLOR_CMYK bit is set; type is process
# unless mtefCOLOR_SPOT bit is set; color is unnamed unless mtefCOLOR_NAME bit is set
# [color values] if RGB, 3 values (red, green, blue); if CMYK, 4 values (cyan,
# magenta, yellow, black); see below for details
# [name] null-terminated color name appears only if mtefCOLOR_NAME is set
# This record defines a color (see Definition records). Each color value is
# written as a 16-bit integer that ranges between 0 and 1000 where 0 is the
# absence of the color and 1000 is a fully saturated color. So, an RGB color
# definition for black has all three components at 0.
module Mathtype5
class RecordColorDef < BinData::Record
endian :little
int8 :options
struct :rgb, onlyif: lambda { options & OPTIONS["mtefCOLOR_CMYK"] == 0 } do
mtef16 :r
mtef16 :g
mtef16 :b
end
struct :cmyk, onlyif: lambda { options & OPTIONS["mtefCOLOR_CMYK"] > 0} do
mtef16 :c
mtef16 :m
mtef16 :y
mtef16 :k
end
stringz :color_name, onlyif: (lambda do
options & OPTIONS["mtefCOLOR_NAME"] > 0
end)
end
end

View File

@@ -0,0 +1,15 @@
# If the record type is 100 or greater, it represents a record that will be
# defined in a future version of MTEF. For now, readers can assume that an
# unsigned integer follows the record type and is the number of bytes following
# it in the record (i.e. it doesn't include the record type and length). This
# makes it easy for software that reads MTEF to skip these records. Although it
# might be handy if all records had such a length value, it will only be present
# on future expansion records (i.e. those with record types >= 100).
module Mathtype5
class RecordMtComment < BinData::Record
mt_uint :comment_length
stringz :comment_type
stringz :comment_data
end
end

View File

@@ -0,0 +1,103 @@
require_relative "snapshot"
# EMBELL record (6):
# Consists of:
# record type (6)
# options
# [nudge] if mtefOPT_NUDGE is set
# [embell] embellishment type
# The embellishment types are:
# value symbol description
# 2 emb1DOT over single dot
# 3 emb2DOT over double dot
# 4 emb3DOT over triple dot
# 5 emb1PRIME single prime
# 6 emb2PRIME double prime
# 7 embBPRIME backwards prime (left of character)
# 8 embTILDE tilde
# 9 embHAT hat (circumflex)
# 10 embNOT diagonal slash through character
# 11 embRARROW over right arrow
# 12 embLARROW over left arrow
# 13 embBARROW over both arrow (left and right)
# 14 embR1ARROW over right single-barbed arrow
# 15 embL1ARROW over left single-barbed arrow
# 16 embMBAR mid-height horizontal bar
# 17 embOBAR over-bar
# 18 emb3PRIME triple prime
# 19 embFROWN over-arc, concave downward
# 20 embSMILE over-arc, concave upward
# 21 embX_BARS double diagonal bars
# 22 embUP_BAR bottom-left to top-right diagonal bar
# 23 embDOWN_BAR top-left to bottom-right diagonal bar
# 24 emb4DOT over quad dot
# 25 embU_1DOT under single dot
# 26 embU_2DOT under double dot
# 27 embU_3DOT under triple dot
# 28 embU_4DOT under quad dot
# 29 embU_BAR under bar
# 30 embU_TILDE under tilde (~)
# 31 embU_FROWN under arc (ends point down)
# 32 embU_SMILE under arc (ends point up)
# 33 embU_RARROW under right arrow
# 34 embU_LARROW under left arrow
# 35 embU_BARROW under both arrow (left and right)
# 36 embU_R1ARROW under right arrow (1 barb)
# 37 embU_L1ARROW under left arrow (1 barb)
module Mathtype5
class RecordEmbell < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge embell)
EMBELL = {
2 => "emb1DOT", # over single dot
3 => "emb2DOT", # over double dot
4 => "emb3DOT", # over triple dot
5 => "emb1PRIME", # single prime
6 => "emb2PRIME", # double prime
7 => "embBPRIME", # backwards prime (left of character)
8 => "embTILDE", # tilde
9 => "embHAT", # hat (circumflex)
10 => "embNOT", # diagonal slash through character
11 => "embRARROW", # over right arrow
12 => "embLARROW", # over left arrow
13 => "embBARROW", # over both arrow (left and right)
14 => "embR1ARROW", # over right single-barbed arrow
15 => "embL1ARROW", # over left single-barbed arrow
16 => "embMBAR", # mid-height horizontal bar
17 => "embOBAR", # over-bar
18 => "emb3PRIME", # triple prime
19 => "embFROWN", # over-arc, concave downward
20 => "embSMILE", # over-arc, concave upward
21 => "embX_BARS", # double diagonal bars
22 => "embUP_BAR", # bottom-left to top-right diagonal bar
23 => "embDOWN_BAR", # top-left to bottom-right diagonal bar
24 => "emb4DOT", # over quad dot
25 => "embU_1DOT", # under single dot
26 => "embU_2DOT", # under double dot
27 => "embU_3DOT", # under triple dot
28 => "embU_4DOT", # under quad dot
29 => "embU_BAR", # under bar
30 => "embU_TILDE", # under tilde (~)
31 => "embU_FROWN", # under arc (ends point down)
32 => "embU_SMILE", # under arc (ends point up)
33 => "embU_RARROW", # under right arrow
34 => "embU_LARROW", # under left arrow
35 => "embU_BARROW", # under both arrow (left and right)
36 => "embU_R1ARROW", # under right arrow (1 barb)
37 => "embU_L1ARROW", # under left arrow (1 barb)
}
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
int8 :_embell
def embell
EMBELL[_embell]
end
end
end

View File

@@ -0,0 +1,22 @@
# ENCODING_DEF records (19):
# Consists of:
# record type (19)
# [name] null-terminated encoding name
# This record defines (see Definition records) a font encoding and is referred
# to by a FONT_DEF record. In order to reduce the size of the MTEF stream,
# the following 4 encodings are predefined:
# ENCODING_DEF index encoding name
# 1 MTCode
# 2 Unknown
# 3 Symbol
# 4 MTExtra
# This means that the first ENCODING_DEF record in the MTEF stream is considered
# to have an index of 5. See Extending MathType's Font and Character Information
# and MathType's Character Encodings for more information on font encodings.
module Mathtype5
class RecordEncodingDef < BinData::Record
stringz :name
end
end

View File

@@ -0,0 +1,44 @@
# EQN_PREFS records (18):
# Consists of:
# record type (18)
# [options] none defined in this version of MTEF
# [sizes] dimension array for typesize definitions
# [spaces] dimension array for spacing definitions (see below)
# [styles] array of style definitions (see below)
# When reading arrays, the number of values may be less than or greater than
# expected. MTEF readers should be driven by the array count.
# If the array is shorter than expected, assume defaults for the missing values.
# If the array is longer than expected, the extra values must be skipped to stay
# in sync with the MTEF stream.
module Mathtype5
class Entry < BinData::Record
bit4 :unit
array :nibbles, read_until: lambda { element == 0xF } do
bit4
end
end
class RecordEqnPrefs < BinData::Record
int8 :options
int8 :sizes_count
array :sizes, initial_length: :sizes_count do
entry
end
int8 :spaces_count
array :spaces, initial_length: :spaces_count do
entry
end
int8 :styles_count
array :styles, initial_length: :styles_count do
int8 :font_def
int8 :font_style, onlyif: lambda { font_def != 0x00 }
end
resume_byte_alignment
end
end

View File

@@ -0,0 +1,14 @@
# FONT_DEF records (17):
# Consists of:
# record type (17)
# [enc_def_index] index of corresponding ENCODING_DEF record (unsigned integer)
# [name] null-terminated font name
# This record associates an font encoding with a font name. See Definition
# records.
module Mathtype5
class RecordFontDef < BinData::Record
mt_uint :enc_def_index
stringz :font_name
end
end

View File

@@ -0,0 +1,14 @@
# FONT_STYLE_DEF record (8):
# Consists of:
# record type (8)
# [font_def_index] index of mtefFONT_DEF record (unsigned integer)
# [char_style] character style bits
# This record associates a character style with a font. See Definition records.
module Mathtype5
class RecordFontStyleDef < BinData::Record
mt_uint :font_def_index
int8 :char_style
end
end

View File

@@ -0,0 +1,14 @@
# If the record type is 100 or greater, it represents a record that will be
# defined in a future version of MTEF. For now, readers can assume that an
# unsigned integer follows the record type and is the number of bytes following
# it in the record (i.e. it doesn't include the record type and length). This
# makes it easy for software that reads MTEF to skip these records. Although it
# might be handy if all records had such a length value, it will only be present
# on future expansion records (i.e. those with record types >= 100).
module Mathtype5
class RecordFuture < BinData::Record
uint8 :skip
skip length: :skip
end
end

View File

@@ -0,0 +1,36 @@
# LINE record (1):
# Consists of:
# record type (1)
# options
# [nudge] if mtefOPT_NUDGE is set
# [line spacing] if mtefOPT_LINE_LSPACE is set (16-bit integer)
# [RULER record] if mtefOPT_LP_RULER is set
# object list of line (single pile, characters and templates, or nothing)
# The line spacing value, if present, is the distance between the baseline of
# this line and the line above it.
module Mathtype5
class NamedRecord < BinData::Record; end
class RecordLine < BinData::Record
endian :little
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
mtef16 :line_spacing, onlyif: (lambda do
options & OPTIONS["mtefOPT_LINE_LSPACE"] > 0
end)
record_ruler :ruler, onlyif: (lambda do
options & OPTIONS["mtefOPT_LP_RULER"] > 0
end)
array :object_list,
onlyif: lambda { options & OPTIONS["mtefOPT_LINE_NULL"] == 0 },
read_until: lambda { element.record_type == 0 } do
named_record
end
def to_formatted_s(indent = 0); to_s; end
end
end

View File

@@ -0,0 +1,77 @@
require_relative "snapshot"
# MATRIX record (5):
# record type (5)
# options
# [nudge] if mtefOPT_NUDGE is set
# [valign] vertical alignment of matrix within container
# [h_just] horizontal alignment within columns
# [v_just] vertical alignment within columns
# [rows] number of rows
# [cols] number of columns
# [row_parts] row partition line types (see below)
# [col_parts] column partition line types (see below)
# [object list] list of lines, one for each element of the matrix, in order from
# left-to-right and top-to-bottom
# The values for valign, h_just, and v_just are described in PILE above.
# The row partition line type list consists of two-bit values for each possible
# partition line (one more than the number of rows), rounded out to the nearest
# byte. Each value determines the line style of the corresponding partition line
# (0 for none, 1 for solid, 2 for dashed, or 3 for dotted). Similarly for the
# column partition lines.
module Mathtype5
class RecordMatrix < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options nudge valign h_just v_just rows cols
row_parts col_parts object_list)
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
int8 :_valign
int8 :_h_just
int8 :_v_just
int8 :rows
int8 :cols
array :_row_parts, initial_length: lambda { (rows + 4) / 4 } do
matrix_line_byte
end
array :_col_parts, initial_length: lambda { (cols + 4) / 4 } do
matrix_line_byte
end
array :object_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def valign
VALIGN[_valign]
end
def h_just
HALIGN[_h_just]
end
def v_just
VALIGN[_v_just]
end
def truncate_parts parts, cnt
full_parts = cnt / 4
all = [parts.first(full_parts), parts.last]
all.flatten[0 .. (full_parts*4 + cnt%4)]
end
def row_parts
truncate_parts _row_parts, rows
end
def col_parts
truncate_parts _col_parts, cols
end
end
end

View File

@@ -0,0 +1,134 @@
require_relative 'mtuint'
require_relative "line"
require_relative "embell"
require_relative "char"
require_relative "pile"
require_relative "tmpl"
require_relative "eqn_prefs"
require_relative "font_def"
require_relative "font_style_def"
require_relative "matrix"
require_relative "encoding_def"
require_relative "embell"
require_relative "size"
require_relative "color"
require_relative "color_def"
require_relative "future"
require_relative "comment"
module Mathtype5
class RecordEnd < Mathtype::RecordEnd; end
class RecordNudge < Mathtype::RecordNudge; end
class RecordRuler < Mathtype::RecordRuler; end
class RecordNudge < Mathtype::RecordNudge; end
class RecordFull < Mathtype::RecordFull; end
class RecordSub < Mathtype::RecordSub; end
class RecordSub2 < Mathtype::RecordSub2; end
class RecordSym < Mathtype::RecordSym; end
class RecordSubsym < Mathtype::RecordSubsym; end
HALIGN = Mathtype::HALIGN
VALIGN = Mathtype::VALIGN
RECORD_NAMES = {
0 => "end",
1 => "slot",
2 => "char",
3 => "tmpl",
4 => "pile",
5 => "matrix",
6 => "embell",
7 => "ruler",
8 => "font_style_def",
9 => "size",
10 => "full",
11 => "sub",
12 => "sub2",
13 => "sym",
14 => "subsym",
15 => "color",
16 => "color_def",
17 => "font_def",
18 => "eqn_prefs",
19 => "encoding_def",
100 => "future",
102 => "mt_comment"
}
OPTIONS = {
# value => symbol # description
# Option flag values for all equation structure records:
"mtefOPT_NUDGE" => 0x08, # nudge values follow tag
# Option flag values for CHAR records:
"mtefOPT_CHAR_EMBELL" => 0x01, # character is followed by an embellishment list
"mtefOPT_CHAR_FUNC_START" => 0x02, # character starts a function (sin, cos, etc.)
"mtefOPT_CHAR_ENC_CHAR_8" => 0x04, # character is written with an 8-bit encoded value
"mtefOPT_CHAR_ENC_CHAR_16" => 0x10, # character is written with an 16-bit encoded value
"mtefOPT_CHAR_ENC_NO_MTCODE" => 0x20, # character is written without an 16-bit MTCode value
# Option flag values for LINE records:
"mtefOPT_LINE_NULL" => 0x01, # line is a placeholder only (i.e. not displayed)
"mtefOPT_LINE_LSPACE" => 0x04, # line spacing value follows tag
# Option flag values for LINE and PILE records:
"mtefOPT_LP_RULER" => 0x02, # RULER record follows LINE or PILE record
# Option flag values for COLOR_DEF records:
"mtefCOLOR_CMYK" => 0x01, # color model is CMYK, else RGB
"mtefCOLOR_SPOT" => 0x02, # color is a spot color, else a process color
"mtefCOLOR_NAME" => 0x04, # color has a name, else no name
}
## Payload is the most important class to understand.
## This abstraction allows recursive formats.
## eg. lists can contain lists can contain lists.
class Payload < BinData::Choice
opt = {:_options => :options}
record_end 0 # end is a reserved keyword
record_line 1
record_char 2
record_tmpl 3
record_pile 4
record_matrix 5
record_embell 6
record_ruler 7
record_font_style_def 8
record_size 9
record_full 10
record_sub 11
record_sub2 12
record_sym 13
record_subsym 14
record_color 15
record_color_def 16
record_font_def 17
record_eqn_prefs 18
record_encoding_def 19
record_future 100
record_mt_comment 102
end
class NamedRecord < BinData::Record
int8 :record_type
payload :payload, :onlyif => :not_end_tag?, :selection => :record_type, :options => :record_options
def not_end_tag?
record_type != 0
end
end
class Equation < Mathtype::Equation
EXPOSED_IN_SNAPSHOT = %i(mtef_version platform product product_version
product_subversion application_key equation_options equation)
stringz :application_key
uint8 :_equation_options
def equation_options
_equation_options == 1 ? "inline" : "block"
end
array :equation, read_until: lambda { element.record_type == 0 } do
named_record
end
end
end

View File

@@ -0,0 +1,14 @@
module Mathtype
class Mt_uint < BinData::Primitive
uint8 :_first
uint8 :_second, onlyif: lambda { _first == 0xFF }
uint8 :_third, onlyif: lambda { _first == 0xFF }
def get
_first < 0xFF ?
_first :
_third << 0x8 | _second
end
end
end

View File

@@ -0,0 +1,41 @@
require_relative "snapshot"
# PILE record (4):
# Consists of:
# record type (4)
# options
# [nudge] if mtefOPT_NUDGE is set
# [halign] horizontal alignment
# [valign] vertical alignment
# [[RULER record]] if mtefOPT_LP_RULER is set
# [object list] list of lines contained by the pile
module Mathtype5
class RecordPile < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(options halign valign object_list)
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
int8 :_halign
int8 :_valign
record_ruler :ruler, onlyif: lambda { options & OPTIONS["mtefOPT_LP_RULER"] > 0 }
array :object_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def halign
HALIGN[_halign]
end
def valign
VALIGN[_valign]
end
end
end

View File

@@ -0,0 +1,72 @@
require_relative "snapshot"
# SIZE record (9):
# Consists of one of the following cases:
# if lsize < 0 (explicit point size):
# record type (9)
# 101
# -point size (16 bit integer)
# else if -128 < dsize < +128:
# record type (9)
# lsize (typesize)
# dsize + 128
# else: (large delta)
# record type (9)
# 100
# lsize (typesize)
# dsize (16 bit integer)
# Sizes in MathType are represented as a pair of values, lsize and dsize. Lsize
# stands for "logical size", dsize for "delta size". If it is negative, it is an
# explicit point size (in 32nds of a point) negated and dsize is ignored.
# Otherwise, lsize is a typesize value and dsize is a delta from that size:
# Simple typesizes, without a delta value, are written using the records
# described in the next section.
module Mathtype5
class RecordSize < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(lsize dsize point_size)
endian :little
int8 :_size_select
mtef16 :_point_size, onlyif: lambda { _size_select == 101 }
uint8 :_lsize_large_delta, onlyif: lambda { _size_select == 100 }
uint8 :_dsize, onlyif: lambda { _size_select != 100 && _size_select != 101 }
mtef16 :_dsize_large, onlyif: lambda { _size_select == 100 }
def dsize
__dsize = nil
case _size_select
when 100
if _dsize_large > 255
__dsize = -(_dsize_large.getlow)
else
__dsize = _dsize_large
end
when 101
__dsize = _dsize
else
__dsize = (_dsize - 128) # in 32nds of a point
end
__dsize / 32
end
def lsize
case _size_select
when 100
_lsize_large_delta
when 101
nil
else
_size_select
end
end
def point_size
-_point_size if _point_size != 0
end
end
end

View File

@@ -0,0 +1,19 @@
require "bindata"
module Mathtype5
module Snapshot
def snapshot
snapshot = BinData::Record::Snapshot.new
exposed = self.class.const_get(:EXPOSED_IN_SNAPSHOT)
exposed.each do |name|
obj = find_obj_for_name(name)
if obj
snapshot[name] = obj.snapshot if include_obj?(obj)
else
snapshot[name] = self.send(name)
end
end
snapshot
end
end
end

View File

@@ -0,0 +1,498 @@
require_relative "snapshot"
# TMPL record (3):
# Consists of:
# record type (3)
# options
# [nudge] if mtefOPT_NUDGE is set
# [selector] template selector code
# [variation] template variation code (1 or 2 bytes; see below)
# [options] template-specific options
# [subobject list] either a single character (e.g. sigma in a summation) and/or lines
# The template selector and variation codes determine the class of the
# template and various properties of the template, such as which subobjects
# can be deleted by the user (see Templates). The class of a template
# determines the order and meaning of each of its subobjects (see Template
# subobject order).
# The variation code may be 1 or 2 bytes long. If the first byte value has the
# high bit set (0x80), the next byte is read and combined with the first
# according to this formula:
# variation code = (byte1 & 0x7F) | (byte2 << 8)
# Template-specific options field only used for integrals & fence templates:
# Fence template option field values (fence alignment):
# 0 center fence on math axis, place math axis of contents on math axis of
# containing line (default);
# 1 center fence on contents, place math axis of contents on math axis of
# containing line;
# 2 center fence on contents, center contents on math axis of containing line.
# Warning: the expanding integral property is duplicated in the integral
# templates variation codes (see Limit variations). On reading, MathType only
# looks at the variation code.
# Integral template option field values:
# 0 fixed-size integral;
# 1 the integral expands vertically to fit its contents.
# Limit variations:
# The following variation codes apply to all templates whose class is BigOpBoxClass or LimBoxClass:
# variation bits symbol description
# 0x0001 tvBO_LOWER lower limit is present
# 0x0002 tvBO_UPPER upper limit is present
# 0x0040 tvBO_SUM summation-style limit positions, else integral-style
# Template selectors and variations:
# Fences (parentheses, etc.):
# selector symbol description class
# 0 tmANGLE angle brackets ParBoxClass
# 1 tmPAREN parentheses ParBoxClass
# 2 tmBRACE braces (curly brackets) ParBoxClass
# 3 tmBRACK square brackets ParBoxClass
# 4 tmBAR vertical bars ParBoxClass
# 5 tmDBAR double vertical bars ParBoxClass
# 6 tmFLOOR floor brackets ParBoxClass
# 7 tmCEILING ceiling brackets ParBoxClass
# 8 tmOBRACK open (white) brackets ParBoxClass
# variations variation bits symbol description
# 0x0001 tvFENCE_L left fence is present
# 0x0002 tvFENCE_R right fence is present
# Intervals:
# selector symbol description class
# 9 tmINTERVAL unmatched brackets and parentheses ParBoxClass
# variations variation bits symbol description
# 0x0000 tvINTV_LEFT_LP left fence is left parenthesis
# 0x0001 tvINTV_LEFT_RP left fence is right parenthesis
# 0x0002 tvINTV_LEFT_LB left fence is left bracket
# 0x0003 tvINTV_LEFT_RB left fence is right bracket
# 0x0000 tvINTV_RIGHT_LP right fence is left parenthesis
# 0x0010 tvINTV_RIGHT_RP right fence is right parenthesis
# 0x0020 tvINTV_RIGHT_LB right fence is left bracket
# 0x0030 tvINTV_RIGHT_RB right fence is right bracket
# Radicals (square and nth roots):
# selector symbol description class
# 10 tmROOT radical RootBoxClass
# variations variation symbol description
# 0 tvROOT_SQ square root
# 1 tvROOT_NTH nth root
# Fractions:
# selector symbol description class
# 11 tmFRACT fractions FracBoxClass
# variations variation bits symbol description
# 0x0001 tvFR_SMALL subscript-size slots (piece fraction)
# 0x0002 tvFR_SLASH fraction bar is a slash
# 0x0004 tvFR_BASE num. and denom. are baseline aligned
# Over and Underbars:
# selector symbol description class
# 12 tmUBAR underbar BarBoxClass
# 13 tmOBAR overbar BarBoxClass
# variations variation bits symbol description
# 0x0001 tvBAR_DOUBLE bar is doubled, else single
# Arrows:
# selector symbol description class
# 14 tmARROW arrow ArroBoxClass
# variations variation symbol description
# 0x0000 tvAR_SINGLE single arrow
# 0x0001 tvAR_DOUBLE double arrow
# 0x0002 tvAR_HARPOON harpoon
# 0x0004 tvAR_TOP top slot is present
# 0x0008 tvAR_BOTTOM bottom slot is present
# 0x0010 tvAR_LEFT if single, arrow points left
# 0x0020 tvAR_RIGHT if single, arrow points right
# 0x0010 tvAR_LOS if double or harpoon, large over small
# 0x0020 tvAR_SOL if double or harpoon, small over large
# Integrals (see Limit Variations):
# selector symbol description class
# 15 tmINTEG integral BigOpBoxClass
# variations variation symbol description
# 0x0001 tvINT_1 single integral sign
# 0x0002 tvINT_2 double integral sign
# 0x0003 tvINT_3 triple integral sign
# 0x0004 tvINT_LOOP has loop w/o arrows
# 0x0008 tvINT_CW_LOOP has clockwise loop
# 0x000C tvINT_CCW_LOOP has counter-clockwise loop
# 0x0100 tvINT_EXPAND integral signs expand
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
# selector symbol description class
# 16 tmSUM sum BigOpBoxClass
# 17 tmPROD product BigOpBoxClass
# 18 tmCOPROD coproduct BigOpBoxClass
# 19 tmUNION union BigOpBoxClass
# 20 tmINTER intersection BigOpBoxClass
# 21 tmINTOP integral-style big operator BigOpBoxClass
# 22 tmSUMOP summation-style big operator BigOpBoxClass
# Limits (see Limit Variations):
# selector symbol description class
# 23 tmLIM limits LimBoxClass
# variations variation symbol description
# 0 tvSUBAR single underbar
# 1 tvDUBAR double underbar
# Horizontal braces and brackets:
# selector symbol description class
# 24 tmHBRACE horizontal brace HFenceBoxClass
# 25 tmHBRACK horizontal bracket HFenceBoxClass
# variation symbol description
# 0x0001 tvHB_TOP slot is on the top, else on the bottom
# Long division:
# selector symbol description class
# 26 tmLDIV long division LDivBoxClass
# variation symbol description
# 0x0001 tvLD_UPPER upper slot is present
# Subscripts and superscripts:
# selector symbol description class
# 27 tmSUB subscript ScrBoxClass
# 28 tmSUP superscript ScrBoxClass
# 29 tmSUBSUP subscript and superscript ScrBoxClass
# variation symbol description
# 0x0001 tvSU_PRECEDES script precedes scripted item,
# else follows
# Dirac bra-ket notation:
# selector symbol description class
# 30 tmDIRAC bra-ket notation DiracBoxClass
# variation symbol description
# 0x0001 tvDI_LEFT left part is present
# 0x0002 tvDI_RIGHT right part is present
# Vectors:
# selector symbol description class
# 31 tmVEC vector HatBoxClass
# variation symbol description
# 0x0001 tvVE_LEFT arrow points left
# 0x0002 tvVE_RIGHT arrow points right
# 0x0004 tvVE_UNDER arrow under slot, else over slot
# 0x0008 tvVE_HARPOON harpoon
# Hats, arcs, tilde, joint status:
# selector symbol description class
# 32 tmTILDE tilde over characters HatBoxClass
# 33 tmHAT hat over characters HatBoxClass
# 34 tmARC arc over characters HatBoxClass
# 35 tmJSTATUS joint status construct HatBoxClass
# Overstrikes (cross-outs):
# selector symbol description class
# 36 tmSTRIKE overstrike (cross-out) StrikeBoxClass
# variation symbol description
# 0x0001 tvST_HORIZ line is horizontal, else slashes
# 0x0002 tvST_UP if slashes, slash from lower-left to upper-right is present
# 0x0004 tvST_DOWN if slashes, slash from upper-left to lower-right is present
# Boxes:
# selector symbol description class
# 37 tmBOX box TBoxBoxClass
# variation symbol description
# 0x0001 tvBX_ROUND corners are round, else square
# 0x0002 tvBX_LEFT left side is present
# 0x0004 tvBX_RIGHT right side is present
# 0x0008 tvBX_TOP top side is present
# 0x0010 tvBX_BOTTOM bottom side is present
module Mathtype5
class RecordTmpl < BinData::Record
include Snapshot
EXPOSED_IN_SNAPSHOT = %i(selector variation template_specific_options nudge
subobject_list)
SELECTORS = {
# Fences (parentheses, etc.):
0 => "tmANGLE",
1 => "tmPAREN",
2 => "tmBRACE",
3 => "tmBRACK",
4 => "tmBAR",
5 => "tmDBAR",
6 => "tmFLOOR",
7 => "tmCEILING",
8 => "tmOBRACK",
# Intervals:
9 => "tmINTERVAL",
# Radicals (square and nth roots):
10 => "tmROOT",
# Fractions:
11 => "tmFRACT",
# Over and Underbars:
12 => "tmUBAR",
13 => "tmOBAR",
# Arrows:
14 => "tmARROW",
# Integrals (see Limit Variations):
15 => "tmINTEG",
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
16 => "tmSUM",
17 => "tmPROD",
18 => "tmCOPROD",
19 => "tmUNION",
20 => "tmINTER",
21 => "tmINTOP",
22 => "tmSUMOP",
# Limits (see Limit Variations):
23 => "tmLIM",
# Horizontal braces and brackets:
24 => "tmHBRACE",
25 => "tmHBRACK",
# Long division:
26 => "tmLDIV",
# Subscripts and superscripts:
27 => "tmSUB",
28 => "tmSUP",
29 => "tmSUBSUP",
# Dirac bra-ket notation:
30 => "tmDIRAC",
# Vectors:
31 => "tmVEC",
# Hats, arcs, tilde, joint status:
32 => "tmTILDE",
33 => "tmHAT",
34 => "tmARC",
35 => "tmJSTATUS",
# Overstrikes (cross-outs):
36 => "tmSTRIKE",
# Boxes:
37 => "tmBOX"
}
# When options overlap in the binary space, ordinary bitmasks
# are not the correct tool to use for detection. We use digit
# position and presence instead.
DIGIT_MODE_VARIATIONS = [9]
# Top-level keys are template identifiers, defined in TEMPLATES.
# Second-level keys are bits for certain variations, negative keys mean
# that the variation is present if the bit is absent.
VARIATIONS = {
# Fences (parentheses, etc.):
0..8 => {
0x0001 => "tvFENCE_L", # left fence is present
0x0002 => "tvFENCE_R", # right fence is present
},
# Intervals:
9 => {
# 0x0000 => "tvINTV_LEFT_LP", # left fence is left parenthesis
# 0x0001 => "tvINTV_LEFT_RP", # left fence is right parenthesis
# 0x0002 => "tvINTV_LEFT_LB", # left fence is left bracket
# 0x0003 => "tvINTV_LEFT_RB", # left fence is right bracket
# 0x0004 => "tvINTV_RIGHT_LP", # right fence is left parenthesis # WARNING: DOCUMENTATION SAYS 0x0000?
# 0x0010 => "tvINTV_RIGHT_RP", # right fence is right parenthesis
# 0x0020 => "tvINTV_RIGHT_LB", # right fence is left bracket
# 0x0030 => "tvINTV_RIGHT_RB", # right fence is right bracket
# Replaced above to match MathML translator
0x0002 => {
0x0020 => "tvINTV_LBLB", # left bracket, left bracket
0x0010 => "tvINTV_LBRP", # left bracket, right parenthesis
},
0x0003 => {
0x0030 => "tvINTV_RBRB", # right bracket, right bracket
0x0020 => "tvINTV_RBLB", # right bracket, left bracket
},
0x0000 => {
0x0030 => "tvINTV_LPRB", # left parenthesis, right bracket
}
},
# Radicals (square and nth roots):
10 => {
0 => "tvROOT_SQ", # square root
1 => "tvROOT_NTH", # nth root
},
# Fractions:
11 => {
0x0001 => "tvFR_SMALL", # subscript-size slots (piece fraction)
0x0002 => "tvFR_SLASH", # fraction bar is a slash
0x0004 => "tvFR_BASE", # num. and denom. are baseline aligned
},
# Over and Underbars:
12..13 => {
0x0001 => "tvBAR_DOUBLE", # bar is doubled, else single
},
# Arrows:
14 => {
0x0000 => "tvAR_SINGLE", # single arrow
0x0001 => "tvAR_DOUBLE", # double arrow
0x0010 => {
0x0001 => "tvAR_LOS", # if double, large over small,
0x0002 => "tvAR_LOS", # if harpoon, large over small,
0x0010 => "tvAR_LEFT" # if single, arrow points left
},
0x0020 => {
0x0001 => "tvAR_SOL", # if double, small over large
0x0002 => "tvAR_SOL", # if harpoon, small over large
0x0010 => "tvAR_RIGHT", # if single, arrow points right
},
0x0002 => "tvAR_HARPOON", # harpoon
0x0004 => { # top slot is present
0x0008 => "tvAR_TOPBOTTOM", # both slots are present
0x0004 => "tvAR_TOP"
},
0x0008 => { # bottom slot is present
0x0004 => "tvAR_TOPBOTTOM", # both slots are present
0x0008 => "tvAR_BOTTOM"
}
},
# Integrals (see Limit Variations):
15 => {
0x0001 => "tvINT_1", # single integral sign
0x0002 => "tvINT_2", # double integral sign
0x0003 => "tvINT_3", # triple integral sign
0x0004 => "tvINT_LOOP", # has loop w/o arrows
0x0008 => "tvINT_CW_LOOP", # has clockwise loop
0x000C => "tvINT_CCW_LOOP", # has counter-clockwise loop
0x0100 => "tvINT_EXPAND", # integral signs expand
},
# Limit variations
15..23 => {
0x0010 => "tvBO_LOWER", # lower limit is present
0x0020 => "tvBO_UPPER", # upper limit is present
0x0040 => "tvBO_SUM", # summation-style limit positions,
-0x0040 => "tvBO_INT" # else integral-style
},
# Sums, products, coproducts, unions, intersections, etc. (see Limit Variations):
23 => {
0 => "tvSUBAR", # single underbar
1 => "tvDUBAR", # double underbar
},
# Horizontal braces and brackets:
24..25 => {
0x0001 => "tvHB_TOP", # slot is on the top, else on the bottom
},
# Long division:
26 => {
0x0001 => "tvLD_UPPER", # upper slot is present
},
# Subscripts and superscripts:
27..29 => {
0x0001 => "tvSU_PRECEDES", # script precedes scripted item, else follows
},
# Dirac bra-ket notation:
30 => {
0x0001 => "tvDI_LEFT", # left part is present
0x0002 => "tvDI_RIGHT", # right part is present
},
# Vectors:
31 => {
0x0001 => "tvVE_LEFT", # arrow points left
0x0002 => "tvVE_RIGHT", # arrow points right
0x0004 => "tvVE_UNDER", # arrow under slot, else over slot
0x0008 => "tvVE_HARPOON", # harpoon
},
# Hats, arcs, tilde, joint status:
32..35 => {},
# Overstrikes (cross-outs):
36 => {
0x0001 => "tvST_HORIZ", # line is horizontal, else slashes
0x0002 => "tvST_UP", # if slashes, slash from lower-left to upper-right is present
0x0004 => "tvST_DOWN", # if slashes, slash from upper-left to lower-right is present
},
# Boxes:
37 => {
0x0001 => "tvBX_ROUND", # corners are round, else square
0x0002 => "tvBX_LEFT", # left side is present
0x0004 => "tvBX_RIGHT", # right side is present
0x0008 => "tvBX_TOP", # top side is present
0x0010 => "tvBX_BOTTOM", # bottom side is present
}
}
int8 :options
record_nudge :nudge, onlyif: lambda { options & OPTIONS["mtefOPT_NUDGE"] > 0 }
int8 :_selector
int8 :_variation_first_byte
int8 :_variation_second_byte, onlyif: (lambda do
_variation_first_byte & 0x80 > 0
end)
int8 :template_specific_options
array :subobject_list, read_until: lambda { element.record_type == 0 } do
named_record
end
def variation
@variation = (_variation_first_byte & 0x7F) | (_variation_second_byte << 8)
@variations = VARIATIONS.select do |selector, _|
selector === _selector
end.values.reduce(Hash.new, :merge)
process_variations
end
def process_variations
@variations.select do |flag, _|
if flag < 0 # flag should NOT be active
!check_flag(-flag)
else # flag should be active
check_flag(flag)
end
end.map do |flag, value|
case value
when Hash # Conditional variations
result = value.detect do |conditional, _|
check_flag(conditional)
end
result.last if result
else
value
end
end.uniq
end
def check_flag(flag)
case mode
when :bitmask
check_bitmask(flag)
when :digit
check_digit(flag)
end
end
def check_bitmask(flag)
if flag == 0
@variation & 0xf == 0
else
@variation & flag == flag
end
end
# E.g. is 0x3 present in 0x33
def check_digit(flag)
digits = if flag == 0
1
else
(Math.log(flag+1)/Math.log(16)).ceil # digits in a hex number
end
mask = (15<<(4*digits-4)) # e.g. 0xf0
variation_digit = (@variation & mask) >> (digits * 4 - 4)
flag_digit = (flag & mask) >> (digits * 4 - 4)
variation_digit == flag_digit
end
def mode
DIGIT_MODE_VARIATIONS.include?(_selector) ? :digit : :bitmask
end
def selector
SELECTORS[_selector]
end
end
end

View File

@@ -0,0 +1,28 @@
# coding: utf-8
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'mathtype/version'
Gem::Specification.new do |spec|
spec.name = "mathtype"
spec.version = Mathtype::VERSION
spec.authors = ["Jure Triglav", "Sebastian Bulka"]
spec.email = ["juretriglav@gmail.com", "sebastian.bulka@le-tex.de"]
spec.summary = %q{ A Ruby gem for reading MathType binaries and converting them to an XML form. }
spec.description = %q{ This gem can read proprietary MathType binary equations that are usually embedded in Word documents and convert these equations into an XML form. This XML form can then be used for further processing, e.g. to convert the equation to MathML.}
spec.homepage = ""
spec.license = "MIT"
spec.files = Dir.glob("{lib,spec}/**/*")
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
spec.require_paths = ["lib"]
spec.add_development_dependency "bundler", "> 1.7"
spec.add_development_dependency "rake", ">= 12.3.3"
spec.add_development_dependency "rspec", "~> 3.3"
spec.add_development_dependency "pry", "~> 0.10"
spec.add_dependency "bindata", "~> 2.1"
spec.add_dependency "nokogiri", "~> 1.6"
spec.add_dependency "ruby-ole", "~> 1.2"
end

Some files were not shown because too many files have changed in this diff Show More