Search code examples
ruby-on-railsrubyherokuimagemagickrmagick

RMagick/Imagemagick text-processing too slow for Heroku


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

Solution

  • 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
    


    enter image description here

    Does this help you? Or is that not practical?