Skip to content

Instantly share code, notes, and snippets.

@jvarn
Created July 31, 2026 06:55
Show Gist options
  • Select an option

  • Save jvarn/fe1059b108d8bbbc9704d1f2c77d84bb to your computer and use it in GitHub Desktop.

Select an option

Save jvarn/fe1059b108d8bbbc9704d1f2c77d84bb to your computer and use it in GitHub Desktop.
macOS Swift script to repair corrupted Arabic PDF text layers using native Apple Vision OCR
/**
* FixArabicPDFs.swift
*
* A zero-dependency CLI tool for macOS that repairs PDFs containing corrupt,
* uncopyable, or reversed Arabic text streams (common with MS Word PDF exports on Windows).
*
* HOW IT WORKS:
* 1. Renders PDF pages to high-DPI in-memory bitmaps, destroying broken font/CMap tables.
* 2. Applies a white margin padding canvas to prevent Arabic RTL edge-character clipping.
* 3. Leverages macOS native Vision framework (`VNRecognizeTextRequest`) for Arabic OCR.
* 4. Overlays a clean, invisible Unicode text layer back onto the PDF using CoreText,
* making it fully searchable and copyable for PDF readers, LLMs, and NotebookLM.
*
* REQUIREMENTS:
* - macOS 10.15 (Catalina) or later
* - Swift command-line toolchain (built into macOS / Xcode Command Line Tools)
* - Zero third-party dependencies (no Homebrew, Python, or Tesseract required)
*
* USAGE (Interpreted):
* swift FixArabicPDFs.swift <file_or_directory_path>
*
* USAGE (Compiled - Recommended for instant startup):
* swiftc -O FixArabicPDFs.swift -o fixpdf
* ./fixpdf <file_or_directory_path>
*
* EXAMPLES:
* ./fixpdf document.pdf
* ./fixpdf ~/Documents/PDF_Folder
*/
import Foundation
import PDFKit
import Vision
import CoreGraphics
import CoreText
import AppKit
// Force immediate terminal output (prevents output buffering delay)
func log(_ message: String, newline: Bool = true) {
if newline {
print(message)
} else {
print(message, terminator: "")
}
fflush(stdout)
}
// MARK: - Core Processing Logic
struct RenderedPage {
let cgImage: CGImage
let canvasWidth: CGFloat
let canvasHeight: CGFloat
let scale: CGFloat
let marginPx: CGFloat
}
func renderPageToCGImage(page: PDFPage, dpi: CGFloat = 300.0, marginPx: CGFloat = 0.0) -> RenderedPage? {
let pageRect = page.bounds(for: .mediaBox)
let scale = dpi / 72.0
let imgWidth = pageRect.width * scale
let imgHeight = pageRect.height * scale
let canvasWidth = imgWidth + (2 * marginPx)
let canvasHeight = imgHeight + (2 * marginPx)
let colorSpace = CGColorSpaceCreateDeviceRGB()
guard let context = CGContext(
data: nil,
width: Int(canvasWidth),
height: Int(canvasHeight),
bitsPerComponent: 8,
bytesPerRow: Int(canvasWidth) * 4,
space: colorSpace,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
) else { return nil }
// Fill with white background
context.setFillColor(CGColor(red: 1, green: 1, blue: 1, alpha: 1))
context.fill(CGRect(x: 0, y: 0, width: canvasWidth, height: canvasHeight))
context.saveGState()
// Translate by margin and scale by DPI factor
context.translateBy(x: marginPx, y: marginPx)
context.scaleBy(x: scale, y: scale)
page.draw(with: .mediaBox, to: context)
context.restoreGState()
guard let cgImage = context.makeImage() else { return nil }
return RenderedPage(cgImage: cgImage, canvasWidth: canvasWidth, canvasHeight: canvasHeight, scale: scale, marginPx: marginPx)
}
func recognizeText(cgImage: CGImage) -> [VNRecognizedTextObservation] {
var observations: [VNRecognizedTextObservation] = []
let request = VNRecognizeTextRequest { req, _ in
if let results = req.results as? [VNRecognizedTextObservation] {
observations = results
}
}
request.recognitionLanguages = ["ar-SA", "en-US"]
request.recognitionLevel = .accurate
request.usesLanguageCorrection = true
let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
try? handler.perform([request])
return observations
}
func processSinglePDF(inputURL: URL, outputURL: URL) {
log("[INFO] Reading PDF: \(inputURL.lastPathComponent)...")
guard let pdfDoc = PDFDocument(url: inputURL) else {
log("[ERROR] Could not open PDF: \(inputURL.lastPathComponent)")
return
}
let totalPages = pdfDoc.pageCount
log("[INFO] Found \(totalPages) page(s). Creating output PDF...")
guard let consumer = CGDataConsumer(url: outputURL as CFURL),
let pdfContext = CGContext(consumer: consumer, mediaBox: nil, nil) else {
log("[ERROR] Failed to create PDF output context.")
return
}
let marginPx: CGFloat = 40.0 // 40px white border buffer for OCR
for i in 0..<totalPages {
let pageNum = i + 1
log(" |-- [Page \(pageNum)/\(totalPages)] Rendering image...", newline: false)
guard let page = pdfDoc.page(at: i) else {
log(" [FAILED to load page]")
continue
}
let pageBounds = page.bounds(for: .mediaBox)
var pageRect = pageBounds
pdfContext.beginPage(mediaBox: &pageRect)
// 1. Render unpadded image for the PDF visual layer
guard let unpaddedPage = renderPageToCGImage(page: page, dpi: 300, marginPx: 0) else {
log(" [FAILED to render image]")
continue
}
pdfContext.draw(unpaddedPage.cgImage, in: pageBounds)
// 2. Render padded image for Apple Vision OCR
guard let paddedPage = renderPageToCGImage(page: page, dpi: 300, marginPx: marginPx) else {
log(" [FAILED to render padded image]")
continue
}
log(" running Vision OCR...", newline: false)
let observations = recognizeText(cgImage: paddedPage.cgImage)
// 3. Draw invisible text layer (translating padded coordinates back to PDF points)
log(" writing text layer...", newline: false)
pdfContext.saveGState()
pdfContext.setTextDrawingMode(.invisible)
for obs in observations {
guard let candidate = obs.topCandidates(1).first else { continue }
let string = candidate.string
let bbox = obs.boundingBox
// Calculate absolute pixel coordinates on padded canvas
let pixelX = bbox.origin.x * paddedPage.canvasWidth
let pixelY = bbox.origin.y * paddedPage.canvasHeight
let pixelH = bbox.size.height * paddedPage.canvasHeight
// Subtract margin offset and convert back to PDF point coordinates
let pointX = (pixelX - marginPx) / paddedPage.scale
let pointY = (pixelY - marginPx) / paddedPage.scale
let fontHeightPoints = pixelH / paddedPage.scale
let fontSize = max(fontHeightPoints * 0.85, 6.0)
let font = CTFontCreateWithName("Helvetica" as CFString, fontSize, nil)
let attributes: [NSAttributedString.Key: Any] = [.font: font]
let attrString = NSAttributedString(string: string, attributes: attributes)
let line = CTLineCreateWithAttributedString(attrString)
pdfContext.textPosition = CGPoint(x: pointX, y: pointY)
CTLineDraw(line, pdfContext)
}
pdfContext.restoreGState()
pdfContext.endPage()
log(" [OK]")
}
pdfContext.closePDF()
log("==> Complete: \(outputURL.lastPathComponent)\n")
}
// MARK: - CLI Entry Point
log("==> Initializing macOS Arabic PDF OCR Pipeline...")
let args = CommandLine.arguments
guard args.count > 1 else {
log("""
Usage:
swift FixArabicPDFs.swift <file_or_folder_path>
""")
exit(1)
}
let inputPath = NSString(string: args[1]).expandingTildeInPath
let inputURL = URL(fileURLWithPath: inputPath)
var isDir: ObjCBool = false
if FileManager.default.fileExists(atPath: inputPath, isDirectory: &isDir) {
if isDir.boolValue {
let outputDir = inputURL.appendingPathComponent("Searchable_PDFs")
try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
let fileURLs = try FileManager.default.contentsOfDirectory(at: inputURL, includingPropertiesForKeys: nil)
.filter { $0.pathExtension.lowercased() == "pdf" && !$0.lastPathComponent.hasPrefix("fixed_") }
log("[INFO] Found \(fileURLs.count) PDF(s) in directory.\n")
for fileURL in fileURLs {
let outputURL = outputDir.appendingPathComponent("fixed_\(fileURL.lastPathComponent)")
processSinglePDF(inputURL: fileURL, outputURL: outputURL)
}
} else {
let outputURL = inputURL.deletingLastPathComponent().appendingPathComponent("fixed_\(inputURL.lastPathComponent)")
processSinglePDF(inputURL: inputURL, outputURL: outputURL)
}
} else {
log("[ERROR] Path does not exist: \(inputPath)")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment