add Pdfium acro fields detector
This commit is contained in:
committed by
Pete Matsyburka
parent
e361cbabf4
commit
185827c951
+611
-31
@@ -109,12 +109,6 @@ class Pdfium
|
||||
attach_function :FPDF_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
|
||||
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
|
||||
|
||||
attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :int
|
||||
attach_function :FPDFPage_GetAnnot, %i[FPDF_PAGE int], :FPDF_ANNOTATION
|
||||
attach_function :FPDFPage_CloseAnnot, [:FPDF_ANNOTATION], :void
|
||||
attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetRect, %i[FPDF_ANNOTATION pointer], :int
|
||||
|
||||
attach_function :FPDFBookmark_GetFirstChild, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_BOOKMARK
|
||||
attach_function :FPDFBookmark_GetNextSibling, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_BOOKMARK
|
||||
attach_function :FPDFBookmark_GetTitle, %i[FPDF_BOOKMARK pointer ulong], :ulong
|
||||
@@ -219,6 +213,14 @@ class Pdfium
|
||||
FPDF_PAGEOBJ_SHADING = 4
|
||||
FPDF_PAGEOBJ_FORM = 5
|
||||
|
||||
PAGE_OBJECT_TYPES = {
|
||||
FPDF_PAGEOBJ_TEXT => :text,
|
||||
FPDF_PAGEOBJ_PATH => :path,
|
||||
FPDF_PAGEOBJ_IMAGE => :image,
|
||||
FPDF_PAGEOBJ_SHADING => :shading,
|
||||
FPDF_PAGEOBJ_FORM => :form
|
||||
}.freeze
|
||||
|
||||
# Path segment types
|
||||
FPDF_SEGMENT_UNKNOWN = -1
|
||||
FPDF_SEGMENT_LINETO = 0
|
||||
@@ -272,6 +274,73 @@ class Pdfium
|
||||
attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
|
||||
attach_function :FPDF_FFLDraw, %i[FPDF_FORMHANDLE FPDF_BITMAP FPDF_PAGE int int int int int int], :void
|
||||
|
||||
attach_function :FORM_OnAfterLoadPage, %i[FPDF_PAGE FPDF_FORMHANDLE], :void
|
||||
attach_function :FORM_OnBeforeClosePage, %i[FPDF_PAGE FPDF_FORMHANDLE], :void
|
||||
|
||||
attach_function :FPDFPage_GetAnnotCount, [:FPDF_PAGE], :int
|
||||
|
||||
begin
|
||||
attach_function :FPDFPage_GetAnnotCountRaw, %i[FPDF_DOCUMENT int], :int
|
||||
rescue FFI::NotFoundError
|
||||
define_singleton_method(:FPDFPage_GetAnnotCountRaw) { |*| -1 } # rubocop:disable Naming/MethodName
|
||||
end
|
||||
|
||||
attach_function :FPDFPage_GetAnnot, %i[FPDF_PAGE int], :FPDF_ANNOTATION
|
||||
attach_function :FPDFPage_CloseAnnot, [:FPDF_ANNOTATION], :void
|
||||
attach_function :FPDFAnnot_GetSubtype, [:FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetRect, %i[FPDF_ANNOTATION pointer], :int
|
||||
attach_function :FPDFAnnot_GetNumberValue, %i[FPDF_ANNOTATION string pointer], :int
|
||||
attach_function :FPDFAnnot_GetStringValue, %i[FPDF_ANNOTATION string pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_GetFormFieldType, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetFormFieldFlags, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetFormFieldName, %i[FPDF_FORMHANDLE FPDF_ANNOTATION pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_GetFormFieldAlternateName, %i[FPDF_FORMHANDLE FPDF_ANNOTATION pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_GetFormFieldValue, %i[FPDF_FORMHANDLE FPDF_ANNOTATION pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_GetFormFieldExportValue, %i[FPDF_FORMHANDLE FPDF_ANNOTATION pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_GetFormControlCount, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetFormControlIndex, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetOptionCount, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetOptionLabel, %i[FPDF_FORMHANDLE FPDF_ANNOTATION int pointer ulong], :ulong
|
||||
attach_function :FPDFAnnot_IsChecked, %i[FPDF_FORMHANDLE FPDF_ANNOTATION], :int
|
||||
attach_function :FPDFAnnot_GetFormAdditionalActionJavaScript,
|
||||
%i[FPDF_FORMHANDLE FPDF_ANNOTATION int pointer ulong], :ulong
|
||||
|
||||
FPDF_ANNOT_LINK = 2
|
||||
FPDF_ANNOT_WIDGET = 20
|
||||
|
||||
FPDF_FORMFIELD_UNKNOWN = 0
|
||||
FPDF_FORMFIELD_PUSHBUTTON = 1
|
||||
FPDF_FORMFIELD_CHECKBOX = 2
|
||||
FPDF_FORMFIELD_RADIOBUTTON = 3
|
||||
FPDF_FORMFIELD_COMBOBOX = 4
|
||||
FPDF_FORMFIELD_LISTBOX = 5
|
||||
FPDF_FORMFIELD_TEXTFIELD = 6
|
||||
FPDF_FORMFIELD_SIGNATURE = 7
|
||||
|
||||
FPDF_FORMFLAG_REQUIRED = 1 << 1
|
||||
FPDF_FORMFLAG_TEXT_PASSWORD = 1 << 13
|
||||
FPDF_FORMFLAG_BUTTON_PUSH = 1 << 16
|
||||
FPDF_FORMFLAG_TEXT_FILE_SELECT = 1 << 20
|
||||
FPDF_FORMFLAG_TEXT_COMB = 1 << 24
|
||||
FPDF_FORMFLAG_TEXT_RICH_TEXT = 1 << 25
|
||||
|
||||
TEXT_TYPE_FLAGS = FPDF_FORMFLAG_TEXT_FILE_SELECT |
|
||||
FPDF_FORMFLAG_TEXT_PASSWORD |
|
||||
FPDF_FORMFLAG_TEXT_RICH_TEXT
|
||||
|
||||
FPDF_ANNOT_AACTION_KEY_STROKE = 12
|
||||
FPDF_ANNOT_AACTION_FORMAT = 13
|
||||
|
||||
typedef :pointer, :FPDF_LINK
|
||||
typedef :pointer, :FPDF_ACTION
|
||||
|
||||
attach_function :FPDFAnnot_GetLink, [:FPDF_ANNOTATION], :FPDF_LINK
|
||||
attach_function :FPDFLink_GetAction, [:FPDF_LINK], :FPDF_ACTION
|
||||
attach_function :FPDFAction_GetType, [:FPDF_ACTION], :ulong
|
||||
attach_function :FPDFAction_GetURIPath, %i[FPDF_DOCUMENT FPDF_ACTION pointer ulong], :ulong
|
||||
|
||||
PDFACTION_URI = 3
|
||||
|
||||
attach_function :FPDFPage_Flatten, %i[FPDF_PAGE int], :int
|
||||
|
||||
FLAT_NORMALDISPLAY = 0
|
||||
@@ -303,6 +372,7 @@ class Pdfium
|
||||
attach_function :FPDFPage_SetRotation, %i[FPDF_PAGE int], :void
|
||||
attach_function :FPDFPage_TransFormWithClip, %i[FPDF_PAGE pointer pointer], :int
|
||||
attach_function :FPDFPage_TransformAnnots, %i[FPDF_PAGE double double double double double double], :void
|
||||
attach_function :FPDF_GetPageBoundingBox, %i[FPDF_PAGE pointer], :int
|
||||
attach_function :FPDFPage_GetMediaBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||
attach_function :FPDFPage_SetMediaBox, %i[FPDF_PAGE float float float float], :void
|
||||
attach_function :FPDFPage_GetCropBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||
@@ -383,6 +453,60 @@ class Pdfium
|
||||
yield instance
|
||||
end
|
||||
|
||||
def self.transform_rect(bounds, box:, rotation:)
|
||||
x0, y0, x1, y1 = bounds
|
||||
box_x0, box_y0, box_x1, box_y1 = box
|
||||
|
||||
box_width = box_x1 - box_x0
|
||||
box_height = box_y1 - box_y0
|
||||
|
||||
left = x0 - box_x0
|
||||
right = x1 - box_x0
|
||||
bottom = y0 - box_y0
|
||||
top = y1 - box_y0
|
||||
|
||||
case rotation
|
||||
when 1
|
||||
[bottom, left, top - bottom, right - left, box_height, box_width]
|
||||
when 2
|
||||
[box_width - right, bottom, right - left, top - bottom, box_width, box_height]
|
||||
when 3
|
||||
[box_height - top, box_width - right, top - bottom, right - left, box_height, box_width]
|
||||
else
|
||||
[left, box_height - top, right - left, top - bottom, box_width, box_height]
|
||||
end
|
||||
end
|
||||
|
||||
def self.read_wide_string
|
||||
length = yield(FFI::Pointer::NULL, 0)
|
||||
|
||||
return if length <= 2
|
||||
|
||||
buffer = FFI::MemoryPointer.new(:uint8, length)
|
||||
|
||||
length = yield(buffer, length)
|
||||
|
||||
return if length <= 2
|
||||
|
||||
buffer.read_bytes(length - 2)
|
||||
.force_encoding('UTF-16LE')
|
||||
.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
||||
end
|
||||
|
||||
def self.read_byte_string
|
||||
length = yield(FFI::Pointer::NULL, 0)
|
||||
|
||||
return if length <= 1
|
||||
|
||||
buffer = FFI::MemoryPointer.new(:uint8, length)
|
||||
|
||||
length = yield(buffer, length)
|
||||
|
||||
return if length <= 1
|
||||
|
||||
buffer.read_bytes(length - 1).force_encoding('UTF-8').scrub
|
||||
end
|
||||
|
||||
def self.check_last_error(context_message = 'PDFium operation failed')
|
||||
error_code = FPDF_GetLastError()
|
||||
|
||||
@@ -401,6 +525,7 @@ class Pdfium
|
||||
@document_ptr = document_ptr
|
||||
|
||||
@pages = {}
|
||||
@annot_counts = {}
|
||||
@closed = false
|
||||
@source_buffer = source_buffer
|
||||
@form_handle = FFI::Pointer::NULL
|
||||
@@ -597,6 +722,14 @@ class Pdfium
|
||||
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
|
||||
end
|
||||
|
||||
def annot_count(page_index)
|
||||
@annot_counts[page_index] ||= Pdfium.FPDFPage_GetAnnotCountRaw(@document_ptr, page_index)
|
||||
end
|
||||
|
||||
def reset_annot_count(page_index)
|
||||
@annot_counts.delete(page_index)
|
||||
end
|
||||
|
||||
def signature_count
|
||||
@signature_count ||= Pdfium.FPDF_GetSignatureCount(@document_ptr)
|
||||
end
|
||||
@@ -757,8 +890,6 @@ class Pdfium
|
||||
end
|
||||
|
||||
class Page
|
||||
RECT_KEYS = %i[left top right bottom].freeze
|
||||
|
||||
attr_reader :document, :page_index, :page_ptr
|
||||
|
||||
def initialize(document, page_index)
|
||||
@@ -789,30 +920,36 @@ class Pdfium
|
||||
end
|
||||
|
||||
def annotations
|
||||
(0...Pdfium.FPDFPage_GetAnnotCount(page_ptr)).filter_map do |index|
|
||||
annotation = Pdfium.FPDFPage_GetAnnot(page_ptr, index)
|
||||
ensure_not_closed!
|
||||
|
||||
next if annotation.null?
|
||||
|
||||
begin
|
||||
rect = Pdfium::FS_RECTF.new
|
||||
Pdfium.FPDFAnnot_GetRect(annotation, rect)
|
||||
|
||||
[Pdfium.FPDFAnnot_GetSubtype(annotation),
|
||||
*RECT_KEYS.map { |key| rect[key].round(3) }]
|
||||
ensure
|
||||
Pdfium.FPDFPage_CloseAnnot(annotation)
|
||||
@annotations ||=
|
||||
(0...Pdfium.FPDFPage_GetAnnotCount(page_ptr)).filter_map do |index|
|
||||
with_annotation(index) do |handle|
|
||||
Annotation.new(page: self, index:, subtype: handle.subtype, rect: handle.rect)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def objects
|
||||
(0...Pdfium.FPDFPage_CountObjects(page_ptr)).map do |index|
|
||||
object = Pdfium.FPDFPage_GetObject(page_ptr, index)
|
||||
bounds = Array.new(4) { FFI::MemoryPointer.new(:float) }
|
||||
Pdfium.FPDFPageObj_GetBounds(object, *bounds)
|
||||
ensure_not_closed!
|
||||
|
||||
[Pdfium.FPDFPageObj_GetType(object), *bounds.map { |b| b.read_float.round(3) }]
|
||||
@objects ||=
|
||||
(0...Pdfium.FPDFPage_CountObjects(page_ptr)).map do |index|
|
||||
PageObject.new(page: self, index:)
|
||||
end
|
||||
end
|
||||
|
||||
def with_annotation(index)
|
||||
ensure_not_closed!
|
||||
|
||||
annot_ptr = Pdfium.FPDFPage_GetAnnot(page_ptr, index)
|
||||
|
||||
return if annot_ptr.null?
|
||||
|
||||
begin
|
||||
yield AnnotationHandle.new(self, annot_ptr)
|
||||
ensure
|
||||
Pdfium.FPDFPage_CloseAnnot(annot_ptr)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -826,6 +963,18 @@ class Pdfium
|
||||
@rotation = value
|
||||
end
|
||||
|
||||
def box
|
||||
@box ||= read_bounding_box || [0, 0, 612, 792]
|
||||
end
|
||||
|
||||
def load_page_view
|
||||
return if @page_view || form_handle.null?
|
||||
|
||||
Pdfium.FORM_OnAfterLoadPage(@page_ptr, form_handle)
|
||||
|
||||
@page_view = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
@@ -987,7 +1136,7 @@ class Pdfium
|
||||
|
||||
@document.add_presave_hook(:cleanup) { @document.cleanup }
|
||||
|
||||
reset_text_memoization
|
||||
reset_memoization
|
||||
|
||||
nil
|
||||
end
|
||||
@@ -1085,7 +1234,7 @@ class Pdfium
|
||||
|
||||
Pdfium.FPDFPage_GenerateContent(@page_ptr) if unwrapped
|
||||
|
||||
reset_text_memoization if unwrapped
|
||||
reset_memoization if unwrapped
|
||||
end
|
||||
|
||||
def find_form_object(rect_bounds = nil)
|
||||
@@ -1202,12 +1351,14 @@ class Pdfium
|
||||
end
|
||||
end
|
||||
|
||||
def reset_text_memoization
|
||||
def reset_memoization
|
||||
remove_instance_variable(:@text) if defined?(@text)
|
||||
|
||||
@text_nodes = nil
|
||||
@text_objects = nil
|
||||
@line_nodes = nil
|
||||
@annotations = nil
|
||||
@objects = nil
|
||||
end
|
||||
|
||||
def remove_page_object(object_ptr)
|
||||
@@ -1603,23 +1754,32 @@ class Pdfium
|
||||
end
|
||||
|
||||
def reload
|
||||
close_page_view
|
||||
Pdfium.FPDF_ClosePage(@page_ptr)
|
||||
|
||||
@page_ptr = Pdfium.FPDF_LoadPage(@document.document_ptr, @page_index)
|
||||
|
||||
raise PdfiumError, "Failed to reload page #{page_index}" if @page_ptr.null?
|
||||
|
||||
@page_view = false
|
||||
@rotation = nil
|
||||
@width = nil
|
||||
@height = nil
|
||||
@box = nil
|
||||
|
||||
reset_text_memoization
|
||||
@document.reset_annot_count(@page_index)
|
||||
|
||||
reset_memoization
|
||||
end
|
||||
|
||||
def close
|
||||
return if closed?
|
||||
|
||||
Pdfium.FPDF_ClosePage(@page_ptr) unless @page_ptr.null?
|
||||
unless @page_ptr.null?
|
||||
close_page_view
|
||||
|
||||
Pdfium.FPDF_ClosePage(@page_ptr)
|
||||
end
|
||||
|
||||
@page_ptr = FFI::Pointer::NULL
|
||||
|
||||
@@ -1628,6 +1788,25 @@ class Pdfium
|
||||
|
||||
private
|
||||
|
||||
def close_page_view
|
||||
Pdfium.FORM_OnBeforeClosePage(@page_ptr, form_handle) unless form_handle.null?
|
||||
|
||||
@page_view = false
|
||||
end
|
||||
|
||||
def read_bounding_box
|
||||
rect = Pdfium::FS_RECTF.new
|
||||
|
||||
return unless Pdfium.FPDF_GetPageBoundingBox(page_ptr, rect) == 1
|
||||
|
||||
x0, x1 = [rect[:left], rect[:right]].minmax
|
||||
y0, y1 = [rect[:bottom], rect[:top]].minmax
|
||||
|
||||
return if x1 - x0 <= 0 || y1 - y0 <= 0
|
||||
|
||||
[x0, y0, x1, y1]
|
||||
end
|
||||
|
||||
def calculate_render_dimensions(width_param, height_param, scale_param)
|
||||
if scale_param
|
||||
render_width = (width * scale_param).round
|
||||
@@ -1654,6 +1833,407 @@ class Pdfium
|
||||
end
|
||||
end
|
||||
|
||||
class AnnotationHandle
|
||||
attr_reader :page, :annot_ptr
|
||||
|
||||
delegate :form_handle, to: :page
|
||||
|
||||
def initialize(page, annot_ptr)
|
||||
@page = page
|
||||
@annot_ptr = annot_ptr
|
||||
end
|
||||
|
||||
def subtype
|
||||
Pdfium.FPDFAnnot_GetSubtype(annot_ptr)
|
||||
end
|
||||
|
||||
def rect
|
||||
rect = Pdfium::FS_RECTF.new
|
||||
|
||||
return unless Pdfium.FPDFAnnot_GetRect(annot_ptr, rect) == 1
|
||||
|
||||
left, right = [rect[:left], rect[:right]].minmax
|
||||
bottom, top = [rect[:bottom], rect[:top]].minmax
|
||||
|
||||
[left, bottom, right, top]
|
||||
end
|
||||
|
||||
def field_type
|
||||
Pdfium.FPDFAnnot_GetFormFieldType(form_handle, annot_ptr)
|
||||
end
|
||||
|
||||
def field_flags
|
||||
Pdfium.FPDFAnnot_GetFormFieldFlags(form_handle, annot_ptr)
|
||||
end
|
||||
|
||||
def field_name
|
||||
read_wide { |buffer, length| Pdfium.FPDFAnnot_GetFormFieldName(form_handle, annot_ptr, buffer, length) }
|
||||
end
|
||||
|
||||
def alternate_name
|
||||
read_wide do |buffer, length|
|
||||
Pdfium.FPDFAnnot_GetFormFieldAlternateName(form_handle, annot_ptr, buffer, length)
|
||||
end
|
||||
end
|
||||
|
||||
def field_value
|
||||
read_wide { |buffer, length| Pdfium.FPDFAnnot_GetFormFieldValue(form_handle, annot_ptr, buffer, length) }
|
||||
end
|
||||
|
||||
def export_value
|
||||
read_wide { |buffer, length| Pdfium.FPDFAnnot_GetFormFieldExportValue(form_handle, annot_ptr, buffer, length) }
|
||||
end
|
||||
|
||||
def control_count
|
||||
Pdfium.FPDFAnnot_GetFormControlCount(form_handle, annot_ptr)
|
||||
end
|
||||
|
||||
def control_index
|
||||
Pdfium.FPDFAnnot_GetFormControlIndex(form_handle, annot_ptr)
|
||||
end
|
||||
|
||||
def checked?
|
||||
Pdfium.FPDFAnnot_IsChecked(form_handle, annot_ptr) == 1
|
||||
end
|
||||
|
||||
def option_labels
|
||||
count = Pdfium.FPDFAnnot_GetOptionCount(form_handle, annot_ptr)
|
||||
|
||||
return [] if count < 1
|
||||
|
||||
(0...count).map do |index|
|
||||
read_wide do |buffer, length|
|
||||
Pdfium.FPDFAnnot_GetOptionLabel(form_handle, annot_ptr, index, buffer, length)
|
||||
end.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def action_javascript(event)
|
||||
read_wide do |buffer, length|
|
||||
Pdfium.FPDFAnnot_GetFormAdditionalActionJavaScript(form_handle, annot_ptr, event, buffer, length)
|
||||
end
|
||||
end
|
||||
|
||||
def link_uri
|
||||
link = Pdfium.FPDFAnnot_GetLink(annot_ptr)
|
||||
|
||||
return if link.null?
|
||||
|
||||
action = Pdfium.FPDFLink_GetAction(link)
|
||||
|
||||
return if action.null? || Pdfium.FPDFAction_GetType(action) != Pdfium::PDFACTION_URI
|
||||
|
||||
document_ptr = page.document.document_ptr
|
||||
|
||||
Pdfium.read_byte_string { |buffer, length| Pdfium.FPDFAction_GetURIPath(document_ptr, action, buffer, length) }
|
||||
end
|
||||
|
||||
def number_value(key)
|
||||
value = FFI::MemoryPointer.new(:float)
|
||||
|
||||
return unless Pdfium.FPDFAnnot_GetNumberValue(annot_ptr, key, value) == 1
|
||||
|
||||
value.read_float
|
||||
end
|
||||
|
||||
def string_value(key)
|
||||
read_wide { |buffer, length| Pdfium.FPDFAnnot_GetStringValue(annot_ptr, key, buffer, length) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_wide(&)
|
||||
Pdfium.read_wide_string(&)
|
||||
end
|
||||
end
|
||||
|
||||
class Annotation
|
||||
FIELD_TYPES = {
|
||||
Pdfium::FPDF_FORMFIELD_PUSHBUTTON => :pushbutton,
|
||||
Pdfium::FPDF_FORMFIELD_CHECKBOX => :checkbox,
|
||||
Pdfium::FPDF_FORMFIELD_RADIOBUTTON => :radio,
|
||||
Pdfium::FPDF_FORMFIELD_COMBOBOX => :combobox,
|
||||
Pdfium::FPDF_FORMFIELD_LISTBOX => :listbox,
|
||||
Pdfium::FPDF_FORMFIELD_TEXTFIELD => :text,
|
||||
Pdfium::FPDF_FORMFIELD_SIGNATURE => :signature
|
||||
}.freeze
|
||||
|
||||
class Link
|
||||
attr_reader :url
|
||||
|
||||
def initialize(url)
|
||||
@url = url
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :page, :index, :subtype, :bounds
|
||||
|
||||
delegate :page_index, to: :page
|
||||
|
||||
def initialize(page:, index:, subtype:, rect: nil)
|
||||
@page = page
|
||||
@index = index
|
||||
@subtype = subtype
|
||||
@bounds = rect.to_a
|
||||
end
|
||||
|
||||
def left
|
||||
bounds[0]
|
||||
end
|
||||
|
||||
def bottom
|
||||
bounds[1]
|
||||
end
|
||||
|
||||
def right
|
||||
bounds[2]
|
||||
end
|
||||
|
||||
def top
|
||||
bounds[3]
|
||||
end
|
||||
|
||||
def widget?
|
||||
subtype == Pdfium::FPDF_ANNOT_WIDGET
|
||||
end
|
||||
|
||||
def link?
|
||||
subtype == Pdfium::FPDF_ANNOT_LINK
|
||||
end
|
||||
|
||||
def rect?
|
||||
bounds.any?
|
||||
end
|
||||
|
||||
def width
|
||||
right - left
|
||||
end
|
||||
|
||||
def height
|
||||
top - bottom
|
||||
end
|
||||
|
||||
def link
|
||||
@link = link? ? page.with_annotation(index) { |handle| Link.new(handle.link_uri) } : nil unless defined?(@link)
|
||||
|
||||
@link
|
||||
end
|
||||
|
||||
def field
|
||||
@field = widget? ? page.with_annotation(index) { |handle| Field.new(handle) } : nil unless defined?(@field)
|
||||
|
||||
@field
|
||||
end
|
||||
|
||||
def to_a
|
||||
[index, subtype, *bounds]
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(Annotation) && to_a == other.to_a
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
[Annotation, *to_a].hash
|
||||
end
|
||||
end
|
||||
|
||||
class Field
|
||||
attr_reader :type, :flags, :name, :alternate_name, :value, :export_value, :control_index,
|
||||
:control_count, :options, :checked, :max_len, :quadding, :format_js,
|
||||
:keystroke_js, :dict_type, :partial_name
|
||||
|
||||
def initialize(handle)
|
||||
code = handle.field_type
|
||||
|
||||
@detached = code.negative?
|
||||
@dict_type = handle.string_value('FT')
|
||||
@partial_name = handle.string_value('T')
|
||||
@options = []
|
||||
|
||||
@detached ? read_detached(handle) : read_form(handle, code)
|
||||
end
|
||||
|
||||
def checked?
|
||||
checked == true
|
||||
end
|
||||
|
||||
def detached?
|
||||
@detached
|
||||
end
|
||||
|
||||
def required?
|
||||
flags.to_i.anybits?(Pdfium::FPDF_FORMFLAG_REQUIRED)
|
||||
end
|
||||
|
||||
def comb?
|
||||
flags.to_i.anybits?(Pdfium::FPDF_FORMFLAG_TEXT_COMB) && flags.to_i.nobits?(Pdfium::TEXT_TYPE_FLAGS)
|
||||
end
|
||||
|
||||
def button?
|
||||
type.in?(%i[checkbox radio])
|
||||
end
|
||||
|
||||
def own?
|
||||
dict_type.present? && partial_name.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_form(handle, code)
|
||||
@type = Annotation::FIELD_TYPES.fetch(code, :unknown)
|
||||
@flags = handle.field_flags
|
||||
@name = handle.field_name.to_s
|
||||
@alternate_name = handle.alternate_name
|
||||
@value = handle.field_value
|
||||
@control_index = handle.control_index
|
||||
@control_count = handle.control_count
|
||||
|
||||
read_form_extras(handle)
|
||||
end
|
||||
|
||||
def read_form_extras(handle)
|
||||
case type
|
||||
when :checkbox, :radio
|
||||
handle.page.load_page_view
|
||||
|
||||
@checked = handle.checked?
|
||||
@export_value = handle.export_value
|
||||
@options = handle.option_labels if control_index.zero?
|
||||
when :combobox, :listbox
|
||||
@options = handle.option_labels
|
||||
when :text
|
||||
read_text_extras(handle)
|
||||
end
|
||||
end
|
||||
|
||||
def read_text_extras(handle)
|
||||
@max_len = handle.number_value('MaxLen')
|
||||
@quadding = handle.number_value('Q')
|
||||
@format_js = handle.action_javascript(Pdfium::FPDF_ANNOT_AACTION_FORMAT)
|
||||
@keystroke_js = handle.action_javascript(Pdfium::FPDF_ANNOT_AACTION_KEY_STROKE)
|
||||
end
|
||||
|
||||
def read_detached(handle)
|
||||
@flags = handle.number_value('Ff').to_i
|
||||
@value = handle.string_value('V')
|
||||
@name = partial_name.to_s
|
||||
@alternate_name = handle.string_value('TU')
|
||||
@control_index = 0
|
||||
@control_count = 1
|
||||
@type = detached_type
|
||||
|
||||
if type == :text
|
||||
@max_len = handle.number_value('MaxLen')
|
||||
@quadding = handle.number_value('Q')
|
||||
else
|
||||
@checked = value.present? && value != 'Off'
|
||||
end
|
||||
end
|
||||
|
||||
def detached_type
|
||||
case dict_type
|
||||
when 'Tx' then :text
|
||||
when 'Sig' then :signature
|
||||
when 'Btn' then flags.anybits?(Pdfium::FPDF_FORMFLAG_BUTTON_PUSH) ? :pushbutton : :checkbox
|
||||
else :unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PageObject
|
||||
attr_reader :page, :index
|
||||
|
||||
def initialize(page:, index:)
|
||||
@page = page
|
||||
@index = index
|
||||
end
|
||||
|
||||
def type
|
||||
@type ||= Pdfium::PAGE_OBJECT_TYPES.fetch(Pdfium.FPDFPageObj_GetType(object_ptr), :unknown)
|
||||
end
|
||||
|
||||
def bounds
|
||||
@bounds ||= read_bounds
|
||||
end
|
||||
|
||||
def left
|
||||
bounds[0]
|
||||
end
|
||||
|
||||
def bottom
|
||||
bounds[1]
|
||||
end
|
||||
|
||||
def right
|
||||
bounds[2]
|
||||
end
|
||||
|
||||
def top
|
||||
bounds[3]
|
||||
end
|
||||
|
||||
def rect?
|
||||
bounds.any?
|
||||
end
|
||||
|
||||
def width
|
||||
right - left
|
||||
end
|
||||
|
||||
def height
|
||||
top - bottom
|
||||
end
|
||||
|
||||
def text?
|
||||
type == :text
|
||||
end
|
||||
|
||||
def image?
|
||||
type == :image
|
||||
end
|
||||
|
||||
def path?
|
||||
type == :path
|
||||
end
|
||||
|
||||
def form?
|
||||
type == :form
|
||||
end
|
||||
|
||||
def to_a
|
||||
[index, type, *bounds]
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.is_a?(PageObject) && to_a == other.to_a
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
[PageObject, *to_a].hash
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object_ptr
|
||||
page.ensure_not_closed!
|
||||
|
||||
Pdfium.FPDFPage_GetObject(page.page_ptr, index)
|
||||
end
|
||||
|
||||
def read_bounds
|
||||
buffer = Array.new(4) { FFI::MemoryPointer.new(:float) }
|
||||
|
||||
return [] unless Pdfium.FPDFPageObj_GetBounds(object_ptr, *buffer) == 1
|
||||
|
||||
buffer.map(&:read_float)
|
||||
end
|
||||
end
|
||||
|
||||
def self.initialize_library
|
||||
config_mem = FFI::MemoryPointer.new(FPDF_LIBRARY_CONFIG.size)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module BuildPdfiumAnnotations
|
||||
URI_PREFIXES = %w[https:// http://].freeze
|
||||
LINKS_LIMIT = 250
|
||||
|
||||
module_function
|
||||
|
||||
def call(doc)
|
||||
annotations = []
|
||||
|
||||
doc.page_count.times do |page_index|
|
||||
break if annotations.size >= LINKS_LIMIT
|
||||
next if doc.annot_count(page_index).zero?
|
||||
|
||||
page = doc.get_page(page_index)
|
||||
geometry = { box: page.box, rotation: page.rotation }
|
||||
|
||||
page.annotations.each do |annotation|
|
||||
next unless annotation.link? && annotation.rect?
|
||||
|
||||
url = annotation.link.url
|
||||
|
||||
next if url.blank? || URI_PREFIXES.none? { |prefix| url.start_with?(prefix) }
|
||||
|
||||
annotations << build_external_link_hash(url, annotation, geometry).merge('page' => page_index)
|
||||
end
|
||||
end
|
||||
|
||||
annotations
|
||||
end
|
||||
|
||||
def build_external_link_hash(url, area, geometry)
|
||||
x, y, w, h, page_width, page_height = Pdfium.transform_rect(area.bounds, **geometry)
|
||||
|
||||
{
|
||||
'type' => 'external_link',
|
||||
'value' => url,
|
||||
'x' => x / page_width,
|
||||
'y' => y / page_height,
|
||||
'w' => w / page_width,
|
||||
'h' => h / page_height
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,211 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module FindPdfiumAcroFields
|
||||
DATE_JS_PREFIX = 'AFDate_'
|
||||
SKIP_FIELD_TYPES = %i[unknown pushbutton].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def call(attachment, doc)
|
||||
pages = {}
|
||||
widgets = []
|
||||
|
||||
doc.page_count.times do |page_index|
|
||||
next if doc.annot_count(page_index).zero?
|
||||
|
||||
page = doc.get_page(page_index)
|
||||
|
||||
pages[page_index] = { box: page.box, rotation: page.rotation }
|
||||
|
||||
page.annotations.each do |annotation|
|
||||
next unless annotation.widget? && annotation.rect?
|
||||
|
||||
field = annotation.field
|
||||
|
||||
next if field.nil? || field.type.in?(SKIP_FIELD_TYPES)
|
||||
|
||||
widgets << annotation
|
||||
end
|
||||
end
|
||||
|
||||
group_widgets(widgets).filter_map { |field_widgets| build_field(field_widgets, pages, attachment) }
|
||||
end
|
||||
|
||||
def group_widgets(widgets)
|
||||
widgets.group_by do |annotation|
|
||||
field = annotation.field
|
||||
|
||||
if (field.control_count > 1 && field.own?) || field.detached? || (field.name.blank? && field.control_count <= 1)
|
||||
[:widget, annotation.page_index, annotation.index]
|
||||
else
|
||||
[:field, field.type, field.name]
|
||||
end
|
||||
end.values
|
||||
end
|
||||
|
||||
def build_field(widgets, pages, attachment)
|
||||
widgets = widgets.sort_by.with_index { |annotation, index| [annotation.field.control_index, index] }
|
||||
|
||||
areas = widgets.filter_map { |annotation| build_area(annotation, pages[annotation.page_index], attachment) }
|
||||
|
||||
return if areas.blank?
|
||||
|
||||
field_properties = build_field_properties(widgets)
|
||||
|
||||
return if field_properties.blank?
|
||||
return if field_properties[:default_value].present?
|
||||
|
||||
if field_properties[:type] == 'radio'
|
||||
if areas.size != field_properties[:options].size
|
||||
field_properties[:options] = build_options(Array.new(areas.size, ''))
|
||||
end
|
||||
|
||||
areas.each_with_index do |area, index|
|
||||
area[:option_uuid] = field_properties[:options][index][:uuid]
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
required: widgets.first.field.required?,
|
||||
preferences: {},
|
||||
areas:,
|
||||
**field_properties
|
||||
}
|
||||
end
|
||||
|
||||
def build_area(annotation, page, attachment)
|
||||
x, y, w, h, page_width, page_height = Pdfium.transform_rect(annotation.bounds, **page)
|
||||
|
||||
attrs = {
|
||||
page: annotation.page_index,
|
||||
x: x / page_width,
|
||||
y: y / page_height,
|
||||
w: w / page_width,
|
||||
h: h / page_height,
|
||||
attachment_uuid: attachment.uuid
|
||||
}
|
||||
|
||||
return if attrs[:w].zero? || attrs[:h].zero?
|
||||
|
||||
field = annotation.field
|
||||
|
||||
attrs[:cell_w] = attrs[:w] / field.max_len if field.comb? && field.max_len.to_f.positive?
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def build_field_properties(widgets)
|
||||
field = widgets.first.field
|
||||
|
||||
field_name = field.name if field.name.match?(FindAcroFields::FIELD_NAME_REGEXP)
|
||||
|
||||
attrs = { name: field_name.to_s }
|
||||
attrs[:description] = field.alternate_name if field.alternate_name.present? &&
|
||||
field.alternate_name != field.name &&
|
||||
!field.alternate_name.in?(FindAcroFields::SKIP_FIELD_DESCRIPTION)
|
||||
|
||||
case field.type
|
||||
when :checkbox, :radio
|
||||
build_button_properties(attrs, widgets)
|
||||
when :combobox
|
||||
build_select_properties(attrs, field)
|
||||
when :text
|
||||
build_text_properties(attrs, field)
|
||||
when :signature
|
||||
{
|
||||
**attrs,
|
||||
type: field.name.to_s.downcase.include?('initials') ? 'initials' : 'signature'
|
||||
}
|
||||
else
|
||||
{}
|
||||
end.compact
|
||||
end
|
||||
|
||||
def build_button_properties(attrs, widgets)
|
||||
field = widgets.first.field
|
||||
options = widgets.find { |w| w.field.options.present? }&.field&.options.to_a
|
||||
checked = widgets.find { |w| w.field.checked? }
|
||||
export_values = widgets.filter_map { |w| w.field.export_value.presence }.uniq
|
||||
|
||||
if field.type == :radio && options.present?
|
||||
{
|
||||
**attrs,
|
||||
type: 'radio',
|
||||
options: build_options(options, 'radio'),
|
||||
default_value: checked && options[checked.field.control_index]
|
||||
}
|
||||
elsif field.control_count > 1 && export_values.size > 1
|
||||
{
|
||||
**attrs,
|
||||
type: 'radio',
|
||||
options: build_options(export_values.map(&:to_sym), 'radio'),
|
||||
default_value: checked&.field&.export_value
|
||||
}
|
||||
else
|
||||
{
|
||||
**attrs,
|
||||
type: 'checkbox',
|
||||
default_value: checked.present?
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def build_select_properties(attrs, field)
|
||||
return {} if field.options.blank?
|
||||
|
||||
{
|
||||
**attrs,
|
||||
type: 'select',
|
||||
options: build_options(field.options, 'select'),
|
||||
default_value: field.value.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP) ? nil : field.value
|
||||
}
|
||||
end
|
||||
|
||||
def build_text_properties(attrs, field)
|
||||
preferences = { align: FindAcroFields::FIELD_ALIGNMENT.fetch(field.quadding.to_i, 'left') }
|
||||
|
||||
attrs = { **attrs, preferences: }
|
||||
|
||||
if field.comb?
|
||||
{ **attrs, type: 'cells', default_value: field.value }
|
||||
elsif date?(field)
|
||||
format = [field.format_js, field.keystroke_js].compact
|
||||
.filter_map { |js| js[FindAcroFields::DATE_FORMAT_REGEXP] }
|
||||
.first
|
||||
|
||||
preferences[:format] = format.upcase if format
|
||||
|
||||
{ **attrs, type: 'date', default_value: field.value }
|
||||
else
|
||||
{ **attrs, type: 'text', default_value: field.value }
|
||||
end
|
||||
end
|
||||
|
||||
def date?(field)
|
||||
field.format_js.to_s.include?(DATE_JS_PREFIX) || field.keystroke_js.to_s.include?(DATE_JS_PREFIX)
|
||||
end
|
||||
|
||||
def build_options(values, type = nil)
|
||||
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
|
||||
|
||||
values.filter_map do |option|
|
||||
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
|
||||
|
||||
option = option[1] if option.is_a?(Array) && option.size == 2
|
||||
|
||||
if option.is_a?(String) || option.is_a?(Symbol)
|
||||
option = option.to_s.encode('utf-8', invalid: :replace, undef: :replace, replace: '')
|
||||
end
|
||||
|
||||
next if type == 'select' && option.to_s.match?(FindAcroFields::SELECT_PLACEHOLDER_REGEXP)
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
value: is_option_number || is_skip_single_value ? '' : option
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user