Skip to content

Commit f7fcc23

Browse files
feat: add pdf-to-docx-swift converter Closes #18
Co-Authored-By: kiki830621 <kiki830621@gmail.com>
1 parent cf6a3bd commit f7fcc23

9 files changed

Lines changed: 802 additions & 3 deletions

File tree

.gitignore

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ DerivedData/
55

66
# Package resolved (optional - can include or exclude)
77
# Package.resolved
8+
packages/*/Package.resolved
89

910
# Xcode
1011
*.xcodeproj
@@ -18,7 +19,7 @@ xcuserdata/
1819
*.code-workspace
1920

2021
# Local packages & MCP (separate git repos)
21-
packages/
22+
packages/*
2223
!packages/srt-to-html-swift/
2324
!packages/srt-to-html-swift/Package.swift
2425
!packages/srt-to-html-swift/Sources/
@@ -43,6 +44,15 @@ packages/
4344
!packages/html-to-md-swift/Tests/
4445
!packages/html-to-md-swift/Tests/HTMLToMDSwiftTests/
4546
!packages/html-to-md-swift/Tests/HTMLToMDSwiftTests/HTMLConverterTests.swift
47+
!packages/pdf-to-docx-swift/
48+
!packages/pdf-to-docx-swift/Package.swift
49+
!packages/pdf-to-docx-swift/README.md
50+
!packages/pdf-to-docx-swift/Sources/
51+
!packages/pdf-to-docx-swift/Sources/PDFToDOCXSwift/
52+
!packages/pdf-to-docx-swift/Sources/PDFToDOCXSwift/PDFToDOCXConverter.swift
53+
!packages/pdf-to-docx-swift/Tests/
54+
!packages/pdf-to-docx-swift/Tests/PDFToDOCXSwiftTests/
55+
!packages/pdf-to-docx-swift/Tests/PDFToDOCXSwiftTests/PDFToDOCXConverterTests.swift
4656
mcp/
4757

4858
# Temp files

CONVERSIONS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
| **Markdown** ||`md-to-html` | 🔬 `md-to-word` | · | · | · | · |
1818
| **HTML** |`html-to-md` || 📋 `html-to-word` | · | · | · | · |
1919
| **Word (.docx)** |`word-to-md` | 📋 `word-to-html` || · | · | · | · |
20-
| **PDF** | 📋 `pdf-to-md` | · | · |`pdf-to-latex` | · || · |
20+
| **PDF** | 📋 `pdf-to-md` | · | 🔄 `pdf-to-docx` |`pdf-to-latex` | · || · |
2121
| **BibLaTeX (.bib)** |`bib-apa-to-md` |`bib-apa-to-html` | · | · |`bib-apa-to-json` | · | · |
2222
| **SRT** | · |`srt-to-html` | · | · | · | · ||
2323

@@ -34,6 +34,7 @@
3434
| BibLaTeX → APA Markdown | `bib-apa-to-md-swift` | ✅ implemented | style-aware renderer |
3535
| BibLaTeX → APA JSON | `bib-apa-to-json-swift` | ✅ implemented | pre-rendered HTML + anchors |
3636
| PDF → Markdown | `pdf-to-md-swift` | 📋 planned | direct path, avoid hub loss through LaTeX |
37+
| PDF → Word (.docx) | `pdf-to-docx-swift` | 🔄 active | direct editable export with headings / lists / tables / page breaks |
3738
| Word → HTML | `word-to-html-swift` | 📋 planned | direct path preserves Word semantics |
3839
| HTML → Word | `html-to-word-swift` | 📋 planned | reverse path after word-to-html |
3940
| Markdown → Word | `md-to-word-swift` | 🔬 research | binary target + protocol shape need design |
@@ -43,6 +44,7 @@
4344
| Priority | Converter | Status | Why now |
4445
|---------:|-----------|--------|---------|
4546
| P1 | `pdf-to-md-swift` | 📋 planned | direct markdown export is a natural companion to existing PDF parsing stack |
47+
| P1 | `pdf-to-docx-swift` | 🔄 active | editable Word output is now architecturally straightforward via OOXML writer path |
4648
| P1 | `word-to-html-swift` | 📋 planned | direct conversion avoids Markdown hub loss for rich Word semantics |
4749
| P2 | `html-to-word-swift` | 📋 planned | reverse path once Word↔HTML design stabilizes |
4850
| P3 | `md-to-word-swift` | 🔬 research | requires target-binary converter story beyond current text-streaming protocol |

Package.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ let package = Package(
1515
.package(url: "https://github.com/PsychQuant/markdown-swift.git", from: "0.1.0"),
1616
.package(url: "https://github.com/PsychQuant/marker-swift.git", from: "0.1.0"),
1717
.package(name: "pdf-to-latex-swift", path: "packages/pdf-to-latex-swift"),
18+
.package(name: "PDFToDOCXSwift", path: "packages/pdf-to-docx-swift"),
1819
.package(name: "HTMLToMDSwift", path: "packages/html-to-md-swift"),
1920
.package(name: "MDToHTMLSwift", path: "packages/md-to-html-swift"),
2021
.package(name: "SRTToHTMLSwift", path: "packages/srt-to-html-swift"),
@@ -41,6 +42,7 @@ let package = Package(
4142
.product(name: "MDToHTMLSwift", package: "MDToHTMLSwift"),
4243
.product(name: "SRTToHTMLSwift", package: "SRTToHTMLSwift"),
4344
"MarkerWordConverter",
45+
.product(name: "PDFToDOCXSwift", package: "PDFToDOCXSwift"),
4446
.product(name: "PDFToLaTeXCore", package: "pdf-to-latex-swift"),
4547
.product(name: "BibAPAToHTML", package: "BibAPAToHTML"),
4648
.product(name: "BibAPAToJSON", package: "BibAPAToJSON"),

Sources/MacDocCLI/MacDoc+PDF.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ extension MacDoc {
99
struct PDF: AsyncParsableCommand {
1010
static let configuration = CommandConfiguration(
1111
commandName: "pdf",
12-
abstract: "PDF 轉 LaTeX 工具(多步驟 pipeline)",
12+
abstract: "PDF 工具(直接轉 DOCX + PDF→LaTeX pipeline)",
1313
subcommands: [
14+
ToDOCX.self,
1415
Init.self,
1516
Segment.self,
1617
Render.self,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import ArgumentParser
2+
import Foundation
3+
import CommonConverterSwift
4+
import PDFToDOCXSwift
5+
6+
// MARK: - pdf to-docx
7+
extension MacDoc.PDF {
8+
struct ToDOCX: AsyncParsableCommand {
9+
static let configuration = CommandConfiguration(
10+
commandName: "to-docx",
11+
abstract: "將 PDF (.pdf) 直接轉換為 Word (.docx)"
12+
)
13+
14+
@Argument(help: "輸入 .pdf 檔案路徑")
15+
var input: String
16+
17+
@Option(name: [.short, .long], help: "輸出 .docx 檔案路徑(預設為與輸入同名)")
18+
var output: String?
19+
20+
@Flag(name: .long, help: "保留 PDF 段內換行為 Word line break")
21+
var hardBreaks: Bool = false
22+
23+
mutating func run() async throws {
24+
let inputURL = URL(fileURLWithPath: input)
25+
guard FileManager.default.fileExists(atPath: inputURL.path) else {
26+
throw ValidationError("找不到輸入檔案: \(input)")
27+
}
28+
29+
var options = ConversionOptions.default
30+
options.hardLineBreaks = hardBreaks
31+
32+
let outputURL = resolvedOutputURL(for: inputURL)
33+
try PDFToDOCXConverter().convertToFile(input: inputURL, output: outputURL, options: options)
34+
print("已寫入: \(outputURL.path)")
35+
}
36+
37+
private func resolvedOutputURL(for inputURL: URL) -> URL {
38+
if let output {
39+
return URL(fileURLWithPath: output)
40+
}
41+
return inputURL.deletingPathExtension().appendingPathExtension("docx")
42+
}
43+
}
44+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// swift-tools-version: 5.9
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "PDFToDOCXSwift",
6+
platforms: [.macOS(.v14)],
7+
products: [
8+
.library(name: "PDFToDOCXSwift", targets: ["PDFToDOCXSwift"]),
9+
],
10+
dependencies: [
11+
.package(url: "https://github.com/PsychQuant/doc-converter-swift.git", from: "0.4.0"),
12+
.package(url: "https://github.com/PsychQuant/ooxml-swift.git", from: "0.5.3"),
13+
],
14+
targets: [
15+
.target(
16+
name: "PDFToDOCXSwift",
17+
dependencies: [
18+
.product(name: "CommonConverterSwift", package: "doc-converter-swift"),
19+
.product(name: "OOXMLSwift", package: "ooxml-swift"),
20+
]
21+
),
22+
.testTarget(
23+
name: "PDFToDOCXSwiftTests",
24+
dependencies: [
25+
"PDFToDOCXSwift",
26+
.product(name: "OOXMLSwift", package: "ooxml-swift"),
27+
]
28+
),
29+
]
30+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# pdf-to-docx-swift
2+
3+
Direct PDF → Word (`.docx`) converter for macdoc.
4+
5+
## What it does
6+
7+
- reads PDF files with `PDFKit`
8+
- maps detected structure into `OOXMLSwift` `WordDocument`
9+
- writes full `.docx` archives via `DocxWriter`
10+
- exposes a streaming `DocumentConverter` surface that emits `word/document.xml`
11+
12+
## Current structure detection
13+
14+
- document metadata (`Title`, `Author`, `Subject`, `Keywords`, creation/modification date)
15+
- headings (first-page title + short heading-like lines)
16+
- paragraphs
17+
- bullet / ordered lists
18+
- simple tables detected from tabs / multi-space aligned columns / pipe rows
19+
- page breaks between PDF pages
20+
21+
## Usage
22+
23+
```swift
24+
import PDFToDOCXSwift
25+
26+
let converter = PDFToDOCXConverter()
27+
try converter.convertToFile(
28+
input: URL(fileURLWithPath: "paper.pdf"),
29+
output: URL(fileURLWithPath: "paper.docx")
30+
)
31+
```
32+
33+
## CLI
34+
35+
After wiring into `macdoc`:
36+
37+
```bash
38+
macdoc pdf to-docx input.pdf -o output.docx
39+
# or let macdoc choose input.docx next to the source PDF
40+
macdoc pdf to-docx input.pdf
41+
```
42+
43+
## Testing
44+
45+
```bash
46+
cd packages/pdf-to-docx-swift
47+
swift build
48+
swift test
49+
```
50+
51+
## Notes
52+
53+
This package prioritizes editable Word output from native PDF text extraction. It does not yet reconstruct embedded images or advanced PDF layout semantics.

0 commit comments

Comments
 (0)