Конвертация в Markdown
PDF Oxide конвертирует страницы PDF в чистый, читаемый Markdown. Конвейер обработки извлекает текстовые фрагменты, группирует их в строки, обращается к /StructTreeRoot для получения ролей заголовков и списков в тегированных PDF, обнаруживает промежутки многоколоночных макетов и переносы в обратном порядке чтения по оси x, группирует абзацы и генерирует синтаксис Markdown.
Начиная с v0.3.36, для тегированных PDF конвертер считывает StructRole(Heading(1..6) | ListItem | ListItemLabel | ListItemBody) напрямую из /StructTreeRoot, вместо того чтобы повторно выводить уровни заголовков из размера шрифта. Информация о ролях передаётся через вложенные MCR (H1 → Span → MCR, LI → LBody → Span → MCR). Для PDF без тегов по-прежнему применяется геометрический запасной вариант: жирный текст + увеличение размера на 5% даёт H4, а is_ordered_list_marker распознаёт 1. / 12. / a) / iv. / A., при этом отклоняя подписи к рисункам и годы.
Работа с многоколоночными макетами: фрагменты с одинаковой базовой линией, разделённые расстоянием > max(3 × font_size, 30 pt), считаются межколоночными. Переносы в обратном порядке чтения по x (от последнего к первому фрагменту в порядке столбцов) разрывают абзацы вместо того, чтобы объединять их в бессмысленные токены.
RTL: bidi-переупорядочивание по умолчанию отключено — прежнее безусловное преобразование из визуального порядка в логический ломало PDF с логическим порядком (еврейское слово בנימין переворачивалось). Лишние маркеры **bold** вокруг контекстных глифов арабского языка удаляются. Если входные данные расположены в визуальном порядке, вызывающий код может вызвать text::bidi::reorder_visual_to_logical вручную (Rust).
Встроенные изображения ограничены полезной нагрузкой base64 в 200 КБ (добавлено в 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 |
– | Индекс страницы, начиная с нуля |
preserve_layout |
bool |
false |
Сохранять визуальное расположение элементов |
detect_headings |
bool |
true |
Обнаруживать заголовки по размеру и насыщенности шрифта |
include_images |
bool |
true |
Включать изображения в вывод |
image_output_dir |
str / None |
None |
Каталог для сохранения извлечённых изображений (только Python/Rust). Не зависит от лимита 200 КБ для встроенных изображений. |
embed_images |
bool |
true |
Встраивать изображения как base64 data URI (только Python/Rust). Нагрузка свыше 200 КБ выводит 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
Конвертирует страницу в Markdown с OCR-резервом для сканированных страниц. Если на странице мало или совсем нет извлекаемого текста, для распознавания текста на отрендеренном изображении страницы применяется OCR. Требует фичи ocr.
| Параметр | Тип | Описание |
|---|---|---|
page_index |
usize |
Индекс страницы, начиная с нуля |
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 data URI |
reading_order_mode |
ReadingOrderMode |
Auto |
Способ определения порядка чтения |
bold_marker_behavior |
BoldMarkerBehavior |
Conservative |
Стратегия применения маркеров жирного текста |
Принцип работы
Конвейер конвертации в Markdown работает в несколько этапов:
-
Извлечение текста – Извлекает объекты
TextSpanиз потока содержимого страницы, захватывая текст, позицию, шрифт, размер, насыщенность и цвет. -
Кластеризация символов – Группирует символы в слова по межсимвольным промежуткам, затем объединяет слова в строки по вертикальной близости.
-
Порядок чтения – Определяет порядок чтения, используя дерево структуры тегированного PDF (предпочтительно) или графовый пространственный анализ позиций текстовых блоков.
-
Обнаружение заголовков – При включённом
detect_headingsкластеризует размеры шрифтов по всей странице для определения уровней заголовков. Более крупный и жирный текст маппируется на заголовки#,##,###. -
Форматирование – Применяет маркеры жирного (
**text**) и курсивного (*text*) текста на основе метаданных насыщенности и стиля шрифта. -
Обнаружение таблиц – Выявляет табличные макеты с помощью пространственного анализа выровненных текстовых блоков и генерирует таблицы в стиле GFM.
-
Нормализация пробелов – Выравнивает отступы, удаляет лишние пустые строки и обеспечивает единообразие разрывов абзацев.
Расширенные примеры
Конвертация всего 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
Связанные страницы
- Извлечение текста – Сырой текст и извлечение фрагментов
- Конвертация в HTML – Конвертация в HTML вместо Markdown
- Извлечение изображений – Извлечение изображений по отдельности