module Asciidoctor::Pdf::FormattedText module InlineImageArranger include ::Asciidoctor::Pdf::Measurements ImagePlaceholderChar = '.' begin require 'thread_safe' unless defined? ::ThreadSafe PlaceholderWidthCache = ::ThreadSafe::Cache.new rescue PlaceholderWidthCache = {} end TemporaryPath = ::Asciidoctor::Pdf::TemporaryPath if respond_to? :prepend def wrap fragments arrange_images fragments super end else class << self def extended base base.class.__send__ :alias_method, :_initial_wrap, :wrap end end def wrap fragments arrange_images fragments _initial_wrap fragments end end # 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. 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 = doc.bounds.width #available_h = doc.bounds.height # NOTE try to fit image within bounds if cursor is within 1% of top of page # QUESTION are we considering the right boundaries here? available_h ||= (doc.cursor / (available_h = doc.bounds.height) >= 0.99 ? doc.cursor : available_h) raw_image_fragments.each do |fragment| drop = scratch begin image_path = fragment[:image_path] # NOTE only attempt to convert an unresolved (i.e., String) value if ::String === (image_w = fragment[:image_width]) image_w = [available_w, (image_w.end_with? '%') ? (image_w.to_f / 100 * available_w) : image_w.to_f].min end # TODO make helper method to calculate width and height of image if fragment[:image_format] == 'svg' svg_obj = ::Prawn::Svg::Interface.new ::IO.read(image_path), doc, at: doc.bounds.top_left, width: image_w, fallback_font_name: doc.default_svg_font svg_size = image_w ? svg_obj.document.sizing : # NOTE convert intrinsic dimensions to points; constrain to content width (svg_obj.resize width: [(to_pt svg_obj.document.sizing.output_width, :px), available_w].min) # NOTE the best we can do is make the image fit within full height of bounds if (image_h = svg_size.output_height) > available_h image_w = (svg_size = svg_obj.resize height: (image_h = available_h)).output_width else image_w = svg_size.output_width 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 } if image_w if image_w == image_info.width image_h = image_info.height.to_f else image_h = image_w * (image_info.height.fdiv image_info.width) end else # NOTE convert intrinsic dimensions to points; constrain to content width if (image_w = to_pt image_info.width, :px) > available_w image_h = (image_w = available_w) * (image_info.height.fdiv image_info.width) else image_h = to_pt image_info.height, :px end end # NOTE the best we can do is make the image fit within full height of bounds image_w = (image_h = available_h) * (image_info.width.fdiv image_info.height) if image_h > available_h fragment[:image_obj] = image_obj fragment[:image_info] = image_info end spacer_w = nil doc.fragment_font fragment 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) 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 unless (spacer_w = PlaceholderWidthCache[f_info = doc.font_info]) spacer_w = PlaceholderWidthCache[f_info] = doc.width_of ImagePlaceholderChar end end # NOTE make room for image by repeating image placeholder character, using character spacing to fine-tune # NOTE image_width is constrained to available_w, so we don't have to check for overflow spacer_cnt, remainder = image_w.divmod spacer_w if spacer_cnt > 0 fragment[:text] = ImagePlaceholderChar * spacer_cnt fragment[:character_spacing] = remainder.fdiv spacer_cnt if remainder > 0 else fragment[:text] = ImagePlaceholderChar fragment[:character_spacing] = -(spacer_w - remainder) end # FIXME we could use a nicer API from Prawn here that lets us reserve a fragment width without text #fragment[:image_width] = fragment[:width] = image_w fragment[:image_width] = image_w fragment[:image_height] = image_h rescue => e warn %(asciidoctor: WARNING: could not embed image: #{image_path}; #{e.message}) 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 # NOTE in main document, temporary image path is unlinked by renderer ::File.unlink image_path if TemporaryPath === image_path && image_path.exist? end end end end end if respond_to? :prepend class ::Prawn::Text::Formatted::Box prepend InlineImageArranger end else ::Prawn::Text::Formatted::Box.extensions << InlineImageArranger end end