module Asciidoctor::PDF::FormattedText::InlineImageArranger

Constants

PlaceholderChar

Public Instance Methods

arrange_images(fragments) click to toggle source

Iterates over the fragments that represent inline images and prepares the image data to be embedded into the document.

This method populates the image_width, image_height, image_obj and image_info (PNG only) keys on the fragment. The text is replaced with placeholder text that will be used to reserve enough room in the line to fit the image.

The image height is scaled down to 75% of the specified width (px to pt conversion). If the image height is more than 1.5x the height of the surrounding line of text, the font size and line metrics of the fragment are modified to fit the image in the line.

If this is the scratch document, the image renderer callback is removed so that the image is not embedded.

When this method is called, the cursor is positioned at start of where this group of fragments will be written (offset by the leading padding).

This method is called each time the set of fragments overflow to another page, so it’s necessary to short-circuit if that case is detected.

# File lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb, line 36
def arrange_images fragments
  doc = @document
  return if (raw_image_fragments = fragments.select {|f| (f.key? :image_path) && !(f.key? :image_obj) }).empty?
  scratch = doc.scratch?
  available_w = available_width
  available_h = doc.bounds.height
  last_fragment = {}
  raw_image_fragments.each do |fragment|
    if fragment[:object_id] == last_fragment[:object_id]
      fragments.delete fragment
      next
    else
      drop = scratch
    end
    image_path = fragment[:image_path]
    image_format = fragment[:image_format]
    if (image_w = fragment[:image_width] || '100%') == 'auto'
      image_w = nil # use intrinsic width
    elsif image_w.start_with? 'auto*'
      image_scale = (image_w.slice 5, image_w.length).to_f
      image_w = nil # use intrinsic width
    elsif (pctidx = image_w.index '%') && pctidx + 1 < image_w.length
      # NOTE: intrinsic width is stored behind % symbol
      pct = (image_w.slice 0, pctidx).to_f / 100
      intrinsic_w = (image_w.slice pctidx + 1, image_w.length).to_f
      image_w = [available_w, pct * intrinsic_w].min
    else
      image_w = [available_w, pctidx ? (image_w.to_f / 100 * available_w) : image_w.to_f].min
    end

    max_image_h = fragment[:image_fit] == 'line' ? [available_h, doc.font.height].min : available_h

    # TODO: make helper method to calculate width and height of image
    if image_format == 'svg'
      svg_obj = ::Prawn::SVG::Interface.new ::File.read(image_path, mode: 'r:UTF-8'), doc,
        at: doc.bounds.top_left,
        width: image_w,
        fallback_font_name: doc.fallback_svg_font_name,
        enable_web_requests: doc.allow_uri_read ? (doc.method :load_open_uri).to_proc : false,
        enable_file_requests_with_root: { base: (::File.dirname image_path), root: doc.jail_dir },
        cache_images: doc.cache_uri
      svg_size = svg_obj.document.sizing
      # NOTE: the best we can do is make the image fit within full height of bounds
      if (image_h = svg_size.output_height) > max_image_h
        image_w = (svg_obj.resize height: (image_h = max_image_h)).output_width
      elsif image_w
        image_w = svg_size.output_width
      else
        fragment[:image_width] = (image_w = svg_size.output_width).to_s
        image_w *= image_scale if image_scale
        image_w = available_w if image_w > available_w
      end
      fragment[:image_obj] = svg_obj
    else
      # TODO: cache image info based on path (Prawn caches based on SHA1 of content)
      # NOTE: image_obj is constrained to image_width by renderer
      image_obj, image_info = ::File.open(image_path, 'rb') {|fd| doc.build_image_object fd }
      unless image_w
        fragment[:image_width] = (image_w = to_pt image_info.width, :px).to_s
        image_w *= image_scale if image_scale
        image_w = available_w if image_w > available_w
      end
      if (image_h = image_w * (image_info.height.fdiv image_info.width)) > max_image_h
        # NOTE: the best we can do is make the image fit within full height of bounds
        image_w = (image_h = max_image_h) * (image_info.width.fdiv image_info.height)
      end
      fragment[:image_obj] = image_obj
      fragment[:image_info] = image_info
    end

    doc.save_font do
      # NOTE: if image height exceeds line height by more than 1.5x, increase the line height
      # FIXME: we could really use a nicer API from Prawn here; this is an ugly hack
      if (f_height = image_h) > (line_font = doc.font).height * 1.5
        # align with descender (equivalent to vertical-align: bottom in CSS)
        fragment[:ascender] = f_height - (fragment[:descender] = line_font.descender)
        if f_height == available_h
          fragment[:ascender] -= (doc.calc_line_metrics (doc.instance_variable_get :@base_line_height), line_font, doc.font_size).padding_top
          fragment[:full_height] = true
        end
        doc.font_size (fragment[:size] = f_height * (doc.font_size / line_font.height))
        # align with baseline (roughly equivalent to vertical-align: baseline in CSS)
        #fragment[:ascender] = f_height
        #fragment[:descender] = 0
        #doc.font_size(fragment[:size] = (f_height + line_font.descender) * (doc.font_size / line_font.height))
        fragment[:line_height_increased] = true
      end
    end

    # NOTE: we can't rely on the fragment width because the line wrap mechanism ignores it;
    # it only considers the text (string) and character spacing, rebuilding the string several times
    fragment[:text] = PlaceholderChar
    fragment[:actual_character_spacing] = doc.character_spacing
    fragment[:character_spacing] = image_w
    fragment[:image_width] = fragment[:width] = image_w
    fragment[:image_height] = image_h
  rescue
    logger.warn %(could not embed image: #{image_path}; #{$!.message}#{(doc.recommend_prawn_gmagick? $!, image_format) ? %(; install prawn-gmagick gem to add support for #{image_format&.upcase || 'unknown'} image format) : ''}) unless scratch
    drop = true # delegate to cleanup logic in ensure block
  ensure
    # NOTE: skip rendering image in scratch document or if image can't be loaded
    if drop
      fragment.delete :callback
      fragment.delete :image_info
      # NOTE: retain key to indicate we've visited fragment already
      fragment[:image_obj] = nil
    end
    last_fragment = fragment
  end
end
wrap(fragments) click to toggle source
Calls superclass method
# File lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb, line 10
def wrap fragments
  arrange_images fragments
  super
end