asciidoctor-pdf 1.5.0.beta.1 → 1.5.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +26 -0
  3. data/README.adoc +2 -2
  4. data/asciidoctor-pdf.gemspec +3 -3
  5. data/bin/asciidoctor-pdf +1 -0
  6. data/data/fonts/ABOUT-mplus1mn-subset +24 -0
  7. data/data/fonts/ABOUT-mplus1p-subset +25 -0
  8. data/data/fonts/ABOUT-notoserif-subset +25 -0
  9. data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
  10. data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
  11. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  12. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  13. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  14. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  15. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  16. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  17. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  18. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  19. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  20. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  21. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  22. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  23. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  24. data/data/themes/base-theme.yml +8 -1
  25. data/data/themes/default-theme.yml +22 -7
  26. data/data/themes/default-with-fallback-font-theme.yml +4 -4
  27. data/docs/theming-guide.adoc +459 -44
  28. data/lib/asciidoctor-pdf.rb +1 -0
  29. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +1 -0
  30. data/lib/asciidoctor-pdf/asciidoctor_ext/abstract_block.rb +1 -0
  31. data/lib/asciidoctor-pdf/asciidoctor_ext/document.rb +1 -0
  32. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +1 -0
  33. data/lib/asciidoctor-pdf/asciidoctor_ext/list.rb +1 -0
  34. data/lib/asciidoctor-pdf/asciidoctor_ext/list_item.rb +1 -0
  35. data/lib/asciidoctor-pdf/asciidoctor_ext/logging_shim.rb +1 -0
  36. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +1 -0
  37. data/lib/asciidoctor-pdf/converter.rb +227 -137
  38. data/lib/asciidoctor-pdf/core_ext.rb +1 -0
  39. data/lib/asciidoctor-pdf/core_ext/array.rb +1 -0
  40. data/lib/asciidoctor-pdf/core_ext/hash.rb +1 -0
  41. data/lib/asciidoctor-pdf/core_ext/numeric.rb +1 -0
  42. data/lib/asciidoctor-pdf/core_ext/object.rb +1 -0
  43. data/lib/asciidoctor-pdf/core_ext/quantifiable_stdout.rb +1 -0
  44. data/lib/asciidoctor-pdf/core_ext/regexp.rb +1 -0
  45. data/lib/asciidoctor-pdf/core_ext/string.rb +1 -0
  46. data/lib/asciidoctor-pdf/formatted_text.rb +1 -0
  47. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +1 -0
  48. data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +1 -0
  49. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +2 -1
  50. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +1 -0
  51. data/lib/asciidoctor-pdf/formatted_text/inline_text_aligner.rb +1 -0
  52. data/lib/asciidoctor-pdf/formatted_text/parser.rb +19 -6
  53. data/lib/asciidoctor-pdf/formatted_text/parser.treetop +2 -3
  54. data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +1 -0
  55. data/lib/asciidoctor-pdf/formatted_text/transform.rb +51 -3
  56. data/lib/asciidoctor-pdf/implicit_header_processor.rb +1 -0
  57. data/lib/asciidoctor-pdf/index_catalog.rb +1 -0
  58. data/lib/asciidoctor-pdf/measurements.rb +1 -0
  59. data/lib/asciidoctor-pdf/pdf-core_ext.rb +1 -0
  60. data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +26 -0
  61. data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +1 -0
  62. data/lib/asciidoctor-pdf/pdfmark.rb +1 -0
  63. data/lib/asciidoctor-pdf/prawn-svg_ext.rb +1 -0
  64. data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +1 -0
  65. data/lib/asciidoctor-pdf/prawn-table_ext.rb +1 -0
  66. data/lib/asciidoctor-pdf/prawn-table_ext/cell.rb +1 -0
  67. data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +1 -0
  68. data/lib/asciidoctor-pdf/prawn-table_ext/cell/text.rb +1 -0
  69. data/lib/asciidoctor-pdf/prawn-templates_ext.rb +1 -0
  70. data/lib/asciidoctor-pdf/prawn_ext.rb +1 -0
  71. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +6 -5
  72. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +11 -17
  73. data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +15 -8
  74. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +1 -0
  75. data/lib/asciidoctor-pdf/prawn_ext/images.rb +2 -1
  76. data/lib/asciidoctor-pdf/roman_numeral.rb +1 -0
  77. data/lib/asciidoctor-pdf/rouge_ext.rb +1 -0
  78. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +1 -0
  79. data/lib/asciidoctor-pdf/rouge_ext/themes/asciidoctor_pdf_default.rb +1 -0
  80. data/lib/asciidoctor-pdf/rouge_ext/themes/bw.rb +1 -0
  81. data/lib/asciidoctor-pdf/sanitizer.rb +1 -0
  82. data/lib/asciidoctor-pdf/temporary_path.rb +1 -0
  83. data/lib/asciidoctor-pdf/theme_loader.rb +13 -13
  84. data/lib/asciidoctor-pdf/ttfunk_ext.rb +1 -0
  85. data/lib/asciidoctor-pdf/version.rb +2 -1
  86. data/lib/asciidoctor/pdf.rb +1 -0
  87. data/lib/asciidoctor/pdf/version.rb +1 -0
  88. metadata +15 -15
  89. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -8
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'asciidoctor' unless defined? Asciidoctor.load
2
3
  require_relative 'asciidoctor-pdf/asciidoctor_ext'
3
4
  require_relative 'asciidoctor-pdf/version'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # NOTE these are either candidates for inclusion in Asciidoctor core or backports
2
3
  require_relative 'asciidoctor_ext/abstract_block'
3
4
  require_relative 'asciidoctor_ext/document'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Asciidoctor::AbstractBlock
2
3
  def sections?
3
4
  !sections.empty?
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Asciidoctor::Document
2
3
  alias catalog references unless method_defined? :catalog
3
4
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Asciidoctor
2
3
  module Image
3
4
  DataUriRx = /^data:image\/(?<fmt>png|jpe?g|gif|pdf|bmp|tiff);base64,(?<data>.*)$/
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # TODO add these methods to Asciidoctor core
2
3
  class Asciidoctor::List
3
4
  # Check whether this list is an outline list (unordered or ordered).
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # TODO add these methods to Asciidoctor core
2
3
  class Asciidoctor::ListItem
3
4
  # Check whether this list item has complex content (i.e., nested blocks other than an outline list).
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Asciidoctor
2
3
  class StubLogger
3
4
  class << self
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  class Asciidoctor::Section
2
3
  def numbered_title opts = {}
3
4
  unless (@cached_numbered_title ||= nil)
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
  # TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
3
3
  require 'prawn'
4
4
  require_relative 'ttfunk_ext'
@@ -58,34 +58,37 @@ class Converter < ::Prawn::Document
58
58
  PageLayouts = [:portrait, :landscape]
59
59
  PageSides = [:recto, :verso]
60
60
  (PDFVersions = { '1.3' => 1.3, '1.4' => 1.4, '1.5' => 1.5, '1.6' => 1.6, '1.7' => 1.7 }).default = 1.4
61
- LF = %(\n)
62
- DoubleLF = %(\n\n)
63
- TAB = %(\t)
64
- InnerIndent = %(\n )
61
+ LF = ?\n
62
+ DoubleLF = LF * 2
63
+ TAB = ?\t
64
+ InnerIndent = LF + ' '
65
65
  # a no-break space is used to replace a leading space to prevent Prawn from trimming indentation
66
66
  # a leading zero-width space can't be used as it gets dropped when calculating the line width
67
- GuardedIndent = %(\u00a0)
68
- GuardedInnerIndent = %(\n\u00a0)
67
+ GuardedIndent = ?\u00a0
68
+ GuardedInnerIndent = LF + GuardedIndent
69
69
  TabRx = /\t/
70
70
  TabIndentRx = /^\t+/
71
- NoBreakSpace = %(\u00a0)
72
- NarrowNoBreakSpace = %(\u202f)
73
- ZeroWidthSpace = %(\u200b)
74
- DummyText = %(\u0000)
71
+ NoBreakSpace = ?\u00a0
72
+ ZeroWidthSpace = ?\u200b
73
+ DummyText = ?\u0000
75
74
  DotLeaderTextDefault = '. '
76
- EmDash = %(\u2014)
77
- RightPointer = %(\u25ba)
78
- LowercaseGreekA = %(\u03b1)
75
+ EmDash = ?\u2014
76
+ RightPointer = ?\u25ba
77
+ LowercaseGreekA = ?\u03b1
79
78
  Bullets = {
80
- disc: %(\u2022),
81
- circle: %(\u25e6),
82
- square: %(\u25aa),
79
+ disc: ?\u2022,
80
+ circle: ?\u25e6,
81
+ square: ?\u25aa,
83
82
  none: ''
84
83
  }
85
84
  # NOTE Default theme font uses ballot boxes from FontAwesome
86
85
  BallotBox = {
87
- checked: %(\u2611),
88
- unchecked: %(\u2610)
86
+ checked: ?\u2611,
87
+ unchecked: ?\u2610
88
+ }
89
+ ConumSets = {
90
+ 'circled' => (?\u2460..?\u2473).to_a,
91
+ 'filled' => (?\u2776..?\u277f).to_a + (?\u24eb..?\u24f4).to_a,
89
92
  }
90
93
  SimpleAttributeRefRx = /(?<!\\)\{\w+(?:[\-]\w+)*\}/
91
94
  MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|p[txc])?'
@@ -98,10 +101,14 @@ class Converter < ::Prawn::Document
98
101
  UriSchemeBoundaryRx = /(?<=:\/\/)/
99
102
  LineScanRx = /\n|.+/
100
103
  BlankLineRx = /\n{2,}/
101
- WhitespaceChars = %( \t\n)
104
+ WhitespaceChars = ' ' + TAB + LF
102
105
  SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
103
106
  PygmentsBgColorRx = /^\.highlight +{ *background: *#([^;]+);/
104
107
  ViewportWidth = ::Module.new
108
+ (TitleStyles = {
109
+ 'toc' => [:numbered_title],
110
+ 'basic' => [:title],
111
+ }).default = [:numbered_title, formal: true]
105
112
 
106
113
  def initialize backend, opts
107
114
  super
@@ -122,7 +129,6 @@ class Converter < ::Prawn::Document
122
129
 
123
130
  def convert node, name = nil, opts = {}
124
131
  method_name = %(convert_#{name ||= node.node_name})
125
- result = nil
126
132
  if respond_to? method_name
127
133
  # NOTE we prepend the prefix "convert_" to avoid conflict with Prawn methods
128
134
  result = send method_name, node
@@ -175,29 +181,16 @@ class Converter < ::Prawn::Document
175
181
  blk_0 = blk_1 = preface = nil
176
182
  end
177
183
 
178
- # NOTE on_page_create is called within a float context
179
- # NOTE on_page_create is not called for imported pages, front and back cover pages, and other image pages
180
- on_page_create do
181
- # NOTE we assume in prepress that physical page number reflects page side
182
- if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin
183
- set_page_margin next_page_margin
184
- end
185
- # TODO implement as a watermark (on top)
186
- if (bg_image = @page_bg_image[page_side])
187
- canvas { image bg_image[0], ({ position: :center, vposition: :center }.merge bg_image[1]) }
188
- elsif @page_bg_color && @page_bg_color != 'FFFFFF'
189
- fill_absolute_bounds @page_bg_color
190
- end
191
- end if respond_to? :on_page_create
184
+ on_page_create &(method :init_page)
192
185
 
193
186
  layout_cover_page doc, :front
194
187
  if (insert_title_page = doc.doctype == 'book' || (doc.attr? 'title-page'))
195
188
  layout_title_page doc
196
189
  # NOTE a new page will already be started if the cover image is a PDF
197
- start_new_page unless page_is_empty?
190
+ start_new_page unless page.empty?
198
191
  else
199
192
  # NOTE a new page will already be started if the cover image is a PDF
200
- start_new_page unless page_is_empty?
193
+ start_new_page unless page.empty?
201
194
  body_start_page_number = page_number
202
195
  if doc.header? && !doc.notitle
203
196
  theme_font :heading, level: 1 do
@@ -208,7 +201,7 @@ class Converter < ::Prawn::Document
208
201
  end
209
202
 
210
203
  # NOTE font must be set before toc dry run to ensure dry run size is accurate
211
- font @theme.base_font_family, size: @theme.base_font_size, style: @theme.base_font_style.to_sym
204
+ font @theme.base_font_family, size: @theme.base_font_size, style: (@theme.base_font_style || :normal).to_sym
212
205
 
213
206
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
214
207
  if (insert_toc = (doc.attr? 'toc') && doc.sections?)
@@ -271,7 +264,7 @@ class Converter < ::Prawn::Document
271
264
 
272
265
  # NOTE delete orphaned page (a page was created but there was no additional content)
273
266
  # QUESTION should we delete page if document is empty? (leaving no pages?)
274
- delete_page if page_is_empty? && page_count > 1
267
+ delete_page if page.empty? && page_count > 1
275
268
 
276
269
  toc_page_nums = insert_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages[1], toc_start) : []
277
270
 
@@ -322,27 +315,32 @@ class Converter < ::Prawn::Document
322
315
  else
323
316
  @ppbook = false
324
317
  end
325
- # QUESTION should ThemeLoader register fonts?
318
+ # QUESTION should ThemeLoader handle registering fonts instead?
326
319
  register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
320
+ default_kerning theme.base_font_kerning != 'none'
321
+ @fallback_fonts = [*theme.font_fallbacks]
327
322
  if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image[0]
328
323
  @page_bg_image = { verso: bg_image, recto: bg_image }
329
324
  else
330
325
  @page_bg_image = { verso: nil, recto: nil }
331
326
  end
332
327
  if (bg_image = resolve_background_image doc, theme, 'page-background-image-verso')
333
- @page_bg_image[:verso] = bg_image[0] ? bg_image : nil
328
+ @page_bg_image[:verso] = bg_image[0] && bg_image
334
329
  end
335
- if (bg_image = resolve_background_image doc, theme, 'page-background-image-recto') && bg_image[0]
336
- @page_bg_image[:recto] = bg_image[0] ? bg_image : nil
330
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image-recto')
331
+ @page_bg_image[:recto] = bg_image[0] && bg_image
337
332
  end
338
333
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
339
- @fallback_fonts = [*theme.font_fallbacks]
340
- @font_color = theme.base_font_color
334
+ @font_color = theme.base_font_color || '000000'
341
335
  @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
342
336
  @text_transform = nil
343
337
  @list_numerals = []
344
338
  @list_bullets = []
345
339
  @footnotes = []
340
+ @conum_glyphs = ConumSets[@theme.conum_glyphs || 'circled'] || (@theme.conum_glyphs.split ',').map {|r|
341
+ from, to = r.rstrip.split '-', 2
342
+ to ? ((get_char from)..(get_char to)).to_a : [(get_char from)]
343
+ }.flatten
346
344
  @index = IndexCatalog.new
347
345
  # NOTE we have to init Pdfmark class here while we have reference to the doc
348
346
  @pdfmark = (doc.attr? 'pdfmark') ? (Pdfmark.new doc) : nil
@@ -354,12 +352,22 @@ class Converter < ::Prawn::Document
354
352
  @theme ||= begin
355
353
  if (theme = doc.options[:pdf_theme])
356
354
  @themesdir = theme.__dir__ || (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir')
355
+ elsif (theme_name = (doc.attr 'pdf-theme') || (doc.attr 'pdf-style'))
356
+ theme = ThemeLoader.load_theme theme_name, (theme_dir = (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir'))
357
+ @themesdir = theme.__dir__
357
358
  else
358
- theme_name = (doc.attr 'pdf-theme') || (doc.attr 'pdf-style')
359
- theme = ThemeLoader.load_theme theme_name, ((doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir'))
359
+ theme = ThemeLoader.load_theme
360
360
  @themesdir = theme.__dir__
361
361
  end
362
362
  theme
363
+ rescue
364
+ if theme_dir
365
+ message = %(could not locate or load the pdf theme `#{theme_name}' in #{::File.absolute_path theme_dir})
366
+ else
367
+ message = %(could not locate or load the built-in pdf theme `#{theme_name}')
368
+ end
369
+ logger.error message
370
+ raise
363
371
  end
364
372
  end
365
373
 
@@ -472,6 +480,23 @@ class Converter < ::Prawn::Document
472
480
  info
473
481
  end
474
482
 
483
+ # NOTE init_page is called within a float context
484
+ # NOTE init_page is not called for imported pages, front and back cover pages, and other image pages
485
+ def init_page *args
486
+ # NOTE we assume in prepress that physical page number reflects page side
487
+ if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin
488
+ set_page_margin next_page_margin
489
+ end
490
+ # TODO implement as a watermark (on top)
491
+ if (bg_image = @page_bg_image[page_side])
492
+ canvas { image bg_image[0], ({ position: :center, vposition: :center }.merge bg_image[1]) }
493
+ page.tare_content_stream
494
+ elsif @page_bg_color && @page_bg_color != 'FFFFFF'
495
+ fill_absolute_bounds @page_bg_color
496
+ page.tare_content_stream
497
+ end
498
+ end
499
+
475
500
  def convert_section sect, opts = {}
476
501
  if sect.sectname == 'abstract'
477
502
  # HACK cheat a bit to hide this section from TOC; TOC should filter these sections
@@ -493,7 +518,7 @@ class Converter < ::Prawn::Document
493
518
  end
494
519
  else
495
520
  # FIXME smarter calculation here!!
496
- start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + (@theme.base_line_height_length * 1.5)
521
+ start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + ((@theme.base_line_height_length || (font_size * @theme.base_line_height)) * 1.5)
497
522
  end
498
523
  # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
499
524
  sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
@@ -711,7 +736,7 @@ class Converter < ::Prawn::Document
711
736
  vposition: label_valign,
712
737
  width: label_width,
713
738
  height: box_height,
714
- fallback_font_name: default_svg_font,
739
+ fallback_font_name: fallback_svg_font_name,
715
740
  enable_web_requests: allow_uri_read,
716
741
  enable_file_requests_with_root: (::File.dirname icon_path)
717
742
  if (icon_height = (svg_size = svg_obj.document.sizing).output_height) > box_height
@@ -962,9 +987,7 @@ class Converter < ::Prawn::Document
962
987
  end
963
988
  add_dest_for_block node if node.id
964
989
  @list_numerals ||= []
965
- # FIXME move \u2460 to constant (or theme setting)
966
- # \u2460 = circled one, \u24f5 = double circled one, \u278b = negative circled one
967
- @list_numerals << %(\u2460)
990
+ @list_numerals << 1
968
991
  #stroke_horizontal_rule @theme.caption_border_bottom_color
969
992
  line_metrics = calc_line_metrics @theme.base_line_height
970
993
  node.items.each_with_index do |item, idx|
@@ -980,13 +1003,13 @@ class Converter < ::Prawn::Document
980
1003
 
981
1004
  def convert_colist_item node
982
1005
  marker_width = nil
1006
+ @list_numerals << (index = @list_numerals.pop).next
983
1007
  theme_font :conum do
984
- marker_width = rendered_width_of_string %(#{conum_glyph 1}x)
1008
+ marker_width = rendered_width_of_string %(#{marker = conum_glyph index}x)
985
1009
  float do
986
1010
  bounding_box [0, cursor], width: marker_width do
987
- @list_numerals << (index = @list_numerals.pop).next
988
1011
  theme_font :conum do
989
- layout_prose index, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
1012
+ layout_prose marker, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
990
1013
  end
991
1014
  end
992
1015
  end
@@ -1014,26 +1037,25 @@ class Converter < ::Prawn::Document
1014
1037
  terms = [*terms]
1015
1038
  # NOTE don't orphan the terms, allow for at least one line of content
1016
1039
  # FIXME extract ensure_space (or similar) method
1017
- advance_page if cursor < @theme.base_line_height_length * (terms.size + 1)
1040
+ advance_page if cursor < (@theme.base_line_height_length || (font_size * @theme.base_line_height)) * (terms.size + 1)
1018
1041
  terms.each do |term|
1019
1042
  # FIXME layout_prose should pass style downward when parsing formatted text
1020
1043
  #layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
1021
- term_text = term.text
1022
- case @theme.description_list_term_font_style.to_sym
1023
- when :bold
1024
- term_text = %(<strong>#{term_text}</strong>)
1025
- when :italic
1026
- term_text = %(<em>#{term_text}</em>)
1027
- when :bold_italic
1028
- term_text = %(<strong><em>#{term_text}</em></strong>)
1044
+ case @theme.description_list_term_font_style.to_s
1045
+ when 'bold'
1046
+ term_text = %(<strong>#{term.text}</strong>)
1047
+ when 'italic'
1048
+ term_text = %(<em>#{term.text}</em>)
1049
+ when 'bold_italic'
1050
+ term_text = %(<strong><em>#{term.text}</em></strong>)
1051
+ else
1052
+ term_text = term.text
1029
1053
  end
1030
1054
  layout_prose term_text, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
1031
1055
  end
1032
- if desc
1033
- indent @theme.description_list_description_indent do
1034
- convert_content_for_list_item desc, :dlist_desc
1035
- end
1036
- end
1056
+ indent(@theme.description_list_description_indent || 0) do
1057
+ convert_content_for_list_item desc, :dlist_desc
1058
+ end if desc
1037
1059
  end
1038
1060
  end
1039
1061
  end
@@ -1136,12 +1158,12 @@ class Converter < ::Prawn::Document
1136
1158
  if node.style == 'unstyled'
1137
1159
  # unstyled takes away all indentation
1138
1160
  list_indent = 0
1139
- elsif (list_indent = @theme.outline_list_indent) > 0
1161
+ elsif (list_indent = @theme.outline_list_indent || 0) > 0
1140
1162
  # no-bullet aligns text with left-hand side of bullet position (as though there's no bullet)
1141
- list_indent = [list_indent - (rendered_width_of_string %(#{node.context == :ulist ? "\u2022" : '1.'}x)), 0].max
1163
+ list_indent = [list_indent - (rendered_width_of_string %(#{node.context == :ulist ? ?\u2022 : '1.'}x)), 0].max
1142
1164
  end
1143
1165
  else
1144
- list_indent = @theme.outline_list_indent
1166
+ list_indent = @theme.outline_list_indent || 0
1145
1167
  end
1146
1168
  indent list_indent do
1147
1169
  node.items.each do |item|
@@ -1154,8 +1176,7 @@ class Converter < ::Prawn::Document
1154
1176
  # However, don't leave gap at the bottom if list is nested in an outline list
1155
1177
  unless complex || (node.nested? && node.parent.parent.outline?)
1156
1178
  # correct bottom margin of last item
1157
- list_margin_bottom = @theme.prose_margin_bottom
1158
- margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
1179
+ margin_bottom((@theme.prose_margin_bottom || 0) - (@theme.outline_list_item_spacing || 0))
1159
1180
  end
1160
1181
  end
1161
1182
 
@@ -1264,7 +1285,7 @@ class Converter < ::Prawn::Document
1264
1285
  if ::File.readable? image_path
1265
1286
  # NOTE import_page automatically advances to next page afterwards
1266
1287
  # QUESTION should we add destination to top of imported page?
1267
- return import_page image_path, replace: page_is_empty? if image_format == 'pdf'
1288
+ return import_page image_path, replace: page.empty? if image_format == 'pdf'
1268
1289
  elsif image_format == 'pdf'
1269
1290
  logger.warn %(pdf to insert not found or not readable: #{image_path}) unless scratch?
1270
1291
  # QUESTION should we use alt text in this case?
@@ -1311,7 +1332,7 @@ class Converter < ::Prawn::Document
1311
1332
  svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
1312
1333
  position: alignment,
1313
1334
  width: width,
1314
- fallback_font_name: default_svg_font,
1335
+ fallback_font_name: fallback_svg_font_name,
1315
1336
  enable_web_requests: allow_uri_read,
1316
1337
  enable_file_requests_with_root: file_request_root
1317
1338
  rendered_w = (svg_size = svg_obj.document.sizing).output_width
@@ -1331,16 +1352,17 @@ class Converter < ::Prawn::Document
1331
1352
  rendered_w = (svg_size = svg_obj.resize height: (rendered_h = available_h)).output_width
1332
1353
  end
1333
1354
  end
1355
+ image_y = y
1334
1356
  add_dest_for_block node if node.id
1335
1357
  # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1336
1358
  # breakage occurs when running content (stamps) are added to page
1359
+ # seems to be resolved as of Prawn 2.2.2
1337
1360
  update_colors if graphic_state.color_space.empty?
1338
1361
  # NOTE prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
1339
- # NOTE cursor advanced automatically
1362
+ # NOTE cursor advances automatically
1340
1363
  svg_obj.draw
1341
1364
  if (link = node.attr 'link', nil, false)
1342
- link_box = [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)]
1343
- link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf }
1365
+ add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1344
1366
  end
1345
1367
  else
1346
1368
  # FIXME this code really needs to be better organized!
@@ -1360,21 +1382,19 @@ class Converter < ::Prawn::Document
1360
1382
  rendered_w, rendered_h = image_info.calc_image_dimensions height: (rendered_h = available_h)
1361
1383
  end
1362
1384
  end
1363
- # NOTE must calculate link position before embedding to get proper boundaries
1364
- if (link = node.attr 'link', nil, false)
1365
- img_x, img_y = image_position rendered_w, rendered_h, position: alignment
1366
- link_box = [img_x, (img_y - rendered_h), (img_x + rendered_w), img_y]
1367
- end
1368
- image_top = cursor
1385
+ image_y = y
1369
1386
  add_dest_for_block node if node.id
1370
1387
  # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1371
1388
  # breakage occurs when running content (stamps) are added to page
1389
+ # seems to be resolved as of Prawn 2.2.2
1372
1390
  update_colors if graphic_state.color_space.empty?
1373
1391
  # NOTE specify both width and height to avoid recalculation
1374
1392
  embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
1375
- link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf } if link
1393
+ if (link = node.attr 'link', nil, false)
1394
+ add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1395
+ end
1376
1396
  # NOTE Asciidoctor disables automatic advancement of cursor for raster images, so move cursor manually
1377
- move_down rendered_h if cursor == image_top
1397
+ move_down rendered_h if y == image_y
1378
1398
  end
1379
1399
  end
1380
1400
  layout_caption node, side: :bottom if node.title?
@@ -1629,7 +1649,7 @@ class Converter < ::Prawn::Document
1629
1649
  end
1630
1650
 
1631
1651
  pad_box @theme.code_padding do
1632
- typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
1652
+ typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height || @theme.base_line_height),
1633
1653
  # QUESTION should we require the code_font_color to be set?
1634
1654
  color: (@theme.code_font_color || @font_color),
1635
1655
  size: adjusted_font_size
@@ -1712,12 +1732,7 @@ class Converter < ::Prawn::Document
1712
1732
  end
1713
1733
 
1714
1734
  def conum_glyph number
1715
- # FIXME make starting glyph a constant and/or theme setting
1716
- # FIXME use lookup table for glyphs instead of relying on counting
1717
- # \u2460 = circled one, \u24f5 = double circled one, \u278b = negative circled one
1718
- glyph = %(\u2460)
1719
- (number - 1).times { glyph = glyph.next }
1720
- glyph
1735
+ @conum_glyphs[number - 1]
1721
1736
  end
1722
1737
 
1723
1738
  # Adds guards to preserve indentation
@@ -1952,7 +1967,7 @@ class Converter < ::Prawn::Document
1952
1967
  (alignment = (node.roles & BlockAlignmentNames)[-1])
1953
1968
  alignment = alignment.to_sym
1954
1969
  else
1955
- alignment = :left
1970
+ alignment = (theme.table_align || :left).to_sym
1956
1971
  end
1957
1972
 
1958
1973
  caption_side = (theme.table_caption_side || :top).to_sym
@@ -1960,7 +1975,8 @@ class Converter < ::Prawn::Document
1960
1975
 
1961
1976
  table_settings = {
1962
1977
  header: table_header,
1963
- position: alignment,
1978
+ # NOTE position is handled by this method
1979
+ position: :left,
1964
1980
  cell_style: {
1965
1981
  # NOTE the border color and style of the outer frame is set later
1966
1982
  border_color: table_grid_color,
@@ -1986,11 +2002,23 @@ class Converter < ::Prawn::Document
1986
2002
 
1987
2003
  theme_margin :block, :top
1988
2004
 
2005
+ left_padding = right_padding = nil
1989
2006
  table table_data, table_settings do
1990
2007
  # NOTE call width to capture resolved table width
1991
2008
  table_width = width
1992
2009
  caption_max_width = caption_max_width == 'fit-content' ? table_width : nil
1993
2010
  @pdf.layout_table_caption node, alignment, caption_max_width if node.title? && caption_side == :top
2011
+ # NOTE align using padding instead of bounding_box as prawn-table does
2012
+ # using a bounding_box across pages mangles the margin box of subsequent pages
2013
+ if alignment != :left && table_width != (this_bounds = @pdf.bounds).width
2014
+ if alignment == :center
2015
+ left_padding = right_padding = (this_bounds.width - width) * 0.5
2016
+ this_bounds.add_left_padding left_padding
2017
+ this_bounds.add_right_padding right_padding
2018
+ else # :right
2019
+ this_bounds.add_left_padding(left_padding = this_bounds.width - width)
2020
+ end
2021
+ end
1994
2022
  if grid == 'none' && frame == 'none'
1995
2023
  if table_header
1996
2024
  rows(0).tap do |r|
@@ -2049,6 +2077,10 @@ class Converter < ::Prawn::Document
2049
2077
  #end
2050
2078
  end
2051
2079
  end
2080
+ if left_padding
2081
+ bounds.subtract_left_padding left_padding
2082
+ bounds.subtract_right_padding right_padding if right_padding
2083
+ end
2052
2084
  layout_table_caption node, alignment, caption_max_width, caption_side if node.title? && caption_side == :bottom
2053
2085
  theme_margin :block, :bottom
2054
2086
  end
@@ -2078,7 +2110,7 @@ class Converter < ::Prawn::Document
2078
2110
  end
2079
2111
 
2080
2112
  if at_page_top?
2081
- if page_layout && page_layout != page.layout && page_is_empty?
2113
+ if page_layout && page_layout != page.layout && page.empty?
2082
2114
  delete_page
2083
2115
  advance_page layout: page_layout
2084
2116
  end
@@ -2244,22 +2276,29 @@ class Converter < ::Prawn::Document
2244
2276
  else
2245
2277
  size_attr = ''
2246
2278
  end
2247
- begin
2248
- if icon_set == 'fa'
2279
+ if icon_set == 'fa'
2280
+ # legacy name from Font Awesome < 5
2281
+ if (remapped_icon_name = resolve_legacy_icon_name icon_name)
2282
+ requested_icon_name = icon_name
2283
+ icon_set, icon_name = remapped_icon_name.split '-', 2
2284
+ glyph = (icon_font_data icon_set).unicode icon_name
2285
+ logger.info { %(#{requested_icon_name} icon found in deprecated fa icon set; using #{icon_name} from #{icon_set} icon set instead) }
2286
+ # new name in Font Awesome >= 5 (but document is configured to use fa icon set)
2287
+ else
2249
2288
  font_data = nil
2250
- resolved_icon_set = FontAwesomeIconSets.find {|candidate| (font_data = icon_font_data candidate).unicode icon_name rescue nil }
2251
- if resolved_icon_set
2289
+ if (resolved_icon_set = FontAwesomeIconSets.find {|candidate| (font_data = icon_font_data candidate).unicode icon_name rescue nil })
2252
2290
  icon_set = resolved_icon_set
2253
- logger.info { %(#{icon_name} icon found in deprecated fa icon set; use #{icon_set} icon set instead) }
2254
- else
2255
- raise
2291
+ glyph = font_data.unicode icon_name
2292
+ logger.info { %(#{icon_name} icon not found in deprecated fa icon set; using match found in #{resolved_icon_set} icon set instead) }
2256
2293
  end
2257
- else
2258
- font_data = icon_font_data icon_set
2259
2294
  end
2295
+ else
2296
+ glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2297
+ end
2298
+ if glyph
2260
2299
  # TODO support rotate and flip attributes
2261
- %(<font name="#{icon_set}"#{size_attr}>#{font_data.unicode icon_name}</font>)
2262
- rescue
2300
+ %(<font name="#{icon_set}"#{size_attr}>#{glyph}</font>)
2301
+ else
2263
2302
  logger.warn %(#{icon_name} is not a valid icon name in the #{icon_set} icon set)
2264
2303
  %([#{node.attr 'alt'}])
2265
2304
  end
@@ -2316,9 +2355,9 @@ class Converter < ::Prawn::Document
2316
2355
 
2317
2356
  def convert_inline_kbd node
2318
2357
  if (keys = node.attr 'keys').size == 1
2319
- %(<code>#{keys[0]}</code>)
2358
+ %(<key>#{keys[0]}</key>)
2320
2359
  else
2321
- keys.map {|key| %(<code>#{key}</code>+) }.join.chop
2360
+ keys.map {|key| %(<key>#{key}</key>) }.join @theme.key_separator || '+'
2322
2361
  end
2323
2362
  end
2324
2363
 
@@ -2347,9 +2386,9 @@ class Converter < ::Prawn::Document
2347
2386
  when :subscript
2348
2387
  open, close, is_tag = ['<sub>', '</sub>', true]
2349
2388
  when :double
2350
- open, close, is_tag = ['“', '”', false]
2389
+ open, close, is_tag = [?\u201c, ?\u201d, false]
2351
2390
  when :single
2352
- open, close, is_tag = ['‘', '’', false]
2391
+ open, close, is_tag = [?\u2018, ?\u2019, false]
2353
2392
  #when :asciimath, :latexmath
2354
2393
  else
2355
2394
  open, close, is_tag = [nil, nil, false]
@@ -2372,16 +2411,24 @@ class Converter < ::Prawn::Document
2372
2411
  def layout_title_page doc
2373
2412
  return unless doc.header? && !doc.notitle
2374
2413
 
2375
- prev_bg_image = @page_bg_image[side = page_side]
2414
+ # NOTE a new page may have already been started at this point, so decide what to do with it
2415
+ if page.empty?
2416
+ page.reset_content if (recycle = @ppbook ? verso_page? : true)
2417
+ elsif @ppbook && verso_page?
2418
+ start_new_page
2419
+ end
2420
+
2421
+ side = recycle ? page_side : (page_side page_number + 1)
2422
+ prev_bg_image = @page_bg_image[side]
2376
2423
  prev_bg_color = @page_bg_color
2377
- @page_bg_image[side] = (bg_image = resolve_background_image doc, @theme, 'title-page-background-image') && bg_image[0] ? bg_image : nil
2424
+ if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
2425
+ @page_bg_image[side] = bg_image[0] && bg_image
2426
+ end
2378
2427
  if (bg_color = resolve_theme_color :title_page_background_color)
2379
2428
  @page_bg_color = bg_color
2380
2429
  end
2381
- # NOTE a new page will already be started if the cover image is a PDF
2382
- start_new_page unless page_is_empty?
2383
- start_new_page if @ppbook && verso_page?
2384
- @page_bg_image[side] = prev_bg_image if prev_bg_image
2430
+ recycle ? (init_page self) : start_new_page
2431
+ @page_bg_image[side] = prev_bg_image if bg_image
2385
2432
  @page_bg_color = prev_bg_color if bg_color
2386
2433
 
2387
2434
  # IMPORTANT this is the first page created, so we need to set the base font
@@ -2540,7 +2587,7 @@ class Converter < ::Prawn::Document
2540
2587
  string = transform_text string, transform
2541
2588
  end
2542
2589
  margin_top top_margin
2543
- typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme[%(heading_h#{opts[:level]}_line_height)] || @theme.heading_line_height), {
2590
+ typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme[%(heading_h#{opts[:level]}_line_height)] || @theme.heading_line_height || @theme.base_line_height), {
2544
2591
  color: @font_color,
2545
2592
  inline_format: true,
2546
2593
  align: @base_align.to_sym
@@ -2646,10 +2693,12 @@ class Converter < ::Prawn::Document
2646
2693
  go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
2647
2694
  start_page_number = page_number
2648
2695
  @y = start_at if start_at
2649
- theme_font :heading, level: 2 do
2650
- theme_font :toc_title do
2651
- toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
2652
- layout_heading((doc.attr 'toc-title'), align: toc_title_align)
2696
+ unless (toc_title = doc.attr 'toc-title').nil_or_empty?
2697
+ theme_font :heading, level: 2 do
2698
+ theme_font :toc_title do
2699
+ toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
2700
+ layout_heading toc_title, align: toc_title_align
2701
+ end
2653
2702
  end
2654
2703
  end
2655
2704
  # QUESTION should we skip this whole method if num_levels < 0?
@@ -2779,6 +2828,7 @@ class Converter < ::Prawn::Document
2779
2828
  sectlevels = (@theme[%(#{periphery}_sectlevels)] || 2).to_i
2780
2829
  sections = doc.find_by(context: :section) {|sect| sect.level <= sectlevels && sect != header } || []
2781
2830
 
2831
+ title_method = TitleStyles[@theme[%(#{periphery}_title_style)]]
2782
2832
  # FIXME we need a proper model for all this page counting
2783
2833
  # FIXME we make a big assumption that part & chapter start on new pages
2784
2834
  # index parts, chapters and sections by the visual page number on which they start
@@ -2790,16 +2840,16 @@ class Converter < ::Prawn::Document
2790
2840
  page_num = (sect.attr 'pdf-page-start').to_i - skip_pagenums
2791
2841
  if is_book && ((sect_is_part = sect.part?) || sect.chapter?)
2792
2842
  if sect_is_part
2793
- part_start_pages[page_num] ||= (sect.numbered_title formal: true)
2843
+ part_start_pages[page_num] ||= sect.send(*title_method)
2794
2844
  else
2795
- chapter_start_pages[page_num] ||= (sect.numbered_title formal: true)
2845
+ chapter_start_pages[page_num] ||= sect.send(*title_method)
2796
2846
  if sect.sectname == 'appendix' && !part_start_pages.empty?
2797
2847
  # FIXME need a better way to indicate that part has ended
2798
2848
  part_start_pages[page_num] = ''
2799
2849
  end
2800
2850
  end
2801
2851
  else
2802
- sect_title = trailing_section_start_pages[page_num] = sect.numbered_title formal: true
2852
+ sect_title = trailing_section_start_pages[page_num] = sect.send(*title_method)
2803
2853
  section_start_pages[page_num] ||= sect_title
2804
2854
  end
2805
2855
  end
@@ -2923,7 +2973,15 @@ class Converter < ::Prawn::Document
2923
2973
  bounding_box [left, bounds.top - trim_styles[:padding][0] - trim_styles[:content_offset]], width: colwidth, height: trim_styles[:content_height] do
2924
2974
  # NOTE image vposition respects padding; use negative image_vertical_align value to revert
2925
2975
  image_opts = content[1].merge position: colspec[:align], vposition: trim_styles[:img_valign]
2926
- image content[0], image_opts rescue logger.warn %(could not embed image in running content: #{content[0]}; #{$!.message})
2976
+ begin
2977
+ image_info = image content[0], image_opts
2978
+ if (image_link = content[2])
2979
+ image_info = { width: image_info.scaled_width, height: image_info.scaled_height } unless image_opts[:format] == 'svg'
2980
+ add_link_to_image image_link, image_info, image_opts
2981
+ end
2982
+ rescue
2983
+ logger.warn %(could not embed image in running content: #{content[0]}; #{$!.message})
2984
+ end
2927
2985
  end
2928
2986
  end
2929
2987
  when ::String
@@ -3063,7 +3121,7 @@ class Converter < ::Prawn::Document
3063
3121
  if ::File.readable? (image_path = (ThemeLoader.resolve_theme_asset $1, @themesdir))
3064
3122
  image_attrs = (AttributeList.new $2).parse ['alt', 'width']
3065
3123
  image_opts = resolve_image_options image_path, image_attrs, container_size: [colspec_dict[side][position][:width], trim_styles[:content_height]], format: image_attrs['format']
3066
- side_content[position] = [image_path, image_opts]
3124
+ side_content[position] = [image_path, image_opts, image_attrs['link']]
3067
3125
  else
3068
3126
  # NOTE allows inline image handler to report invalid reference and replace with alt text
3069
3127
  side_content[position] = %(image:#{image_path}[#{$2}])
@@ -3131,7 +3189,9 @@ class Converter < ::Prawn::Document
3131
3189
  # FIXME link to title page if there's a cover page (skip cover page and ensure blank page)
3132
3190
  page title: doctitle, destination: (document.dest_top 1)
3133
3191
  end
3134
- page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) unless toc_page_nums.none?
3192
+ unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
3193
+ page title: toc_title, destination: (document.dest_top toc_page_nums.first)
3194
+ end
3135
3195
  # QUESTION any way to get add_outline_level to invoke in the context of the outline?
3136
3196
  document.add_outline_level self, doc.sections, num_levels
3137
3197
  end
@@ -3175,9 +3235,6 @@ class Converter < ::Prawn::Document
3175
3235
  (font_catalog || {}).each do |key, styles|
3176
3236
  register_font key => styles.map {|style, path| [style.to_sym, (font_path path, fonts_dir)]}.to_h
3177
3237
  end
3178
-
3179
- # FIXME read kerning setting from theme!
3180
- default_kerning true
3181
3238
  end
3182
3239
 
3183
3240
  def font_path font_file, fonts_dir
@@ -3185,8 +3242,8 @@ class Converter < ::Prawn::Document
3185
3242
  ::File.absolute_path font_file, fonts_dir
3186
3243
  end
3187
3244
 
3188
- def default_svg_font
3189
- @theme.svg_font_family || @theme.base_font_family
3245
+ def fallback_svg_font_name
3246
+ @theme.svg_fallback_font_family || @theme.svg_font_family || @theme.base_font_family
3190
3247
  end
3191
3248
 
3192
3249
  attr_reader :allow_uri_read
@@ -3231,7 +3288,7 @@ class Converter < ::Prawn::Document
3231
3288
  # Insert a margin space at the specified side unless cursor is at the top of the page.
3232
3289
  # Start a new page if n value is greater than remaining space on page.
3233
3290
  def margin n, side
3234
- unless n == 0 || at_page_top?
3291
+ unless (n || 0) == 0 || at_page_top?
3235
3292
  # NOTE use low-level cursor calculation to workaround cursor bug in column_box context
3236
3293
  if y - reference_bounds.absolute_bottom > n
3237
3294
  move_down n
@@ -3599,7 +3656,7 @@ class Converter < ::Prawn::Document
3599
3656
  image_opts = {
3600
3657
  enable_file_requests_with_root: (::File.dirname image_path),
3601
3658
  enable_web_requests: allow_uri_read,
3602
- fallback_font_name: default_svg_font,
3659
+ fallback_font_name: fallback_svg_font_name,
3603
3660
  format: 'svg',
3604
3661
  }
3605
3662
  else
@@ -3694,7 +3751,9 @@ class Converter < ::Prawn::Document
3694
3751
  str_to_pt width
3695
3752
  end
3696
3753
  elsif opts[:use_fallback] && (width = @theme.image_width)
3697
- if width.end_with? '%'
3754
+ if ::Numeric === width
3755
+ width
3756
+ elsif (width = width.to_s).end_with? '%'
3698
3757
  (width.to_f / 100) * max_width
3699
3758
  elsif opts[:support_vw] && (width.end_with? 'vw')
3700
3759
  (width.chomp 'vw').extend ViewportWidth
@@ -3739,6 +3798,33 @@ class Converter < ::Prawn::Document
3739
3798
  end
3740
3799
  end
3741
3800
 
3801
+ def add_link_to_image uri, image_info, image_opts
3802
+ image_width = image_info[:width]
3803
+ image_height = image_info[:height]
3804
+
3805
+ case image_opts[:position]
3806
+ when :center
3807
+ image_x = bounds.left_side + (bounds.width - image_width) * 0.5
3808
+ when :right
3809
+ image_x = bounds.right_side - image_width
3810
+ else # :left or not set
3811
+ image_x = bounds.left_side
3812
+ end
3813
+
3814
+ case image_opts[:vposition]
3815
+ when :top
3816
+ image_y = bounds.absolute_top
3817
+ when :center
3818
+ image_y = bounds.absolute_top - (bounds.height - image_height) * 0.5
3819
+ when :bottom
3820
+ image_y = bounds.absolute_bottom + image_height
3821
+ else
3822
+ image_y = y
3823
+ end unless (image_y = image_opts[:y])
3824
+
3825
+ link_annotation [image_x, (image_y - image_height), (image_x + image_width), image_y], Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: uri.as_pdf }
3826
+ end
3827
+
3742
3828
  # QUESTION is there a better way to do this?
3743
3829
  # I suppose we could have @tmp_files as an instance variable on converter instead
3744
3830
  # It might be sufficient to delete temporary files once per conversion
@@ -3778,6 +3864,10 @@ class Converter < ::Prawn::Document
3778
3864
  end
3779
3865
  end
3780
3866
 
3867
+ def get_char code
3868
+ (code.start_with? '\u') ? ([((code.slice 2, code.length).to_i 16)].pack 'U1') : code
3869
+ end
3870
+
3781
3871
  # QUESTION move to prawn/extensions.rb?
3782
3872
  def init_scratch_prototype
3783
3873
  @save_state = nil