Class: PdfOxide::DocumentEditor
- Inherits:
-
Object
- Object
- PdfOxide::DocumentEditor
- 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.
Instance Attribute Summary collapse
- #handle ⇒ Object readonly private
Class Method Summary collapse
- .finalizer(tracker) ⇒ Object private
-
.open(source) {|DocumentEditor| ... } ⇒ DocumentEditor, Object
Open an editor session over a PDF on disk (or in-memory bytes).
Instance Method Summary collapse
-
#add_redaction(page:, rect:, color: [0.0, 0.0, 0.0]) ⇒ self
Queue a redaction rectangle for the given page.
-
#apply_redactions!(scrub_metadata: false, fill_color: [0.0, 0.0, 0.0]) ⇒ self
Apply all queued redactions destructively.
-
#close ⇒ Object
Idempotent close.
- #closed? ⇒ Boolean
-
#initialize(source) ⇒ DocumentEditor
constructor
A new instance of DocumentEditor.
-
#redaction_count(page) ⇒ Integer
Total redactions queued for the page.
-
#save_to(path) ⇒ String
Save the edited PDF to the given path.
-
#scrub_metadata ⇒ self
Metadata scrubbing without redaction regions.
-
#set_form_field(name, value) ⇒ self
Set an AcroForm text field.
-
#to_bytes ⇒ String
BINARY-encoded PDF bytes.
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
#handle ⇒ Object (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).
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.
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.
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 |
#close ⇒ Object
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
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.
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.
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_metadata ⇒ self
Metadata scrubbing without redaction regions.
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.
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_bytes ⇒ String
Returns BINARY-encoded PDF bytes.
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 |