pull/663/merge
Jeremy Kritt 1 month ago committed by GitHub
commit 75d48e11b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -73,11 +73,18 @@
>
<div
v-else-if="field.type === 'signature' && signature"
class="flex justify-between h-full gap-1 overflow-hidden w-full"
class="flex h-full gap-1 overflow-hidden w-full relative"
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'flex-row' : 'flex-col'"
>
<div
class="flex overflow-hidden"
v-if="isShowSignatureId && !isNarrow"
class="bg-amber-400 text-amber-950 uppercase tracking-wider truncate px-1 leading-tight text-[0.8vw] lg:text-[0.55rem] flex-shrink-0"
style="border-bottom: 1px solid rgba(120, 53, 15, 0.25);"
>
{{ t('digitally_signed_by') }} &middot; ID {{ String(signature.uuid).slice(0, 8).toUpperCase() }}
</div>
<div
class="flex overflow-hidden border border-base-content/20 bg-base-100/60"
:class="isNarrow && (isShowSignatureId || field.preferences?.reason_field_uuid) ? 'w-1/2' : 'flex-grow'"
style="min-height: 50%"
>
@ -88,28 +95,49 @@
</div>
<div
v-if="isShowSignatureId || field.preferences?.reason_field_uuid"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.7rem] px-0.5"
:class="isNarrow ? 'w-1/2' : 'w-full'"
>
<div class="truncate uppercase">
ID: {{ signature.uuid }}
<div
v-if="isNarrow && isShowSignatureId"
class="truncate uppercase opacity-70"
>
ID: {{ String(signature.uuid).slice(0, 8).toUpperCase() }}
</div>
<div>
<span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
<template v-if="submitter.email">
&lt;{{ submitter.email }}&gt;
</template>
<div class="truncate">
<span class="font-semibold">{{ submitter.name }}</span><template v-if="submitter.email"> &lt;{{ submitter.email }}&gt;</template>
<span class="opacity-60"> &middot; </span>{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
</div>
<div>
{{ new Date(signature.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'short' }) }}
</div>
</div>
<div
v-else-if="field.type === 'initials' && initials"
class="flex flex-col h-full overflow-hidden w-full"
>
<div
v-if="isShowSignatureId"
class="bg-amber-400 text-amber-950 uppercase tracking-wider truncate px-1 leading-tight text-[0.8vw] lg:text-[0.5rem] flex-shrink-0"
style="border-bottom: 1px solid rgba(120, 53, 15, 0.25);"
>
ID {{ String(initials.uuid).slice(0, 8).toUpperCase() }}
</div>
<div
class="flex-grow flex overflow-hidden border border-base-content/20 bg-base-100/60"
style="min-height: 50%"
>
<img
v-else-if="field.type === 'initials' && initials"
class="object-contain mx-auto"
:src="initials.url"
>
</div>
<div
v-if="isShowSignatureId"
class="truncate text-[1vw] lg:text-[0.5rem] lg:leading-[0.65rem] px-0.5 flex-shrink-0"
>
<span class="font-semibold">{{ submitter.name }}</span>
<span class="opacity-60"> &middot; </span>{{ new Date(initials.created_at).toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) }}
</div>
</div>
<div
v-else-if="(field.type === 'file' || field.type === 'payment') && attachments.length"
class="px-0.5 flex flex-col justify-center"

@ -8,33 +8,56 @@
<field-value dir="auto" aria-hidden="true" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? && color.match?(Templates::COLOR_REGEXP) %><%= "background: #{bg_color}; " if bg_color.present? && bg_color.match?(Templates::COLOR_REGEXP) %>width: <%= area['w'].to_f * 100 %>%; height: <%= area['h'].to_f * 100 %>%; left: <%= area['x'].to_f * 100 %>%; top: <%= area['y'].to_f * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
<% if field['type'] == 'signature' %>
<% is_narrow = area['h'].to_f.positive? && ((area['w'].to_f * local_assigns[:page_width]) / (area['h'].to_f * local_assigns[:page_height])) > 4.5 %>
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
<div class="flex overflow-hidden <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
<% sig_attachment = attachments_index[value] %>
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
<% sig_timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
<% sig_time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
<div class="flex w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || reason_value) ? 'flex-row' : 'flex-col' %>">
<% if local_assigns[:with_signature_id] && !is_narrow && sig_attachment %>
<div class="bg-amber-400 text-amber-950 uppercase tracking-wider truncate px-1 leading-tight text-[0.8vw] lg:text-[0.55rem] flex-shrink-0" style="border-bottom: 1px solid rgba(120, 53, 15, 0.25);">
<%= t('digitally_signed_by') %> &middot; ID <%= sig_attachment.uuid.to_s.first(8).upcase %>
</div>
<% if (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) && attachment = attachments_index[value] %>
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem] <%= is_narrow ? 'w-1/2' : 'w-full' %>">
<div class="truncate uppercase">
ID: <%= attachment.uuid %>
<% end %>
<div class="flex overflow-hidden border border-base-content/20 bg-base-100/60 <%= is_narrow && (local_assigns[:with_signature_id] || reason_value) ? 'w-1/2' : 'flex-grow' %>" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= sig_attachment.url %>" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
</div>
<% if (local_assigns[:with_signature_id] || reason_value) && sig_attachment %>
<div class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.7rem] px-0.5 <%= is_narrow ? 'w-1/2' : 'w-full' %>">
<% if is_narrow && local_assigns[:with_signature_id] %>
<div class="truncate uppercase opacity-70">ID: <%= sig_attachment.uuid.to_s.first(8).upcase %></div>
<% end %>
<% if local_assigns[:with_signature_id_reason] != false %>
<div>
<% reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence %>
<% if reason_value %><%= t('reason') %>: <% end %><%= reason_value || t('digitally_signed_by') %> <%= submitter.name %>
<% if submitter.email %>
&lt;<%= submitter.email %>&gt;
<div class="truncate">
<span class="font-semibold"><%= submitter.name %></span><% if submitter.email %> &lt;<%= submitter.email %>&gt;<% end %>
<span class="opacity-60"> &middot; </span><%= l(sig_attachment.created_at.in_time_zone(sig_timezone), format: sig_time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(sig_timezone, sig_attachment.created_at) %>
</div>
<% else %>
<div class="truncate opacity-60">
<%= l(sig_attachment.created_at.in_time_zone(sig_timezone), format: sig_time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(sig_timezone, sig_attachment.created_at) %>
</div>
<% end %>
</div>
<% end %>
<div>
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
<% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
<%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
</div>
<% elsif field['type'] == 'initials' && (initials_attachment = attachments_index[value]) && initials_attachment.image? %>
<% initials_timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
<div class="flex flex-col w-full h-full overflow-hidden">
<% if local_assigns[:with_signature_id] %>
<div class="bg-amber-400 text-amber-950 uppercase tracking-wider truncate px-1 leading-tight text-[0.8vw] lg:text-[0.5rem] flex-shrink-0" style="border-bottom: 1px solid rgba(120, 53, 15, 0.25);">
ID <%= initials_attachment.uuid.to_s.first(8).upcase %>
</div>
<% end %>
<div class="flex-grow flex overflow-hidden border border-base-content/20 bg-base-100/60" style="min-height: 50%">
<img class="object-contain mx-auto" src="<%= initials_attachment.url %>" loading="lazy" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
</div>
<% if local_assigns[:with_signature_id] && local_assigns[:with_signature_id_reason] != false %>
<div class="truncate text-[1vw] lg:text-[0.5rem] lg:leading-[0.65rem] px-0.5 flex-shrink-0">
<span class="font-semibold"><%= submitter.name %></span>
<span class="opacity-60"> &middot; </span><%= l(initials_attachment.created_at.in_time_zone(initials_timezone), format: :long, locale: local_assigns[:locale]) %>
</div>
<% end %>
</div>
<% elsif field['type'].in?(['image', 'initials', 'stamp', 'kba']) && attachments_index[value].image? %>
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'].presence || field['title'].presence || field['type'] %>">
<% elsif field['type'].in?(['file', 'payment', 'image']) %>
<autosize-field></autosize-field>

@ -145,7 +145,7 @@ module Submissions
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value != false
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
is_rotate_incremental = configs.find { |c| c.key == AccountConfig::ROTATE_INCREMENTAL_PDF_KEY }&.value == true
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
@ -308,7 +308,7 @@ module Submissions
end
case field_type
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
when ->(type) { (type == 'signature' || type == 'initials') && with_signature_id }
attachment = submitter.attachments.find { |a| a.uuid == value }
image =
@ -321,130 +321,138 @@ module Submissions
raise
end
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
page_base_size = (([width, height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
base_font_size = (page_base_size / 1.8).to_i
base_font_size = 4 if base_font_size < 4
reason_string =
I18n.with_locale(locale) do
timezone = submitter.account.timezone
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
area_x = area['x'] * width
area_y = area['y'] * height
area_w = area['w'] * width
area_h = area['h'] * height
timezone = with_submitter_timezone ? (submitter.timezone || submitter.account.timezone) : submitter.account.timezone
time_format = with_timestamp_seconds ? :detailed : :long
if with_signature_id_reason || field.dig('preferences', 'reasons').present?
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
caption_string =
I18n.with_locale(locale) do
if field_type == 'initials'
date_str = I18n.l(attachment.created_at.in_time_zone(timezone).to_date, format: :long)
with_signature_id_reason ? "#{submitter.name} · #{date_str}" : date_str
else
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
timestamp_str = "#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
with_signature_id_reason ? "#{submitter.name} · #{timestamp_str}" : timestamp_str
end
end
base_font_size = (font_size / 1.8).to_i
result = nil
area_x = area['x'] * width
area_y = area['y'] * height
area_w = area['w'] * width
area_h = area['h'] * height
if area_h.positive? && (area_w.to_f / area_h) > 4.5
half_width = area_w / 2.0
scale = [half_width / image.width, area_h / image.height].min
image_width = image.width * scale
image_height = image.height * scale
image_x = area_x + ((half_width - image_width) / 2.0)
image_y = height - area_y - image_height
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
canvas.image(io, at: [image_x, image_y], width: image_width, height: image_height)
id_string = "ID: #{attachment.uuid}".upcase
loop do
text = HexaPDF::Layout::TextFragment.create(id_string, font:, font_size: base_font_size)
result = layouter.fit([text], half_width, base_font_size / 0.65)
break if result.status == :success
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
break if id_string.length < 8
doc_id_full = Digest::MD5.hexdigest(submitter.submission.slug).upcase
header_string =
if field_type == 'signature'
"#{I18n.with_locale(locale) { I18n.t('digitally_signed_by') }}:"
else
"#{I18n.with_locale(locale) { I18n.t('initials') }}:"
end
id_string = "#{doc_id_full[0, 16]}..."
string = [id_string, reason_string].join("\n")
amber_border = HexaPDF::Content::ColorSpace::DeviceRGB.new.color(0.71, 0.45, 0.05)
slate_text = HexaPDF::Content::ColorSpace::DeviceRGB.new.color(0.16, 0.16, 0.20)
muted_text = HexaPDF::Content::ColorSpace::DeviceRGB.new.color(0.42, 0.45, 0.50)
loop do
text = HexaPDF::Layout::TextFragment.create(string, font:, font_size: base_font_size)
bracket_r = [area_h * 0.18, base_font_size * 1.2, 7].max
inner_left = area_x + bracket_r + 4
inner_right = area_x + area_w - 1
content_w = inner_right - inner_left
result = layouter.fit([text], half_width, area_h)
header_h = base_font_size * 1.4
header_gap = base_font_size * 0.2
line_h = base_font_size * 1.25
min_image_h = base_font_size * 2.5
break if result.status == :success
base_font_size *= 0.9
break if base_font_size < 2
# Signature: header + image + caption (name · timestamp) + ID. Initials: header + image only.
footer_lines = field_type == 'signature' ? 2 : 0
while footer_lines > 0 && (area_h - header_h - header_gap - (line_h * footer_lines)) < min_image_h
footer_lines -= 1
end
footer_h = line_h * footer_lines
text = HexaPDF::Layout::TextFragment.create(string, font:, font_size: base_font_size)
text_x = area_x + half_width
text_y = height - area_y
available_image_h = area_h - header_h - header_gap - footer_h
available_image_h = min_image_h if available_image_h < min_image_h
layouter.fit([text], half_width, area_h).draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y)
else
reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
font:,
font_size: base_font_size)
id_string = "ID: #{attachment.uuid}".upcase
box_y_top = height - area_y
box_y_bottom = (height - area_y) - area_h
loop do
text = HexaPDF::Layout::TextFragment.create(id_string,
font:,
font_size: base_font_size)
result = layouter.fit([text], area_w, base_font_size / 0.65)
size_factor = field_type == 'initials' ? 0.55 : 0.85
inner_image_w = (content_w - 2) * size_factor
inner_image_h = available_image_h * size_factor
scale = [[inner_image_w / image.width, inner_image_h / image.height].min, 0.0001].max
image_w_drawn = image.width * scale
image_h_drawn = image.height * scale
break if result.status == :success
tail_h = field_type == 'initials' ? base_font_size * 0.6 : 0
content_h = header_h + header_gap + image_h_drawn + footer_h + tail_h
content_h = [content_h, area_h].min
bracket_y_top = box_y_top
bracket_y_bottom = box_y_top - content_h
id_string = "#{id_string.delete_suffix('...')[0..-2]}..."
image_y_top = box_y_top - header_h - header_gap
image_y_bottom = image_y_top - image_h_drawn
image_x_left = inner_left + ((content_w - image_w_drawn) / 2.0)
break if id_string.length < 8
canvas.save_graphics_state do
canvas.fill_color(255, 255, 255)
.rectangle(area_x, box_y_bottom, area_w, area_h)
.fill
end
reason_result = layouter.fit([reason_text], area_w, height)
text_height = result.lines.sum(&:height) + reason_result.lines.sum(&:height)
image_height = area_h - text_height
image_height = area_h / 2 if image_height < area_h / 2
scale = [area_w / image.width, image_height / image.height].min
header_font = pdf.fonts.add('Helvetica', variant: :bold)
label_font = pdf.fonts.add('Helvetica')
header_text = HexaPDF::Layout::TextFragment.create(header_string, font: header_font,
font_size: base_font_size,
fill_color: slate_text)
HexaPDF::Layout::TextLayouter.new(font: header_font, font_size: base_font_size, text_align: :left)
.fit([header_text], content_w, header_h)
.draw(canvas, inner_left, box_y_top - 1)
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
layouter.fit([text], area_w, base_font_size / 0.65)
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
height - area_y - TEXT_TOP_MARGIN - image_height)
layouter.fit([reason_text], area_w, reason_result.lines.sum(&:height))
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
height - area_y - TEXT_TOP_MARGIN -
result.lines.sum(&:height) - image_height)
canvas.image(
io,
at: [
area_x + (area_w / 2) - ((image.width * scale) / 2),
height - area_y - (image.height * scale / 2) - (image_height / 2)
],
width: image.width * scale,
height: image.height * scale
)
image_x_left_aligned = inner_left
canvas.image(io,
at: [image_x_left_aligned, image_y_bottom],
width: image_w_drawn,
height: image_h_drawn)
canvas.save_graphics_state do
canvas.stroke_color(amber_border).line_width(0.9).line_cap_style(:round).line_join_style(:round)
k = bracket_r * 0.5523
top_stub_end_x = inner_left - 1
top_stub_len = top_stub_end_x - (area_x + bracket_r)
bottom_stub_end_x = (area_x + bracket_r) + (top_stub_len * 1.12)
canvas.move_to(top_stub_end_x, bracket_y_top)
.line_to(area_x + bracket_r, bracket_y_top)
.curve_to(area_x, bracket_y_top - bracket_r,
p1: [area_x + bracket_r - k, bracket_y_top],
p2: [area_x, bracket_y_top - bracket_r + k])
.line_to(area_x, bracket_y_bottom + bracket_r)
.curve_to(area_x + bracket_r, bracket_y_bottom,
p1: [area_x, bracket_y_bottom + bracket_r - k],
p2: [area_x + bracket_r - k, bracket_y_bottom])
.line_to(bottom_stub_end_x, bracket_y_bottom)
.stroke
end
if footer_lines >= 1
caption_text = HexaPDF::Layout::TextFragment.create(caption_string, font: label_font,
font_size: base_font_size,
fill_color: slate_text)
HexaPDF::Layout::TextLayouter.new(font: label_font, font_size: base_font_size, text_align: :left)
.fit([caption_text], content_w, line_h)
.draw(canvas, inner_left, image_y_bottom)
end
if footer_lines >= 2
id_font_size = [base_font_size * 0.85, 4].max
id_text = HexaPDF::Layout::TextFragment.create(id_string, font: label_font, font_size: id_font_size,
fill_color: muted_text)
HexaPDF::Layout::TextLayouter.new(font: label_font, font_size: id_font_size, text_align: :left)
.fit([id_text], content_w, line_h)
.draw(canvas, inner_left, image_y_bottom - line_h)
end
when 'image', 'signature', 'initials', 'stamp', 'kba'
attachment = submitter.attachments.find { |a| a.uuid == value }

@ -11,7 +11,7 @@ module Submitters
}.freeze
FONT_ALIASES = {
'initials' => 'Go Noto Kurrent-Bold Bold',
'initials' => 'Dancing Script Regular',
'signature' => 'Dancing Script Regular'
}.freeze

Loading…
Cancel
Save