manage translation skills

This commit is contained in:
iacore
2026-06-09 19:41:08 +08:00
parent d5482547ff
commit 10c449f4d9
8 changed files with 10 additions and 3 deletions
+322
View File
@@ -0,0 +1,322 @@
---
name: pdf-to-docx-conversion
description: "Convert flowing text PDFs (Chinese or multi-language) to DOCX with proper fonts, styles, native bullets, lists, and embedded images. Preserves visual hierarchy from PDF font/size/color data."
version: 1.0.0
author: Claude
license: MIT
platforms: [linux, macos, windows]
metadata:
hermes:
tags: [PDF, DOCX, Documents, python-docx, pymupdf]
---
# PDF-to-DOCX Conversion
Convert PDF documents (especially flowing text documents in any language, including CJK) into well-structured DOCX files that preserve fonts, sizes, colors, and layout intent.
## Prerequisites
```bash
pip install pymupdf python-docx
```
## Features
- **Style-aware**: reads actual font, size, bold, color from PDF spans
- **Native bullets**: uses Word `List Bullet` style instead of Wingdings glyphs
- **Native numbering**: uses numbered list style for sequential items
- **Image extraction**: detects and embeds PDF images into the DOCX
- **Verse/poetry handling**: splits merged verse lines at semantic boundaries
- **Multi-language**: works with CJK, RTL, and mixed-script documents
- **Flowing text**: text flows across pages; no forced page breaks
## Quick Start
```python
from pdf_to_docx import convert_pdf_to_docx
convert_pdf_to_docx("input.pdf", "output.docx")
```
## Step-by-Step Workflow
### 1. Inspect the PDF
First, dump the PDF to understand its structure:
```bash
python3 << 'PY'
import pymupdf
doc = pymupdf.open("input.pdf")
for pi in range(len(doc)):
page = doc[pi]
blocks = page.get_text("dict")["blocks"]
for block in blocks:
if block["type"] != 0: continue
for line in block["lines"]:
for span in line["spans"]:
bbox = span["bbox"]
flags = span["flags"]
attrs = []
if flags & 2**1: attrs.append("I")
if flags & 2**4: attrs.append("B")
print(f" Y={bbox[1]:.0f} [{span['size']:.1f}pt {'+'.join(attrs) or '-'}] {span['font']} | {span['text']}")
PY
```
Key things to identify:
- **Fonts used** (map to DOCX fonts)
- **Bullet mechanism** (Wingdings? Unicode?)
- **Header hierarchy** (what size = section header vs sub-header)
- **Numbered lists** (what delimiter: `1)` `1.` `1`)
- **Images** (check `page.get_images()`)
- **Special sections** (tables, verses, forms)
### 2. Configure the Converter
Create a config dict matching your PDF's patterns:
```python
config = {
"fonts": {
"title": "STHeitiSC-Medium",
"body": "HYShuSongErKW",
"page_number": "HelveticaNeue",
},
"header_sizes": {"section": 18, "sub": 15},
"body_size": 12,
"bullet_fonts": ["Wingdings", "Wingdings 2", "Wingdings 3"],
"page_number_font": "HelveticaNeue",
"numbered_patterns": [r'^\d+\)', r'^\d+\.'], # detect numbered items
"skip_fonts": ["HelveticaNeue"], # fonts to skip (page numbers)
}
```
### 3. Run the Conversion
```python
from pdf_to_docx import PDFToDOCXConverter
converter = PDFToDOCXConverter(config)
converter.convert("input.pdf", "output.docx")
```
## Core Classes
### PDFToDOCXConverter
```python
class PDFToDOCXConverter:
def __init__(self, config=None):
self.cfg = config or self._default_config()
def convert(self, pdf_path: str, docx_path: str):
"""Main entry point."""
# 1. Extract all spans
# 2. Merge Wingdings bullets with body text
# 3. Group lines into paragraphs by Y-gap and style changes
# 4. Post-process: split compact lists, verses, merged steps
# 5. Build DOCX with proper styles
# 6. Embed images
pass
def extract_spans(self, pdf_path: str) -> list[dict]:
"""Extract all text spans with full style info."""
doc = pymupdf.open(pdf_path)
raw = []
for pi in range(len(doc)):
for block in doc[pi].get_text("dict")["blocks"]:
if block["type"] != 0: continue
for line in block["lines"]:
for span in line["spans"]:
raw.append({
"text": span["text"], "font": span["font"],
"size": span["size"], "flags": span["flags"],
"color": span["color"], "bbox": span["bbox"],
})
return raw
def detect_bullets(self, line: list) -> bool:
"""Check if first span in line is a Wingdings bullet."""
return "Wingdings" in line[0]["font"]
def group_paragraphs(self, lines: list) -> list[dict]:
"""Group raw lines into logical paragraphs."""
# Group by Y-gap threshold (typically 20-30pt)
# Break on style change (font change, size > threshold, bold toggle)
# Break on attribution lines (——节选自...)
# Break on special fonts (STHeitiSC-Light etc.)
pass
def post_process(self, paras: list) -> list[dict]:
"""Split merged compact lists and verses."""
# See examples below for common patterns
pass
```
## Common Post-Processing Patterns
### Compact Numbered Lists
When the PDF flows list items together in one paragraph:
```python
def split_compact_list(text: str) -> list[str]:
"""Split '1) foo 2) bar 3) baz' into separate items."""
parts = re.split(r'(?=\d+\))', text)
return [p for p in parts if p.strip()]
```
### Verse / Poetry Lines
When the PDF merges verse lines that should be on separate lines:
```python
def split_verse(text: str, split_markers: list[str]) -> list[str]:
"""Split verse at semantic phrase boundaries.
Example markers: ['感恩', '', '更愿']"""
pattern = '|'.join(f'(?={m})' for m in split_markers)
return [p for p in re.split(pattern, text) if p.strip()]
```
### 小组交流流程 / Process Steps
Split Chinese process steps numbered 一、二、三、etc.:
```python
def split_chinese_steps(text: str) -> list[str]:
"""Split '一、foo 二、bar' into separate items."""
parts = re.split(r'(?=[一二三四五六七八九十]、)', text)
return [p.strip() for p in parts if p.strip()]
```
## DOCX Construction
### Font Setup (East Asian fonts)
```python
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def set_east_asian_font(run, name: str):
"""Set CJK font properly in python-docx."""
rPr = run._element.get_or_add_rPr()
rFonts = rPr.find(qn('w:rFonts'))
if rFonts is None:
rFonts = OxmlElement('w:rFonts')
rPr.insert(0, rFonts)
rFonts.set(qn('w:eastAsia'), name)
rFonts.set(qn('w:ascii'), name)
rFonts.set(qn('w:hAnsi'), name)
```
### Bullet Items
Use native Word bullets, NOT Wingdings characters:
```python
p = doc.add_paragraph(style='List Bullet')
p.clear()
run = p.add_run("Your bullet text here")
set_east_asian_font(run, font_name)
```
### Image Embedding
```python
from docx.shared import Inches
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.add_picture(image_path, width=Inches(3.5))
```
Extract images from PDF first:
```python
doc = pymupdf.open("input.pdf")
for pi in range(len(doc)):
images = doc[pi].get_images()
for idx, img in enumerate(images):
xref = img[0]
base = doc.extract_image(xref)
with open(f"extracted_{pi}_{idx}.{base['ext']}", 'wb') as f:
f.write(base['image'])
```
## Verification Checklist
After conversion, verify the DOCX:
```bash
python3 -c "
from docx import Document
doc = Document('output.docx')
print(f'Paragraphs: {len(doc.paragraphs)}')
for i, p in enumerate(doc.paragraphs):
style = p.style.name if p.style else '-'
txt = p.text[:80]
print(f'[{i:2d}] [{style:15s}] {txt}')
"
```
Check for:
1. [ ] All sections present (count paragraphs)
2. [ ] No merged verses or lists
3. [ ] Headers are bold + larger size
4. [ ] Bullets use `List Bullet` style
5. [ ] Numbered items are separate paragraphs
6. [ ] Images present in `word/media/`
7. [ ] Attribution lines right-aligned/indented
8. [ ] No page numbers leaked into body
## Troubleshooting
| Symptom | Cause | Fix |
|---------|-------|-----|
| Text over-merged | Y-gap threshold too high | Lower gap threshold (e.g. 20 → 15) |
| Missing sections | Skipped by font filter | Add font to skip_fonts or remove filter |
| Over-split lines | Y-gap threshold too low | Raise gap threshold (e.g. 20 → 30) |
| Wingdings boxes | Unicode bullet inserted | Use `style='List Bullet'` instead |
| CJK font wrong | East Asian font not set | Use `set_east_asian_font()` helper |
| Image missing | Not extracted before DOCX build | Run `extract_images()` first |
| Verse mangled | Regex too aggressive | Tune verse splitting pattern |
## Full Example Script
See `scripts/convert_pdf_to_docx.py` for a production-ready converter with all patterns pre-configured.
```python
# scripts/convert_pdf_to_docx.py
import pymupdf, re
from docx import Document
from docx.shared import Pt, Cm, Inches, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
def convert_pdf_to_docx(pdf_path: str, docx_path: str, config: dict = None):
"""Convert a flowing text PDF to DOCX."""
cfg = config or {}
# Extract
doc_pdf = pymupdf.open(pdf_path)
raw = []
for pi in range(len(doc_pdf)):
for block in doc_pdf[pi].get_text("dict")["blocks"]:
if block["type"] != 0: continue
for line in block["lines"]:
ss = line["spans"]
is_b = any("Wingdings" in s["font"] for s in ss[:1])
text = "".join(s["text"] for s in ss)
dom = ss[1] if (is_b and len(ss) > 1) else ss[0]
raw.append(dict(text=text, font=dom["font"], size=dom["size"],
bold=bool(dom["flags"] & 2**4), color=dom["color"],
x=dom["bbox"][0], y=dom["bbox"][1], is_bullet=is_b))
# ... (merge bullets, group paragraphs, post-process, build DOCX)
# Run:
# python scripts/convert_pdf_to_docx.py input.pdf output.docx
```