Files
translation-files/translate-files/skills/pdf-to-docx-conversion/SKILL.md
T
2026-06-09 12:28:56 +08:00

9.9 KiB
Raw Blame History

name, description, version, author, license, platforms, metadata
name description version author license platforms metadata
pdf-to-docx-conversion 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. 1.0.0 Claude MIT
linux
macos
windows
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

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

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:

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:

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

from pdf_to_docx import PDFToDOCXConverter

converter = PDFToDOCXConverter(config)
converter.convert("input.pdf", "output.docx")

Core Classes

PDFToDOCXConverter

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:

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:

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.:

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)

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:

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

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:

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:

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.

# 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