Skip to content

Commit

Permalink
Merge pull request #13633 from opf/feature/49684-upload-custom-pictur…
Browse files Browse the repository at this point in the history
…e-for-cover-page-of-pdf-export

[#49684] Upload custom picture for cover page of pdf export
  • Loading branch information
as-op authored Sep 25, 2023
2 parents 6345eac + e77f0ec commit 8124f1b
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 53 deletions.
35 changes: 32 additions & 3 deletions app/controllers/custom_styles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ class CustomStylesController < ApplicationController
layout 'admin'
menu_item :custom_style

before_action :require_admin, except: %i[logo_download export_logo_download favicon_download touch_icon_download]
before_action :require_ee_token, except: %i[upsale logo_download export_logo_download favicon_download touch_icon_download]
skip_before_action :check_if_login_required, only: %i[logo_download export_logo_download favicon_download touch_icon_download]
before_action :require_admin,
except: %i[logo_download export_logo_download export_cover_download favicon_download touch_icon_download]
before_action :require_ee_token,
except: %i[upsale logo_download export_logo_download export_cover_download favicon_download touch_icon_download]
skip_before_action :check_if_login_required,
only: %i[logo_download export_logo_download export_cover_download favicon_download touch_icon_download]

def show
@custom_style = CustomStyle.current || CustomStyle.new
Expand Down Expand Up @@ -62,6 +65,22 @@ def update
end
end

def update_export_cover_text_color
color = params[:export_cover_text_color]

@custom_style = CustomStyle.current
if @custom_style.nil?
return render_404
end

color_hexcode_regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
if !color.nil? && color.match(color_hexcode_regex)
@custom_style.export_cover_text_color = color
@custom_style.save
end
redirect_to custom_style_path
end

def logo_download
file_download(:logo_path)
end
Expand All @@ -70,6 +89,10 @@ def export_logo_download
file_download(:export_logo_path)
end

def export_cover_download
file_download(:export_cover_path)
end

def favicon_download
file_download(:favicon_path)
end
Expand All @@ -86,6 +109,10 @@ def export_logo_delete
file_delete(:remove_export_logo)
end

def export_cover_delete
file_delete(:remove_export_cover)
end

def favicon_delete
file_delete(:remove_favicon)
end
Expand Down Expand Up @@ -151,6 +178,8 @@ def require_ee_token
def custom_style_params
params.require(:custom_style).permit(:logo, :remove_logo,
:export_logo, :remove_export_logo,
:export_cover, :remove_export_cover,
:export_cover_text_color,
:favicon, :remove_favicon,
:touch_icon, :remove_touch_icon)
end
Expand Down
3 changes: 2 additions & 1 deletion app/models/custom_style.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class CustomStyle < ApplicationRecord
mount_uploader :logo, OpenProject::Configuration.file_uploader
mount_uploader :export_logo, OpenProject::Configuration.file_uploader
mount_uploader :export_cover, OpenProject::Configuration.file_uploader
mount_uploader :favicon, OpenProject::Configuration.file_uploader
mount_uploader :touch_icon, OpenProject::Configuration.file_uploader

Expand All @@ -21,7 +22,7 @@ def digest
updated_at.to_i
end

%i(favicon touch_icon export_logo logo).each do |name|
%i(favicon touch_icon export_logo export_cover logo).each do |name|
define_method "#{name}_path" do
image = send(name)

Expand Down
80 changes: 61 additions & 19 deletions app/models/work_package/pdf_export/cover.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,41 +60,70 @@ def write_cover_hr
)
end

def cover_text_color
@cover_text_color ||= validate_cover_text_color
end

def validate_cover_text_color
return nil if CustomStyle.current.blank?

hexcode = CustomStyle.current.export_cover_text_color
return nil if hexcode.blank?

color = Color.new({ hexcode: })
color.normalize_hexcode
return nil if color.hexcode.blank?

# pdf hex colors are defined without leading hash
color.hexcode.sub('#', '')
end

def write_hero_title(top, width)
text_style = styles.cover_hero_title
formatted_text_box_measured(
[text_style.merge({ text: project.name, size: nil, leading: nil })],
size: text_style[:size], leading: text_style[:leading],
at: [0, top], width:, height: styles.cover_hero_title_max_height, overflow: :shrink_to_fit
write_hero_text(
top:, width:,
text: project.name,
text_style: styles.cover_hero_title,
height: styles.cover_hero_title_max_height
) + styles.cover_hero_title_spacing
end

def write_hero_heading(top, width)
max_title_height = available_title_height(top)
text_style = styles.cover_hero_heading
formatted_text_box_measured(
[text_style.merge({ text: heading, size: nil, leading: nil })],
size: text_style[:size], leading: text_style[:leading],
at: [0, top], width:, height: max_title_height, overflow: :shrink_to_fit
write_hero_text(
top:, width:,
text: heading,
text_style: styles.cover_hero_heading,
height: available_title_height(top)
) + styles.cover_hero_heading_spacing
end

def write_hero_subheading(top, width)
text_style = styles.cover_hero_subheading
pdf.formatted_text_box(
[text_style.merge({ text: User.current.name, size: nil, leading: nil })],
write_hero_text(
top:, width:,
text: User.current.name,
text_style: styles.cover_hero_subheading,
height: styles.cover_hero_subheading_max_height
)
end

def write_hero_text(top:, width:, text:, text_style:, height:)
formatted_text = text_style.merge({ text:, size: nil, leading: nil })
formatted_text[:color] = cover_text_color if cover_text_color.present?
formatted_text_box_measured(
[formatted_text],
size: text_style[:size], leading: text_style[:leading],
at: [0, top], width:, height: styles.cover_hero_subheading_max_height, overflow: :shrink_to_fit
at: [0, top], width:, height:, overflow: :shrink_to_fit
)
end

def write_cover_footer
text_style = styles.cover_footer
text_style[:color] = cover_text_color if cover_text_color.present?
draw_text_multiline_left(
text: footer_date,
max_left: pdf.bounds.width / 2,
max_lines: 1,
top: pdf.bounds.bottom - styles.cover_footer_offset,
text_style: styles.cover_footer
text_style:
)
end

Expand All @@ -107,20 +136,33 @@ def write_cover_logo
end

def cover_background_image
image_file = Rails.root.join("app/assets/images/pdf/cover.png")
image_file = custom_cover_image
image_file = Rails.root.join("app/assets/images/pdf/cover.png") if image_file.nil?
image_obj, image_info = pdf.build_image_object(image_file)
scale = pdf.bounds.width / image_info.width.to_f
height = image_info.height.to_f * scale
image_opts = { at: [0, height], scale: }
[image_obj, image_info, image_opts, height]
end

def custom_cover_image
return unless CustomStyle.current.present? &&
CustomStyle.current.export_cover.present? && CustomStyle.current.export_cover.local_file.present?

image_file = CustomStyle.current.export_cover.local_file.path
content_type = OpenProject::ContentTypeDetector.new(image_file).detect
return unless pdf_embeddable?(content_type)

image_file
end

def write_background_image
height = pdf.bounds.height / 2
half = pdf.bounds.height / 2
height = half
pdf.canvas do
image_obj, image_info, image_opts, height = cover_background_image
pdf.embed_image image_obj, image_info, image_opts
end
height - styles.cover_hero_padding[:top_padding]
height.clamp(half, pdf.bounds.height) - styles.cover_hero_padding[:top_padding]
end
end
124 changes: 100 additions & 24 deletions app/views/custom_styles/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -155,39 +155,115 @@ See COPYRIGHT and LICENSE files for more details.
</section>
<% end %>
<%= form_for @custom_style, url: custom_style_path, html: { multipart: true, class: "form -vertical" } do |f| %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t(:label_custom_export_logo) %></legend>

<% if @custom_style.id && @custom_style.export_logo.present? %>
<div>
<%= tag('img', src: custom_style_export_logo_path(digest: @custom_style.digest, filename: @custom_style.export_logo_identifier), class: 'custom-export-logo-preview') %>
<%= link_to t(:button_delete),
custom_style_export_logo_delete_path,
method: :delete,
class: 'icon icon-delete confirm-form-submit' %>
<% custom_export_expanded = @custom_style.id && (@custom_style.export_logo.present? || @custom_style.export_cover.present? || @custom_style.export_cover_text_color.present?) %>
<collapsible-section-augment initially-expanded="<%= custom_export_expanded %>"
section-title="<%= I18n.t(:label_custom_pdf_export_settings) %>">
</collapsible-section-augment>
<div class="collapsible-section-augment--body" hidden>

<%= form_for @custom_style, url: custom_style_path, html: { multipart: true, class: "form -vertical" } do |f| %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t(:label_custom_export_logo) %></legend>

<% if @custom_style.id && @custom_style.export_logo.present? %>
<div>
<%= tag('img', src: custom_style_export_logo_path(digest: @custom_style.digest, filename: @custom_style.export_logo_identifier), class: 'custom-export-logo-preview') %>
<%= link_to t(:button_delete),
custom_style_export_logo_delete_path,
method: :delete,
class: 'icon icon-delete confirm-form-submit' %>
</div>
<% end %>

<div class="grid-block">
<div class="form--field -required">
<div class="attachment_field form--field-container -vertical -shrink">
<div class="form--file-container">
<%= f.file_field :export_logo, required: true, class: "attachment_choose_file", size: "15" %>
</div>
</div>
<div class="form--field-instructions">
<%= t('text_custom_export_logo_instructions') %>
</div>
</div>
</div>
<% end %>

<div class="grid-block">
<div class="form--field -required">
<div class="attachment_field form--field-container -vertical -shrink">
<div class="form--file-container">
<%= f.file_field :export_logo, required: true, class: "attachment_choose_file", size: "15" %>
<%= styled_button_tag t(@custom_style.export_logo.present? ? :button_replace : :button_upload), class: "button #{@custom_style.export_logo.blank? ? '-with-icon icon-add' : ''}" %>

</fieldset>
</section>
<% end %>
<%= form_for @custom_style, url: custom_style_path, html: { multipart: true, class: "form -vertical" } do |f| %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t(:label_custom_export_cover) %></legend>

<% if @custom_style.id && @custom_style.export_cover.present? %>
<div>
<%= tag('img', src: custom_style_export_cover_path(digest: @custom_style.digest, filename: @custom_style.export_cover_identifier), class: 'custom-export-cover-preview') %>
<%= link_to t(:button_delete),
custom_style_export_cover_delete_path,
method: :delete,
class: 'icon icon-delete confirm-form-submit' %>
</div>
<% end %>

<div class="grid-block">
<div class="form--field -required">
<div class="attachment_field form--field-container -vertical -shrink">
<div class="form--file-container">
<%= f.file_field :export_cover, required: true, class: "attachment_choose_file", size: "15" %>
</div>
</div>
<div class="form--field-instructions">
<%= t('text_custom_export_cover_instructions') %>
</div>
</div>
</div>

<%= styled_button_tag t(@custom_style.export_cover.present? ? :button_replace : :button_upload), class: "button #{@custom_style.export_cover.blank? ? '-with-icon icon-add' : ''}" %>

</fieldset>
</section>
<% end %>
<%= form_tag update_custom_style_export_cover_text_color_path, html: { multipart: true, class: "form -vertical" } do |f| %>
<section class="form--section">
<fieldset class="form--fieldset">
<legend class="form--fieldset-legend"><%= I18n.t(:label_custom_export_cover_overlay) %></legend>
<div class="form--field">
<label class="form--label"><%= I18n.t(:label_custom_export_cover_text_color) %>:</label>
<span class="form--field-container">
<div class="form--field-affix">
<% design_color_name = 'export_cover_text_color' %>
<% design_color_hex = @custom_style.export_cover_text_color.present? ? @custom_style.export_cover_text_color : '' %>
<%= icon_for_color(OpenStruct.new(variable: design_color_name,
hexcode: design_color_hex),
data: { target: "#" + design_color_name }) %>
</div>
<span class="form--text-field-container">
<%= styled_text_field_tag design_color_name,
design_color_hex,
placeholder: '#000'
%>
</span>
</span>
<div class="form--field-instructions">
<%= t('text_custom_export_logo_instructions') %>
<% instruction_key = "admin.custom_styles.instructions.#{design_color_name}" %>
<% if I18n.exists?(instruction_key, :en) %>
<%= I18n.t(instruction_key) %>
<% end %>
</div>
</div>
</div>

<%= styled_button_tag t(@custom_style.export_logo.present? ? :button_replace : :button_upload), class: "button #{@custom_style.export_logo.blank? ? '-with-icon icon-add' : ''}" %>
<button type="submit" class="button -hide-when-collapsed"><%= I18n.t(:button_save) %></button>

</fieldset>
</section>
<% end %>
</fieldset>
</section>
<% end %>
</div>

<%= form_tag update_design_colors_path, method: :post, class: "form" do %>
<section class="form--section">
Expand Down
7 changes: 7 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1883,6 +1883,10 @@ en:
label_login: "Sign in"
label_custom_logo: "Custom logo"
label_custom_export_logo: "Custom export logo"
label_custom_export_cover: "Custom export cover background"
label_custom_export_cover_overlay: "Custom export cover background overlay"
label_custom_export_cover_text_color: "Text color"
label_custom_pdf_export_settings: "Custom PDF export settings"
label_custom_favicon: "Custom favicon"
label_custom_touch_icon: "Custom touch icon"
label_logout: "Sign out"
Expand Down Expand Up @@ -2921,6 +2925,9 @@ en:
This is the logo that appears in your PDF exports.
It needs to be a PNG or JPEG image file.
A black or colored logo on transparent or white background is recommended.
text_custom_export_cover_instructions: >
This is the image that appears in the background of a cover page in your PDF exports.
It needs to be an about 800px width by 500px height sized PNG or JPEG image file.
text_custom_favicon_instructions: >
This is the tiny icon that appears in your browser window/tab next to the
page's title.
Expand Down
8 changes: 8 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@
as: 'custom_style_export_logo',
constraints: { filename: /[^\/]*/ }

get 'custom_style/:digest/export_cover/:filename' => 'custom_styles#export_cover_download',
as: 'custom_style_export_cover',
constraints: { filename: /[^\/]*/ }

get 'custom_style/:digest/favicon/:filename' => 'custom_styles#favicon_download',
as: 'custom_style_favicon',
constraints: { filename: /[^\/]*/ }
Expand Down Expand Up @@ -352,11 +356,15 @@

delete 'design/logo' => 'custom_styles#logo_delete', as: 'custom_style_logo_delete'
delete 'design/export_logo' => 'custom_styles#export_logo_delete', as: 'custom_style_export_logo_delete'
delete 'design/export_cover' => 'custom_styles#export_cover_delete', as: 'custom_style_export_cover_delete'
delete 'design/favicon' => 'custom_styles#favicon_delete', as: 'custom_style_favicon_delete'
delete 'design/touch_icon' => 'custom_styles#touch_icon_delete', as: 'custom_style_touch_icon_delete'
get 'design/upsale' => 'custom_styles#upsale', as: 'custom_style_upsale'
post 'design/colors' => 'custom_styles#update_colors', as: 'update_design_colors'
post 'design/themes' => 'custom_styles#update_themes', as: 'update_design_themes'
post 'design/export_cover_text_color' => 'custom_styles#update_export_cover_text_color',
as: 'update_custom_style_export_cover_text_color'

resource :custom_style, only: %i[update show create], path: 'design'

resources :attribute_help_texts, only: %i(index new create edit update destroy) do
Expand Down
Loading

0 comments on commit 8124f1b

Please sign in to comment.