Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resolves #185 autofit font size in listing & literal blocks #186

Merged
merged 1 commit into from
Jun 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions data/themes/default-theme.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ base:
line_height: $base_line_height_length / $base_font_size
font_size_large: round($base_font_size * 1.25)
font_size_small: round($base_font_size * 0.85)
font_size_min: $base_font_size * 0.75
font_style: normal
align: justify
border_radius: 4
Expand Down
22 changes: 13 additions & 9 deletions examples/chronicles.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -268,31 +268,35 @@ code {
```

Where could we go without some Java?
Naturally, some autosizing is necessary.

```java
[source%autofit,java]
----
package org.javaee7.cdi.events;

import javax.enterprise.context.SessionScoped;
import javax.enterprise.event.Observes;
import java.io.Serializable;
import java.util.*;
import javax.ws.rs.*;

/**
* @author The Duke
*/
@SessionScoped
public class GreetingReceiver implements EventReceiver, Serializable {
private String greet = "Willkommen";
public class GreetingReceiver implements EventReceiver, java.io.Serializable {
private List<String> greetings = new ArrayList<String>();

void receive(@Observes String greet) {
this.greet = greet;
this.greetings.add(greet);
}

@Override
public String getGreet() {
return greet;
@GET @Produces("application/json")
public List<String> listAll(@QueryParam("start") Integer start, @QueryParam("max") Integer max) {
// TODO implement filtering by start and max
this.greetings;
}
}
```
----

We already showed you an XML example in <<ravages>>.

Expand Down
Binary file modified examples/chronicles.pdf
Binary file not shown.
95 changes: 88 additions & 7 deletions lib/asciidoctor-pdf/converter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# encoding: UTF-8
# TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
require_relative 'core_ext/array'
require_relative 'core_ext'
require 'prawn'
require 'prawn-svg'
require 'prawn/table'
Expand Down Expand Up @@ -67,6 +67,7 @@ def self.unicode_char number
# CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core
CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*$)/
ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
LineScanRx = /\n|.+/

def initialize backend, opts
super
Expand Down Expand Up @@ -864,7 +865,7 @@ def convert_image node
unlink_tmp_file image_path
end

# TODO shrink text if it's too wide to fit in the bounding box
# QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
def convert_listing_or_literal node
add_dest_for_block node if node.id
# HACK disable built-in syntax highlighter; must be done before calling node.content!
Expand All @@ -873,13 +874,11 @@ def convert_listing_or_literal node
highlighter = node.document.attr 'source-highlighter'
# NOTE the source highlighter logic below handles the callouts and highlight subs
prev_subs = subs.dup
subs.delete :callouts
subs.delete :highlight
subs.delete_all :highlight, :callouts
else
highlighter = nil
prev_subs = nil
end
# FIXME source highlighter freaks out about the non-breaking space characters; does it?
source_string = preserve_indentation node.content
source_chunks = case highlighter
when 'coderay'
Expand All @@ -897,14 +896,24 @@ def convert_listing_or_literal node
conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
else
# NOTE only format if we detect a need
(source_string =~ BuiltInEntityCharOrTagRx) ? (text_formatter.format source_string) : [{ text: source_string }]
if source_string =~ BuiltInEntityCharOrTagRx
text_formatter.format source_string
else
[{ text: source_string }]
end
end

node.subs.replace prev_subs if prev_subs

#move_down @theme.block_margin_top unless at_page_top?
theme_margin :block, :top

if (node.option? 'autofit') || (node.document.attr? 'autofit-option')
adjusted_font_size = theme_font_size_autofit source_chunks, :code
else
adjusted_font_size = nil
end

keep_together do |box_height = nil|
caption_height = node.title? ? (layout_caption node) : 0
theme_font :code do
Expand All @@ -927,7 +936,9 @@ def convert_listing_or_literal node
end

pad_box @theme.code_padding do
typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height), color: (@theme.code_font_color || @font_color)
typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
color: (@theme.code_font_color || @font_color),
size: adjusted_font_size
end
end
end
Expand Down Expand Up @@ -962,6 +973,9 @@ def extract_conums string
end

# Restore the conums into the Array of formatted text fragments
#--
# QUESTION can this be done more efficiently?
# QUESTION can we reuse arrange_fragments_by_line?
def restore_conums fragments, conum_mapping
lines = []
line_num = 0
Expand Down Expand Up @@ -2004,6 +2018,73 @@ def theme_font category, opts = {}
@text_transform = prev_transform if transform
end

# Calculate the font size (down to the minimum font size) that would allow
# all the specified fragments to fit in the available width without wrapping lines.
#
# Return the calculated font size if an adjustment is necessary or nil if no
# font size adjustment is necessary.
def theme_font_size_autofit fragments, category
arranger = arrange_fragments_by_line fragments
adjusted_font_size = nil
theme_font category do
# NOTE finalizing the line here generates fragments using current font settings
arranger.finalize_line
actual_width = width_of_fragments arranger.fragments
unless ::Array === (padding = @theme[%(#{category}_padding)])
padding = [padding] * 4
end
bounds.add_left_padding(p_left = padding[3] || 0)
bounds.add_right_padding(p_right = padding[1] || 0)
if actual_width > bounds.width
adjusted_font_size = ((bounds.width * font_size).to_f / actual_width).with_precision 4
if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
adjusted_font_size = min
end
end
bounds.subtract_left_padding p_left
bounds.subtract_right_padding p_right
end
adjusted_font_size
end

# Arrange fragments by line in an arranger and return an unfinalized arranger.
#
# Finalizing the arranger is deferred since it must be done in the context of
# the global font settings you want applied to each fragment.
def arrange_fragments_by_line fragments, opts = {}
arranger = ::Prawn::Text::Formatted::Arranger.new self
by_line = arranger.consumed = []
fragments.each do |fragment|
if (txt = fragment[:text]) == EOL
by_line << fragment
elsif txt.include? EOL
txt.scan(LineScanRx) do |line|
by_line << fragment.merge(text: line)
end
else
by_line << fragment
end
end
arranger
end

# Calculate the width that is needed to print all the
# fragments without wrapping any lines.
#
# This method assumes endlines are represented as discrete entries in the
# fragments array.
def width_of_fragments fragments
line_widths = [0]
fragments.each do |fragment|
if fragment.text == EOL
line_widths << 0
else
line_widths[-1] += fragment.width
end
end
line_widths.max
end

# TODO document me, esp the first line formatting functionality
def typeset_text string, line_metrics, opts = {}
move_down line_metrics.padding_top
Expand Down
3 changes: 3 additions & 0 deletions lib/asciidoctor-pdf/core_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
require_relative 'core_ext/array'
require_relative 'core_ext/numeric'
#require_relative 'core_ext/ostruct'
14 changes: 10 additions & 4 deletions lib/asciidoctor-pdf/core_ext/array.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
class Array
def to_h
Hash[to_a]
end unless respond_to? :to_h
end if RUBY_VERSION < '2.1.0'
if RUBY_VERSION < '2.1.0'
def to_h
Hash[to_a]
end unless respond_to? :to_h
end

def delete_all *entries
entries.map {|entry| delete entry }.compact
end unless respond_to? :delete_all
end
11 changes: 11 additions & 0 deletions lib/asciidoctor-pdf/core_ext/numeric.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Numeric
def with_precision precision
precision = precision.to_i
if precision > 0
factor = 10 ** precision
(self * factor).truncate / factor.to_f
else
self.truncate
end
end unless respond_to? :with_precision
end