Skip to content

Commit

Permalink
resolves asciidoctor#353 integrate support for image float into conve…
Browse files Browse the repository at this point in the history
…rter

- add supports_float_wrapping? to check if the following node supports wrapping around a float
- add ink_paragraph_in_float_box to handle logic to render paragraph within float box
- move cursor as content is inked in float box
- update example to show how to enlist a code block in float wrapping
- add additional information to float_box
  • Loading branch information
mojavelinux committed May 13, 2022
1 parent 59edb65 commit 244bda7
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 177 deletions.
13 changes: 13 additions & 0 deletions docs/modules/extend/examples/pdf-converter-code-float-wrapping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class PDFConverterCodeFloatWrapping < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'

def supports_float_wrapping? node
%i(paragraph listing literal).include? node.context
end

def convert_code node
return super unless (float_box = @float_box ||= nil)
indent(float_box[:left] - bounds.left, bounds.width - float_box[:right]) { super }
@float_box = nil unless page_number == float_box[:page] && cursor > float_box[:bottom]
end
end
80 changes: 0 additions & 80 deletions docs/modules/extend/examples/pdf-converter-image-float.rb

This file was deleted.

17 changes: 11 additions & 6 deletions docs/modules/extend/pages/use-cases.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -165,16 +165,21 @@ image:
indent: [0.5in, 0]
----

== Float block image
== Wrap code blocks around an image float

For the most part, Asciidoctor PDF stacks block elements vertically.
The main exception is a table, which allows content to be arranged in a grid.
By extending the converter and overriding the convert handler for a paragraph, you can arrange consecutive paragraphs that follow an image to fit into the empty space next to the image, then wrap around it.
Asciidoctor PDF provides basic support for image floats.
It will wrap paragraph text on the opposing side of the float.
However, if it encounters a non-paragraph, the converter will clear the float and continue positioning content below the image.

.Extended converter that wraps text around an image float
As a companion to this basics support, the converter provides a framework for broadening support for float wrapping.
We can take advantage of this framework in an extended converter.
By extending the converter and overriding the `supports_float_wrapping?` as well as the handler for the block you want to enlist (e.g., `convert_code`), you can arrange additional content into the empty space adjacent to the floated image.
In the following example, code (listing and literal) blocks are included in the float wrapping.

.Extended converter that additionally wraps code blocks around an image float
[,ruby]
----
include::example$pdf-converter-image-float.rb[]
include::example$pdf-converter-code-float-wrapping.rb[]
----

Using this extended converter, you can configure the gap between next to and below the image using the `image_float_gap` key.
Expand Down
108 changes: 94 additions & 14 deletions lib/asciidoctor/pdf/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -839,8 +839,13 @@ def convert_preamble node
def convert_paragraph node
add_dest_for_block node if node.id
prose_opts = { margin_bottom: 0, hyphenate: true }
if (align = resolve_text_align_from_role (roles = node.roles), query_theme: true, remove_predefined: true)
prose_opts[:align] = align
if (text_align = resolve_text_align_from_role (roles = node.roles), query_theme: true, remove_predefined: true)
prose_opts[:align] = text_align
end
role_keys = roles.map {|role| %(role_#{role}).to_sym } unless roles.empty?

if (float_box = (@float_box ||= nil))
return ink_paragraph_in_float_box node, float_box, prose_opts, role_keys
end

if (text_indent = @theme.prose_text_indent) > 0 ||
Expand All @@ -857,13 +862,7 @@ def convert_paragraph node
prose_opts[:bottom_gutter] = bottom_gutter
end

if roles.empty?
ink_prose node.content, prose_opts
else
theme_font_cascade (roles.map {|role| %(role_#{role}).to_sym }) do
ink_prose node.content, prose_opts
end
end
role_keys ? theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } : (ink_prose node.content, prose_opts)

block_next = next_enclosed_block node
if (margin_inner_val = @theme.prose_margin_inner) && block_next&.context == :paragraph
Expand Down Expand Up @@ -1580,7 +1579,7 @@ def convert_image node, opts = {}

caption_end = @theme.image_caption_end&.to_sym || :bottom
caption_max_width = @theme.image_caption_max_width
caption_max_width = 'fit-content' if (node.attr? 'float') && !(caption_max_width&.start_with? 'fit-content')
caption_max_width = 'fit-content' if (float_to = node.attr 'float') && !(caption_max_width&.start_with? 'fit-content')
# NOTE: if width is not set explicitly and max-width is fit-content, caption height may not be accurate
caption_h = node.title? ? (ink_caption node, category: :image, end: caption_end, block_align: alignment, block_width: width, max_width: caption_max_width, dry_run: true, force_top_margin: caption_end == :bottom) : 0

Expand Down Expand Up @@ -1667,16 +1666,37 @@ def convert_image node, opts = {}
end
end
ink_caption node, category: :image, end: :bottom, block_align: alignment, block_width: rendered_w, max_width: caption_max_width if caption_end == :bottom && node.title?
node.set_attr 'caption-height', (caption_h.round 5) if caption_h > 0
node.set_attr 'rendered-height', (rendered_h.round 5)
node.set_attr 'rendered-width', (rendered_w.round 5)
theme_margin :block, :bottom, (next_enclosed_block node) unless pinned
if !pinned && (block_next = next_enclosed_block node)
if float_to && (supports_float_wrapping? block_next) && rendered_w < bounds.width
init_float_box node, rendered_w, rendered_h + caption_h, float_to
else
theme_margin :block, :bottom, block_next
end
end
rescue => e
raise if ::StopIteration === e
on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{e.message}#{::Prawn::Errors::UnsupportedImageType === e && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
end
end

def supports_float_wrapping? node
node.context == :paragraph
end

def init_float_box _node, block_width, block_height, float_to
gap = ::Array === (gap = @theme.image_float_gap) ? gap.dup : [gap, gap]
float_w = block_width + (gap[0] ||= 12)
float_h = block_height + (gap[1] ||= 6)
box_l = bounds.left + (float_to == 'right' ? 0 : float_w)
box_t = cursor + block_height
box_w = bounds.width - float_w
box_r = box_l + box_w
box_h = [box_t, float_h].min
box_b = box_t - box_h
move_cursor_to box_t
@float_box = { page: page_number, top: box_t, right: box_r, bottom: box_b, left: box_l, width: box_w, height: box_h, gap: gap }
end

def draw_image_border top, w, h, alignment
if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
if (@theme.image_border_fit || 'content') == 'auto'
Expand Down Expand Up @@ -3044,6 +3064,66 @@ def ink_heading string, opts = {}
end
end

def ink_paragraph_in_float_box node, float_box, prose_opts, role_keys
@float_box = para_font_descender = para_font_size = nil
if role_keys
line_metrics = theme_font_cascade role_keys do
para_font_descender = font.descender
para_font_size = font_size
calc_line_metrics @base_line_height
end
else
para_font_descender = font.descender
para_font_size = font_size
line_metrics = calc_line_metrics @base_line_height
end
# allocate the space of at least one empty line below block
line_height_length = line_metrics.height + line_metrics.leading + line_metrics.padding_top
start_page_number = float_box[:page]
start_cursor = cursor
end_cursor = nil
block_bottom = float_box[:bottom] + float_box[:gap][1]
# use :at to incorporate padding top from line metrics
# use :final_gap to incorporate padding bottom from line metrics
# use :draw_text_callback to track end cursor (requires applying :final_gap to result manually)
prose_opts.update \
at: [float_box[:left], start_cursor - line_metrics.padding_top],
width: float_box[:width],
height: [cursor, float_box[:height] - (float_box[:top] - start_cursor) + line_height_length].min,
final_gap: para_font_descender + line_metrics.padding_bottom,
draw_text_callback: (proc do |text, opts|
draw_text! text, opts
end_cursor = opts[:at][1] # does not include :final_gap value
end)
overflow_text = role_keys ?
theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } :
(ink_prose node.content, prose_opts)
move_cursor_to end_cursor -= prose_opts[:final_gap] if end_cursor # ink_prose with :height does not move cursor
block_next = next_enclosed_block node
if overflow_text.empty?
if block_next && (supports_float_wrapping? block_next)
theme_margin :prose, :bottom, block_next
@float_box = float_box if page_number == start_page_number && cursor > start_cursor - prose_opts[:height]
elsif end_cursor > float_box[:bottom] # paragraph does not extend past floating block
move_cursor_to block_bottom
theme_margin :block, :bottom, block_next
else # paragraph extends past floating block
theme_margin :prose, :bottom, block_next
end
else
unless end_cursor
move_cursor_to block_bottom
# Q: should we only add the delta so `cursor - start_cursor == @theme.prose_margin_bottom`?
theme_margin :block, :bottom
end
text_align = prose_opts[:align] || @base_text_align.to_sym
role_keys ?
theme_font_cascade(role_keys) { typeset_formatted_text overflow_text, line_metrics, align: text_align } :
(typeset_formatted_text overflow_text, line_metrics, align: text_align)
theme_margin :prose, :bottom, block_next
end
end

# NOTE: inline_format is true by default
def ink_prose string, opts = {}
top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || 0
Expand Down
Loading

0 comments on commit 244bda7

Please sign in to comment.