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_GetTrailerEnds, %i[FPDF_DOCUMENT pointer ulong], :ulong
|
||||||
attach_function :FPDF_DocumentHasValidCrossReferenceTable, [:FPDF_DOCUMENT], :int
|
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_GetFirstChild, %i[FPDF_DOCUMENT FPDF_BOOKMARK], :FPDF_BOOKMARK
|
||||||
attach_function :FPDFBookmark_GetNextSibling, %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
|
attach_function :FPDFBookmark_GetTitle, %i[FPDF_BOOKMARK pointer ulong], :ulong
|
||||||
@@ -219,6 +213,14 @@ class Pdfium
|
|||||||
FPDF_PAGEOBJ_SHADING = 4
|
FPDF_PAGEOBJ_SHADING = 4
|
||||||
FPDF_PAGEOBJ_FORM = 5
|
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
|
# Path segment types
|
||||||
FPDF_SEGMENT_UNKNOWN = -1
|
FPDF_SEGMENT_UNKNOWN = -1
|
||||||
FPDF_SEGMENT_LINETO = 0
|
FPDF_SEGMENT_LINETO = 0
|
||||||
@@ -272,6 +274,73 @@ class Pdfium
|
|||||||
attach_function :FPDFDOC_ExitFormFillEnvironment, [:FPDF_FORMHANDLE], :void
|
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 :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
|
attach_function :FPDFPage_Flatten, %i[FPDF_PAGE int], :int
|
||||||
|
|
||||||
FLAT_NORMALDISPLAY = 0
|
FLAT_NORMALDISPLAY = 0
|
||||||
@@ -303,6 +372,7 @@ class Pdfium
|
|||||||
attach_function :FPDFPage_SetRotation, %i[FPDF_PAGE int], :void
|
attach_function :FPDFPage_SetRotation, %i[FPDF_PAGE int], :void
|
||||||
attach_function :FPDFPage_TransFormWithClip, %i[FPDF_PAGE pointer pointer], :int
|
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 :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_GetMediaBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
attach_function :FPDFPage_SetMediaBox, %i[FPDF_PAGE float float float float], :void
|
attach_function :FPDFPage_SetMediaBox, %i[FPDF_PAGE float float float float], :void
|
||||||
attach_function :FPDFPage_GetCropBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
attach_function :FPDFPage_GetCropBox, %i[FPDF_PAGE pointer pointer pointer pointer], :int
|
||||||
@@ -383,6 +453,60 @@ class Pdfium
|
|||||||
yield instance
|
yield instance
|
||||||
end
|
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')
|
def self.check_last_error(context_message = 'PDFium operation failed')
|
||||||
error_code = FPDF_GetLastError()
|
error_code = FPDF_GetLastError()
|
||||||
|
|
||||||
@@ -401,6 +525,7 @@ class Pdfium
|
|||||||
@document_ptr = document_ptr
|
@document_ptr = document_ptr
|
||||||
|
|
||||||
@pages = {}
|
@pages = {}
|
||||||
|
@annot_counts = {}
|
||||||
@closed = false
|
@closed = false
|
||||||
@source_buffer = source_buffer
|
@source_buffer = source_buffer
|
||||||
@form_handle = FFI::Pointer::NULL
|
@form_handle = FFI::Pointer::NULL
|
||||||
@@ -597,6 +722,14 @@ class Pdfium
|
|||||||
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
|
Pdfium.FPDF_DocumentHasValidCrossReferenceTable(@document_ptr) == 1
|
||||||
end
|
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
|
def signature_count
|
||||||
@signature_count ||= Pdfium.FPDF_GetSignatureCount(@document_ptr)
|
@signature_count ||= Pdfium.FPDF_GetSignatureCount(@document_ptr)
|
||||||
end
|
end
|
||||||
@@ -757,8 +890,6 @@ class Pdfium
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Page
|
class Page
|
||||||
RECT_KEYS = %i[left top right bottom].freeze
|
|
||||||
|
|
||||||
attr_reader :document, :page_index, :page_ptr
|
attr_reader :document, :page_index, :page_ptr
|
||||||
|
|
||||||
def initialize(document, page_index)
|
def initialize(document, page_index)
|
||||||
@@ -789,30 +920,36 @@ class Pdfium
|
|||||||
end
|
end
|
||||||
|
|
||||||
def annotations
|
def annotations
|
||||||
(0...Pdfium.FPDFPage_GetAnnotCount(page_ptr)).filter_map do |index|
|
ensure_not_closed!
|
||||||
annotation = Pdfium.FPDFPage_GetAnnot(page_ptr, index)
|
|
||||||
|
|
||||||
next if annotation.null?
|
@annotations ||=
|
||||||
|
(0...Pdfium.FPDFPage_GetAnnotCount(page_ptr)).filter_map do |index|
|
||||||
begin
|
with_annotation(index) do |handle|
|
||||||
rect = Pdfium::FS_RECTF.new
|
Annotation.new(page: self, index:, subtype: handle.subtype, rect: handle.rect)
|
||||||
Pdfium.FPDFAnnot_GetRect(annotation, rect)
|
end
|
||||||
|
|
||||||
[Pdfium.FPDFAnnot_GetSubtype(annotation),
|
|
||||||
*RECT_KEYS.map { |key| rect[key].round(3) }]
|
|
||||||
ensure
|
|
||||||
Pdfium.FPDFPage_CloseAnnot(annotation)
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def objects
|
def objects
|
||||||
(0...Pdfium.FPDFPage_CountObjects(page_ptr)).map do |index|
|
ensure_not_closed!
|
||||||
object = Pdfium.FPDFPage_GetObject(page_ptr, index)
|
|
||||||
bounds = Array.new(4) { FFI::MemoryPointer.new(:float) }
|
|
||||||
Pdfium.FPDFPageObj_GetBounds(object, *bounds)
|
|
||||||
|
|
||||||
[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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -826,6 +963,18 @@ class Pdfium
|
|||||||
@rotation = value
|
@rotation = value
|
||||||
end
|
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?
|
def closed?
|
||||||
@closed
|
@closed
|
||||||
end
|
end
|
||||||
@@ -987,7 +1136,7 @@ class Pdfium
|
|||||||
|
|
||||||
@document.add_presave_hook(:cleanup) { @document.cleanup }
|
@document.add_presave_hook(:cleanup) { @document.cleanup }
|
||||||
|
|
||||||
reset_text_memoization
|
reset_memoization
|
||||||
|
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -1085,7 +1234,7 @@ class Pdfium
|
|||||||
|
|
||||||
Pdfium.FPDFPage_GenerateContent(@page_ptr) if unwrapped
|
Pdfium.FPDFPage_GenerateContent(@page_ptr) if unwrapped
|
||||||
|
|
||||||
reset_text_memoization if unwrapped
|
reset_memoization if unwrapped
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_form_object(rect_bounds = nil)
|
def find_form_object(rect_bounds = nil)
|
||||||
@@ -1202,12 +1351,14 @@ class Pdfium
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset_text_memoization
|
def reset_memoization
|
||||||
remove_instance_variable(:@text) if defined?(@text)
|
remove_instance_variable(:@text) if defined?(@text)
|
||||||
|
|
||||||
@text_nodes = nil
|
@text_nodes = nil
|
||||||
@text_objects = nil
|
@text_objects = nil
|
||||||
@line_nodes = nil
|
@line_nodes = nil
|
||||||
|
@annotations = nil
|
||||||
|
@objects = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_page_object(object_ptr)
|
def remove_page_object(object_ptr)
|
||||||
@@ -1603,23 +1754,32 @@ class Pdfium
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reload
|
def reload
|
||||||
|
close_page_view
|
||||||
Pdfium.FPDF_ClosePage(@page_ptr)
|
Pdfium.FPDF_ClosePage(@page_ptr)
|
||||||
|
|
||||||
@page_ptr = Pdfium.FPDF_LoadPage(@document.document_ptr, @page_index)
|
@page_ptr = Pdfium.FPDF_LoadPage(@document.document_ptr, @page_index)
|
||||||
|
|
||||||
raise PdfiumError, "Failed to reload page #{page_index}" if @page_ptr.null?
|
raise PdfiumError, "Failed to reload page #{page_index}" if @page_ptr.null?
|
||||||
|
|
||||||
|
@page_view = false
|
||||||
@rotation = nil
|
@rotation = nil
|
||||||
@width = nil
|
@width = nil
|
||||||
@height = nil
|
@height = nil
|
||||||
|
@box = nil
|
||||||
|
|
||||||
reset_text_memoization
|
@document.reset_annot_count(@page_index)
|
||||||
|
|
||||||
|
reset_memoization
|
||||||
end
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
return if closed?
|
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
|
@page_ptr = FFI::Pointer::NULL
|
||||||
|
|
||||||
@@ -1628,6 +1788,25 @@ class Pdfium
|
|||||||
|
|
||||||
private
|
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)
|
def calculate_render_dimensions(width_param, height_param, scale_param)
|
||||||
if scale_param
|
if scale_param
|
||||||
render_width = (width * scale_param).round
|
render_width = (width * scale_param).round
|
||||||
@@ -1654,6 +1833,407 @@ class Pdfium
|
|||||||
end
|
end
|
||||||
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
|
def self.initialize_library
|
||||||
config_mem = FFI::MemoryPointer.new(FPDF_LIBRARY_CONFIG.size)
|
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