module OBF::PDF

Constants

NEPALI_ALPHABET
RTL_SCRIPTS

Public Class Methods

build_page(pdf, obj, options) click to toggle source

b.generate_download(‘1_2’, ‘pdf’, {‘include’ => ‘all’, ‘headerless’ => true, ‘symbol_background’ => ‘transparent’})

# File lib/obf/pdf.rb, line 155
def self.build_page(pdf, obj, options)
  OBF::Utils.as_progress_percent(0, 1.0) do
    doc_width = 11*72 - 72
    doc_height = 8.5*72 - 72
    default_radius = 3
    text_height = 20
    header_height = 0
    page_num = 0
  
    if options['pages']
      page_num = options['pages'][obj['id']].to_i
      pdf.add_dest("page#{page_num}", pdf.dest_fit) if options['links']
    end
    pdf.font('Arial') do
      # header
      if !options['headerless']
        header_height = 80
        pdf.bounding_box([0, doc_height], :width => doc_width, :height => header_height) do
          pdf.line_width = 2
          pdf.font_size 16
          pdf.fill_color "eeeeee"
          pdf.stroke_color "888888"
            include_back = options['pages'] && page_num != 1 && page_num != 0
            # Go Back
            if include_back
              OBF::Utils.log "  board backlinks #{obj['id']} #{options['backlinks'].to_json}"
              options['backlinks'] ||= []
              if options['backlinks'].length > 0
                x = 110
                pdf.fill_and_stroke_rounded_rectangle [x, header_height], 100, header_height, default_radius
                pdf.fill_color "6D81D1"
                pdf.fill_and_stroke_polygon([x + 5, 45], [x + 35, 70], [x + 35, 60], [x + 95, 60], [x + 95, 30], [x + 35, 30], [x + 35, 20])
                pdf.fill_color "666666"
                text_options = {:text => "Go Back"}
                text_options[:anchor] = "page1" if options['links']
                pdf.formatted_text_box [text_options], :at => [x + 10, header_height], :width => 80, :height => 80, :align => :center, :valign => :bottom, :overflow => :shrink_to_fit
                if options['backlinks'].length <= 3
                  backlinks = (options['backlinks'] || []).join(',')
                  pdf.fill_color "ffffff"
                  pdf.formatted_text_box [{:text => backlinks}], :at => [x + 20, header_height + 5 - 25], :width => 70, :height => 30, :align => :center, :valign => :center, :overflow => :shrink_to_fit
                end
              end
            end

            # Say it Out Loud
            pdf.fill_color "ffffff"
            shift = include_back ? 0 : 55
            offset = include_back ? 110 : 55
            box_shift = include_back ? 110 : 0
            
            pdf.fill_and_stroke_rounded_rectangle [110 + box_shift, header_height], 170 + shift + shift, header_height, default_radius
            pdf.fill_color "DDDB54"
            pdf.fill_and_stroke do
              pdf.move_to 160 + offset, 40
              pdf.line_to 190 + offset, 55
              pdf.curve_to [125 + offset, 40], :bounds => [[180 + offset, 80], [125 + offset, 80]]
              pdf.curve_to [190 + offset, 25], :bounds => [[125 + offset, 0], [180 + offset, 0]]
              pdf.line_to 160 + offset, 40
            end
            pdf.fill_color "444444"
            pdf.text_box "Say that out loud for me", :at => [210 + offset, header_height], :width => 60, :height => 80, :align => :center, :valign => :center, :overflow => :shrink_to_fit

            # Start Over
            x = doc_width
            pdf.fill_color "eeeeee"
            pdf.fill_and_stroke_rounded_rectangle [(doc_width - x), header_height], 100, header_height, default_radius
            pdf.fill_color "5c9c6d"
            pdf.stroke_color "25783b"
            pdf.fill_and_stroke_polygon([doc_width - x + 50, 75], [doc_width - x + 80, 50], [doc_width - x + 80, 20], [doc_width - x + 20, 20], [doc_width - x + 20, 50])
            pdf.stroke_color "888888"
            pdf.fill_color "666666"
            pdf.text_box "Start Over", :styles => [:bold], :at => [(doc_width - x + 10), header_height], :width => 80, :height => 80, :align => :center, :valign => :bottom, :overflow => :shrink_to_fit

            # Oops
            x = 210
            pdf.fill_color "eeeeee"
            pdf.fill_and_stroke_rounded_rectangle [(doc_width - x), header_height], 100, header_height, default_radius
            pdf.fill_color "6653a6"
            pdf.stroke_color "554a78"
            pdf.fill_and_stroke_polygon([doc_width - x + 50 - 7, 75], [doc_width - x + 50 + 7, 75], [doc_width - x + 50 + 7, 40], [doc_width - x + 50 - 7, 40])
            pdf.fill_and_stroke_polygon([doc_width - x + 50 - 7, 33], [doc_width - x + 50 + 7, 33], [doc_width - x + 50 + 7, 20], [doc_width - x + 50 - 7, 20])
            pdf.stroke_color "888888"
            pdf.fill_color "666666"
            pdf.text_box "Oops", :at => [(doc_width - x + 10), header_height], :width => 80, :height => 80, :align => :center, :valign => :bottom, :overflow => :shrink_to_fit

            # Stop
            x = 320
            pdf.fill_color "eeeeee"
            pdf.fill_and_stroke_rounded_rectangle [(doc_width - x), header_height], 100, header_height, default_radius
            pdf.fill_color "944747"
            pdf.stroke_color "693636"
            pdf.fill_and_stroke_polygon([doc_width - x + 39, 70], [doc_width - x + 61, 70], [doc_width - x + 75, 56], [doc_width - x + 75, 34], [doc_width - x + 61, 20], [doc_width - x + 39, 20], [doc_width - x + 25, 34], [doc_width - x + 25, 56])
            pdf.stroke_color "888888"
            pdf.fill_color "666666"
            pdf.text_box "Stop", :at => [(doc_width - x + 10), header_height], :width => 80, :height => 80, :align => :center, :valign => :bottom, :overflow => :shrink_to_fit
            
            # Clear
            x = 100
            pdf.fill_color "eeeeee"
            pdf.fill_and_stroke_rounded_rectangle [(doc_width - x), header_height], 100, header_height, default_radius
            pdf.stroke_color "666666"
            pdf.fill_color "888888"
            pdf.fill_and_stroke_polygon([doc_width - x + 10, 45], [doc_width - x + 35, 70], [doc_width - x + 90, 70], [doc_width - x + 90, 20], [doc_width - x + 35, 20])
            pdf.stroke_color "888888"
            pdf.fill_color "666666"
            pdf.text_box "Clear", :at => [(doc_width - x + 10), header_height], :width => 80, :height => 80, :align => :center, :valign => :bottom, :overflow => :shrink_to_fit
        end
      end
      # footer
      pdf.fill_color "bbbbbb"
      obj['name'] = nil if obj['name'] == 'Unnamed Board'
      if OBF::PDF.footer_text || obj['name']
        text = [obj['name'], OBF::PDF.footer_text].compact.join(', ')
        offset = options['pages'] ? 400 : 300
        pdf.formatted_text_box [{:text => text, :link => OBF::PDF.footer_url}], :at => [doc_width - offset, text_height], :width => 300, :height => text_height, :align => :right, :valign => :center, :overflow => :shrink_to_fit
      end
      pdf.fill_color "000000"
      if options['pages'] && page_num != 0
        text_options = {:text => page_num.to_s}
        text_options[:anchor] = "page1" if options['links']
        pdf.formatted_text_box [text_options], :at => [doc_width - 100, text_height], :width => 100, :height => text_height, :align => :right, :valign => :center, :overflow => :shrink_to_fit
      end
    end
  
    # board
    pdf.font(options['font'])
    pdf.font_size 12
    padding = 10
    grid_height = doc_height - header_height - text_height - (padding * 2)
    grid_width = doc_width
    if obj['grid'] && obj['grid']['rows'] > 0 && obj['grid']['columns'] > 0
      button_height = (grid_height - (padding * (obj['grid']['rows'] - 1))) / obj['grid']['rows'].to_f
      button_width = (grid_width - (padding * (obj['grid']['columns'] - 1))) / obj['grid']['columns'].to_f

      # Grab all the images per board in parallel
      OBF::Utils.log "  batch-retrieving remote images"
      hydra = OBF::Utils.hydra
      grabs = []
      obj['buttons'].each do |btn|
        image = (obj['images_hash'] || {})[btn['image_id']]
        if image && image['url'] && !image['data'] && !(image['path'] && options['zipper'])
          # download the raw data from the remote URL
          url = image['url']
          res = OBF::Utils.get_url(url, true)
          if res['request']
            hydra.queue(res['request'])
            grabs << {url: url, res: res, req: res['request'], image: image, fill: btn['background_color'] ? OBF::Utils.fix_color(btn['background_color'], 'hex') : "ffffff"}
          end
        elsif image && (image['data'] || (image['path'] && options['zipper']))
          # process the data-uri or zipped image
          grabs << {image: image, fill: btn['background_color'] ? OBF::Utils.fix_color(btn['background_color'], 'hex') : "ffffff"}
        end
      end
      hydra.run
      blocks = []
      block = nil
      grabs.each do |grab|
        # prevent too many svg converts from happening at the same time
        block = block || {grabs: []}
        block[:grabs] << grab
        grab[:svg] = true if grab[:image] && grab[:image]['content_type'] && grab[:image]['content_type'].match(/svg/)
        grab[:svg] = true if grab[:res] && grab[:res]['content_type'] && grab[:res]['content_type'].match(/svg/)
        if block[:grabs].length > 20 || block[:grabs].select{|g| g[:svg] }.length > 3
          blocks << block
          block = nil
        end
      end
      blocks << block if block
      # OBF::Utils.log("  final block #{block.to_json}")
      blocks.each_with_index do |block, idx|
        threads = []
        OBF::Utils.log("   block #{idx}")
        block[:grabs].each do |grab|
          if grab[:res] && grab[:res]['data']
            grab[:image]['raw_data'] = grab[:res]['data']
            grab[:image]['content_type'] ||= grab[:res]['content_type']
            grab[:image]['extension'] ||= grab[:res]['extension']
          end
          grab[:image]['threadable'] = true
          bg = 'white'
          if options['transparent_background'] || options['symbol_background'] == 'transparent'
            bg = "\##{grab[:fill]}"
          elsif options['symbol_background'] == 'black'
            bg = 'black'
          end
          OBF::Utils.log("    img")
          res = OBF::Utils.save_image(grab[:image], options['zipper'], bg)
          threads << res if res && !res.is_a?(String)
        end
        threads.each{|t| t[:thread].join }
      end
      grabs.each do |grab|
        if grab[:image]
          grab[:image].delete('threadable')
          grab[:image].delete('local_path') unless grab[:image]['local_path'] && File.exist?(grab[:image]['local_path'])
        end
      end
      OBF::Utils.log "  done with #{grabs.length} remote images!"

      obj['grid']['order'].each_with_index do |buttons, row|
        buttons.each_with_index do |button_id, col|
          button = obj['buttons'].detect{|b| b['id'] == button_id }
          blank_button = (!button || button['hidden'] == true)
          next if options['skip_blank'] && blank_button
          x = (padding * col) + (col * button_width)
          y = text_height + padding - (padding * row) + grid_height - (row * button_height)
          pdf.bounding_box([x, y], :width => button_width, :height => button_height) do
            fill = "ffffff"
            border = "eeeeee"
            if !blank_button && button['background_color']
              fill = OBF::Utils.fix_color(button['background_color'], 'hex')
            end   
            if !blank_button && button['border_color']
              border = OBF::Utils.fix_color(button['border_color'], 'hex')
            end         
            pdf.fill_color fill
            pdf.stroke_color border
            pdf.fill_and_stroke_rounded_rectangle [0, button_height], button_width, button_height, default_radius
            if !blank_button
              vertical = options['text_on_top'] ? button_height - text_height : button_height - 5

              text = (button['label'] || button['vocalization']).to_s
              font = options['font']
              # Nepali text isn't working as a fallback for some reason, it says "bad font family"
              if text.match(Regexp.new("[" + NEPALI_ALPHABET + "]"))
                font = File.expand_path('../../MiedingerBook.ttf', __FILE__)
              end
              pdf.font(font)
              direction = text.match(rtl_regex) ? :rtl : :ltr
              if options['text_case'] == 'upper'
                text = text.upcase
              elsif options['text_case'] == 'lower'
                text = text.downcase
              end
              text_color = OBF::Utils.fix_color(fill, 'contrast')
              
              if options['text_only']
                # render text
                pdf.fill_color text_color
                pdf.text_box text, :at => [0, 0], :width => button_width, :height => button_height, :align => :center, :valign => :center, :overflow => :shrink_to_fit, :direction => direction
              else
                # render image
                pdf.bounding_box([5, vertical], :width => button_width - 10, :height => button_height - text_height - 5) do
                  image = (obj['images_hash'] || {})[button['image_id']]
                  if image
                    bg = 'white'
                    if options['transparent_background'] || options['symbol_background'] == 'transparent'
                      bg = "\##{fill}"
                    elsif options['symbol_background'] == 'black'
                      bg = 'black'
                    end
                    image['threadable'] = false
                    image_local_path = image['local_path'] if image && image['local_path'] && File.exist?(image['local_path'])
                    image_local_path ||= image && OBF::Utils.save_image(image, options['zipper'], bg)
                    if image_local_path && File.exist?(image_local_path)
                      pdf.image(image_local_path, :fit => [button_width - 10, button_height - text_height - 5], :position => :center, :vposition => :center) rescue nil
                      File.unlink image_local_path
                    else
                      OBF::Utils.log("  missing image #{image['id']} #{image_local_path}")
                    end
                  end
                end
                if options['pages'] && button['load_board']
                  page = options['pages'][button['load_board']['id']]
                  if page
                    page_vertical = options['text_on_top'] ? -2 + text_height : button_height + 2
                    pdf.fill_color "ffffff"            
                    pdf.stroke_color "eeeeee"            
                    pdf.fill_and_stroke_rounded_rectangle [button_width - 18, page_vertical], 20, text_height, 5
                    pdf.fill_color text_color
                    text_options = {:text => page}
                    text_options[:anchor] = "page#{page}" if options['links']
                    pdf.formatted_text_box [text_options], :at => [button_width - 18, page_vertical], :width => 20, :height => text_height, :align => :center, :valign => :center
                  end
                end
              
                # render text
                pdf.fill_color text_color
                vertical = options['text_on_top'] ? button_height : text_height
                pdf.text_box text, :at => [0, vertical], :width => button_width, :height => text_height, :align => :center, :valign => :center, :overflow => :shrink_to_fit, :direction => direction
              end
            end
          end
          index = col + (row * obj['grid']['columns'])
          OBF::Utils.update_current_progress(index.to_f / (obj['grid']['rows'] * obj['grid']['columns']).to_f, "updated button #{button_id}")
        end
      end
    end
  end
end
build_pdf(obj, dest_path, zipper, opts={}) click to toggle source
# File lib/obf/pdf.rb, line 68
def self.build_pdf(obj, dest_path, zipper, opts={})
  # parse obf, draw as pdf
  doc_opts = {
    :page_layout => :landscape, 
    :page_size => [8.5*72, 11*72],
    :info => {
      :Title => obj['name']
    }
  }
  pdf = Prawn::Document.new(doc_opts)
  default_font = load_fonts(pdf, opts)

  multi_render_paths = []
  if obj['boards']
    multi_render = obj['boards'].length > 20 && `which gs`.length > 0
    obj['backlinks'] = {}
    if obj['pages']
      obj['boards'].each do |board|
        board['buttons'].each do |button|
          if button['load_board'] && button['load_board']['id']
            obj['backlinks'][button['load_board']['id']] ||= []
            obj['backlinks'][button['load_board']['id']] << obj['pages'][board['id']] if obj['pages'][board['id']]
          end
        end
      end
      OBF::Utils.log "backlinks #{obj['backlinks'].to_json}"
    end
    incr = 1.0 / obj['boards'].length.to_f
    tally = 0
    obj['boards'].each_with_index do |board, idx|
      started = Time.now.to_i
      OBF::Utils.log "starting pdf of board #{idx} #{board['name'] || board['id']} at #{started}"
      pre = idx.to_f / obj['boards'].length.to_f
      post = (idx + 1).to_f / obj['boards'].length.to_f
      OBF::Utils.as_progress_percent(tally, tally + incr) do
        # if more than 20 pages, build each page individually
        # and combine them afterwards
        if multi_render
          path = OBF::Utils.temp_path("stash-#{idx}.pdf")
          pdf = Prawn::Document.new(doc_opts)
          load_fonts(pdf, opts)
        else
          pdf.start_new_page unless idx == 0
        end
        build_page(pdf, board, {
          'zipper' => zipper, 
          'pages' => obj['pages'],
          'backlinks' => obj['backlinks'][board['id']] || [], 
          'headerless' => !!opts['headerless'], 
          'font' => default_font,
          'links' => false,
          'text_on_top' => !!opts['text_on_top'], 
          'transparent_background' => !!opts['transparent_background'],
          'symbol_background' => opts['symbol_background'],
          'text_case' => opts['text_case']
        })
        if multi_render
          pdf.render_file(path)
          multi_render_paths << path
        end
      end
      tally += incr
      OBF::Utils.log "  finished pdf of board #{idx}/#{obj['boards'].length} #{Time.now.to_i - started}s"
    end
  else
    build_page(pdf, obj, {
      'headerless' => !!opts['headerless'], 
      'font' => default_font,
      'text_on_top' => !!opts['text_on_top'], 
      'transparent_background' => !!opts['transparent_background'],
      'symbol_background' => opts['symbol_background'],
      'text_case' => opts['text_case']
    })
  end
  if multi_render_paths.length > 0
    # `cp #{multi_render_paths[0]} #{dest_path}`
    `gs -dBATCH -dNOPAUSE -q -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress -sOutputFile=#{dest_path} #{multi_render_paths.join(' ')}`
  else
    pdf.render_file(dest_path)
  end
end
from_external(content, dest_path, opts={}) click to toggle source
# File lib/obf/pdf.rb, line 496
def self.from_external(content, dest_path, opts={})
  tmp_path = OBF::Utils.temp_path("stash")
  if content['boards']
    from_obz(OBF::OBZ.from_external(content, tmp_path, opts), dest_path, opts)
  else
    from_obf(OBF::OBF.from_external(content, tmp_path), dest_path, nil, opts)
  end
  File.unlink(tmp_path) if File.exist?(tmp_path)
  dest_path
end
from_obf(obf_json_or_path, dest_path, zipper=nil, opts={}) click to toggle source
# File lib/obf/pdf.rb, line 24
def self.from_obf(obf_json_or_path, dest_path, zipper=nil, opts={})
  obj = obf_json_or_path
  if obj.is_a?(String)
    obj = OBF::Utils.parse_obf(File.read(obf_json_or_path))
  else
    obj = OBF::Utils.parse_obf(obf_json_or_path)
  end
  build_pdf(obj, dest_path, zipper, opts)
  return dest_path
end
from_obz(obz_path, dest_path, opts={}) click to toggle source
# File lib/obf/pdf.rb, line 446
def self.from_obz(obz_path, dest_path, opts={})
  OBF::Utils.load_zip(obz_path) do |zipper|
    manifest = JSON.parse(zipper.read('manifest.json'))
    root = manifest['root']
    board = OBF::Utils.parse_obf(zipper.read(root))
    board['path'] = root
    unvisited_boards = [board]
    visited_boards = []
    OBF::Utils.update_current_progress(0.2, "prepping for pdf")
    while unvisited_boards.length > 0
      board = unvisited_boards.shift
      visited_boards << board
      children = []
      board['buttons'].each do |button|
        if button['load_board']
          children << button['load_board']
          all_boards = visited_boards + unvisited_boards
          if all_boards.none?{|b| b['id'] == button['load_board']['id'] || b['path'] == button['load_board']['path'] }
            path = button['load_board']['path'] || (manifest['paths'] && manifest['paths']['boards'] && manifest['paths']['boards'][button['load_board']['id']])
            if path
              b = OBF::Utils.parse_obf(zipper.read(path))
              b['path'] = path
              button['load_board']['id'] = b['id']
              unvisited_boards << b
            end
          end
        end
      end
    end
    
    pages = {}
    visited_boards.each_with_index do |board, idx|
      pages[board['id']] = (idx + 1).to_s
    end
    
    OBF::Utils.as_progress_percent(0.2, 1.0) do
        build_pdf({
        'name' => 'Communication Board Set',
        'boards' => visited_boards,
        'pages' => pages
      }, dest_path, zipper, opts)
    end
  end
  # parse obz, draw as pdf

  # TODO: helper files included at the end for emergencies (eg. body parts)
  
  return dest_path
end
load_fonts(pdf, opts) click to toggle source
# File lib/obf/pdf.rb, line 35
def self.load_fonts(pdf, opts)
  # remember: https://www.alphabet-type.com/tools/charset-checker/
  pdf.font_families.update('THFahKwangBold' => {
    normal: File.expand_path('../../THFahKwangBold.ttf', __FILE__)
  })
  pdf.font_families.update('MiedingerBook' => {
    normal: File.expand_path('../../MiedingerBook.ttf', __FILE__)
  })
  pdf.font_families.update('Arial' => {
    normal: File.expand_path('../../Arial.ttf', __FILE__)
  })
  pdf.font_families.update('TimesNewRoman' => {
    normal: File.expand_path('../../TimesNewRoman.ttf', __FILE__)
  })
  # TODO: option to add more fonts
  pdf.font_families.update('Sazanami-Hanazono-Mincho' => {
    normal: File.expand_path('../../Sazanami-Hanazono-Mincho.ttf', __FILE__)
  })
  pdf.font_families.update('Khmer' => {
    normal: File.expand_path('../../Khmer.ttf', __FILE__)
  })
  default_font = 'TimesNewRoman'
  if opts['font'] && !opts['font'].match(/TimesNewRoman/) && File.exists?(opts['font'])
    pdf.font_families.update('DocDefault' => {
      normal: opts['font']
    })
    default_font = 'DocDefault'
  end
  pdf.fallback_fonts = ['TimesNewRoman', 'THFahKwangBold', 'MiedingerBook', 'Helvetica', 'Sazanami-Hanazono-Mincho', 'Khmer']
  pdf.font(default_font)
  default_font
end
rtl_regex() click to toggle source
# File lib/obf/pdf.rb, line 150
def self.rtl_regex
  @res ||= /[#{RTL_SCRIPTS.map{ |script| "\\p{#{script}}" }.join}]/
end
to_png(pdf, dest_path) click to toggle source
# File lib/obf/pdf.rb, line 507
def self.to_png(pdf, dest_path)
  OBF::PNG.from_pdf(pdf, dest_path)
end