Class: PdfOxide::DocumentEditor

Inherits:
Object
  • Object
show all
Defined in:
lib/pdf_oxide/document_editor.rb

Overview

Write-side counterpart to PdfDocument: form-fill, destructive redaction (v0.3.50 #231), metadata scrubbing, and incremental save.

Mirrors ‘fyi.oxide.pdf.DocumentEditor`. Lifecycle: holds a native `DocumentEditor*` handle; must be closed via #close or a block-form factory. Close is idempotent.

Per ‘feedback_extraction_graceful_fallback`: destructive redaction is a **security operation** — every non-zero return code raises rather than silently degrading.

Examples:

destructive redaction (block-form auto-closes).

PdfOxide::DocumentEditor.open('source.pdf') do |ed|
  ed.add_redaction(page: 0, rect: [100, 200, 300, 250])
  ed.apply_redactions!
  ed.save_to('redacted.pdf')
end

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source) ⇒ DocumentEditor

Returns a new instance of DocumentEditor.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/pdf_oxide/document_editor.rb', line 37

def initialize(source)
  raise ::PdfOxide::ArgumentError, 'source cannot be nil' if source.nil?

  err = ::FFI::MemoryPointer.new(:int32)
  @handle =
    if source.is_a?(String) && File.exist?(source)
      Bindings.document_editor_open(File.absolute_path(source), err)
    elsif source.is_a?(String) && !source.empty?
      binary = source.dup.force_encoding(Encoding::BINARY)
      buf = ::FFI::MemoryPointer.new(:uint8, binary.bytesize)
      buf.write_bytes(binary, 0, binary.bytesize)
      Bindings.document_editor_open_from_bytes(buf, binary.bytesize, err)
    else
      raise FileNotFoundError, "file not found: #{source}"
    end

  code = err.read_int32
  raise IoError, "document_editor_open failed (#{code})" if code != 0
  raise IoError, 'document_editor_open returned null' if @handle.nil? || @handle.null?

  @closed  = false
  @applied = false
  @tracker = [@handle]
  ObjectSpace.define_finalizer(self, self.class.finalizer(@tracker))
end

Instance Attribute Details

#handleObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



64
65
66
# File 'lib/pdf_oxide/document_editor.rb', line 64

def handle
  @handle
end

Class Method Details

.finalizer(tracker) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



203
204
205
206
207
208
209
210
211
# File 'lib/pdf_oxide/document_editor.rb', line 203

def self.finalizer(tracker)
  proc do
    h = tracker[0]
    if h && !h.null?
      Bindings.document_editor_free(h)
      tracker[0] = nil
    end
  end
end

.open(source) {|DocumentEditor| ... } ⇒ DocumentEditor, Object

Open an editor session over a PDF on disk (or in-memory bytes).

Parameters:

  • source (String)

    file path or raw PDF bytes.

Yields:

Returns:



26
27
28
29
30
31
32
33
34
35
# File 'lib/pdf_oxide/document_editor.rb', line 26

def self.open(source, &block)
  ed = new(source)
  return ed unless block_given?

  begin
    yield ed
  ensure
    ed.close
  end
end

Instance Method Details

#add_redaction(page:, rect:, color: [0.0, 0.0, 0.0]) ⇒ self

Queue a redaction rectangle for the given page. The redaction is not applied until #apply_redactions! runs.

Parameters:

  • page (Integer)

    0-based page index.

  • rect (Array<Numeric>)

    ‘[x1, y1, x2, y2]` in PDF user-space.

  • color (Array<Numeric>) (defaults to: [0.0, 0.0, 0.0])

    ‘[r, g, b]` overlay color (0.0–1.0).

Returns:

  • (self)

    (fluent chaining).

Raises:



74
75
76
77
78
79
80
81
82
83
84
# File 'lib/pdf_oxide/document_editor.rb', line 74

def add_redaction(page:, rect:, color: [0.0, 0.0, 0.0])
  check_open!
  raise ::PdfOxide::ArgumentError, 'rect must have 4 numeric values' unless rect.respond_to?(:length) && rect.length == 4

  x1, y1, x2, y2 = rect.map(&:to_f)
  r, g, b = color.map(&:to_f)
  err = ::FFI::MemoryPointer.new(:int32)
  rc = Bindings.pdf_redaction_add(@handle, Integer(page), x1, y1, x2, y2, r, g, b, err)
  fail_closed!(rc, err.read_int32, 'pdf_redaction_add')
  self
end

#apply_redactions!(scrub_metadata: false, fill_color: [0.0, 0.0, 0.0]) ⇒ self

Apply all queued redactions destructively.

Parameters:

  • scrub_metadata (Boolean) (defaults to: false)

    also strip /Info, XMP, JS.

  • fill_color (Array<Numeric>) (defaults to: [0.0, 0.0, 0.0])

    overlay ‘[r, g, b]`.

Returns:

  • (self)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/pdf_oxide/document_editor.rb', line 101

def apply_redactions!(scrub_metadata: false, fill_color: [0.0, 0.0, 0.0])
  check_open!
  r, g, b = fill_color.map(&:to_f)
  err = ::FFI::MemoryPointer.new(:int32)
  rc = Bindings.pdf_redaction_apply(@handle, , r, g, b, err)
  fail_closed!(rc, err.read_int32, 'pdf_redaction_apply')

  if 
    err2 = ::FFI::MemoryPointer.new(:int32)
    rc2 = Bindings.(@handle, err2)
    fail_closed!(rc2, err2.read_int32, 'pdf_redaction_scrub_metadata')
  end
  @applied = true
  self
end

#closeObject

Idempotent close.



188
189
190
191
192
193
194
195
196
# File 'lib/pdf_oxide/document_editor.rb', line 188

def close
  return if @closed

  h = @handle
  @handle = nil
  @closed = true
  @tracker[0] = nil if @tracker
  Bindings.document_editor_free(h) if h && !h.null?
end

#closed?Boolean

Returns:

  • (Boolean)


198
199
200
# File 'lib/pdf_oxide/document_editor.rb', line 198

def closed?
  @closed
end

#redaction_count(page) ⇒ Integer

Total redactions queued for the page.

Parameters:

  • page (Integer)

Returns:

  • (Integer)


89
90
91
92
93
94
95
# File 'lib/pdf_oxide/document_editor.rb', line 89

def redaction_count(page)
  check_open!
  err = ::FFI::MemoryPointer.new(:int32)
  n = Bindings.pdf_redaction_count(@handle, Integer(page), err)
  fail_closed!(0, err.read_int32, 'pdf_redaction_count')
  n
end

#save_to(path) ⇒ String

Save the edited PDF to the given path.

Returns:

  • (String)

    absolute path written.

Raises:



159
160
161
162
163
164
165
166
167
168
# File 'lib/pdf_oxide/document_editor.rb', line 159

def save_to(path)
  check_open!
  raise ::PdfOxide::ArgumentError, 'path cannot be empty' if path.nil? || path.empty?

  check_applied! if @needs_apply
  err = ::FFI::MemoryPointer.new(:int32)
  rc = Bindings.document_editor_save(@handle, File.absolute_path(path), err)
  fail_closed!(rc, err.read_int32, 'document_editor_save')
  File.absolute_path(path)
end

#scrub_metadataself

Metadata scrubbing without redaction regions.

Returns:

  • (self)


119
120
121
122
123
124
125
126
# File 'lib/pdf_oxide/document_editor.rb', line 119

def 
  check_open!
  err = ::FFI::MemoryPointer.new(:int32)
  rc = Bindings.(@handle, err)
  fail_closed!(rc, err.read_int32, 'pdf_redaction_scrub_metadata')
  @applied = true
  self
end

#set_form_field(name, value) ⇒ self

Set an AcroForm text field.

Parameters:

  • name (String)

    dot-separated full field name.

  • value (String, Boolean)

    new value (Boolean = checkbox/radio).

Returns:

  • (self)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/pdf_oxide/document_editor.rb', line 134

def set_form_field(name, value)
  check_open!
  raise ::PdfOxide::ArgumentError, 'name cannot be nil' if name.nil?

  err = ::FFI::MemoryPointer.new(:int32)
  ok = if [true, false].include?(value)
         Bindings.pdf_form_field_set_value_by_name_boolean(@handle, name, value, err)
       else
         Bindings.pdf_form_field_set_value_by_name_string(@handle, name, value.to_s, err)
       end
  code = err.read_int32
  raise InternalError, "set_form_field failed (#{code})" if code != 0
  raise InternalError, 'set_form_field rejected by cdylib (field missing?)' unless ok

  self
rescue ::FFI::NotFoundError
  # phantom in this cdylib build — leave the field-write a no-op
  # and surface a clear error rather than crashing.
  raise UnsupportedFeatureError, 'form-fill not supported by this cdylib build'
end

#to_bytesString

Returns BINARY-encoded PDF bytes.

Returns:

  • (String)

    BINARY-encoded PDF bytes.

Raises:



171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/pdf_oxide/document_editor.rb', line 171

def to_bytes
  check_open!
  len_ptr = ::FFI::MemoryPointer.new(:size_t)
  err     = ::FFI::MemoryPointer.new(:int32)
  buf     = Bindings.document_editor_save_to_bytes(@handle, len_ptr, err)
  fail_closed!(0, err.read_int32, 'document_editor_save_to_bytes')
  raise InternalError, 'document_editor_save_to_bytes returned null' if buf.nil? || buf.null?

  len = len_ptr.read(:size_t)
  bytes = buf.read_string(len)
  Bindings.free_bytes(buf) if Bindings.respond_to?(:free_bytes)
  bytes.force_encoding(Encoding::BINARY)
end