Prawn/PDF: Multiple formats/text on one line? - ruby-on-rails

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)

Related

Is it possible to modify an existing style object while using axlsx in Ruby on Rails?

I am using axlsx gem to generate Excel sheets.
I have multiple styles in my Excel. One example is shown below
style1 = wb.styles.add_style(:font_name => "Arial", :sz => 10, :i => true, :fg_color => "A6A6A6")
Now, I need to write a function which will take this style (and a currency value) as a parameter. This function should just modify the fg_color (font color) to red if the currency value is negative (all the other stylings like background color, font size, italics etc should remain the same) and return the modified style.
Is it possible to achieve the same?
def get_currency_style(style, currency_value)
if currency_value < 0
new_style = <modify ONLY the font color to red in 'style' object>
else
new_style = style
end
return new_style
end
Let your style become a hash, and modify it.
def get_currency_style(style, currency_value)
if currency_value < 0
style[:fb_color] = 'red'
end
return style
end
style1 = wb.styles.add_style(get_currency_style({ font_name: "Arial", sz: 10, i: true, fg_color: "A6A6A6" }, -10))

Rails prawn chart not rendered completely at the bottom of the page

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

Rails: adding to last object if conditions met

I've got a method that scans an HTML string and sort of formats it for prawnpdf:
def format_for_prawn(pdf, string, colour)
body = Nokogiri::HTML::DocumentFragment.parse(string)
result = body.xpath('./*|./text()')
result.each do |breaker|
if breaker.name == "h3"
pdf.fill_color colour
pdf.text breaker.text.to_s, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
pdf.text breaker.text.to_s, :size => 10, :leading => 1
pdf.move_down 10
end
end
end
It works great for <h3>s. In the event that some mid-paragraph <b> (or similar) tags are found it starts a new paragraph because that's where Nokogiri broke the string--which is the correct behaviour.
How could I add the bolded string to the last pdf.text function instead of calling a new pdf.text which results in a new paragraph?
I thought about making an array out of it all but then it'll be out of order with the <h3>s.
Any help would be appreciated.
My first thought was to do a negative match :
body.xpath( './node()[not(self::b)]' )
Sadly, this would exclude <b> rather than ignoring it :
> body = Nokogiri::HTML::DocumentFragment.parse %(<h3><b>foo</b></h3><h3>bar</h3>fooz<b>baz</b>whatever); true
> body.xpath( './node()[not(self::b)]' ).to_a
[
[0] <h3>
<b>foo</b>
</h3>,
[1] <h3>bar</h3>,
[2] fooz,
[3] whatever
]
So, you'll have no choice but using a buffer, here : we can iterate through nodes first, to populate a buffer regarding if we should have a new line or not, then iterate this buffer to have your lines added to pdf :
buffer = []
body.xpath( './node()' ).each do |node|
if %w[text b].include? node.name
# add to previous line or create one
buffer << [] unless buffer.count
buffer.last << { node: node }
else
# set content and create a new line
buffer << [ { node: node, title: node.name == 'h3' } ]
buffer << []
end
end
# Now, each first level item in buffer is a line,
# containing elements we just have to concatenate text of
# to pass to `pdf#text`
buffer.each do |line|
text = line.map do |part|
node = part[ :node ]
inner = node.text.to_s
# restore <b> tag if you want bold style in pdf
node.name == 'b' ? "<b>#{inner}</b>" : inner
end.join
if line.first
if line.first[ :title ]
pdf.fill_color colour
pdf.text text, :size => 16
pdf.move_down 5
else
pdf.fill_color '#444444'
# inline_format ensure basic html formating is used, <b> in our case
# See http://prawn.majesticseacreature.com/docs/0.11.1/Prawn/Text.html#method-i-text
pdf.text text, size: 10, leading: 1, inline_format: true
pdf.move_down 10
end
end
end
Of course, all of this is considering you do not control original html. Else, you should place your text nodes inside <p> or something, and there would not be problems anymore.

How can I generate fonts images using Rmagick?

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.

Rails PDF Prawn Headers and Footers

Is there a simpler code to set the Headers and Footers on all N pages in a PDF file that is generated by PRAWN Plugin.
the codes are to be written in a
"report.pdf.prawn" file, which is a direct coding area of the PDF view page
The below are the codes in the file report.pdf.prawn
pdf.text "Creation Date " + Time.now.strftime('%d-%m-%Y')
pdf.text "Page Count:#{pdf.page_count}"
All i want is to position these values to headers or/and footers left and right side.
Any suggestion would be helpful.
Thank you so much for your concern.I got wat i want from the link
This is what i did to make it work..
creation_date = Time.now.strftime('%d-%m-%Y')
page_count=pdf.page_count
powered_by = #companylogo
# will get #companylogo from the controller
Prawn::Document.generate("show_sd_report_pdf.pdf", :skip_page_creation => true) do
pdf.page_count.times do |i|
pdf.go_to_page(i+1)
pdf.draw_text "Creation Date : " + creation_date, :at=>[590,530]
pdf.draw_text "Page Count : #{i+1} / #{pdf.page_count}", :at=>[1,1]
pdf.draw_text "Powered by : ", :at=>[590,1]
pdf.image powered_by, :width => 25, :at=>[670,15]
end
end
**or insted of draw_text**
pdf.bounding_box([1,540], :width => 300, :height => 300) do
pdf.text "Username : " + user_name
end

Resources