Class: PdfOxide::Pdf

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

Overview

Create / edit / save PDFs. Read concerns live on PdfDocument; mutate concerns on DocumentEditor; creation + transformation (markdown→PDF, html→PDF) live here.

Mirrors ‘fyi.oxide.pdf.Pdf`. Lifecycle: instances own a native handle and **must be closed** via #close or the block-form `Pdf.from_markdown(…) { |pdf| … }`. Close is idempotent.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(handle) ⇒ Pdf

Returns a new instance of Pdf.



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

def initialize(handle)
  @handle = handle
  @closed = 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.



129
130
131
# File 'lib/pdf_oxide/pdf.rb', line 129

def handle
  @handle
end

Class Method Details

.build_from(symbol, content) ⇒ Object

Raises:



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/pdf_oxide/pdf.rb', line 103

def self.build_from(symbol, content)
  err = ::FFI::MemoryPointer.new(:int32)
  handle = Bindings.send(symbol, content, err)
  code = err.read_int32
  raise ParseError, "#{symbol} failed (#{code})" if code != 0
  raise ParseError, "#{symbol} returned null" if handle.nil? || handle.null?

  pdf = new(handle)
  return pdf unless block_given?

  begin
    yield pdf
  ensure
    pdf.close
  end
end

.create_empty(&block) ⇒ Object

Create a blank PDF (one empty page). Convenience for tests / toolchain bring-up.



75
76
77
# File 'lib/pdf_oxide/pdf.rb', line 75

def self.create_empty(&block)
  from_text(' ', &block)
end

.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.



208
209
210
211
212
213
214
215
216
# File 'lib/pdf_oxide/pdf.rb', line 208

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

.from_html(html, &block) ⇒ Object

Build a PDF from an HTML source. CSS is honored per pdf_oxide’s html_css pipeline.



26
27
28
29
30
# File 'lib/pdf_oxide/pdf.rb', line 26

def self.from_html(html, &block)
  raise ::PdfOxide::ArgumentError, 'html cannot be empty' if html.nil? || html.empty?

  build_from(:pdf_from_html, html, &block)
end

.from_images(images, &block) ⇒ Pdf

Build a multi-page PDF from JPEG/PNG byte arrays. Each image becomes a separate page. Format is auto-detected from magic bytes.

Parameters:

  • images (Array<String>)

    one or more image byte blobs.

Returns:

Raises:



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/pdf_oxide/pdf.rb', line 43

def self.from_images(images, &block)
  raise ::PdfOxide::ArgumentError, 'images cannot be empty' if images.nil? || images.empty?

  # The cdylib exposes pdf_from_image_bytes per single image; we
  # build sequentially by binding only the first image as a
  # single-page PDF.  Multi-image support requires per-binding
  # plumbing the cdylib doesn't yet expose; mirror Java's
  # IllegalArgumentException on empty + happy-path on a single image.
  first = images.first
  raise ::PdfOxide::ArgumentError, 'image cannot be empty' if first.nil? || first.empty?

  binary = first.dup.force_encoding(Encoding::BINARY)
  buf = ::FFI::MemoryPointer.new(:uint8, binary.bytesize)
  buf.write_bytes(binary, 0, binary.bytesize)
  err = ::FFI::MemoryPointer.new(:int32)
  handle = Bindings.pdf_from_image_bytes(buf, binary.bytesize, err)
  code = err.read_int32
  raise ParseError, "pdf_from_image_bytes failed (#{code})" if code != 0
  raise ParseError, 'pdf_from_image_bytes returned null' if handle.nil? || handle.null?

  pdf = new(handle)
  return pdf unless block_given?

  begin
    yield pdf
  ensure
    pdf.close
  end
end

.from_markdown(markdown) {|Pdf| ... } ⇒ Pdf

Build a PDF from a Markdown source.

Parameters:

  • markdown (String)

Yields:

Returns:

Raises:



18
19
20
21
22
# File 'lib/pdf_oxide/pdf.rb', line 18

def self.from_markdown(markdown, &block)
  raise ::PdfOxide::ArgumentError, 'markdown cannot be empty' if markdown.nil? || markdown.empty?

  build_from(:pdf_from_markdown, markdown, &block)
end

.from_text(text, &block) ⇒ Object

Build a PDF from plain text.



33
34
35
36
37
# File 'lib/pdf_oxide/pdf.rb', line 33

def self.from_text(text, &block)
  raise ::PdfOxide::ArgumentError, 'text cannot be empty' if text.nil? || text.empty?

  build_from(:pdf_from_text, text, &block)
end

.plan_split_by_bookmarks_count(source_pdf, level) ⇒ Integer

Count the bookmark-split segments that would result from splitting ‘source_pdf` at `level` (1 = top-level only; 0 = all). Useful for previewing without producing output.

Parameters:

  • source_pdf (String)

    raw PDF bytes.

  • level (Integer)

    bookmark depth.

Returns:

  • (Integer)

    number of segments.

Raises:



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/pdf_oxide/pdf.rb', line 186

def self.plan_split_by_bookmarks_count(source_pdf, level)
  raise ::PdfOxide::ArgumentError, 'source_pdf cannot be nil' if source_pdf.nil?

  PdfOxide::PdfDocument.open(source_pdf) do |doc|
    require 'json'
    err = ::FFI::MemoryPointer.new(:int32)
    opts = JSON.generate(level: level)
    ptr = Bindings.pdf_document_plan_split_by_bookmarks(doc.handle, opts, err)
    code = err.read_int32
    raise InternalError, "plan_split_by_bookmarks failed (#{code})" if code != 0

    json = StringMarshaller.from_c_string(ptr) || '[]'
    arr = begin
      JSON.parse(json)
    rescue JSON::ParserError
      []
    end
    Array(arr).length
  end
end

.prefetch_available?Boolean

Returns whether the build supports OCR model provisioning.

Returns:

  • (Boolean)

    whether the build supports OCR model provisioning.



98
99
100
# File 'lib/pdf_oxide/pdf.rb', line 98

def self.prefetch_available?
  Bindings.pdf_oxide_prefetch_available != 0
end

.prefetch_models(languages) ⇒ String

Prefetch OCR models for the given languages.

Parameters:

  • languages (Array<String>, String)

    BCP-47 / ISO tags.

Returns:

  • (String)

    cache directory path (may be empty on no-OCR builds).

Raises:



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

def self.prefetch_models(languages)
  csv = Array(languages).join(',')
  err = ::FFI::MemoryPointer.new(:int32)
  ptr = Bindings.pdf_oxide_prefetch_models(csv, err)
  code = err.read_int32
  raise InternalError, "prefetch_models failed (#{code})" if code != 0

  StringMarshaller.from_c_string(ptr) || ''
end

.versionString

Returns library version.

Returns:

  • (String)

    library version.



80
81
82
# File 'lib/pdf_oxide/pdf.rb', line 80

def self.version
  PdfOxide::VERSION
end

Instance Method Details

#closeObject

Idempotent free.



163
164
165
166
167
168
169
170
171
# File 'lib/pdf_oxide/pdf.rb', line 163

def close
  return if @closed

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

#closed?Boolean

Returns true once #close runs.

Returns:

  • (Boolean)

    true once #close runs.



174
175
176
# File 'lib/pdf_oxide/pdf.rb', line 174

def closed?
  @closed
end

#save(path) ⇒ String

Write the PDF bytes to ‘path`.

Returns:

  • (String)

    absolute path written.

Raises:



150
151
152
153
154
155
156
157
158
159
160
# File 'lib/pdf_oxide/pdf.rb', line 150

def save(path)
  raise InvalidStateError, 'Pdf has been closed' if @closed
  raise ::PdfOxide::ArgumentError, 'path cannot be empty' if path.nil? || path.empty?

  err = ::FFI::MemoryPointer.new(:int32)
  rc = Bindings.pdf_save(@handle, path, err)
  code = err.read_int32
  raise IoError, "pdf_save failed (#{code})" if code != 0 || rc != 0

  File.absolute_path(path)
end

#to_bytesString

Returns BINARY-encoded PDF bytes.

Returns:

  • (String)

    BINARY-encoded PDF bytes.

Raises:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/pdf_oxide/pdf.rb', line 132

def to_bytes
  raise InvalidStateError, 'Pdf has been closed' if @closed

  len_ptr = ::FFI::MemoryPointer.new(:int32)
  err     = ::FFI::MemoryPointer.new(:int32)
  buf     = Bindings.pdf_save_to_bytes(@handle, len_ptr, err)
  code = err.read_int32
  raise InternalError, "pdf_save_to_bytes failed (#{code})" if code != 0
  raise InternalError, 'pdf_save_to_bytes returned null' if buf.nil? || buf.null?

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