RMagick/Imagemagick text-processing too slow for Heroku - ruby-on-rails

Japan has massive gift giving culture and every year we have to print out tons of those "Noshi"s. I made a simply rails program for adding text to a blank noshi image to add to our system (already built in rails).
For reference, basically I wanted to make an open version of this that dosen't have a watermark: www.noshi.jp
Here's what the controller looks like:
def create
#noshi = Noshi.new(noshi_params)
# Set up variables
ntype = #noshi.ntype
omote = #noshi.omotegaki
olength = omote.length
opsize = (168 - (olength * 12))
namae = #noshi.namae
namae2 = #noshi.namae2
# namae3 = #noshi.namae3
# namae4 = #noshi.namae4
# namae5 = #noshi.namae5
replacements = [ ["(株)", "㈱"], ["(有)", "㈲"] ]
replacements.each {|replacement| namae.gsub!(replacement[0], replacement[1])}
replacements.each {|replacement| namae2.gsub!(replacement[0], replacement[1])}
# replacements.each {|replacement| namae3.gsub!(replacement[0], replacement[1])}
# replacements.each {|replacement| namae4.gsub!(replacement[0], replacement[1])}
# replacements.each {|replacement| namae5.gsub!(replacement[0], replacement[1])}
names = []
names += [namae, namae2] # removed namae3, namae4, namae5 for the time being
longest = names.max_by(&:length)
nlength = longest.length
npsize = (144 - (nlength * 12))
i = 0
# Pull Noshi Type
noshi_img = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi#{ntype}.jpg")
# Resize to A4 # 300dpi
noshi_img.resize "2480x3508"
# Iterate through each character
omote.each_char do |c|
# Open new blank/transparent noshi
chars = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi_blank.png")
chars.resize "2480x3508"
# Draw Each Omotegaki Character
chars.combine_options do |d|
d.gravity 'North'
# Placement based on point size
plcmnt = ((opsize / 12 * 12) + (opsize * i * 1.2))
d.draw "text 0,#{plcmnt} '#{c}'"
d.font 'TakaoPMincho'
d.pointsize opsize
d.fill("#000000")
i += 1
end
# Composite each letter as iterated
noshi_img = noshi_img.composite(chars) do |comp|
comp.compose "Over" # OverCompositeOp
comp.geometry "+0+0" # copy second_image onto first_image from (0, 0)
end
end
# Iterator Reset
i = 0
# Draw Name Text (Line 1)
namae.each_char do |c|
# Iterate through each character
# Open new blank/transparent noshi
chars = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi_blank.png")
# Resize to a square so it's easy to flip
chars.resize "2480x3508"
chars.combine_options do |d|
# Middle position for first line so set to 0
xplcmnt = (npsize / 12) * 0
yplcmnt = (625 - npsize) - (npsize * i)
d.gravity 'south'
# Placement based on point size, fix for katakana dash
# positive x is
if c == 'ー'
yplcmnt += 15
d.draw "text 0,#{yplcmnt} '|'"
d.pointsize (npsize * 0.85)
else
d.draw "text 0,#{yplcmnt} '#{c}'"
d.pointsize npsize
end
d.font 'TakaoPMincho'
d.fill("#000000")
i += 1
end
# Composite each letter as iterated
noshi_img = noshi_img.composite(chars) do |comp|
comp.compose "Over" # OverCompositeOp
comp.geometry "+0+0" # copy second_image onto first_image from (0, 0)
end
end
# Iterator Reset
i = 0
# Draw Name Text (Line 2)
namae2.each_char do |c|
# Iterate through each character
# Open new blank/transparent noshi
chars = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi_blank.png")
# Resize to a square so it's easy to flip
chars.resize "2480x3508"
chars.combine_options do |d|
# Next position for second line so set by font size
xplcmnt = (npsize / 6) - npsize * 1.45
yplcmnt = (625 - (npsize * 2)) - (npsize * i)
d.gravity 'south'
# Placement based on point size, fix for katakana dash
if c == 'ー'
yplcmnt += 15
d.draw "text #{xplcmnt},#{yplcmnt} '|'"
d.pointsize (npsize * 0.85)
else
d.draw "text #{xplcmnt},#{yplcmnt} '#{c}'"
d.pointsize npsize
end
d.font 'TakaoPMincho'
d.fill("#000000")
i += 1
end
# Composite each letter as iterated
noshi_img = noshi_img.composite(chars) do |comp|
comp.compose "Over" # OverCompositeOp
comp.geometry "+0+0" # copy second_image onto first_image from (0, 0)
end
end
# Setup and save the file
noshi_img.format "png"
fname = "#{#noshi.omotegaki}_#{#noshi.namae}"
dkey = Time.now.strftime('%Y%m%d%H%M%S')
ext = '.png'
finlname = fname + dkey + ext
noshi_img.write finlname
#noshi.image = File.open(finlname)
File.delete(finlname) if File.exist?(finlname)
respond_to do |format|
if #noshi.save
format.html { redirect_to #noshi, notice: '熨斗が作成されました。' }
format.json { render :show, status: :created, location: #noshi }
else
format.html { render :new }
format.json { render json: #noshi.errors, status: :unprocessable_entity }
end
end end
How it works.
1. User pics a noshi background, selects a noshi header type (for お歳暮 or お祝い or whatever), and inputs a name
2. The app then takes a corresponding file from the gCloud for the noshi background.
3. The app takes each letter and calculates the font size and placement based on the number of total letters and lines.
4. It takes an empty image file and puts each letter onto it's own image and then merges all of them into a final image.
YES it is necessary to make a new image for each letter because as far as I can tell there is no (right-side-up) vertical text format for ImageMagick (a pretty crucial function for a large portion of the planet [China, Japan, Korea] so I find it pretty surprising that it's missing this).
This works fine in development and for our purposes I don't mind it being slow. However, on Heroku this returns an error if it takes over 30 seconds to process, even though the noshi is correctly created every time.
THE QUESTION:
I read that "scale" instead of "resize" may help, but looking at my code I feel like there has to be a more efficient way to do what I've done here. I tried using base images with smaller file sizes, this didn't help much.
Is there a more efficient way to do this?
If not, is there a way to send the user somewhere to wait while the noshi completes so it doesn't return an error every time?
UPDATE:
Just coming back to show the working Ruby on Rails controller I ended up with:
def create
#noshi = Noshi.new(noshi_params)
# Set up variables
ntype = #noshi.ntype
omote = #noshi.omotegaki
omote_length = omote.length
omote_point_size = (168 - (omote_length * 12))
#make an array with each of the name lines entered
name_array = Array.new
name_array << #noshi.namae
name_array << #noshi.namae2
name_array << #noshi.namae3
name_array << #noshi.namae4
name_array << #noshi.namae5
#replace multi-character prefixes with their single charcter versions
#replace katakana dash with capital I
#insert line breaks after each letter for Japanese vertical type
name_array.each do |namae|
replacements = [ ["(株)", "㈱"], ["(有)", "㈲"], ["ー", "|"] ]
replacements.each {|replacement| namae.gsub!(replacement[0], replacement[1])}
end
def add_line_breaks(string)
string.scan(/.{1}/).join("\n")
end
name_array.map!{ |namae| add_line_breaks(namae)}
#add line breaks after each character for the omote as well
omote = add_line_breaks(omote)
#find the longest string (important: after the character concatenation) in the name array to calculate the point size for the names section
name_array_max_length = (name_array.map { |namae| namae.length }).max
name_point_size = (204 - (name_array_max_length * 10))
#max omote size is 156, and the name needs to be an order smaller than that by default.
if name_point_size > 108
name_point_size = 108
end
# Pull Noshi Type
noshi_img = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi#{ntype}.jpg")
# Resize to A4 # 300dpi
noshi_img.resize "2480x3508"
#create the overlay image
name_overlay = MiniMagick::Image.open("#{ENV['GBUCKET_PREFIX']}noshi/noshi_blank.png")
name_overlay.resize "2480x3508"
#first time for omote
name_overlay.combine_options do |image|
image.gravity 'North'
# Placement based on point size
omote_placement_y = (348 - (omote_length * (omote_point_size / 2)))
image.font 'TakaoPMincho'
image.pointsize omote_point_size
image.fill("#000000")
image.draw "text 0,#{omote_placement_y} '#{omote}'"
end
#count number of names in array, add a name for each time
name_array.count.times do |i|
name_overlay.combine_options do |image|
image.gravity 'North'
# Placement based on point size and iteration
name_placement_x = (0 - i * name_point_size)
name_placement_y = 1150 + ((i * name_point_size) - (name_point_size / 2))
image.font 'TakaoPMincho'
image.pointsize name_point_size
image.fill("#000000")
image.draw "text #{name_placement_x},#{name_placement_y} '#{name_array[i]}'"
end
end
noshi_img = noshi_img.composite(name_overlay) do |comp|
comp.compose "Over" #OverCompositeOp
comp.geometry "+0+0" #copy second_image onto first_image from (0, 0)
end
# Setup and save the file
noshi_img.format "png"
#name the file
fname = "#{#noshi.omotegaki}_#{#noshi.namae}"
dkey = Time.now.strftime('%Y%m%d%H%M%S')
ext = '.png'
final_name = fname + dkey + ext
#write a temporary version
noshi_img.write final_name
#write/stream the file to the uploader
#noshi.image = File.open(final_name)
#delete the original temporary
File.delete(final_name) if File.exist?(final_name)
respond_to do |format|
if #noshi.save
format.html { redirect_to #noshi, notice: '熨斗が作成されました。' }
format.json { render :show, status: :created, location: #noshi }
else
format.html { render :new }
format.json { render json: #noshi.errors, status: :unprocessable_entity }
end
end
end

YES it is necessary to make a new image for each letter because as far
as I can tell there is no (right-side-up) vertical text format for
ImageMagick
In ImageMagick command line, you can create a vertically aligned text string image by placing line feeds after each character.
convert -background white -fill black -pointsize 18 -font arial -gravity center label:"t\ne\ns\nt\ni\nn\ng" result.png
Does this help you? Or is that not practical?

Related

Rails Helper to display rating in stars

Using Rails 6. Here's a piece that I wrote just to display number of stars. Obviously I am disgusted by my own code. How would you refactor?
# application_helper.rb
module ApplicationHelper
def show_star_rating(rating)
zero_star_icon_name = "star"
full_star_icon_name = "star_fill"
half_star_icon_name = "star_lefthalf_fill"
total_stars = []
round_by_half = (rating * 2).round / 2.0
(round_by_half.to_i).times { total_stars << full_star_icon_name }
if round_by_half - round_by_half.to_i == 0.5
total_stars << half_star_icon_name
end
if total_stars.size != 5
(5 - total_stars.size).times { total_stars << zero_star_icon_name }
end
total_stars
end
end
# show.html.erb
<% show_star_rating(agent_review.rating).each do |star| %>
<i class="f7-icons"><%= star %></i>
<% end %>
You can make use of the Array.new, passing in the maximum number of stars you want to show, and defaulting all the stars to empty. Then, you can fill in the number of full stars you need. Then, finally, thanks to Numeric's divmod returning either 0 or 1 for the number of half stars you need, you make one more pass and fill in the number of half stars you need:
module StarHelper
EMPTY_STAR_ICON = 'star'.freeze
FULL_STAR_ICON = 'star_fill'.freeze
HALF_STAR_ICON = 'star_lefthalf_fill'.freeze
def full_and_half_star_count(rating)
(rating * 2).round.divmod(2)
end
def stars(rating, max_stars: 5)
full_stars, half_stars = full_and_half_star_count(rating)
Array.new(max_stars, EMPTY_STAR_ICON).
fill(FULL_STAR_ICON, 0, full_stars).
fill(HALF_STAR_ICON, full_stars, half_stars)
end
end
The way I would implement show_star_rating:
def show_star_rating(rating)
zero_star_icon_name = "star"
full_star_icon_name = "star_fill"
half_star_icon_name = "star_lefthalf_fill"
rating_round_point5 = (rating * 2).round / 2.0
(1..5).map do |i|
next(full_star_icon_name) if i <= rating_round_point5
next(half_star_icon_name) if rating_round_point5 + 0.5 == i
zero_star_icon_name
end
end

Finding letters that are near, exact or not in a user input string

I am currently developing a small modified version of Hangman in Rails for children. The game starts by randomly generating a word from a text file and the user has to guess the word by entering a four letter word. Each word is the split by each character for example "r", "e", "a", "l" and returns a message on how they are to the word.
Random Generated word is "real"
Input
rlax
Output
Correct, Close, Correct, Incorrect
I have tried other things which I have found online but haven't worked and I am fairly new to Ruby and Rails. Hopefully someone can guide me in the right direction.
Here is some code
def letterCheck(lookAtLetter)
lookAHead = lookAtLetter =~ /[[:alpha:]]/
end
def displayWord
$ranWordBool.each_index do |i|
if($ranWordBool[i])
print $ranWordArray[i]
$isWin += 1
else
print "_"
end
end
end
def gameLoop
turns = 10
turnsLeft = 0
lettersUsed = []
while(turnsLeft < turns)
$isWin = 0
displayWord
if($isWin == $ranWordBool.length)
system "cls"
puts "1: Quit"
puts "The word is #{$ranWord} and You Win"
puts "Press any key to continue"
return
end
print "\n" + "Words Used: "
lettersUsed.each_index do |looper|
print " #{lettersUsed[looper]} "
end
puts "\n" + "Turns left: #{turns - turnsLeft}"
puts "Enter a word"
input = gets.chomp
system "cls"
if(input.length != 4)
puts "Please enter 4 lettered word"
elsif(letterCheck(input))
if(lettersUsed.include?(input))
puts "#{input} already choosen"
elsif($ranWordArray.include?(input))
puts "Close"
$ranWordArray.each_index do |i|
if(input == $ranWordArray[i])
$ranWordBool[i] = true
end
if($ranWordBool[i] = true)
puts "Correct"
else
puts "Incorrect"
end
end
else
lettersUsed << input
turnsLeft += 1
end
else
puts "Not a letter"
end
end
puts "You lose"
puts "The word was #{$ranWord}"
puts "Press any key to continue"
end
words = []
File.foreach('words.txt') do |line|
words << line.chomp
end
while(true)
$ranWord = words[rand(words.length) + 1]
$ranWordArray = $ranWord.chars
$ranWordBool = []
$ranWordArray.each_index do |i|
$ranWordBool[i] = false
end
system "cls"
gameLoop
input = gets.chomp
shouldQuit(input)
end
Something like that:
# Picking random word to guess
word = ['open', 'real', 'hang', 'mice'].sample
loop do
puts "So, guess the word:"
input_word = gets.strip
if word == input_word
puts("You are right, the word is: #{input_word}")
break
end
puts "You typed: #{input_word}"
# Split both the word to guess and the suggested word into array of letters
word_in_letters = word.split('')
input_in_letters = input_word.split('')
result = []
# Iterate over each letter in the word to guess
word_in_letters.each_with_index do |letter, index|
# Pick the corresponding letter in the entered word
letter_from_input = input_in_letters[index]
if letter == letter_from_input
result << "#{letter_from_input} - Correct"
next
end
# Take nearby letters by nearby indexes
# `reject` is here to skip negative indexes
# ie: letter 'i' in a word "mice"
# this will return 'm' and 'c'
# ie: letter 'm' in a word "mice"
# this will return 'i'
letters_around =
[index - 1, index + 1]
.reject { |i| i < 0 }
.map { |i| word_in_letters[i] }
if letters_around.include?(letter_from_input)
result << "#{letter_from_input} - Close"
next
end
result << "#{letter_from_input} - Incorrect"
end
puts result.join("\n")
end

How to compare two images in rails3

How to compare images from my local directory with the downloaded image. Comparison showld be based on image content and size.
How to do this in ruby?
There are several ways to do this.
1) For my opinion the best way is using Rmagick signature(thanks to this useful manual):
require 'RMagick'
new_image = Magick::Image.read(new_photo)[0] ## new_photo = "/your/dir/file.jpg"
expected_image = Magick::Image.read(expected_photo)[0]
new_image.signature.should eql expected_image.signature
or
diff_img, diff_metric = img1[0].compare_channel( img2[0], Magick::MeanSquaredErrorMetric )
2) You also can use raster_graphics library (rosettacode.org):
require 'raster_graphics'
class RGBColour
# the difference between two colours
def -(a_colour)
(#red - a_colour.red).abs +
(#green - a_colour.green).abs +
(#blue - a_colour.blue).abs
end
end
class Pixmap
# the difference between two images
def -(a_pixmap)
if #width != a_pixmap.width or #height != a_pixmap.height
raise ArgumentError, "can't compare images with different sizes"
end
sum = 0
each_pixel {|x,y| sum += self[x,y] - a_pixmap[x,y]}
Float(sum) / (#width * #height * 255 * 3)
end
end
lenna50 = Pixmap.open_from_jpeg('Lenna50.jpg')
lenna100 = Pixmap.open_from_jpeg('Lenna100.jpg')
puts "difference: %.5f%%" % (100.0 * (lenna50 - lenna100))
#=>:
#difference: 1.62559%

Prawn: Table of content with page numbers

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

Ruby/Rails: Determine variables from plain text to update via form

I'm creating an app where users can edit their own CSS (in SCSS syntax). That works fine, however, I eventually want these CSS files to be "programmable" so that users that don't know CSS can still edit them in a basic manner. How?
If I can mark certain things as editable, I don't have to make an impossible database schema. For example I have a scss file called style.scss:
// #type color
$header_bg_color: #555;
// #type image
$header_image: "http://someurl.com/image.jpg";
Then I can do this:
SomeParser.parse(contents of style.scss here)
This will return a hash or something similar of variables:
{:header_bg_color => {:type => "color", :value => "#555"}, :header_image => {:type => "image", :value => "http://someurl.com/image.jpg"} }
I can use the above hash to create a form which the novice user can use to change the data and submit. I believe I know how to do the GET and POST part.
What would be the best way to create / configure my own parser so that I could read the comments and extract the "variables" from this? And then, update the text file easily again?
Another possible way is something like this:
o = SomeParser.new(contents of style.scss here)
o.header_bg_color #returns "#555"
o.header_image = "http://anotherurl.com/image2.jpg" # "updates" or replaces the old header image variable with the new one
o.render # returns the text with the new values
Thanks in advance!
I haven't used it thoroughly, but my tests pass. I think it's enough to get the idea :) It took me several hours of study, then several more to implement it.
Btw, I did not do any optimization here. For me, it doesn't need to be quick
Look at my spec file:
require 'spec_helper'
describe StyleParser do
describe "given properly formatted input" do
it "should set and return variables properly" do
text = %{# #name Masthead Background Image
# #kind file
# #description Background image.
$mbc2: "http://someurl.com/image.jpg";
# #name Masthead BG Color
# #kind color
# #description Background color.
$mbc: #555;}
#s = StyleParser.new(text)
#s.mbc.name.should == "Masthead BG Color"
#s.mbc.kind.should == "color"
#s.mbc.description.should == "Background color."
#s.mbc.value.should == "#555"
#s.mbc2.name.should == "Masthead Background Image"
#s.mbc2.kind.should == "file"
#s.mbc2.description.should == "Background image."
#s.mbc2.value.should == %Q("http://someurl.com/image.jpg")
end
end
describe "when assigning values" do
it "should update its values" do
text = %{# #name Masthead Background Image
# #kind file
# #description Background image.
$mbc2: "http://someurl.com/image.jpg";}
#s = StyleParser.new(text)
#s.mbc2.value = %Q("Another URL")
#s.mbc2.value.should == %Q("Another URL")
rendered_text = #s.render
rendered_text.should_not match(/http:\/\/someurl\.com\/image\.jpg/)
rendered_text.should match(/\$mbc2: "Another URL";/)
#s.mbc2.value = %Q("Some third URL")
#s.mbc2.value.should == %Q("Some third URL")
rendered_text = #s.render
rendered_text.should_not match(/\$mbc2: "Another URL";/)
rendered_text.should match(/\$mbc2: "Some third URL";/)
end
it "should render the correct values" do
text_old = %{# #name Masthead Background Image
# #kind file
# #description Background image.
$mbc2: "http://someurl.com/image.jpg";}
text_new = %{# #name Masthead Background Image
# #kind file
# #description Background image.
$mbc2: "Another URL";}
#s = StyleParser.new(text_old)
#s.mbc2.value = %Q("Another URL")
#s.render.should == text_new
end
end
end
Then the following 2 files:
# Used to parse through an scss stylesheet to make editing of that stylesheet simpler
# Ex. Given a file called style.scss
#
# // #name Masthead Background Color
# // #type color
# // #description Background color of the masthead.
# $masthead_bg_color: #444;
#
# sp = StyleParser.new(contents of style.scss)
#
# # Reading
# sp.masthead_bg_color.value # returns "#444"
# sp.masthead_bg_color.name # returns "Masthead Background Color"
# sp.masthead_bg_color.type # returns "color"
# sp.masthead_bg_color.description # returns "Background color of the masthead."
#
# # Writing
# sp.masthead_bg_color.value = "#555"
# sp.render # returns all the text above except masthead_bg_color is now #555;
class StyleParser
def initialize(text)
#text = text
#variables = {}
#eol = '\n'
#context_lines = 3
#context = "((?:.*#{#eol}){#{#context_lines}})"
end
# Works this way: http://rubular.com/r/jWSYvfVrjj
# Derived from http://stackoverflow.com/questions/2760759/ruby-equivalent-to-grep-c-5-to-get-context-of-lines-around-the-match
def get_context(s)
regexp = /.*\${1}#{s}:.*;[#{#eol}]*/
#text =~ /^#{#context}(#{regexp})/
before, match = $1, $2
"#{before}#{match}"
end
def render
#variables.each do |key, var|
#text.gsub!(/^\$#{key}: .+;/, %Q($#{key}: #{var.value};))
end
#text
end
def method_missing(method_name)
if method_name.to_s =~ /[\w]+/
context = get_context(method_name)
#variables[method_name] ||= StyleVariable.new(method_name, context)
end
end
end
class StyleVariable
METADATA = %w(name kind description)
def initialize(var, text)
#var = var
#text = text
end
def method_missing(method_name)
if METADATA.include? method_name.to_s
content_of(method_name.to_s)
end
end
def value
#text.each do |string|
string =~ /^\${1}#{#var}: (.+);/
return $1 if $1
end
end
def value=(val)
#text.gsub!(/^\$#{#var}: .+;/, "$#{#var}: #{val};")
end
private
def content_of(variable)
#text.each do |string|
string =~ /^# #([\w]+[^\s]) (.+)/
return $2 if $1 == variable
end
end
end

Resources