Skip to content

Markdown 변환

PDF Oxide는 PDF 페이지를 깔끔하고 읽기 쉬운 Markdown으로 변환합니다. 변환 파이프라인은 텍스트 스팬을 추출하고 행으로 클러스터링한 뒤, 태그된 PDF의 경우 /StructTreeRoot에서 제목과 목록 역할을 참조하고, 다단 컬럼 간격과 역방향 x 읽기 순서 줄바꿈을 감지하며, 단락을 그룹화하여 Markdown 구문을 출력합니다.

v0.3.36부터 태그된 PDF의 경우, 폰트 크기에서 제목 수준을 다시 유추하는 대신 /StructTreeRoot에서 StructRole(Heading(1..6) | ListItem | ListItemLabel | ListItemBody)를 직접 읽어옵니다. 역할 정보는 중첩된 MCR을 통해 전파됩니다(H1 → Span → MCR, LI → LBody → Span → MCR). 태그 없는 문서에서는 기존의 기하학적 폴백이 그대로 적용됩니다. 굵은 텍스트 + 5% 크기 증가는 H4로 승격되며, is_ordered_list_marker1. / 12. / a) / iv. / A.를 인식하면서 그림 캡션과 연도는 제외합니다.

다단 컬럼 처리: > max(3 × font_size, 30 pt)로 분리된 동일 기준선의 스팬은 컬럼 간 요소로 처리됩니다. 역방향 x 읽기 순서 줄바꿈(컬럼 우선 마지막→첫 스팬)은 의미 없는 토큰으로 합치는 대신 단락을 나눕니다.

RTL: bidi 재정렬은 기본적으로 꺼져 있습니다. 이전의 무조건적인 시각적→논리적 재정렬은 논리 순서 PDF(히브리어 בנימין가 뒤집어졌던 문제)를 망가뜨렸습니다. 아랍어 컨텍스트 글리프 주변의 불필요한 **bold** 마커는 제거됩니다. 입력이 시각적 순서인 경우 호출자가 text::bidi::reorder_visual_to_logical을 수동으로 호출할 수 있습니다(Rust).

인라인 이미지는 base64 페이로드가 200 KB로 제한됩니다(v0.3.36 추가). 한도를 초과한 이미지는 원본 크기를 명시하는 HTML 주석을 출력합니다. 디스크에 저장하려면 image_output_dir을 사용하세요.

빠른 예제

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("paper.pdf")
md = doc.to_markdown(0, detect_headings=True)
print(md)

Node.js

const { PdfDocument } = require("pdf-oxide");

const doc = new PdfDocument("paper.pdf");
const md = doc.toMarkdown(0, { detectHeadings: true });
console.log(md);
doc.close();

Go

import pdfoxide "github.com/yfedoseev/pdf_oxide/go"

doc, _ := pdfoxide.Open("paper.pdf")
defer doc.Close()
md, _ := doc.ToMarkdown(0)
fmt.Println(md)

C#

using PdfOxide.Core;

using var doc = PdfDocument.Open("paper.pdf");
var md = doc.ToMarkdown(0);
Console.WriteLine(md);

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdown(0, true);
console.log(md);

Rust

use pdf_oxide::PdfDocument;
use pdf_oxide::converters::ConversionOptions;

let mut doc = PdfDocument::open("paper.pdf")?;
let options = ConversionOptions { detect_headings: true, ..Default::default() };
let md = doc.to_markdown(0, &options)?;
println!("{}", md);

Java

import fyi.oxide.pdf.PdfDocument;

try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("paper.pdf"))) {
    String md = doc.toMarkdown(0);
    System.out.println(md);
}

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open(java.nio.file.Path.of("paper.pdf")).use { doc ->
    val md = doc.toMarkdown(0)
    println(md)
}

Scala

import fyi.oxide.pdf.PdfDocument
import scala.util.Using

Using.resource(PdfDocument.open("paper.pdf")) { doc =>
  val md = doc.toMarkdown(0)
  println(md)
}

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "paper.pdf")]
  (println (pdf/to-markdown doc 0)))

PHP

use PdfOxide\PdfDocument;

$doc = PdfDocument::open('paper.pdf');
echo $doc->toMarkdown(0);
$doc->close();

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('paper.pdf') do |doc|
  puts doc.to_markdown(0)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("paper.pdf");
auto md = doc.to_markdown(0);
std::cout << md << std::endl;

Swift

import PdfOxide

let doc = try Document.open("paper.pdf")
let md = try doc.toMarkdown(0)
print(md)

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('paper.pdf');
final md = doc.toMarkdown(0);
print(md);

R

library(pdfoxide)

doc <- pdf_open("paper.pdf")
md <- pdf_to_markdown(doc, 0)
cat(md)

Julia

using PdfOxide

doc = open_document("paper.pdf")
md = to_markdown(doc, 0)
println(md)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("paper.pdf");
const md = try doc.toMarkdown(a, 0);
std.debug.print("{s}\n", .{md});

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"paper.pdf" error:&err];
NSString *md = [doc toMarkdown:0 error:&err];
NSLog(@"%@", md);

Elixir

{:ok, doc} = PdfOxide.open("paper.pdf")
{:ok, md} = PdfOxide.to_markdown(doc, 0)
IO.puts(md)

API 레퍼런스

to_markdown(page_index, ...) -> str

단일 페이지를 Markdown으로 변환합니다.

Python Signature

doc.to_markdown(
    page: int,
    preserve_layout: bool = False,
    detect_headings: bool = True,
    include_images: bool = True,
    image_output_dir: str | None = None,
    embed_images: bool = True,
) -> str

JavaScript Signature

doc.toMarkdown(pageIndex, detectHeadings?, includeImages?, includeFormFields?) -> string

Rust Signature

pub fn to_markdown(
    &mut self,
    page_index: usize,
    options: &ConversionOptions,
) -> Result<String>

Java Signature

String toMarkdown(int pageIndex)

Kotlin Signature

fun toMarkdown(pageIndex: Int): String

Scala Signature

def toMarkdown(pageIndex: Int): String

Clojure Signature

(pdf/to-markdown doc page-index) ; => String

PHP Signature

public function toMarkdown(int $pageIndex): string

Ruby Signature

doc.to_markdown(page_index) # => String

C++ Signature

std::string to_markdown(int page_index) const;

Swift Signature

func toMarkdown(_ pageIndex: Int) throws -> String

Dart Signature

String toMarkdown(int pageIndex)

R Signature

pdf_to_markdown(doc, page_index)  # character

Julia Signature

to_markdown(doc, page_index)::String

Zig Signature

pub fn toMarkdown(self: *Document, allocator: std.mem.Allocator, page_index: usize) ![]u8

Objective-C Signature

- (NSString *)toMarkdown:(NSInteger)pageIndex error:(NSError **)error;

Elixir Signature

PdfOxide.to_markdown(doc, page_index) :: {:ok, String.t()} | {:error, term()}
매개변수 타입 기본값 설명
page_index int / usize / number 0부터 시작하는 페이지 인덱스
preserve_layout bool false 시각적 레이아웃 배치 유지
detect_headings bool true 폰트 크기와 굵기로 제목 감지
include_images bool true 출력에 이미지 포함
image_output_dir str / None None 추출된 이미지를 저장할 디렉터리 (Python/Rust만 해당). 200 KB 인라인 한도의 영향을 받지 않음.
embed_images bool true 이미지를 base64 데이터 URI로 삽입 (Python/Rust만 해당). 200 KB 초과 페이로드는 원본 크기를 명시하는 HTML 주석 출력 (v0.3.36).
include_form_fields bool true 폼 필드 값 포함 (Python/JS)

반환값: 해당 페이지의 Markdown 문자열.


to_markdown_all(...) -> str

모든 페이지를 Markdown으로 변환하고 수평선(---)으로 구분하여 결합합니다.

Python Signature

doc.to_markdown_all(
    preserve_layout: bool = False,
    detect_headings: bool = True,
    include_images: bool = True,
    image_output_dir: str | None = None,
    embed_images: bool = True,
) -> str

JavaScript Signature

doc.toMarkdownAll(detectHeadings?, includeImages?, includeFormFields?) -> string

Rust Signature

pub fn to_markdown_all(
    &mut self,
    options: &ConversionOptions,
) -> Result<String>

Java Signature

String toMarkdown()  // no-arg overload = whole document

Kotlin Signature

fun toMarkdown(): String  // no-arg = whole document

Scala Signature

def toMarkdown(): String  // no-arg = whole document

Clojure Signature

(pdf/to-markdown doc) ; no page index = whole document => String

PHP Signature

public function toMarkdownAll(): string

Ruby Signature

doc.to_markdown # nil page index = whole document => String

C++ Signature

std::string to_markdown_all() const;

Swift Signature

func toMarkdownAll() throws -> String

Dart Signature

String toMarkdownAll()

R Signature

pdf_to_markdown_all(doc)  # character

Julia Signature

to_markdown_all(doc)::String

Zig Signature

pub fn toMarkdownAll(self: *Document, allocator: std.mem.Allocator) ![]u8

Objective-C Signature

- (NSString *)toMarkdownAllWithError:(NSError **)error;

Elixir Signature

PdfOxide.to_markdown_all(doc) :: {:ok, String.t()} | {:error, term()}
매개변수 타입 기본값 설명
preserve_layout bool false 시각적 레이아웃 유지
detect_headings bool true 제목 감지
include_images bool true 이미지 포함
image_output_dir str / None None 이미지 출력 디렉터리
embed_images bool true 이미지를 base64로 삽입

반환값: 모든 페이지를 --- 구분자로 결합한 Markdown 문자열.


to_markdown_with_ocr(page_index, model_path, options) -> str

스캔된 페이지에 OCR 폴백을 적용하여 Markdown으로 변환합니다. 페이지에서 추출 가능한 텍스트가 거의 없거나 전혀 없을 때 렌더링된 페이지 이미지에서 텍스트를 인식하기 위해 OCR을 사용합니다. ocr 기능이 필요합니다.

매개변수 타입 설명
page_index usize 0부터 시작하는 페이지 인덱스
model_path &str OCR 모델 파일 경로
options &ConversionOptions 변환 옵션

Rust

let mut doc = PdfDocument::open("scanned.pdf")?;
let options = ConversionOptions { detect_headings: true, ..Default::default() };
let md = doc.to_markdown_with_ocr(0, "/path/to/models", &options)?;
println!("{}", md);

ConversionOptions

ConversionOptions 구조체는 모든 변환 동작을 제어합니다.

필드 타입 기본값 설명
preserve_layout bool false 위치 정보와 함께 시각적 레이아웃 유지
detect_headings bool true 폰트 크기 클러스터로 제목 자동 감지
extract_tables bool false 표 추출 (실험적)
include_images bool true 출력에 이미지 포함
image_output_dir Option<String> None 이 디렉터리에 이미지 저장
embed_images bool true 이미지를 base64 데이터 URI로 삽입
reading_order_mode ReadingOrderMode Auto 읽기 순서 결정 방식
bold_marker_behavior BoldMarkerBehavior Conservative 굵은 텍스트 마커 적용 전략

동작 원리

Markdown 변환 파이프라인은 여러 단계로 작동합니다:

  1. 텍스트 추출 – 페이지 콘텐츠 스트림에서 TextSpan 객체를 추출하여 텍스트, 위치, 폰트, 크기, 굵기, 색상을 캡처합니다.

  2. 문자 클러스터링 – 문자 간 간격을 기준으로 문자를 단어로 묶고, 수직 근접성을 기준으로 단어를 행으로 묶습니다.

  3. 읽기 순서 – 태그된 PDF 구조 트리(우선) 또는 텍스트 블록 위치의 그래프 기반 공간 분석을 사용하여 읽기 순서를 결정합니다.

  4. 제목 감지detect_headings가 활성화된 경우 페이지 전체의 폰트 크기를 클러스터링하여 제목 수준을 식별합니다. 더 크고 굵은 텍스트는 #, ##, ### 제목으로 매핑됩니다.

  5. 서식 지정 – 폰트 굵기 및 스타일 메타데이터를 기반으로 굵은 텍스트(**text**)와 이탤릭(*text*) 마커를 적용합니다.

  6. 표 감지 – 정렬된 텍스트 블록의 공간 분석을 통해 표 형식 레이아웃을 식별하고 GFM 스타일 Markdown 표를 출력합니다.

  7. 공백 정리 – 간격을 정규화하고 중복 빈 줄을 제거하며 일관된 단락 구분을 확보합니다.


고급 예제

전체 PDF를 Markdown 파일로 변환

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("book.pdf")
md = doc.to_markdown_all(detect_headings=True)

with open("book.md", "w", encoding="utf-8") as f:
    f.write(md)

Node.js

const fs = require("node:fs");

const doc = new PdfDocument("book.pdf");
const md = doc.toMarkdownAll();
fs.writeFileSync("book.md", md);
doc.close();

Go

doc, _ := pdfoxide.Open("book.pdf")
defer doc.Close()
md, _ := doc.ToMarkdownAll()
os.WriteFile("book.md", []byte(md), 0644)

C#

using var doc = PdfDocument.Open("book.pdf");
var md = doc.ToMarkdownAll();
File.WriteAllText("book.md", md);

WASM

const doc = new WasmPdfDocument(bytes);
const md = doc.toMarkdownAll(true);
writeFileSync("book.md", md);
doc.free();

Java

import fyi.oxide.pdf.PdfDocument;
import java.nio.file.*;

try (PdfDocument doc = PdfDocument.open(Path.of("book.pdf"))) {
    String md = doc.toMarkdown();
    Files.writeString(Path.of("book.md"), md);
}

Kotlin

import fyi.oxide.pdf.PdfDocument
import java.nio.file.*

PdfDocument.open(Path.of("book.pdf")).use { doc ->
    Files.writeString(Path.of("book.md"), doc.toMarkdown())
}

Scala

import fyi.oxide.pdf.PdfDocument
import java.nio.file.{Files, Path}
import scala.util.Using

Using.resource(PdfDocument.open("book.pdf")) { doc =>
  Files.writeString(Path.of("book.md"), doc.toMarkdown())
}

Clojure

(require '[pdf-oxide.core :as pdf]
         '[clojure.java.io :as io])

(with-open [doc (pdf/open "book.pdf")]
  (spit "book.md" (pdf/to-markdown doc)))

PHP

use PdfOxide\PdfDocument;

$doc = PdfDocument::open('book.pdf');
file_put_contents('book.md', $doc->toMarkdownAll());
$doc->close();

Ruby

require 'pdf_oxide'

PdfOxide::PdfDocument.open('book.pdf') do |doc|
  File.write('book.md', doc.to_markdown)
end

C++

#include <pdf_oxide/pdf_oxide.hpp>
#include <fstream>

auto doc = pdf_oxide::Document::open("book.pdf");
auto md = doc.to_markdown_all();
std::ofstream("book.md") << md;

Swift

import PdfOxide

let doc = try Document.open("book.pdf")
let md = try doc.toMarkdownAll()
try md.write(toFile: "book.md", atomically: true, encoding: .utf8)

Dart

import 'dart:io';
import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('book.pdf');
final md = doc.toMarkdownAll();
File('book.md').writeAsStringSync(md);

R

library(pdfoxide)

doc <- pdf_open("book.pdf")
md <- pdf_to_markdown_all(doc)
writeLines(md, "book.md")

Julia

using PdfOxide

doc = open_document("book.pdf")
md = to_markdown_all(doc)
write("book.md", md)

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("book.pdf");
const md = try doc.toMarkdownAll(a);
try std.fs.cwd().writeFile(.{ .sub_path = "book.md", .data = md });

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"book.pdf" error:&err];
NSString *md = [doc toMarkdownAllWithError:&err];
[md writeToFile:@"book.md" atomically:YES encoding:NSUTF8StringEncoding error:&err];

Elixir

{:ok, doc} = PdfOxide.open("book.pdf")
{:ok, md} = PdfOxide.to_markdown_all(doc)
File.write!("book.md", md)

이미지를 디렉터리에 저장하여 변환

use pdf_oxide::PdfDocument;
use pdf_oxide::converters::ConversionOptions;

let mut doc = PdfDocument::open("report.pdf")?;
let options = ConversionOptions {
    detect_headings: true,
    include_images: true,
    embed_images: false,
    image_output_dir: Some("output/images".to_string()),
    ..Default::default()
};

let md = doc.to_markdown_all(&options)?;
std::fs::write("output/report.md", &md)?;

진행 상황 표시와 함께 페이지별 변환

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")
pages = doc.page_count()

parts = []
for i in range(pages):
    md = doc.to_markdown(i, detect_headings=True)
    parts.append(md)
    print(f"Converted page {i + 1}/{pages}")

full_md = "\n\n---\n\n".join(parts)
with open("report.md", "w") as f:
    f.write(full_md)

일반 텍스트를 위한 제목 감지 비활성화

doc = PdfDocument("form.pdf")
md = doc.to_markdown(0, detect_headings=False)
# All text rendered as paragraphs, no # headings

관련 페이지