Skip to content

Commit

Permalink
resolves asciidoctor#353 add example with tests of extended converter…
Browse files Browse the repository at this point in the history
… that supports image float
  • Loading branch information
mojavelinux committed May 7, 2022
1 parent 7f31cfb commit 8c641eb
Show file tree
Hide file tree
Showing 8 changed files with 816 additions and 3 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Enhancements::

* add support for `text-transform` property on first line of abstract in theme (#2141)
* rename `resolve_alignment_from_role` to `resolve_text_align_from_role` to reflect proper terminology and purpose; alias old method name
* add support for text box with fixed height via `:height` option to `typeset_text` helper
* configure `typeset_text` and `ink_prose` to return remaining fragments when `:height` option is specified
* set `rendered-height`, `rendered-width`, and `caption-height` attributes on node of rendered block image to respective values
* if `float` attribute is set on block image, set max width on caption to `fit-content` if max width not already set to a `fit-content` value
* add example of extended converter that wraps text around preceding block image with `float` attribute (#353)

Bug Fixes::

Expand Down
80 changes: 80 additions & 0 deletions docs/modules/extend/examples/pdf-converter-image-float.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
class PDFConverterImageFloat < (Asciidoctor::Converter.for 'pdf')
register_for 'pdf'

def convert_paragraph node
prev_sibling = (self_idx = (siblings = node.parent.blocks).index node) > 0 ? siblings[self_idx - 1] : nil
if (prev_sibling&.context == :image && (float_to = prev_sibling.attr 'float')) || (@float_box ||= nil)
start_cursor = cursor # start_cursor is always start of block below image
roles = node.roles
text_align = (resolve_text_align_from_role roles, query_theme: true, remove_predefined: true) || @base_text_align.to_sym
para_font_descender = para_font_size = nil
if roles.empty?
para_font_descender = font.descender
para_font_size = font_size
line_metrics = calc_line_metrics @base_line_height
else
line_metrics = theme_font_cascade (role_keys = roles.map {|role| %(role_#{role}).to_sym }) do
para_font_descender = font.descender
para_font_size = font_size
calc_line_metrics @base_line_height
end
end
if (float_box = @float_box)
@float_box = nil
float_box[:at][1] -= line_metrics.padding_top # typeset_text with box doesn't add line_metrics padding
else
float_gap_s, float_gap_b = ::Array === (float_gap = theme.image_float_gap) ? float_gap : [float_gap || 12, float_gap || 6]
float_w = (prev_sibling.attr 'rendered-width') + float_gap_s
float_h = (prev_sibling.attr 'rendered-height') + (prev_sibling.attr 'caption-height', 0) + float_gap_b
if (box_w = bounds.width - float_w) < para_font_size
prev_sibling.remove_attr 'float'
return convert_paragraph node
end
box_t = start_cursor + (float_h - float_gap_b) + theme.block_margin_bottom
box_l = float_to == 'right' ? 0 : float_w
box_t -= line_metrics.padding_top # typeset_text with box doesn't add line_metrics padding
# NOTE: allocate at least one whole empty line below image
line_height_length = line_metrics.height + line_metrics.leading + line_metrics.padding_top
float_box = { at: [box_l, box_t], width: box_w, height: [box_t, float_h + line_height_length].min }
end
float_box[:final_gap] = para_font_descender + line_metrics.padding_bottom
add_dest_for_block node if node.id
end_cursor = nil
prose_opts = float_box.merge align: text_align, hyphenate: true, margin_bottom: 0, draw_text_callback: (proc do |text, opts|
draw_text! text, opts
end_cursor = opts[:at][1]
end)
if role_keys
remaining = theme_font_cascade(role_keys) { ink_prose node.content, prose_opts }
else
remaining = ink_prose node.content, prose_opts
end
end_cursor = end_cursor ? end_cursor - float_box[:final_gap] : start_cursor
if remaining.empty?
if siblings[self_idx + 1]&.context == :paragraph
end_cursor -= theme.prose_margin_bottom
inked_height = float_box[:at][1] - end_cursor
if (float_box[:height] -= inked_height) > 0
float_box[:at][1] -= inked_height
float_box[:line_metrics] = line_metrics
@float_box = float_box
else
move_cursor_to end_cursor
end
else
move_cursor_to start_cursor
end
else
move_cursor_to end_cursor
if role_keys
theme_font_cascade(role_keys) { typeset_formatted_text remaining, line_metrics, align: text_align }
else
typeset_formatted_text remaining, line_metrics, align: text_align
end
theme_margin :prose, :bottom, (next_enclosed_block node)
end
else
super
end
end
end
20 changes: 20 additions & 0 deletions docs/modules/extend/pages/use-cases.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ image:
indent: [0.5in, 0]
----

== Float block image

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.

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

Using this extended converter, you can configure the gap between next to and below the image using the `image_float_gap` key.

[,yaml]
----
image:
float_gap: [12, 6]
----

== Resources

To find even more examples of how to override the behavior of the converter, refer to the extended converter in the {url-infoq-template}[InfoQ Mini-Book template^].
12 changes: 9 additions & 3 deletions lib/asciidoctor/pdf/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1572,14 +1572,15 @@ 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')
# 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

align_to_page = node.option? 'align-to-page'
pinned = opts[:pinned]

begin
rendered_w = nil
rendered_h = rendered_w = nil
span_page_width_if align_to_page do
if image_format == 'svg'
if ::Base64 === image_path
Expand Down Expand Up @@ -1658,6 +1659,9 @@ 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
rescue => e
raise if ::StopIteration === e
Expand Down Expand Up @@ -3052,12 +3056,13 @@ def ink_prose string, opts = {}
text_decoration_width: (opts.delete :text_decoration_width),
}.compact
end
typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
result = typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
color: @font_color,
inline_format: [inline_format_opts],
align: @base_text_align.to_sym,
}.merge(opts)
margin_bottom bot_margin
result
end

def generate_manname_section node
Expand Down Expand Up @@ -4156,9 +4161,10 @@ def rendered_width_of_char char, opts = {}

# TODO: document me, esp the first line formatting functionality
def typeset_text string, line_metrics, opts = {}
move_down line_metrics.padding_top
opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
return text_box string, opts if opts[:height]
move_down line_metrics.padding_top
if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
indent hanging_indent do
text string, (opts.merge indent_paragraphs: -hanging_indent)
Expand Down
5 changes: 5 additions & 0 deletions spec/fixtures/lorem-ipsum.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
2-sentences-1-paragraph: |-
Lorem ipsum dolor sit amet consectetur adipiscing elit conubia turpis, elementum porttitor sagittis pellentesque dictumst egestas ante praesent.
Magna velit egestas quam sociis placerat facilisis felis mauris, primis ridiculus commodo scelerisque eleifend morbi non.
4-sentences-1-paragraph: |-
Lorem ipsum dolor sit amet consectetur adipiscing elit conubia turpis, elementum porttitor sagittis pellentesque dictumst egestas ante praesent.
Magna velit egestas quam sociis placerat facilisis felis mauris, primis ridiculus commodo scelerisque eleifend morbi non.
Blandit nam porta arcu sociosqu eros libero cras morbi, enim tempus est elementum ut interdum accumsan, hendrerit ornare imperdiet proin vivamus inceptos fames mi, sociis leo odio donec mattis hac.
Condimentum egestas velit accumsan lobortis montes quisque mattis curae placerat magna, primis justo tristique elementum facilisis penatibus pretium in scelerisque est, euismod tempor luctus tincidunt sed potenti enim ac ut.
2-sentences-2-paragraphs: |-
Lorem ipsum dolor sit amet consectetur adipiscing elit conubia turpis, elementum porttitor sagittis pellentesque dictumst egestas ante praesent.
Expand Down
Binary file added spec/fixtures/rect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 8c641eb

Please sign in to comment.