I am creating a pdf report in order to show some data using the "squid" gem. This would allow me to display charts in my pdf.
The only issue i found is that when the chart does not fit at the bottom of the page then it looks rendered partially which does not look good at all. Any idea how can i fix this?
Here is the code i am using to render the charts
require 'squid'
class SurveyPdf < Prawn::Document
def initialize(survey, view)
super()
font "#{Rails.root}/app/assets/fonts/roboto-condensed.ttf"
#survey = survey
#view = view
questions
end
def questions
#survey.questions.each do |question|
text "#{question.title}", size: 20
text "Answers #{question.answers.size}", size: 15
if ["single", "select"].include? question.question_type.prefix
if question.answers.choice_counter.any?
chart choices: question.answers.choice_counter
end
end
if question.question_type.prefix == "image"
if question.answers.image_counter.any?
chart images: question.answers.image_counter
end
end
if question.question_type.prefix == "multiple"
if question.answers.multiple_choice_counter.any?
chart choices: question.answers.multiple_choice_counter
end
end
if question.question_type.prefix == "raiting"
move_down 5
if question.answers.any?
text_box "Average rating", size: 12, width: 120, :at => [0, cursor - 2]
text_box "#{average_rating(question.answers.rating_average)}", size: 12, width: 120, :at => [4 * 30, cursor - 2]
else
text_box "Average rating", size: 12, width: 120, :at => [0, cursor - 2]
text_box "0", size: 12, width: 120, :at => [4 * 30, cursor - 2]
end
end
end
end
end
For a similar issue I used the prawn-grouping gem
It pre-renders whatever you place in a group block to test whether it fits on the current page. If not, it skips to the next page and renders.
In your case you would do something like:
def questions
#survey.questions.each do |question|
group :too_tall => lambda { start_new_page } do |g|
g.text "#{question.title}", size: 20
g.text "Answers #{question.answers.size}", size: 15
if ["single", "select"].include? question.question_type.prefix
if question.answers.choice_counter.any?
g.chart choices: question.answers.choice_counter
end
end
if question.question_type.prefix == "image"
if question.answers.image_counter.any?
g.chart images: question.answers.image_counter
end
end
if question.question_type.prefix == "multiple"
if question.answers.multiple_choice_counter.any?
g.chart choices: question.answers.multiple_choice_counter
end
end
if question.question_type.prefix == "raiting"
move_down 5
if question.answers.any?
g.text_box "Average rating", size: 12, width: 120, :at => [0, cursor - 2]
g.text_box "#{average_rating(question.answers.rating_average)}", size: 12, width: 120, :at => [4 * 30, cursor - 2]
else
g.text_box "Average rating", size: 12, width: 120, :at => [0, cursor - 2]
g.text_box "0", size: 12, width: 120, :at => [4 * 30, cursor - 2]
end
end
end
end
end
disclaimer: I've never used squid so the only piece I'm not sure of is g.chart let me know if you have issues there and I will try to figure it out.
Update for squid
The prawn-grouping gem doesn't know about the squid methods (like chart). So we can extract the logic from the prawn-grouping gem and add it directly in your survey_pdf.rb. Copy lines 7-63 from this file, and remove prawn-grouping gem from your app.
if you are curious why this works...
Squid uses the Prawn::Document.extensions method to force Prawn::Document to inherit the squid methods. You can see that in the squid gem code here on line 37.
For prawn-grouping to work it creates a new Prawn::Document as part of the group method. You can see that here on line 55. The problem was that the Prawn::Document instantiated via the prawn-grouping gem wasn't inheriting the squid methods... but your SurveyPdf instance of Prawn::Document does inherit the squid methods, so by adding the grouping logic into your SurveyPdf class, now the Prawn::Document instantiated in your group method will work.
To answer the question in your comment as to determining page size I will run through a few useful methods too long for a comment:
d = Prawn::Document.new
d.y #full page height
d.margin_box.bottom #actually top since prawn starts at the bottom
d.margin_box.absolute_bottom #actual top with margins included
d.margin_box.top #usable page height
d.margin_box.absolute_top #same as #y
d.cursor #where you are vertically on the page
So you can use some basic math to determine fit:
#this is all chart does excepting chart calls #draw
#which we don't want to do until we determine if it fits
c = Squid::Graph.new(d, choices: question.answers.choice_counter)
#determine if the chart will fit vertically
#if not start a new page and move to the top
unless d.cursor + c.height < d.margin_box.top
d.start_new_page
d.move_cursor_to(0)
end
#draw the chart onto the appropriate page
c.draw
Hope this helps in some way
Related
I have two user typed refereed texts that are placed one below the other and I'm using prawn gem to generate pdf and I position my code as follows:
# show.pdf.prawn
...
pdf_notes_1 = firt_text # firt_text is an arbitrary text body of the user
pdf_notes_2 = second_text # second_text is an arbitrary text body of the user
# firt text
pdf.bounding_box [0, 5], width: 300 do
pdf.table(pdf_notes_1, cell_style: { borders: [] }) do
cells.padding = 0
end
end
...
pdf.bounding_box [0, end_position_pdf_notes_1], width: 300 do
pdf.table(pdf_notes_2, cell_style: { borders: [] }) do
cells.padding = 0
end
end
....
I want to find the end of the user's first arbitrary text position so I can place it in the other user text position in "end_position_pdf_notes"
For example, if user writte a text and its end in position: [0, 10], then the new text will have position:
end_position_pdf_notes: 11
"pdf.bounding_box [0, end_position_pdf_notes_1]"
Rails 5.2.0 (as API)
/config/application.rb
config.active_storage.variant_processor = :vips
Problem:
/serializers/api/v1/user/current_user_serializer.rb
class Api::V1::User::CurrentUserSerializer < Api::V1::User::BaseSerializer
include Rails.application.routes.url_helpers
attributes(
[...]
:avatar
:created_at
)
def avatar
if object.avatar.attachment
avatar = {
image: url_for( object.avatar ), # This one works
thumb: url_for( object.avatar.variant(resize_to_fit: [800, 800]) ), # EXCEPTION
thumb_test: url_for( object.avatar.variant(resize: '800x800') ) # Returns image of size: 640x800 (expected 800x800)
}
end
end
end
I get the following exception:
exception: "<MiniMagick::Error: `mogrify -resize-to-fit [800, 800] /tmp/mini_magick20180625-19749-rghjbg.jpg` failed with error: mogrify.im6: unrecognized option `-resize-to-fit' # error/mogrify.c/MogrifyImageCommand/5519. >"
EDIT
Thanks #George Claghorn
I now created my own variant based on this post:
https://prograils.com/posts/rails-5-2-active-storage-new-approach-to-file-uploads
lib/active_storage_variants.rb
class ActiveStorageVariants
class << self
def resize_to_fill(width:, height:, blob:, gravity: 'Center')
blob.analyze unless blob.analyzed?
cols = blob.metadata[:width].to_f
rows = blob.metadata[:height].to_f
if width != cols || height != rows
scale_x = width / cols
scale_y = height / rows
if scale_x >= scale_y
cols = (scale_x * (cols + 0.5)).round
resize = cols.to_s
else
rows = (scale_y * (rows + 0.5)).round
resize = "x#{rows}"
end
end
{
resize: resize,
gravity: gravity,
background: 'rgba(255,255,255,0.0)',
extent: cols != width || rows != height ? "#{width}x#{height}" : ''
}.merge(optimize_hash(blob))
end
end
end
/models/concerns/users/active_storage_variants.rb
require 'active_storage_variants' # /lib/active_storage_variants.rb
module Users::ActiveStorageVariants
def avatar_thumbnail
variation = ActiveStorage::Variation.new(
ActiveStorageVariants.resize_to_fill(
width: 300, height: 300, blob: avatar.blob
)
)
ActiveStorage::Variant.new(avatar.blob, variation)
end
end
/models/user.rb
class User < ApplicationRecord
...
## Concerns
include Users::ActiveStorageVariants
...
end
To call it:
user.avatar_thumbnail
resize_to_fit: [800, 800] is an ImageProcessing transformation. Rails 5.2 doesn’t use ImageProcessing and thus doesn’t support libvips; it uses MiniMagick directly instead.
Rails 6 will switch to ImageProcessing and add libvips support. To use libvips now, prior to the release of Rails 6, bundle the master branch of the rails/rails repository on GitHub:
# Gemfile
gem "rails", github: "rails/rails"
Found a solution here
Replacing this,
<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
with,
<%= image_tag user.avatar.variant(resize: "100 x100") %>
Worked for me. Even though the office document does the former way.
When I try the following code:
text "Hello "
text "World"
They render Hello on top of World instead of World right after Hello. I have some complicated formatting (highlighting, different font sizes etc) on text that I need on one line. I know that the :inline_formatting option exists but it seems this is too complicated to use that option.
I have the following code:
highlight_callback.rb:
class HighlightCallback
def initialize(options)
#color = options[:color]
#document = options[:document]
end
def render_behind(fragment)
original_color = #document.fill_color
#document.fill_color = #color
#document.fill_rectangle(fragment.top_left,
fragment.width,
fragment.height)
#document.fill_color = original_color
end
end
order.pdf.prawn:
highlight = HighlightCallback.new(:color => 'ffff00', :document => self)
#code....
text "Authorized Signature: "
formatted_text [{:text => "_" * 15, :callback => highlight }], :size => 20
which is producing the attached image. How can I get the signature line on the same level as the text?
Ruby 2.5.1
Rails 5.2.0
It's enough to change method text to text_box, i.e.:
bounding_box([0, cursor], width: 540, height: 40) do
stroke_color 'FFFF00'
stroke_bounds
date = 'Date: '
text_box date, style: :bold
text_box DateTime.now.strftime('%Y/%m/%d'), at: [bounds.left + width_of(date), cursor]
text_box "Signature ________________", align: :right
end
Example:
To place text at a exact position you can use text_box with the option :at.
You can get the width of your text with pdf.width_of(str) (use the same style optione :size etc. otherwise it will use the default settings to calculate)
I need to create a table of contents with Prawn. I have add_dest function calls in my code and the
right links in the table of content:
add_dest('Komplett', dest_fit(page_count - 1))
and
text "* <link anchor='Komplett'> Vollstaendiges Mitgliederverzeichnis </link>", :inline_format = true
This works and I get clickable links which forward me to the right pages. However, I need to have page numbers in the table of content. How do I get it printed out?
I would suggest a much simpler solution.
Use pdf.page_number to store the page number of all your sections in a hash as you populate the pages
In the code, output the table of contents after populating the rest of your pages. Insert the TOC into the doc in the right spot by navigating in the PDF pdf.go_to_page(page_num).
For example:
render "pdf/frontpage", p: p
toc.merge!(p.page_number => "Section_Title")
p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/calendar"
p.start_new_page
toc.merge!(p.page_number => "Section_Title")
render "pdf/another_section"
p.go_to_page(1)
p.start_new_page
toc.merge!(p.page_number => "Table of Contents")
render "pdf/table_of_contents", table_of_contents: toc
you should read the chapter on Outline in this document http://prawn.majesticseacreature.com/manual.pdf, p.96. It explains with examples on how to create TOC.
UPDATE
destinations, page_references = {}, {}
page_count.downto(1).each {|num| page_references[num] = state.store.object_id_for_page(num)}
dests.data.to_hash.each_value do |values|
values.each do |value|
value_array = value.to_s.split(":")
dest_name = value_array[0]
dest_id = value_array[1].split[0]
destinations[dest_name] = Integer(dest_id)
end
end
state.store.each do |reference|
if !(dest_name = destinations.key(reference.identifier)).nil?
puts "Destination - #{dest_name} is on Page #{page_references.key(Integer(reference.data[0].to_s.split[0]))}"
end
end
I also needed to create a dynamic TOC. I put together a quick spike that needs some clean-up but does pretty much what I want. I didn't include click-able links but they could easily be added. The example also assumes the TOC is being placed on the 2nd page of the document.
The basic strategy I used was to store the TOC in a hash. Each time I add a new section to the document that I want to appear in the TOC I add it to the hash, i.e.
#toc[pdf.page_count] = "the toc text for this section"
Then prior to adding the page numbers to the document I iterate thru the hash:
number_of_toc_entries_per_page = 10
offset = (#toc.count.to_f / number_of_toc_entries_per_page).ceil
#toc.each_with_index do |(key, value), index|
pdf.start_new_page if index % number_of_toc_entries_per_page == 0
pdf.text "#{value}.... page #{key + offset}", size: 38
end
Anyway, the full example is below, hope it helps.
require 'prawn'
class TocTest
def self.create
#toc = Hash.new
#current_section_header_number = 0 # used to fake up section header's
pdf = Prawn::Document.new
add_title_page(pdf)
21.times { add_a_content_page(pdf) }
fill_in_toc(pdf)
add_page_numbers(pdf)
pdf.render_file './output/test.pdf'
end
def self.add_title_page(pdf)
pdf.move_down 200
pdf.text "This is my title page", size: 38, style: :bold, align: :center
end
def self.fill_in_toc(pdf)
pdf.go_to_page(1)
number_of_toc_entries_per_page = 10
offset = (#toc.count.to_f / number_of_toc_entries_per_page).ceil
#toc.each_with_index do |(key, value), index|
pdf.start_new_page if index % number_of_toc_entries_per_page == 0
pdf.text "#{value}.... page #{key + offset}", size: 38
end
end
def self.add_a_content_page(pdf)
pdf.start_new_page
toc_heading = grab_some_section_header_text
#toc[pdf.page_count] = toc_heading
pdf.text toc_heading, size: 38, style: :bold
pdf.text "Here is the content for this section"
# randomly span a section over 2 pages
if [true, false].sample
pdf.start_new_page
pdf.text "The content for this section spans 2 pages"
end
end
def self.add_page_numbers(pdf)
page_number_string = 'page <page> of <total>'
options = {
at: [pdf.bounds.right - 175, 9],
width: 150,
align: :right,
size: 10,
page_filter: lambda { |pg| pg > 1 },
start_count_at: 2,
}
pdf.number_pages(page_number_string, options)
end
def self.grab_some_section_header_text
"Section #{#current_section_header_number += 1}"
end
end
I built a report generator featuring a clickable table of contents using code and ideas gathered from this discussion. Here is the relevant parts of the code, in case somebody else needs to do the same.
What it does:
include Prawn::View to use Prawn's methods without having to prefix them with pdf
insert a blank page where the table of contents will be displayed
add the document contents, using h1 and h2 helpers for titles
the h1 and h2 helpers store the position of headings in the document
rewind and generate the actual table of contents
indent subsections in the table of contents
right-align the dots between toc entry and page number for visual consistency
if the table doesn't fit on one page, it adds new pages and increments the relevant page numbers
add a PDF outline with the section and subsection titles for bonus points.
Enjoy!
PDF generator
class ReportPdf
include Prawn::View
COLOR_GRAY = 'BBBBBB' # Color used for the dots in the table of contents
def initialize(report)
#toc = []
#report = report
generate_report
end
private
def generate_report
add_table_of_contents
add_contents
update_table_of_contents
add_outline
end
def add_table_of_contents
# Insert a blank page, which will be filled in later using update_table_of_contents
start_new_page
end
def add_contents
#report.sections.each do |section|
h1(section.title, section.anchor)
section.subsections.each do |subsection|
h2(subsection.title, subsection.anchor)
# subsection contents
end
end
end
def update_table_of_contents
go_to_page(1) # Rewind to where the table needs to be displayed
text 'Table of contents', styles_for(:toc_title)
move_down 20
added_pages = 0
#toc.each do |entry|
unless fits_on_current_page?(entry[:name])
added_pages += 1
start_new_page
end
entry[:page] += added_pages
add_toc_line(entry)
entry[:subsections].each do |subsection_entry|
unless fits_on_current_page?(subsection_entry[:name])
added_pages += 1
start_new_page
end
subsection_entry[:page] += added_pages
add_toc_line(subsection_entry, true)
end
end
end
def add_outline
outline.section 'Table of contents', destination: 2
#toc.each do |entry|
outline.section entry[:name], destination: entry[:page] do
entry[:subsections].each do |subsection|
outline.page title: subsection[:name], destination: subsection[:page]
end
end
end
end
def h1(name, anchor)
add_anchor(anchor, name)
text name, styles_for(:h1)
end
def h2(name, anchor)
add_anchor(anchor, name, true)
text name, styles_for(:h2)
end
def styles_for(element = :p)
case element
when :toc_title then { size: 24, align: :center }
when :h1 then { size: 20, align: :left }
when :h2 then { size: 16, align: :left }
when :p then { size: 12, align: :justify }
end
end
def add_anchor(name, anchor, is_subsection = false)
add_dest anchor, dest_xyz(bounds.absolute_left, y + 20)
if is_subsection
#toc.last[:subsections] << { anchor: anchor, name: name, page: page_count }
else
#toc << { anchor: anchor, name: name, page: page_count, subsections: [] }
end
end
def add_toc_line(entry, is_subsection = false)
anchor = entry[:anchor]
name = entry[:name]
name = "#{Prawn::Text::NBSP * 5}#{name}" if is_subsection
page_number = entry[:page].to_s
dots_info = dots_for(name + ' ' + page_number)
float do
text "<link anchor='#{anchor}'>#{name}</link>", inline_format: true
end
float do
indent(dots_info[:dots_start], dots_info[:right_margin]) do
text "<color rgb='#{COLOR_GRAY}'>#{dots_info[:dots]}</color>", inline_format: true, align: :right
end
end
indent(dots_info[:dots_end]) do
text "<link anchor='#{anchor}'>#{page_number}</link>", inline_format: true, align: :right
end
end
def dots_for(text)
dot_width = text_width('.')
dots_start = text_width(text)
right_margin = text_width(' ') * 6
space_for_dots = bounds.width - dots_start - right_margin
dots = space_for_dots.negative? ? '' : '.' * (space_for_dots / dot_width)
dots_end = space_for_dots - right_margin
{
dots: dots,
dots_start: dots_start,
dots_end: dots_end,
right_margin: right_margin
}
end
def fits_on_current_page?(str)
remaining_height = bounds.top - bounds.absolute_top + y
height_of(str) < remaining_height
end
def text_width(str, size = 12)
font(current_font).compute_width_of(str, size: size)
end
def current_font
#current_font ||= font.inspect.split('<')[1].split(':')[0].strip
end
end
Using the generator
Using Rails, I generate PDFs from a report using the following code:
# app/models/report.rb
class Report < ApplicationRecord
# Additional methods
def pdf
#pdf ||= ReportPdf.new(self)
end
end
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
def show
respond_to do |format|
format.html
format.pdf do
doc = #report.pdf
send_data doc.render, filename: doc.filename, disposition: :inline, type: Mime::Type.lookup_by_extension(:pdf)
end
end
end
In my app, there is a controller with a method for generate images from TrueType and OpenType files, which receives parameters like "color", "arbitrary text", "background".
The problem is when the path file contains white space:
def imagefont
font = Font.find params[:font]
file = File.basename font.file, File.extname(font.file)
fontfile = File.path(Pathname.new("public/downloads/#{font.name.slice(0,1).capitalize}/#{file}/#{font.file}"))
options = {
:max_width => 240,
:text_color => params[:color],
:font_size => 35,
:text => params[:text],
:bg_color => params[:background],
:font => fontfile
}
if File.exists?(options[:font])
canvas = Magick::Image.new 50, 50
image = Magick::Draw.new
begin
image.annotate(canvas, 0, 0, 0, 0, options[:text]) do
image.pointsize = options[:font_size]
image.font = options[:font]
end
metrics = image.get_type_metrics canvas, options[:text]
canvas = Magick::Image.new(metrics.width, metrics.height){ self.background_color = options[:bg_color] }
options[:font_size] -= 1
end while metrics.width > options[:max_width]
image = Magick::Draw.new
image.font options[:font]
image.font_size options[:font_size]
image.fill options[:text_color]
image.text 0, 0, options[:text]
image.gravity = Magick::CenterGravity
image.draw canvas
temp = Tempfile.new([font.file, '.png'])
canvas.write(temp.path)
render :text => open(temp.path).read
end
end
The above code works, unless the font name contains whitespaces. In that case it displays the following error:
Magick::ImageMagickError in FontController#imagefont
non-conforming drawing primitive definition `Blick' # error/draw.c/DrawImage/3146
In this case the font name is "A Blick for All Seasons", so I think it's a problem with quotes. I tried put simple quotes, but I wasn't successful.
I hadn't answers, but I got a possible solution. I found and I have modified the method in the gem files that receives the fontfile parameter. Initially appears thus:
# File lib/RMagick.rb, line 335
def font(name)
primitive "font #{name}"
end
I've changed adding simple quotes and I got it, works fine:
# File lib/RMagick.rb, line 335
def font(name)
primitive "font \'#{name}\'"
end
I think I should send a "pull request" with this change. Unless there is another answer.