Skip to content

Редактирование и санитизация

Настоящее редактирование является деструктивным: базовые глифы, изображения и пути должны быть физически удалены из потока содержимого, а не просто перекрыты чёрным прямоугольником. PDF Oxide v0.3.69 реализует редактирование по ISO 32000-1:2008 §12.5.6.23 — redaction_apply возвращает количество физически удалённых глифов и завершается отказом (отклоняет операцию, вместо того чтобы молча пропустить часть содержимого), если на странице используется составной шрифт/Type0, который невозможно безопасно переписать.

На этой странице описывается семейство канонического деструктивного редактирования (redaction_add / redaction_apply / redaction_count / redaction_scrub_metadata), а также семейство удаления колонтитулов и артефактов (remove_headers / remove_footers / remove_artifacts и постраничные erase_header / erase_footer / erase_artifacts).

Два способа редактирования. В v0.3.69 по-прежнему доступен устаревший путь выравнивания аннотаций (apply_page_redactions / apply_all_redactions), сжигающий аннотации /Redact в косметическое наложение. Если вам нужно, чтобы содержимое исчезло, предпочитайте деструктивное семейство, описанное здесь. Деструктивный redaction_apply также обрабатывает любые аннотации /Redact, уже присутствующие в источнике, поэтому оба способа совместимы.

Поддержка привязок. Семейство деструктивного редактирования доступно в Rust, Python, Go, C# и сборке WASM/JavaScript. Семейство remove_* для колонтитулов и артефактов доступно в Rust, Python, Go и WASM/JavaScript; постраничное семейство erase_* — в Rust, Python и WASM/JavaScript. C# пока не предоставляет семейства remove_* / erase_* для элементов страницы.

Как удалить текст из PDF с помощью редактирования?

Деструктивный рабочий процесс состоит из трёх шагов:

  1. Добавьте в очередь прямоугольники редактирования с помощью redaction_add (координаты в пользовательском пространстве страницы; необязательная заливка наложения в DeviceRGB). Аннотации /Redact, уже имеющиеся в источнике, подхватываются автоматически.
  2. Применяйте с помощью redaction_apply — это физически удаляет глифы/изображения/пути под прямоугольниками, отрисовывает непрозрачное наложение, при необходимости очищает метаданные документа и возвращает количество удалённых глифов.
  3. Сохраните переписанный PDF. Исходные объекты /Contents жёстко удаляются (G6), поэтому секретные данные не могут выжить как пропущенные сборщиком мусора сироты.

Добавление прямоугольника редактирования в очередь

redaction_add(page, rect, fill) добавляет деструктивный прямоугольник в очередь. Координаты задаются в пользовательском пространстве страницы (x0, y0, x1, y1); fill — необязательный цвет наложения DeviceRGB [r, g, b] (по умолчанию чёрный).

Rust

use pdf_oxide::editor::{DocumentEditor, EditableDocument};
use pdf_oxide::redaction::RedactionOptions;

let mut editor = DocumentEditor::open("confidential.pdf")?;

// Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
editor.add_redaction(0, [100.0, 700.0, 300.0, 714.0], None)?;

// Apply destructively, then save. Returns a RedactionReport.
let report = editor.apply_redactions_destructive(RedactionOptions::default())?;
println!("glyphs removed: {}", report.glyphs_removed);

editor.save("redacted.pdf")?;

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("confidential.pdf")

# Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
doc.add_redaction(0, (100.0, 700.0, 300.0, 714.0))

# Apply destructively (returns a report dict), then save.
report = doc.apply_redactions_destructive()
print("glyphs removed:", report["glyphs_removed"])

doc.save("redacted.pdf")

Go

editor, _ := pdfoxide.OpenEditor("confidential.pdf")
defer editor.Close()

// Queue a redaction box on page 0; pass nil fill for the default black.
_ = editor.AddRedaction(0, [4]float64{100, 700, 300, 714}, nil)

// Apply destructively (scrubMetadata = true). Returns glyphs removed.
glyphs, _ := editor.ApplyRedactions(true)
fmt.Printf("glyphs removed: %d\n", glyphs)

_ = editor.Save("redacted.pdf")

C#

using PdfOxide;

using var editor = DocumentEditor.Open("confidential.pdf");

// Queue a redaction box on page 0 (x1, y1, x2, y2; r, g, b default to black).
editor.AddRedaction(0, 100, 700, 300, 714);

// Apply destructively (scrubMetadata defaults to true). Returns glyphs removed.
int glyphs = editor.ApplyRedactions(scrubMetadata: true);
Console.WriteLine($"glyphs removed: {glyphs}");

editor.Save("redacted.pdf");

JavaScript (WASM)

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);

// Queue a redaction box on page 0; pass an [r, g, b] array for a custom fill.
doc.addRedaction(0, 100, 700, 300, 714);

// Apply destructively. Returns a RedactionReport object.
const report = doc.applyRedactionsDestructive(true);
console.log("glyphs removed:", report.glyphs_removed);

const output = doc.save();
doc.free();

Java

import fyi.oxide.pdf.DocumentEditor;
import fyi.oxide.pdf.geometry.BBox;

try (DocumentEditor editor = DocumentEditor.open("confidential.pdf")) {
    // Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
    editor.addRedaction(0, new BBox(100.0, 700.0, 300.0, 714.0));

    // Apply destructively (scrubs metadata), then save.
    editor.applyRedactionsDestructive();
    editor.saveTo(java.nio.file.Path.of("redacted.pdf"));
}

Kotlin

import fyi.oxide.pdf.DocumentEditor
import fyi.oxide.pdf.geometry.BBox

DocumentEditor.open("confidential.pdf").use { editor ->
    // Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
    editor.addRedaction(0, BBox(100.0, 700.0, 300.0, 714.0))

    // Apply destructively (scrubs metadata), then save.
    editor.applyRedactionsDestructive()
    editor.saveTo(java.nio.file.Path.of("redacted.pdf"))
}

Scala

import fyi.oxide.pdf.DocumentEditor
import fyi.oxide.pdf.geometry.BBox
import scala.util.Using

Using.resource(DocumentEditor.open("confidential.pdf")) { editor =>
  // Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
  editor.addRedaction(0, BBox(100.0, 700.0, 300.0, 714.0))

  // Apply destructively (scrubs metadata), then save.
  editor.applyRedactionsDestructive()
  editor.saveTo(java.nio.file.Path.of("redacted.pdf"))
}

Clojure

(require '[pdf-oxide.core :as pdf])
(import '[fyi.oxide.pdf.geometry BBox])

(with-open [ed (pdf/editor "confidential.pdf")]
  ;; Queue a black redaction box on page 0 (x0, y0, x1, y1 in points).
  (pdf/add-redaction ed 0 (BBox. 100.0 700.0 300.0 714.0))
  ;; Apply destructively (scrubs metadata), then save.
  (pdf/apply-redactions ed)
  (java.nio.file.Files/write
    (java.nio.file.Path/of "redacted.pdf" (into-array String []))
    (pdf/editor-save ed)))

PHP

use PdfOxide\DocumentEditor;

$editor = DocumentEditor::open('confidential.pdf');

// Queue a black redaction box on page 0 (x1, y1, x2, y2 in points).
$editor->addRedaction(0, 100.0, 700.0, 300.0, 714.0);

// Apply destructively (scrubMetadata defaults to true). Returns glyphs removed.
$glyphs = $editor->applyRedactionsDestructive(true);
echo "glyphs removed: $glyphs\n";

$editor->saveTo('redacted.pdf');

Ruby

require 'pdf_oxide'

PdfOxide::DocumentEditor.open('confidential.pdf') do |ed|
  # Queue a black redaction box on page 0 (x1, y1, x2, y2 in points).
  ed.add_redaction(page: 0, rect: [100.0, 700.0, 300.0, 714.0])

  # Apply destructively, scrubbing metadata (raises on fail-closed), then save.
  ed.apply_redactions!(scrub_metadata: true)
  ed.save_to('redacted.pdf')
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto editor = pdf_oxide::DocumentEditor::open("confidential.pdf");

// Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
editor.redaction_add(0, 100.0, 700.0, 300.0, 714.0, 0.0, 0.0, 0.0);

// Apply destructively (scrub_metadata = true). Returns glyphs removed.
int glyphs = editor.redaction_apply(/*scrub_metadata=*/true, 0.0, 0.0, 0.0);
std::cout << "glyphs removed: " << glyphs << "\n";

editor.save("redacted.pdf");

Swift

import PdfOxide

let editor = try DocumentEditor.open("confidential.pdf")

// Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
try editor.redactionAdd(0, x1: 100, y1: 700, x2: 300, y2: 714, r: 0, g: 0, b: 0)

// Apply destructively (scrub metadata). Returns glyphs removed.
let glyphs = try editor.redactionApply(scrubMetadata: true, r: 0, g: 0, b: 0)
print("glyphs removed: \(glyphs)")

try editor.save("redacted.pdf")

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final editor = DocumentEditor.open('confidential.pdf');

// Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
editor.redactionAdd(0, 100, 700, 300, 714);

// Apply destructively, scrubbing metadata. Returns glyphs removed.
final glyphs = editor.redactionApply(scrubMetadata: true);
print('glyphs removed: $glyphs');

editor.save('redacted.pdf');

R

library(pdfoxide)

editor <- pdf_editor_open("confidential.pdf")

# Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
pdf_redaction_add(editor, 0, 100, 700, 300, 714, 0, 0, 0)

# Apply destructively, scrubbing metadata. Returns glyphs removed.
glyphs <- pdf_redaction_apply(editor, scrub_metadata = TRUE, 0, 0, 0)
cat("glyphs removed:", glyphs, "\n")

pdf_editor_save(editor, "redacted.pdf")

Julia

using PdfOxide

editor = open_editor("confidential.pdf")

# Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
redaction_add(editor, 0, 100, 700, 300, 714, 0, 0, 0)

# Apply destructively, scrubbing metadata. Returns glyphs removed.
glyphs = redaction_apply(editor, true, 0, 0, 0)
println("glyphs removed: ", glyphs)

save(editor, "redacted.pdf")

Zig

const pdf_oxide = @import("pdf_oxide");

var editor = try pdf_oxide.DocumentEditor.openEditor("confidential.pdf");
defer editor.deinit();

// Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
try editor.redactionAdd(0, 100, 700, 300, 714, 0, 0, 0);

// Apply destructively, scrubbing metadata. Returns glyphs removed.
const glyphs = try editor.redactionApply(true, 0, 0, 0);
std.debug.print("glyphs removed: {d}\n", .{glyphs});

try editor.save("redacted.pdf");

Objective-C

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

POXDocumentEditor *editor = [POXDocumentEditor openEditor:@"confidential.pdf" error:&err];

// Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
[editor redactionAddPage:0 x1:100 y1:700 x2:300 y2:714 r:0 g:0 b:0 error:&err];

// Apply destructively, scrubbing metadata. Returns glyphs removed.
int32_t glyphs = [editor redactionApplyScrubMetadata:YES r:0 g:0 b:0 error:&err];
NSLog(@"glyphs removed: %d", glyphs);

[editor saveToPath:@"redacted.pdf" error:&err];

Elixir

{:ok, editor} = PdfOxide.open_editor("confidential.pdf")

# Queue a black redaction box on page 0 (x1, y1, x2, y2; r, g, b in 0..1).
:ok = PdfOxide.redaction_add(editor, 0, 100, 700, 300, 714, 0.0, 0.0, 0.0)

# Apply destructively, scrubbing metadata. Returns {:ok, glyphs_removed}.
{:ok, glyphs} = PdfOxide.redaction_apply(editor, true, 0.0, 0.0, 0.0)
IO.puts("glyphs removed: #{glyphs}")

PdfOxide.editor_save(editor, "redacted.pdf")

Подсчёт областей в очереди

redaction_count(page) возвращает количество областей, поставленных в очередь для страницы, — аннотации /Redact в источнике плюс программные прямоугольники, добавленные через redaction_add. Используйте его, чтобы убедиться в наличии содержимого для редактирования перед применением.

let mut editor = DocumentEditor::open("marked.pdf")?;
editor.add_redaction(0, [100.0, 700.0, 300.0, 714.0], None)?;
assert_eq!(editor.redaction_count(0)?, 1);
doc = PdfDocument("marked.pdf")
doc.add_redaction(0, (100.0, 700.0, 300.0, 714.0))
assert doc.redaction_count(0) == 1
editor, _ := pdfoxide.OpenEditor("marked.pdf")
defer editor.Close()

_ = editor.AddRedaction(0, [4]float64{100, 700, 300, 714}, nil)
n, _ := editor.RedactionCount(0)  // 1
using var editor = DocumentEditor.Open("marked.pdf");
editor.AddRedaction(0, 100, 700, 300, 714);
int n = editor.RedactionCount(0);  // 1
const doc = new WasmPdfDocument(bytes);
doc.addRedaction(0, 100, 700, 300, 714);
const n = doc.redactionCount(0);  // 1

В чём разница между редактированием и санитизацией?

Геометрическое редактирование удаляет содержимое под прямоугольником. Санитизация устраняет секреты на уровне документа, которые прямоугольники редактирования никогда не покрывают: словарь /Info, поток XMP /Metadata каталога, JavaScript документа (/OpenAction, /AA, /Names/JavaScript) и /Names/EmbeddedFiles. Удалённые поддеревья объектов жёстко исключаются из вывода (G6).

redaction_apply автоматически запускает санитизацию, когда установлен флаг scrub_metadata (по умолчанию в каждой привязке). redaction_scrub_metadata запускает тот же проход санитизации самостоятельно, без геометрического редактирования — используйте его, когда нужно только очистить документ, не редактируя области. Он возвращает количество удалённых конструкций верхнего уровня.

Rust

use pdf_oxide::editor::{DocumentEditor, EditableDocument};
use pdf_oxide::redaction::RedactionOptions;

let mut editor = DocumentEditor::open("input.pdf")?;
let report = editor.sanitize_document(RedactionOptions::default())?;
println!("constructs removed: {}", report.annotations_removed);
editor.save("sanitized.pdf")?;

Python

doc = PdfDocument("input.pdf")
report = doc.sanitize_document()
print("constructs removed:", report["annotations_removed"])
doc.save("sanitized.pdf")

Go

editor, _ := pdfoxide.OpenEditor("input.pdf")
defer editor.Close()

removed, _ := editor.SanitizeDocument()  // top-level constructs removed
_ = editor.Save("sanitized.pdf")

C#

using var editor = DocumentEditor.Open("input.pdf");
int removed = editor.SanitizeDocument();  // top-level constructs removed
editor.Save("sanitized.pdf");

JavaScript (WASM)

const doc = new WasmPdfDocument(bytes);
const report = doc.sanitizeDocument(true, true, true);  // scrub, removeJS, removeEmbedded
const output = doc.save();
doc.free();

Java

import fyi.oxide.pdf.DocumentEditor;

try (DocumentEditor editor = DocumentEditor.open("input.pdf")) {
    editor.scrubMetadata();  // strip /Info, XMP, JS, embedded files — no geometry
    editor.saveTo(java.nio.file.Path.of("sanitized.pdf"));
}

Kotlin

import fyi.oxide.pdf.DocumentEditor

DocumentEditor.open("input.pdf").use { editor ->
    editor.scrubMetadata()  // strip /Info, XMP, JS, embedded files — no geometry
    editor.saveTo(java.nio.file.Path.of("sanitized.pdf"))
}

Scala

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

Using.resource(DocumentEditor.open("input.pdf")) { editor =>
  editor.scrubMetadata()  // strip /Info, XMP, JS, embedded files — no geometry
  editor.saveTo(java.nio.file.Path.of("sanitized.pdf"))
}

Clojure

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

(with-open [ed (pdf/editor "input.pdf")]
  (pdf/scrub-metadata ed)  ; strip /Info, XMP, JS, embedded files — no geometry
  (java.nio.file.Files/write
    (java.nio.file.Path/of "sanitized.pdf" (into-array String []))
    (pdf/editor-save ed)))

PHP

use PdfOxide\DocumentEditor;

$editor = DocumentEditor::open('input.pdf');
$editor->scrubMetadata();  // strip /Info, XMP, JS, embedded files — no geometry
$editor->saveTo('sanitized.pdf');

Ruby

require 'pdf_oxide'

PdfOxide::DocumentEditor.open('input.pdf') do |ed|
  removed = ed.scrub_metadata  # top-level constructs removed (no geometry)
  puts "constructs removed: #{removed}"
  ed.save_to('sanitized.pdf')
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto editor = pdf_oxide::DocumentEditor::open("input.pdf");
int removed = editor.redaction_scrub_metadata();  // constructs removed (no geometry)
std::cout << "constructs removed: " << removed << "\n";
editor.save("sanitized.pdf");

Swift

import PdfOxide

let editor = try DocumentEditor.open("input.pdf")
let removed = try editor.redactionScrubMetadata()  // constructs removed (no geometry)
print("constructs removed: \(removed)")
try editor.save("sanitized.pdf")

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final editor = DocumentEditor.open('input.pdf');
final removed = editor.redactionScrubMetadata();  // constructs removed (no geometry)
print('constructs removed: $removed');
editor.save('sanitized.pdf');

R

library(pdfoxide)

editor <- pdf_editor_open("input.pdf")
removed <- pdf_redaction_scrub_metadata(editor)  # constructs removed (no geometry)
cat("constructs removed:", removed, "\n")
pdf_editor_save(editor, "sanitized.pdf")

Julia

using PdfOxide

editor = open_editor("input.pdf")
removed = redaction_scrub_metadata(editor)  # constructs removed (no geometry)
println("constructs removed: ", removed)
save(editor, "sanitized.pdf")

Zig

const pdf_oxide = @import("pdf_oxide");

var editor = try pdf_oxide.DocumentEditor.openEditor("input.pdf");
defer editor.deinit();

const removed = try editor.redactionScrubMetadata();  // constructs removed (no geometry)
std.debug.print("constructs removed: {d}\n", .{removed});

try editor.save("sanitized.pdf");

Objective-C

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

POXDocumentEditor *editor = [POXDocumentEditor openEditor:@"input.pdf" error:&err];
int32_t removed = [editor redactionScrubMetadataWithError:&err];  // constructs removed
NSLog(@"constructs removed: %d", removed);
[editor saveToPath:@"sanitized.pdf" error:&err];

Elixir

{:ok, editor} = PdfOxide.open_editor("input.pdf")
{:ok, removed} = PdfOxide.redaction_scrub_metadata(editor)  # constructs removed
IO.puts("constructs removed: #{removed}")
PdfOxide.editor_save(editor, "sanitized.pdf")

Как удалить повторяющиеся колонтитулы?

Колонтитулы и артефакты оформления страницы повторяются на многих страницах и зачастую являются шаблонным содержимым, которое нужно убрать перед извлечением данных или повторной публикацией. PDF Oxide обнаруживает их двумя способами: он отдаёт приоритет тегам /Artifact по спецификации ISO 32000 (100% точность при наличии) и возвращается к эвристике, которая отмечает текст, повторяющийся в верхних или нижних 15% страниц.

remove_headers(threshold) / remove_footers(threshold) / remove_artifacts(threshold) работают в рамках всего документа и возвращают количество удалённых элементов. threshold — это доля страниц (0.0–1.0), на которых текст должен повторяться, чтобы считаться оформлением в эвристическом режиме (по умолчанию 0.8). remove_artifacts — удобная функция для одновременного удаления колонтитулов.

Rust

use pdf_oxide::PdfDocument;

let doc = PdfDocument::open("report.pdf")?;

let headers = doc.remove_headers(0.8)?;   // count removed
let footers = doc.remove_footers(0.8)?;
// Or both at once:
let total = doc.remove_artifacts(0.8)?;   // headers + footers
println!("removed {} furniture items", total);

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("report.pdf")

doc.remove_headers(0.8)        # threshold defaults to 0.8
doc.remove_footers(0.8)
total = doc.remove_artifacts(0.8)   # headers + footers, returns count
print("removed", total, "furniture items")

doc.save("clean.pdf")

Go

doc, _ := pdfoxide.Open("report.pdf")
defer doc.Close()

headers, _ := doc.RemoveHeaders(0.8)   // count removed
footers, _ := doc.RemoveFooters(0.8)
total, _ := doc.RemoveArtifacts(0.8)   // headers + footers
fmt.Printf("removed %d furniture items\n", total)

JavaScript (WASM)

import { WasmPdfDocument } from "pdf-oxide-wasm";

const doc = new WasmPdfDocument(bytes);

doc.removeHeaders(0.8);   // returns count removed
doc.removeFooters(0.8);
const total = doc.removeArtifacts(0.8);  // headers + footers

const output = doc.save();
doc.free();

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("report.pdf");

int headers = doc.remove_headers(0.8f);   // count removed
int footers = doc.remove_footers(0.8f);
int total   = doc.remove_artifacts(0.8f); // headers + footers
std::cout << "removed " << total << " furniture items\n";

Swift

import PdfOxide

let doc = try Document.open("report.pdf")

let headers = try doc.removeHeaders(threshold: 0.8)   // count removed
let footers = try doc.removeFooters(threshold: 0.8)
let total   = try doc.removeArtifacts(threshold: 0.8) // headers + footers
print("removed \(total) furniture items")

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('report.pdf');

doc.removeHeaders(0.8);   // returns count removed
doc.removeFooters(0.8);
final total = doc.removeArtifacts(0.8);  // headers + footers
print('removed $total furniture items');

R

library(pdfoxide)

doc <- pdf_open("report.pdf")

headers <- pdf_remove_headers(doc, 0.8)   # count removed
footers <- pdf_remove_footers(doc, 0.8)
total   <- pdf_remove_artifacts(doc, 0.8) # headers + footers
cat("removed", total, "furniture items\n")

Julia

using PdfOxide

doc = open_document("report.pdf")

headers = remove_headers(doc, 0.8)   # count removed
footers = remove_footers(doc, 0.8)
total   = remove_artifacts(doc, 0.8) # headers + footers
println("removed ", total, " furniture items")

Zig

const pdf_oxide = @import("pdf_oxide");

var doc = try pdf_oxide.Document.open("report.pdf");
defer doc.deinit();

const headers = try doc.removeHeaders(0.8);   // count removed
const footers = try doc.removeFooters(0.8);
const total   = try doc.removeArtifacts(0.8); // headers + footers
std.debug.print("removed {d} furniture items\n", .{total});

Objective-C

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

POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];

int32_t headers = [doc removeHeaders:0.8 error:&err];   // count removed
int32_t footers = [doc removeFooters:0.8 error:&err];
int32_t total   = [doc removeArtifacts:0.8 error:&err]; // headers + footers
NSLog(@"removed %d furniture items", total);

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")

{:ok, headers} = PdfOxide.remove_headers(doc, 0.8)   # count removed
{:ok, footers} = PdfOxide.remove_footers(doc, 0.8)
{:ok, total}   = PdfOxide.remove_artifacts(doc, 0.8) # headers + footers
IO.puts("removed #{total} furniture items")

Стирание оформления на отдельной странице

Если вы точно знаете, на какой странице находится оформление, семейство постраничных функций erase_* помечает область заголовка (верхние 15%), область подвала (нижние 15%) или обе сразу для стирания без анализа повторений по страницам. Эти функции принимают индекс страницы с нуля.

Rust

use pdf_oxide::PdfDocument;

let doc = PdfDocument::open("report.pdf")?;

doc.erase_header(0)?;     // erase the top 15% of page 0
doc.erase_footer(0)?;     // erase the bottom 15% of page 0
doc.erase_artifacts(0)?;  // erase both header and footer of page 0

Python

doc = PdfDocument("report.pdf")

doc.erase_header(0)      # erase the top 15% of page 0
doc.erase_footer(0)      # erase the bottom 15% of page 0
doc.erase_artifacts(0)   # erase both header and footer of page 0

doc.save("clean.pdf")

JavaScript (WASM)

const doc = new WasmPdfDocument(bytes);

doc.eraseHeader(0);     // erase the top 15% of page 0
doc.eraseFooter(0);     // erase the bottom 15% of page 0
doc.eraseArtifacts(0);  // erase both header and footer of page 0

const output = doc.save();
doc.free();

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("report.pdf");

doc.erase_header(0);     // erase the top 15% of page 0
doc.erase_footer(0);     // erase the bottom 15% of page 0
doc.erase_artifacts(0);  // erase both header and footer of page 0

Swift

import PdfOxide

let doc = try Document.open("report.pdf")

try doc.eraseHeader(0)     // erase the top 15% of page 0
try doc.eraseFooter(0)     // erase the bottom 15% of page 0
try doc.eraseArtifacts(0)  // erase both header and footer of page 0

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('report.pdf');

doc.eraseHeader(0);     // erase the top 15% of page 0
doc.eraseFooter(0);     // erase the bottom 15% of page 0
doc.eraseArtifacts(0);  // erase both header and footer of page 0

R

library(pdfoxide)

doc <- pdf_open("report.pdf")

pdf_erase_header(doc, 0)     # erase the top 15% of page 0
pdf_erase_footer(doc, 0)     # erase the bottom 15% of page 0
pdf_erase_artifacts(doc, 0)  # erase both header and footer of page 0

Julia

using PdfOxide

doc = open_document("report.pdf")

erase_header(doc, 0)     # erase the top 15% of page 0
erase_footer(doc, 0)     # erase the bottom 15% of page 0
erase_artifacts(doc, 0)  # erase both header and footer of page 0

Zig

const pdf_oxide = @import("pdf_oxide");

var doc = try pdf_oxide.Document.open("report.pdf");
defer doc.deinit();

_ = try doc.eraseHeader(0);     // erase the top 15% of page 0
_ = try doc.eraseFooter(0);     // erase the bottom 15% of page 0
_ = try doc.eraseArtifacts(0);  // erase both header and footer of page 0

Objective-C

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

POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];

[doc eraseHeader:0 error:&err];     // erase the top 15% of page 0
[doc eraseFooter:0 error:&err];     // erase the bottom 15% of page 0
[doc eraseArtifacts:0 error:&err];  // erase both header and footer of page 0

Elixir

{:ok, doc} = PdfOxide.open("report.pdf")

{:ok, _} = PdfOxide.erase_header(doc, 0)     # erase the top 15% of page 0
{:ok, _} = PdfOxide.erase_footer(doc, 0)     # erase the bottom 15% of page 0
{:ok, _} = PdfOxide.erase_artifacts(doc, 0)  # erase both header and footer of page 0

Полный рабочий процесс редактирования

В этом примере выполняется поиск конфиденциальных областей, постановка в очередь деструктивных редактирований, их применение с очисткой метаданных и запись чистого файла.

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("sensitive-report.pdf")

# Step 1: locate sensitive text and queue a destructive box for each match.
for i in range(doc.page_count()):
    page = doc.page(i)
    for t in page.find_text_containing("SSN"):
        x, y, w, h = t.bbox            # (x, y, width, height)
        doc.add_redaction(i, (x, y, x + w, y + h))

# Step 2: apply destructively + scrub metadata (the default).
report = doc.apply_redactions_destructive(scrub_metadata=True)
print("regions:", report["regions"], "glyphs removed:", report["glyphs_removed"])

# Step 3: save the rewritten document.
doc.save("report-redacted.pdf")

Rust

use pdf_oxide::editor::{DocumentEditor, EditableDocument};
use pdf_oxide::redaction::RedactionOptions;

let mut editor = DocumentEditor::open("sensitive-report.pdf")?;

// Step 1: queue destructive boxes (x0, y0, x1, y1 in points).
editor.add_redaction(0, [100.0, 700.0, 300.0, 714.0], None)?;
editor.add_redaction(0, [100.0, 680.0, 300.0, 694.0], None)?;

// Step 2: apply destructively with safe defaults (scrubs metadata).
let report = editor.apply_redactions_destructive(RedactionOptions::default())?;
println!(
    "regions: {}, glyphs removed: {}",
    report.regions, report.glyphs_removed
);

// Step 3: save.
editor.save("report-redacted.pdf")?;

Java

import fyi.oxide.pdf.*;
import fyi.oxide.pdf.search.SearchMatch;

try (PdfDocument doc = PdfDocument.open("sensitive-report.pdf");
     DocumentEditor editor = DocumentEditor.open("sensitive-report.pdf")) {
    // Step 1: locate sensitive text and queue a destructive box for each hit.
    for (SearchMatch m : doc.search("SSN")) {
        editor.addRedaction(m.pageIndex(), m.bbox());  // bbox is x0,y0,x1,y1
    }
    // Step 2: apply destructively + scrub metadata (the default).
    editor.applyRedactionsDestructive();
    // Step 3: save.
    editor.saveTo(java.nio.file.Path.of("report-redacted.pdf"));
}

Kotlin

import fyi.oxide.pdf.*

PdfDocument.open("sensitive-report.pdf").use { doc ->
    DocumentEditor.open("sensitive-report.pdf").use { editor ->
        // Step 1: locate sensitive text and queue a box for each hit.
        for (m in doc.search("SSN")) {
            editor.addRedaction(m.pageIndex(), m.bbox())  // bbox is x0,y0,x1,y1
        }
        // Step 2: apply destructively + scrub metadata (the default).
        editor.applyRedactionsDestructive()
        // Step 3: save.
        editor.saveTo(java.nio.file.Path.of("report-redacted.pdf"))
    }
}

Scala

import fyi.oxide.pdf.{PdfDocument, DocumentEditor, searchSeq}
import scala.util.Using

Using.resource(PdfDocument.open("sensitive-report.pdf")) { doc =>
  Using.resource(DocumentEditor.open("sensitive-report.pdf")) { editor =>
    // Step 1: locate sensitive text and queue a box for each hit.
    for (m <- doc.searchSeq("SSN"))
      editor.addRedaction(m.pageIndex, m.bbox)  // bbox is x0,y0,x1,y1
    // Step 2: apply destructively + scrub metadata (the default).
    editor.applyRedactionsDestructive()
    // Step 3: save.
    editor.saveTo(java.nio.file.Path.of("report-redacted.pdf"))
  }
}

Clojure

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

(with-open [doc (pdf/open "sensitive-report.pdf")
            ed  (pdf/editor "sensitive-report.pdf")]
  ;; Step 1: locate sensitive text and queue a box for each hit.
  (doseq [m (pdf/search doc "SSN")]
    (pdf/add-redaction ed (.pageIndex m) (.bbox m)))  ; bbox is x0,y0,x1,y1
  ;; Step 2: apply destructively + scrub metadata (the default).
  (pdf/apply-redactions ed)
  ;; Step 3: save.
  (java.nio.file.Files/write
    (java.nio.file.Path/of "report-redacted.pdf" (into-array String []))
    (pdf/editor-save ed)))

Ruby

require 'pdf_oxide'

doc = PdfOxide::PdfDocument.open('sensitive-report.pdf')
PdfOxide::DocumentEditor.open('sensitive-report.pdf') do |ed|
  # Step 1: locate sensitive text and queue a box for each hit.
  doc.search('SSN').each do |m|
    b = m[:bbox]  # { x:, y:, width:, height: }
    ed.add_redaction(page: m[:page], rect: [b[:x], b[:y], b[:x] + b[:width], b[:y] + b[:height]])
  end
  # Step 2: apply destructively + scrub metadata (raises on fail-closed).
  ed.apply_redactions!(scrub_metadata: true)
  # Step 3: save.
  ed.save_to('report-redacted.pdf')
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc    = pdf_oxide::Document::open("sensitive-report.pdf");
auto editor = pdf_oxide::DocumentEditor::open("sensitive-report.pdf");

// Step 1: locate sensitive text and queue a box for each hit.
for (const auto& m : doc.search_all("SSN", /*case_sensitive=*/false)) {
    editor.redaction_add(m.page, m.bbox.x, m.bbox.y,
                         m.bbox.x + m.bbox.width, m.bbox.y + m.bbox.height,
                         0.0, 0.0, 0.0);
}
// Step 2: apply destructively + scrub metadata.
int glyphs = editor.redaction_apply(/*scrub_metadata=*/true, 0.0, 0.0, 0.0);
std::cout << "glyphs removed: " << glyphs << "\n";
// Step 3: save.
editor.save("report-redacted.pdf");

Swift

import PdfOxide

let doc    = try Document.open("sensitive-report.pdf")
let editor = try DocumentEditor.open("sensitive-report.pdf")

// Step 1: locate sensitive text and queue a box for each hit.
for m in try doc.searchAll("SSN", false) {
    try editor.redactionAdd(m.page,
                            x1: m.bbox.x, y1: m.bbox.y,
                            x2: m.bbox.x + m.bbox.width, y2: m.bbox.y + m.bbox.height,
                            r: 0, g: 0, b: 0)
}
// Step 2: apply destructively + scrub metadata.
let glyphs = try editor.redactionApply(scrubMetadata: true, r: 0, g: 0, b: 0)
print("glyphs removed: \(glyphs)")
// Step 3: save.
try editor.save("report-redacted.pdf")

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc    = PdfDocument.open('sensitive-report.pdf');
final editor = DocumentEditor.open('sensitive-report.pdf');

// Step 1: locate sensitive text and queue a box for each hit.
for (final m in doc.searchAll('SSN', false)) {
  editor.redactionAdd(m.page, m.bbox.x, m.bbox.y,
      m.bbox.x + m.bbox.width, m.bbox.y + m.bbox.height);
}
// Step 2: apply destructively + scrub metadata.
final glyphs = editor.redactionApply(scrubMetadata: true);
print('glyphs removed: $glyphs');
// Step 3: save.
editor.save('report-redacted.pdf');

R

library(pdfoxide)

doc    <- pdf_open("sensitive-report.pdf")
editor <- pdf_editor_open("sensitive-report.pdf")

# Step 1: locate sensitive text and queue a box for each hit.
for (m in pdf_search_all(doc, "SSN", FALSE)) {
  b <- m$bbox  # list(x=, y=, width=, height=)
  pdf_redaction_add(editor, m$page, b$x, b$y, b$x + b$width, b$y + b$height, 0, 0, 0)
}
# Step 2: apply destructively + scrub metadata.
glyphs <- pdf_redaction_apply(editor, scrub_metadata = TRUE, 0, 0, 0)
cat("glyphs removed:", glyphs, "\n")
# Step 3: save.
pdf_editor_save(editor, "report-redacted.pdf")

Julia

using PdfOxide

doc    = open_document("sensitive-report.pdf")
editor = open_editor("sensitive-report.pdf")

# Step 1: locate sensitive text and queue a box for each hit.
for m in search_all(doc, "SSN", false)
    b = m.bbox
    redaction_add(editor, m.page, b.x, b.y, b.x + b.width, b.y + b.height, 0, 0, 0)
end
# Step 2: apply destructively + scrub metadata.
glyphs = redaction_apply(editor, true, 0, 0, 0)
println("glyphs removed: ", glyphs)
# Step 3: save.
save(editor, "report-redacted.pdf")

Zig

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

var doc    = try pdf_oxide.Document.open("sensitive-report.pdf");
defer doc.deinit();
var editor = try pdf_oxide.DocumentEditor.openEditor("sensitive-report.pdf");
defer editor.deinit();

// Step 1: locate sensitive text and queue a box for each hit.
const hits = try doc.searchAll(a, "SSN", false);
for (hits) |m| {
    try editor.redactionAdd(@intCast(m.page), m.bbox.x, m.bbox.y,
        m.bbox.x + m.bbox.width, m.bbox.y + m.bbox.height, 0, 0, 0);
}
// Step 2: apply destructively + scrub metadata.
const glyphs = try editor.redactionApply(true, 0, 0, 0);
std.debug.print("glyphs removed: {d}\n", .{glyphs});
// Step 3: save.
try editor.save("report-redacted.pdf");

Objective-C

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

POXDocument *doc = [POXDocument openPath:@"sensitive-report.pdf" error:&err];
POXDocumentEditor *editor = [POXDocumentEditor openEditor:@"sensitive-report.pdf" error:&err];

// Step 1: locate sensitive text and queue a box for each hit.
for (POXSearchResult *m in [doc searchAll:@"SSN" caseSensitive:NO error:&err]) {
    POXBbox b = m.bbox;
    [editor redactionAddPage:m.page x1:b.x y1:b.y x2:b.x + b.width y2:b.y + b.height
                           r:0 g:0 b:0 error:&err];
}
// Step 2: apply destructively + scrub metadata.
int32_t glyphs = [editor redactionApplyScrubMetadata:YES r:0 g:0 b:0 error:&err];
NSLog(@"glyphs removed: %d", glyphs);
// Step 3: save.
[editor saveToPath:@"report-redacted.pdf" error:&err];

Elixir

{:ok, doc}    = PdfOxide.open("sensitive-report.pdf")
{:ok, editor} = PdfOxide.open_editor("sensitive-report.pdf")

# Step 1: locate sensitive text and queue a box for each hit.
{:ok, hits} = PdfOxide.search_all(doc, "SSN", false)
Enum.each(hits, fn m ->
  b = m.bbox
  PdfOxide.redaction_add(editor, m.page, b.x, b.y, b.x + b.width, b.y + b.height, 0.0, 0.0, 0.0)
end)
# Step 2: apply destructively + scrub metadata.
{:ok, glyphs} = PdfOxide.redaction_apply(editor, true, 0.0, 0.0, 0.0)
IO.puts("glyphs removed: #{glyphs}")
# Step 3: save.
PdfOxide.editor_save(editor, "report-redacted.pdf")

Справочник по методам

Деструктивное редактирование

Каноническое имя (C ABI) Rust (DocumentEditor) Python (PdfDocument) Go (DocumentEditor) C# (DocumentEditor) JS (WASM)
pdf_redaction_add add_redaction(page, [x0,y0,x1,y1], Option<[r,g,b]>) -> Result<()> add_redaction(page, rect, fill=None) AddRedaction(page, [4]float64, *[3]float64) error AddRedaction(pageIndex, x1, y1, x2, y2, r=0, g=0, b=0) addRedaction(page, x0, y0, x1, y1, fill?)
pdf_redaction_count redaction_count(page) -> Result<usize> redaction_count(page) -> int RedactionCount(page) (int, error) RedactionCount(pageIndex) -> int redactionCount(page) -> number
pdf_redaction_apply apply_redactions_destructive(RedactionOptions) -> Result<RedactionReport> apply_redactions_destructive(scrub_metadata=True, remove_javascript=True, remove_embedded_files=True, fill=(0,0,0)) -> dict ApplyRedactions(scrubMetadata bool) (int, error) ApplyRedactions(scrubMetadata=true, r=0, g=0, b=0) -> int applyRedactionsDestructive(scrubMetadata?) -> RedactionReport
pdf_redaction_scrub_metadata sanitize_document(RedactionOptions) -> Result<RedactionReport> sanitize_document(scrub_metadata=True, remove_javascript=True, remove_embedded_files=True) -> dict SanitizeDocument() (int, error) SanitizeDocument() -> int sanitizeDocument(scrub?, removeJS?, removeEmbedded?) -> RedactionReport

Обёртка Swift предоставляет то же семейство в виде redactionAdd, redactionCount, redactionApply(scrubMetadata:r:g:b:) и redactionScrubMetadata() на DocumentEditor.

Удаление колонтитулов и артефактов

Каноническое имя (C ABI) Rust (PdfDocument) Python (PdfDocument) Go (PdfDocument) JS (WASM)
pdf_document_remove_headers remove_headers(threshold: f32) -> Result<usize> remove_headers(threshold=0.8) -> int RemoveHeaders(threshold float32) (int, error) removeHeaders(threshold) -> number
pdf_document_remove_footers remove_footers(threshold: f32) -> Result<usize> remove_footers(threshold=0.8) -> int RemoveFooters(threshold float32) (int, error) removeFooters(threshold) -> number
pdf_document_remove_artifacts remove_artifacts(threshold: f32) -> Result<usize> remove_artifacts(threshold=0.8) -> int RemoveArtifacts(threshold float32) (int, error) removeArtifacts(threshold) -> number
pdf_document_erase_header erase_header(page: usize) -> Result<()> erase_header(page) -> None eraseHeader(page)
pdf_document_erase_footer erase_footer(page: usize) -> Result<()> erase_footer(page) -> None eraseFooter(page)
pdf_document_erase_artifacts erase_artifacts(page: usize) -> Result<()> erase_artifacts(page) -> None eraseArtifacts(page)

Swift предоставляет removeHeaders(threshold:), removeFooters(threshold:), removeArtifacts(threshold:), eraseHeader(_:), eraseFooter(_:) и eraseArtifacts(_:) на Document. PdfDocument в Go предоставляет семейство Remove*, но не постраничное семейство Erase*. C# в настоящее время не предоставляет ни одного семейства элементов оформления.

RedactionReport

redaction_apply и redaction_scrub_metadata возвращают отчёт, чтобы вы могли убедиться, что проход действительно выполнил реальную работу:

Поле Значение
regions Количество применённых областей.
glyphs_removed Глифы, физически удалённые из потоков содержимого.
images_modified / images_removed Изображения, покрытые пиксели которых были перезаписаны / изображения, удалённые полностью.
paths_pruned Подпути, удалённые или геометрически обрезанные.
annotations_removed Удалённые элементы верхнего уровня (аннотации при редактировании; санитизированные корни при sanitize_document).
fonts_scrubbed Шрифты, у которых были очищены /Widths / /ToUnicode.
bytes_removed Наилучшая оценка общего количества удалённых байт.

Важные замечания

  • Деструктивное, не косметическое. redaction_apply физически удаляет покрытое содержимое из переписанного файла согласно ISO 32000-1:2008 §12.5.6.23 — секретный текст исчезает из файла, а не просто перекрывается наложением. Исходные объекты /Contents жёстко удаляются, чтобы они не выжили как пропущенные сборщиком мусора сироты.
  • Завершается отказом. Если редактируемая страница содержит текст составного/Type0 или иного не поддающегося перезаписи шрифта, redaction_apply возвращает ошибку, а не рискует пропустить часть содержимого. Обрабатывайте ошибку; не предполагайте успех.
  • Безопасные значения по умолчанию. RedactionOptions::default() очищает метаданные, удаляет JavaScript документа и встроенные файлы, устраняет скрытые слои дополнительного содержимого и рисует непрозрачное чёрное наложение даже тогда, когда исходная аннотация /Redact не задаёт цвет /IC.
  • Необратимо. После сохранения удаление постоянно. Всегда работайте с копией исходного документа.
  • Производительность. Редактирование и санитизация работают поверх того же парсера, который обеспечивает извлечение со средним временем 0.8 мс и 100% успешных результатов, поэтому проход редактирования добавляет минимальные накладные расходы сверх самой перезаписи содержимого.

Часто задаваемые вопросы

Действительно ли редактирование в PDF Oxide является деструктивным, или это просто чёрный прямоугольник поверх текста? По-настоящему деструктивное. redaction_apply (apply_redactions_destructive) удаляет покрытые глифы, изображения и геометрию путей из потока содержимого и жёстко удаляет исходные объекты /Contents из вывода согласно ISO 32000-1:2008 §12.5.6.23. Чёрное наложение рисуется в дополнение к удалению содержимого, а не вместо него.

Что произойдёт, если страница использует составной шрифт, который нельзя безопасно отредактировать? Редактирование завершается отказом: redaction_apply возвращает ошибку, а не частично редактирует и оставляет восстанавливаемый фрагмент. Перехватите ошибку и при необходимости растеризуйте страницу.

Нужно ли мне редактировать области, чтобы просто удалить метаданные? Нет. Вызовите redaction_scrub_metadata (sanitize_document) для самостоятельного прохода, удаляющего /Info, XMP /Metadata, JavaScript документа и встроенные файлы без воздействия на геометрию страницы.

Как обнаруживаются колонтитулы для удаления? PDF Oxide отдаёт приоритет тегам /Artifact по ISO 32000 (100% точность при наличии) и возвращается к эвристике, которая отмечает текст, повторяющийся по крайней мере в threshold доле страниц (по умолчанию 0.8) в верхних или нижних 15% страницы.

Связанные страницы