Module: Asciidoctor::PDF::FormattedText::InlineImageArranger

Includes:
Logging, Measurements
Defined in:
lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb

Constant Summary collapse

PlaceholderChar =
::Asciidoctor::Prawn::Extensions::PlaceholderChar

Constants included from Measurements

Measurements::InsetMeasurementValueRx, Measurements::MeasurementValueHintRx, Measurements::MeasurementValueRx

Instance Method Summary collapse

Methods included from Measurements

#resolve_measurement_values, #str_to_pt, #to_pt

Instance Method Details

#arrange_images(fragments) ⇒ Object

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.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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

    if (fit = fragment[:image_fit]) === 'line'
      max_image_h = [available_h, doc.font.height].min
    elsif fit == 'none'
      max_image_h = doc.margin_box.height
    elsif doc.bounds.instance_variable_get :@table_cell
      max_image_h = doc.bounds.parent.height
    else
      max_image_h = available_h
    end

    # 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) ⇒ Object



10
11
12
13
# File 'lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb', line 10

def wrap fragments
  arrange_images fragments
  super
end