asciidoctor-pdf 2.0.6 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be0ccb9b1f50f98f904f500c7b2bf0118157714825e28739ab80df39ff101bf9
4
- data.tar.gz: 1ce0fafde09b3e41ad86782397e0034bc8bd6f82d06dff01346bf055ead214d0
3
+ metadata.gz: 3fc9b0baae841d33e6205109c5aeacbb88bc196e6396b5dac8df187e0d209f9c
4
+ data.tar.gz: 21fd8ea4cebcb721dc273fa1a8f81104ec8f14f6a4c1a6f396bf6194f5e2309d
5
5
  SHA512:
6
- metadata.gz: ea8d2b78a501da0ce41cc9dabf88f19fe1926262725661012c4f5f00ffc10f34e1a479ca4cf6b66a6601d4f9f2ada928fc23eb0cb422b391503955f21b072a8d
7
- data.tar.gz: 3916e7d631167b57babd839b9f352932ca3df05022a4487195a2d8c8ee07165bdccb0127cd447d1e525f23a6bd2ef88d7710ad63075ec70c5763e5e7b8f6a237
6
+ metadata.gz: 956e66763693837a9483919f0b8b0428adbda5858efbfa75aa7b3a2fc04aff92a8e666643f7aae74ee01b656c1b9ad16984173dec4d0b0552bf668d0ad73d024
7
+ data.tar.gz: e6e2ca9d393d1eafbea531bdf9b828d50075bf398723ecca99549175331aa9f1422a9640128ccbf511e8ec97db3e7ce0c6d763a3bc28aeadf92fc6fd2a130f77
data/CHANGELOG.adoc CHANGED
@@ -5,6 +5,61 @@
5
5
  This document provides a high-level view of the changes to the {project-name} by release.
6
6
  For a detailed view of what has changed, refer to the {url-repo}/commits/main[commit history] on GitHub.
7
7
 
8
+ == 2.1.0 (2022-06-11) - @mojavelinux
9
+
10
+ Enhancements::
11
+
12
+ * arrange body of article or manpage doctype into multiple columns if `page-columns` key is set in theme (#327)
13
+ * allow column gap to be specified using `page-column-gap` key (#327)
14
+ * introduce `convert_index_categories` method to handle rendering of categories for index inside column box (#327)
15
+ * rename `convert_index_list` method to `convert_index_term` to make its purpose more clear (#327)
16
+ * add `save_theme` helper to work with a copy of the theme within a scope (#2196)
17
+ * add support for `scale` attribute or `iw` unit on `pdfwidth` attribute on image macros (#1933)
18
+ * add backlink from bibref on bibliography entry to first reference to that entry in the document (#1737)
19
+ * preserve text formatting on index term in index section (#897)
20
+ * don't insert page break between part and first chapter if `heading-part-break-after` key in theme is `avoid` (#1795)
21
+
22
+ === Details
23
+
24
+ {url-repo}/releases/tag/v2.1.0[git tag] | {url-repo}/compare/v2.0.8\...v2.1.0[full diff]
25
+
26
+ == 2.0.8 (2022-06-08) - @mojavelinux
27
+
28
+ Improvements::
29
+
30
+ * encapsulate logic to adjust column box inside float and dry run
31
+ * automatically set height on column box if not specified; update value automatically when reflowing margins
32
+
33
+ Bug Fixes::
34
+
35
+ * correctly compute value of to cursor on extent when column box starts below top of page (#2230)
36
+ * fix crash in `ColumnBox#move_past_bottom` when `:reflow_margins` option is not set
37
+ * fix x position of SVG when advanced to next column of column box
38
+ * `at_page_top?` should consider top of column box to be top of page
39
+ * prevent SVG image taller than column from being advanced to next column
40
+ * don't push section that follows index section in article to new page if last page of index does not extend to bottom of page
41
+
42
+ === Details
43
+
44
+ {url-repo}/releases/tag/v2.0.8[git tag] | {url-repo}/compare/v2.0.7\...v2.0.8[full diff]
45
+
46
+ == 2.0.7 (2022-06-03) - @mojavelinux
47
+
48
+ Improvements::
49
+
50
+ * don't recommend prawn-gmagick if PNG or JPG is corrupt or incomplete
51
+ * add helper method to determine when to recommend the prawn-gmagick gem
52
+
53
+ Bug Fixes::
54
+
55
+ * fix crash when doctitle or section title with automatic ID contains inline image without explicit width (#2228)
56
+ * use prawn-gmagick, if available, to read raster image referenced by SVG (#2223)
57
+ * allow image path in SVG to refer to any location within Asciidoctor jail (no restriction if safe mode is unsafe) (#1941)
58
+
59
+ === Details
60
+
61
+ {url-repo}/releases/tag/v2.0.7[git tag] | {url-repo}/compare/v2.0.6\...v2.0.7[full diff]
62
+
8
63
  == 2.0.6 (2022-05-30) - @mojavelinux
9
64
 
10
65
  Bug Fixes::
data/README.adoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = Asciidoctor PDF: A native PDF converter for AsciiDoc
2
2
  Dan Allen <https://github.com/mojavelinux[@mojavelinux]>; Sarah White <https://github.com/graphitefriction[@graphitefriction]>
3
- v2.0.6, 2022-05-30
3
+ v2.1.0, 2022-06-11
4
4
  // Settings:
5
5
  :experimental:
6
6
  :idprefix:
@@ -23,8 +23,7 @@ endif::[]
23
23
  :project-name: Asciidoctor PDF
24
24
  :project-handle: asciidoctor-pdf
25
25
  // Variables:
26
- :release-line: 2.0.x
27
- :release-version: 2.0.6
26
+ :release-line: 2.1.x
28
27
  // URLs:
29
28
  :url-gem: https://rubygems.org/gems/asciidoctor-pdf
30
29
  :url-project: https://github.com/asciidoctor/asciidoctor-pdf
@@ -47,7 +46,7 @@ The aim of this library is to take the pain out of creating PDF documents from A
47
46
 
48
47
  [NOTE]
49
48
  ====
50
- The documentation for Asciidoctor PDF {release-line} is now available at {url-project-docs}/.
49
+ The documentation for the latest, stable release of Asciidoctor PDF 2.0.x is available at {url-project-docs}/.
51
50
 
52
51
  If you're looking for the documentation for Asciidoctor PDF 1.6, refer to the {url-project-repo}/tree/v1.6.x#readme[README] in the v1.6.x branch.
53
52
  Asciidoctor PDF 1.6 is no longer being developed and will reach EOL later this year.
@@ -62,7 +61,7 @@ Asciidoctor PDF converts an AsciiDoc document directly to a PDF document.
62
61
  The style and layout of the PDF are controlled by a dedicated theme file.
63
62
  To the degree possible, Asciidoctor PDF supports all the features of AsciiDoc that are supported by Asciidoctor.
64
63
  It also provides {url-project-docs}/features/[PDF-specific features].
65
- However, there are {url-project-docs}/features/#limitations[certain limits] imposed by the PDF format and the PDF library this extension uses.
64
+ However, there are {url-project-docs}/features/#limitations[certain limitations] imposed by the PDF format and the PDF library this extension uses.
66
65
 
67
66
  Asciidoctor PDF uses the Prawn gem and Prawn's extensions, such as prawn-svg and prawn-table, to generate a PDF document.
68
67
  {url-prawn}[Prawn] is a general purpose PDF generator for Ruby that features high-level APIs for common needs like setting up the page and inserting images and low-level APIs for positioning and rendering text and graphics.
@@ -114,11 +113,11 @@ There are several optional features of this converter that require additional ge
114
113
  Those features are as follows.
115
114
 
116
115
  Source highlighting::
117
- You'll need to {url-project-repo}/syntax-highlighting/[install a syntax highlighter] to use source highlighting (build-time only).
116
+ You'll need to {url-project-docs}/syntax-highlighting/[install a syntax highlighter] to use source highlighting (build-time only).
118
117
 
119
118
  PDF optimization::
120
119
  If you want to optimize your PDF, you'll need rghost or hexapdf.
121
- See {url-project-repo}/optimize-pdf/[Optimize the PDF] for installation and usage instructions.
120
+ See {url-project-docs}/optimize-pdf/[Optimize the PDF] for installation and usage instructions.
122
121
 
123
122
  Automatic hyphenation::
124
123
  To turn on automatic hyphenation using the `hyphens` attribute, you'll need to install the `text-hyphen` gem:
@@ -128,7 +127,7 @@ To turn on automatic hyphenation using the `hyphens` attribute, you'll need to i
128
127
  Accelerated image decoding::
129
128
  Ruby is not particularly fast at decoding images, and the image formats it supports are limited.
130
129
  To help, you can install prawn-gmagick, which delegates the work of decoding images to GraphicsMagick.
131
- Refer to {url-project-repo}/image-paths-and-formats/#other-image-formats[Supporting additional image file formats] for instructions about how to enable this integration.
130
+ Refer to {url-project-docs}/image-paths-and-formats/#other-image-formats[Supporting additional image file formats] for instructions about how to enable this integration.
132
131
 
133
132
  Check the {url-project-docs}/install/#table-minimum-version[minimum supported version table] to make sure you're using a supported version of the dependency.
134
133
 
@@ -182,7 +181,7 @@ Asciidoctor creates the output file in the same directory as the input file by d
182
181
  Open the [.path]_basic-example.pdf_ file with a PDF viewer to see the result.
183
182
 
184
183
  .Example PDF document rendered in a PDF viewer
185
- image::docs/modules/ROOT/images/basic-example-pdf-screenshot.png[Screenshot of PDF document,960,540,pdfwidth=100%]
184
+ image::docs/modules/ROOT/images/basic-example-pdf-screenshot.png[Screenshot of PDF document,960,pdfwidth=100%]
186
185
 
187
186
  For more information about how to use Asciidoctor PDF and PDF-specific AsciiDoc syntax, see the {url-project-docs}/[Asciidoctor PDF documentation].
188
187
 
@@ -197,11 +196,6 @@ ifndef::env-site[]
197
196
  See the <<CONTRIBUTING.adoc#,contributing guide>>.
198
197
  To help develop {project-name}, or to simply use the development version, refer to the <<CONTRIBUTING-CODE.adoc#,developing and contributing code guide>>.
199
198
 
200
- [[resources,Links]]
201
- == Resources
202
-
203
- * https://groups.google.com/forum/#!msg/prawn-ruby/MbMsCx862iY/6ImCsvLGfVcJ[Discussion about image quality in PDFs]
204
-
205
199
  == Authors
206
200
 
207
201
  {project-name} was written by https://github.com/mojavelinux[Dan Allen] and https://github.com/graphitefriction[Sarah White] of OpenDevise Inc. on behalf of the Asciidoctor Project.
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'formatted_string'
3
4
  require_relative 'formatted_text'
4
5
  require_relative 'index_catalog'
5
6
  require_relative 'pdfmark'
@@ -20,6 +21,8 @@ module Asciidoctor
20
21
 
21
22
  attr_reader :cache_uri
22
23
 
24
+ attr_reader :jail_dir
25
+
23
26
  attr_accessor :font_color
24
27
 
25
28
  attr_accessor :font_scale
@@ -122,6 +125,7 @@ module Asciidoctor
122
125
  DropAnchorRx = %r(<(?:a\b[^>]*|/a)>)
123
126
  SourceHighlighters = %w(coderay pygments rouge).to_set
124
127
  ViewportWidth = ::Module.new
128
+ ImageWidth = ::Module.new
125
129
  (TitleStyles = {
126
130
  'toc' => [:numbered_title],
127
131
  'basic' => [:title],
@@ -150,7 +154,7 @@ module Asciidoctor
150
154
  log :warn, %(missing convert handler for #{name} node in #{@backend} backend)
151
155
  end
152
156
  # NOTE: inline node handlers generate HTML-like strings; all other handlers write directly to the PDF object
153
- ::Asciidoctor::Inline === node ? result : self
157
+ node.inline? ? result : self
154
158
  end
155
159
 
156
160
  def convert_document doc
@@ -160,7 +164,6 @@ module Asciidoctor
160
164
  doc.attributes['outline'] = '' unless (doc.attribute_locked? 'outline') || ((doc.instance_variable_get :@attributes_modified).include? 'outline')
161
165
  doc.attributes['outline-title'] = '' unless (doc.attribute_locked? 'outline-title') || ((doc.instance_variable_get :@attributes_modified).include? 'outline-title')
162
166
  doc.attributes['pagenums'] = '' unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
163
- #assign_missing_section_ids doc
164
167
 
165
168
  on_page_create(&(method :init_page))
166
169
 
@@ -168,7 +171,8 @@ module Asciidoctor
168
171
  # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
169
172
  ink_cover_page doc, :front
170
173
  has_front_cover = page_number > marked_page_number
171
- if (has_title_page = (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page')) && (start_title_page doc))
174
+ doctype = doc.doctype
175
+ if (has_title_page = (title_page_on = doctype == 'book' || (doc.attr? 'title-page')) && (start_title_page doc))
172
176
  # NOTE: the base font must be set before any content is written to the main or scratch document
173
177
  font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style
174
178
  if perform_on_single_page { ink_title_page doc }
@@ -277,12 +281,19 @@ module Asciidoctor
277
281
  doc.set_attr 'pdf-anchor', (derive_anchor_from_id doc.id, 'top')
278
282
  doc.set_attr 'pdf-page-start', page_number
279
283
 
280
- convert_section generate_manname_section doc if doc.doctype == 'manpage' && (doc.attr? 'manpurpose')
281
-
282
- traverse doc
283
-
284
- # NOTE: for a book, these are leftover footnotes; for an article this is everything
285
- outdent_section { ink_footnotes doc }
284
+ if doctype == 'book' || (columns = @theme.page_columns || 1) < 2
285
+ convert_section generate_manname_section doc if doctype == 'manpage' && (doc.attr? 'manpurpose')
286
+ traverse doc
287
+ # NOTE: for a book, these are leftover footnotes; for an article this is everything
288
+ outdent_section { ink_footnotes doc }
289
+ else
290
+ column_box [bounds.left, cursor], columns: columns, width: bounds.width, reflow_margins: true, spacer: @theme.page_column_gap do
291
+ convert_section generate_manname_section doc if doctype == 'manpage' && (doc.attr? 'manpurpose')
292
+ traverse doc
293
+ # NOTE: for a book, these are leftover footnotes; for an article this is everything
294
+ outdent_section { ink_footnotes doc }
295
+ end
296
+ end
286
297
 
287
298
  if (toc_extent = @toc_extent)
288
299
  if title_page_on && !insert_toc
@@ -336,7 +347,11 @@ module Asciidoctor
336
347
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
337
348
  ((::Prawn::Document.instance_method :initialize).bind self).call pdf_opts
338
349
  renderer.min_version (@pdf_version = PDFVersions[doc.attr 'pdf-version'])
339
- @media = doc.attr 'media', 'screen'
350
+ @tmp_files ||= {}
351
+ @allow_uri_read = doc.attr? 'allow-uri-read'
352
+ @cache_uri = doc.attr? 'cache-uri'
353
+ @jail_dir = doc.safe < ::Asciidoctor::SafeMode::SAFE ? nil : doc.base_dir
354
+ @media ||= doc.attr 'media', 'screen'
340
355
  @page_margin_by_side = { recto: (page_margin_recto = page_margin), verso: (page_margin_verso = page_margin), cover: page_margin }
341
356
  case doc.attr 'pdf-folio-placement', (@media == 'prepress' ? 'physical' : 'virtual')
342
357
  when 'physical'
@@ -361,13 +376,6 @@ module Asciidoctor
361
376
  else
362
377
  @ppbook = nil
363
378
  end
364
- # QUESTION: should ThemeLoader handle registering fonts instead?
365
- register_fonts theme.font_catalog, ((doc.attr 'pdf-fontsdir')&.sub '{docdir}', (doc.attr 'docdir')) || 'GEM_FONTS_DIR'
366
- default_kerning theme.base_font_kerning != 'none'
367
- @fallback_fonts = Array theme.font_fallbacks
368
- @allow_uri_read = doc.attr? 'allow-uri-read'
369
- @cache_uri = doc.attr? 'cache-uri'
370
- @tmp_files = {}
371
379
  if (bg_image = resolve_background_image doc, theme, 'page-background-image')&.first
372
380
  @page_bg_image = { verso: bg_image, recto: bg_image }
373
381
  else
@@ -380,6 +388,10 @@ module Asciidoctor
380
388
  @page_bg_image[:recto] = bg_image[0] && bg_image
381
389
  end
382
390
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
391
+ # QUESTION: should ThemeLoader handle registering fonts instead?
392
+ register_fonts theme.font_catalog, ((doc.attr 'pdf-fontsdir')&.sub '{docdir}', (doc.attr 'docdir')) || 'GEM_FONTS_DIR'
393
+ default_kerning theme.base_font_kerning != 'none'
394
+ @fallback_fonts = Array theme.font_fallbacks
383
395
  @root_font_size = theme.base_font_size
384
396
  @font_scale = 1
385
397
  @font_color = theme.base_font_color
@@ -400,6 +412,7 @@ module Asciidoctor
400
412
  @list_bullets = []
401
413
  @bottom_gutters = [{}]
402
414
  @rendered_footnotes = []
415
+ @bibref_refs = ::Set.new
403
416
  @conum_glyphs = ConumSets[@theme.conum_glyphs || 'circled'] || (@theme.conum_glyphs.split ',').map do |r|
404
417
  from, to = r.lstrip.split '-', 2
405
418
  to ? ((get_char from)..(get_char to)).to_a : [(get_char from)]
@@ -573,6 +586,8 @@ module Asciidoctor
573
586
  theme.base_font_style = theme.base_font_style&.to_sym || :normal
574
587
  theme.page_numbering_start_at ||= 'body'
575
588
  theme.running_content_start_at ||= 'body'
589
+ theme.heading_chapter_break_before ||= 'always'
590
+ theme.heading_part_break_before ||= 'always'
576
591
  theme.heading_margin_page_top ||= 0
577
592
  theme.heading_margin_top ||= 0
578
593
  theme.heading_margin_bottom ||= 0
@@ -607,6 +622,13 @@ module Asciidoctor
607
622
  theme
608
623
  end
609
624
 
625
+ def save_theme
626
+ @theme = (original_theme = theme).dup
627
+ yield
628
+ ensure
629
+ @theme = original_theme
630
+ end
631
+
610
632
  def indent_section
611
633
  if (values = @section_indent)
612
634
  indent(values[0], values[1]) { yield }
@@ -645,13 +667,14 @@ module Asciidoctor
645
667
  hidden = sect.option? 'notitle'
646
668
  hopts = { align: text_align, level: hlevel, part: part, chapterlike: chapterlike, outdent: !(part || chapterlike) }
647
669
  if part
648
- unless @theme.heading_part_break_before == 'auto'
670
+ if @theme.heading_part_break_before == 'always'
649
671
  started_new = true
650
672
  start_new_part sect
651
673
  end
652
674
  elsif chapterlike
653
- if @theme.heading_chapter_break_before != 'auto' ||
654
- (@theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0])
675
+ if (@theme.heading_chapter_break_before == 'always' &&
676
+ !(@theme.heading_part_break_after == 'avoid' && sect.first_section_of_part?)) ||
677
+ (@theme.heading_part_break_after == 'always' && sect.first_section_of_part?)
655
678
  started_new = true
656
679
  start_new_chapter sect
657
680
  end
@@ -698,49 +721,74 @@ module Asciidoctor
698
721
  end
699
722
 
700
723
  def convert_index_section node
701
- space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
702
- pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style'
703
- column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do
704
- @index.categories.each do |category|
705
- bounds.move_past_bottom if space_needed_for_category > cursor
706
- ink_prose category.name,
707
- align: :left,
708
- inline_format: false,
709
- margin_bottom: @theme.description_list_term_spacing,
710
- style: @theme.description_list_term_font_style&.to_sym
711
- category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
712
- @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
724
+ if ColumnBox === bounds || (columns = @theme.index_columns || 1) < 2
725
+ convert_index_categories @index.categories, (node.document.attr 'index-pagenum-sequence-style')
726
+ else
727
+ end_cursor = nil
728
+ column_box [bounds.left, cursor], columns: columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do
729
+ convert_index_categories @index.categories, (node.document.attr 'index-pagenum-sequence-style')
730
+ end_cursor = cursor if bounds.current_column == 0
713
731
  end
732
+ # Q: could we move this logic into column_box?
733
+ move_cursor_to end_cursor if end_cursor
714
734
  end
715
735
  nil
716
736
  end
717
737
 
718
- def convert_index_list_item term, pagenum_sequence_style = nil
719
- text = escape_xml term.name
738
+ def convert_index_categories categories, pagenum_sequence_style = nil
739
+ space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
740
+ categories.each do |category|
741
+ bounds.move_past_bottom if space_needed_for_category > cursor
742
+ ink_prose category.name,
743
+ align: :left,
744
+ inline_format: false,
745
+ margin_bottom: @theme.description_list_term_spacing,
746
+ style: @theme.description_list_term_font_style&.to_sym
747
+ category.terms.each {|term| convert_index_term term, pagenum_sequence_style }
748
+ @theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
749
+ end
750
+ end
751
+
752
+ def convert_index_term term, pagenum_sequence_style = nil
753
+ term_fragments = term.name.fragments
720
754
  unless term.container?
755
+ pagenum_fragment = (parse_text %(<a>#{DummyText}</a>), inline_format: true)[0]
721
756
  if @media == 'screen'
722
757
  case pagenum_sequence_style
723
758
  when 'page'
724
- pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
759
+ pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| pagenum_fragment.merge anchor: dest[:anchor], text: dest[:page] }
725
760
  when 'range'
726
761
  first_anchor_per_page = {}.tap {|accum| term.dests.each {|dest| accum[dest[:page]] ||= dest[:anchor] } }
727
762
  pagenums = (consolidate_ranges first_anchor_per_page.keys).map do |range|
728
763
  anchor = first_anchor_per_page[(range.include? '-') ? (range.partition '-')[0] : range]
729
- %(<a anchor="#{anchor}">#{range}</a>)
764
+ pagenum_fragment.merge text: range, anchor: anchor
730
765
  end
731
766
  else # term
732
- pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
767
+ pagenums = term.dests.map {|dest| pagenum_fragment.merge text: dest[:page], anchor: dest[:anchor] }
733
768
  end
734
769
  else
735
770
  pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq
736
771
  end
737
- text = %(#{text}, #{pagenums.join ', '})
772
+ pagenums.each do |pagenum|
773
+ # NOTE: addresses a very minor kerning issue for text adjacent to the comma
774
+ if (prev_fragment = term_fragments[-1]).size == 1
775
+ if ::String === pagenum
776
+ term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, #{pagenum})
777
+ next
778
+ else
779
+ term_fragments[-1] = prev_fragment.merge text: %(#{prev_fragment[:text]}, )
780
+ end
781
+ else
782
+ term_fragments << ({ text: ', ' })
783
+ end
784
+ term_fragments << (::String === pagenum ? { text: pagenum } : pagenum)
785
+ end
738
786
  end
739
787
  subterm_indent = @theme.description_list_description_indent
740
- ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
788
+ typeset_formatted_text term_fragments, (calc_line_metrics @base_line_height), align: :left, color: @font_color, hanging_indent: subterm_indent * 2
741
789
  indent subterm_indent do
742
790
  term.subterms.each do |subterm|
743
- convert_index_list_item subterm, pagenum_sequence_style
791
+ convert_index_term subterm, pagenum_sequence_style
744
792
  end
745
793
  end unless term.leaf?
746
794
  end
@@ -936,7 +984,7 @@ module Asciidoctor
936
984
  height: label_height,
937
985
  fallback_font_name: fallback_svg_font_name,
938
986
  enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
939
- enable_file_requests_with_root: (::File.dirname icon_path),
987
+ enable_file_requests_with_root: { base: (::File.dirname icon_path), root: @jail_dir },
940
988
  cache_images: cache_uri
941
989
  svg_obj.resize height: label_height if svg_obj.document.sizing.output_height > label_height
942
990
  svg_obj.draw
@@ -1709,9 +1757,14 @@ module Asciidoctor
1709
1757
  return on_image_error :missing, node, target, (opts.merge align: alignment) unless image_path
1710
1758
 
1711
1759
  # TODO: support cover (aka canvas) image layout using "canvas" (or "cover") role
1712
- width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
1713
- # TODO: add `to_pt page_width` method to ViewportWidth type
1714
- width = (width.to_f / 100) * page_width if ViewportWidth === width
1760
+ case (width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true)
1761
+ when ViewportWidth
1762
+ # TODO: add `to_pt page_width` method to ViewportWidth type
1763
+ width = page_width * (width.to_f / 100)
1764
+ when ImageWidth
1765
+ scale = width.to_f / 100
1766
+ width = nil
1767
+ end
1715
1768
 
1716
1769
  caption_end = @theme.image_caption_end&.to_sym || :bottom
1717
1770
  caption_max_width = @theme.image_caption_max_width
@@ -1731,7 +1784,7 @@ module Asciidoctor
1731
1784
  file_request_root = false
1732
1785
  else
1733
1786
  svg_data = ::File.read image_path, mode: 'r:UTF-8'
1734
- file_request_root = ::File.dirname image_path
1787
+ file_request_root = { base: (::File.dirname image_path), root: @jail_dir }
1735
1788
  end
1736
1789
  svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
1737
1790
  position: alignment,
@@ -1741,7 +1794,9 @@ module Asciidoctor
1741
1794
  enable_file_requests_with_root: file_request_root,
1742
1795
  cache_images: cache_uri
1743
1796
  rendered_w = (svg_size = svg_obj.document.sizing).output_width
1744
- if !width && (svg_obj.document.root.attributes.key? 'width') && rendered_w > available_w
1797
+ if scale
1798
+ svg_size = svg_obj.resize width: (rendered_w = [available_w, rendered_w * scale].min)
1799
+ elsif !width && (svg_obj.document.root.attributes.key? 'width') && rendered_w > available_w
1745
1800
  # NOTE: restrict width to available width (prawn-svg already coerces to pixels)
1746
1801
  svg_size = svg_obj.resize width: (rendered_w = available_w)
1747
1802
  end
@@ -1749,6 +1804,7 @@ module Asciidoctor
1749
1804
  if (rendered_h = svg_size.output_height) > (available_h = cursor - caption_h)
1750
1805
  unless pinned || at_page_top?
1751
1806
  advance_page
1807
+ (svg_obj.options[:at] = svg_obj.position)[0] += bounds.left if ColumnBox === bounds
1752
1808
  available_h = cursor - caption_h
1753
1809
  end
1754
1810
  rendered_w = (svg_obj.resize height: (rendered_h = available_h)).output_width if rendered_h > available_h
@@ -1774,8 +1830,10 @@ module Asciidoctor
1774
1830
  image_obj, image_info = ::Base64 === image_path ?
1775
1831
  ::StringIO.open((::Base64.decode64 image_path), 'rb') {|fd| build_image_object fd } :
1776
1832
  ::File.open(image_path, 'rb') {|fd| build_image_object fd }
1833
+ actual_w = to_pt image_info.width, :px
1834
+ width = actual_w * scale if scale
1777
1835
  # NOTE: if width is not specified, scale native width & height from px to pt and restrict width to available width
1778
- rendered_w, rendered_h = image_info.calc_image_dimensions width: (width || [available_w, (to_pt image_info.width, :px)].min)
1836
+ rendered_w, rendered_h = image_info.calc_image_dimensions width: (width || [available_w, actual_w].min)
1779
1837
  # NOTE: shrink image so it fits within available space; group image & caption
1780
1838
  if rendered_h > (available_h = cursor - caption_h)
1781
1839
  unless pinned || at_page_top?
@@ -1811,7 +1869,7 @@ module Asciidoctor
1811
1869
  end
1812
1870
  rescue => e
1813
1871
  raise if ::StopIteration === e
1814
- on_image_error :exception, node, target, (opts.merge align: alignment, message: %(could not embed image: #{image_path}; #{e.message}#{::Prawn::Errors::UnsupportedImageType === e && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
1872
+ on_image_error :exception, node, target, (opts.merge align: alignment, message: %(could not embed image: #{image_path}; #{e.message}#{(recommend_prawn_gmagick? e, image_format) ? %(; install prawn-gmagick gem to add support for #{image_format&.upcase || 'unknown'} image format) : ''}))
1815
1873
  end
1816
1874
  end
1817
1875
 
@@ -2373,9 +2431,12 @@ module Asciidoctor
2373
2431
  if (text = ref.xreftext node.attr 'xrefstyle', nil, true)&.include? '<a'
2374
2432
  text = text.gsub DropAnchorRx, ''
2375
2433
  end
2434
+ if ref.inline? && ref.type == :bibref && !scratch? && (@bibref_refs.add? refid)
2435
+ anchor = %(<a id="_bibref_ref_#{refid}">#{DummyText}</a>)
2436
+ end
2376
2437
  @resolving_xref = nil
2377
2438
  end
2378
- %(<a anchor="#{derive_anchor_from_id refid}">#{text || "[#{refid}]"}</a>).gsub ']', '&#93;'
2439
+ %(#{anchor || ''}<a anchor="#{derive_anchor_from_id refid}">#{text || "[#{refid}]"}</a>).gsub ']', '&#93;'
2379
2440
  else
2380
2441
  %(<a anchor="#{doc.attr 'pdf-anchor'}">#{node.text || '[^top&#93;'}</a>)
2381
2442
  end
@@ -2383,10 +2444,12 @@ module Asciidoctor
2383
2444
  # NOTE: destination is created inside callback registered by FormattedTextTransform#build_fragment
2384
2445
  %(<a id="#{node.id}">#{DummyText}</a>)
2385
2446
  when :bibref
2386
- # NOTE: destination is created inside callback registered by FormattedTextTransform#build_fragment
2447
+ id = node.id
2387
2448
  # NOTE: technically node.text should be node.reftext, but subs have already been applied to text
2388
- reftext = (reftext = node.reftext) ? %([#{reftext}]) : %([#{node.id}])
2389
- %(<a id="#{node.id}">#{DummyText}</a>#{reftext})
2449
+ reftext = (reftext = node.reftext) ? %([#{reftext}]) : %([#{id}])
2450
+ reftext = %(<a anchor="_bibref_ref_#{id}">#{reftext}</a>) if @bibref_refs.include? id
2451
+ # NOTE: destination is created inside callback registered by FormattedTextTransform#build_fragment
2452
+ %(<a id="#{id}">#{DummyText}</a>#{reftext})
2390
2453
  else
2391
2454
  log :warn, %(unknown anchor type: #{node.type.inspect})
2392
2455
  nil
@@ -2505,14 +2568,25 @@ module Asciidoctor
2505
2568
  # NOTE: an image with a data URI is handled using a temporary file
2506
2569
  elsif (image_path = resolve_image_path node, target, image_format)
2507
2570
  if ::File.readable? image_path
2571
+ class_attr = (role = node.role) ? %( class="#{role}") : ''
2572
+ fit_attr = (fit = node.attr 'fit') ? %( fit="#{fit}") : ''
2508
2573
  if (width = resolve_explicit_width node.attributes)
2509
- width += (intrinsic_image_dimensions image_path, image_format)[:width].to_s if node.parent.context == :table_cell && ::String === width && (width.end_with? '%')
2574
+ if ImageWidth === width
2575
+ if state # check that converter is initialized
2576
+ width = (intrinsic_image_width image_path, image_format) * (width.to_f / 100)
2577
+ else
2578
+ width = %(auto*#{width})
2579
+ end
2580
+ elsif node.parent.context == :table_cell && ::String === width && (width.end_with? '%')
2581
+ width += (intrinsic_image_width image_path, image_format).to_s
2582
+ end
2583
+ width_attr = %( width="#{width}")
2584
+ elsif state # check that converter is initialized
2585
+ width_attr = %( width="#{intrinsic_image_width image_path, image_format}")
2510
2586
  else
2511
- width = (intrinsic_image_dimensions image_path, image_format)[:width]
2587
+ width_attr = ' width="auto"' # defer operation until arranger runs
2512
2588
  end
2513
- class_attr = (role = node.role) ? %( class="#{role}") : ''
2514
- fit_attr = (fit = node.attr 'fit') ? %( fit="#{fit}") : ''
2515
- img = %(<img src="#{image_path}" format="#{image_format}" alt="#{encode_quotes node.attr 'alt'}" width="#{width}"#{class_attr}#{fit_attr}>)
2589
+ img = %(<img src="#{image_path}" format="#{image_format}" alt="#{encode_quotes node.attr 'alt'}"#{width_attr}#{class_attr}#{fit_attr}>)
2516
2590
  else
2517
2591
  log :warn, %(image to embed not found or not readable: #{image_path})
2518
2592
  img = %([#{node.attr 'alt'}&#93;)
@@ -2529,17 +2603,20 @@ module Asciidoctor
2529
2603
  if scratch?
2530
2604
  visible ? node.text : ''
2531
2605
  else
2532
- # NOTE: initialize index in case converter is called before PDF is initialized
2533
- @index ||= IndexCatalog.new
2606
+ unless defined? @index
2607
+ # NOTE: initialize index and text formatter in case converter is called before PDF is initialized
2608
+ @index = IndexCatalog.new
2609
+ @text_formatter = FormattedText::Formatter.new theme: (load_theme node.document)
2610
+ end
2534
2611
  # NOTE: page number (:page key) is added by InlineDestinationMarker
2535
2612
  dest = { anchor: (anchor_name = @index.next_anchor_name) }
2536
2613
  anchor = %(<a id="#{anchor_name}" type="indexterm"#{visible ? ' visible="true"' : ''}>#{DummyText}</a>)
2537
2614
  if visible
2538
2615
  visible_term = node.text
2539
- @index.store_primary_term (sanitize visible_term), dest
2616
+ @index.store_primary_term (FormattedString.new parse_text visible_term, inline_format: [normalize: true]), dest
2540
2617
  %(#{anchor}#{visible_term})
2541
2618
  else
2542
- @index.store_term (node.attr 'terms').map {|term| sanitize term }, dest
2619
+ @index.store_term (node.attr 'terms').map {|term| FormattedString.new parse_text term, inline_format: [normalize: true] }, dest
2543
2620
  anchor
2544
2621
  end
2545
2622
  end
@@ -2566,8 +2643,6 @@ module Asciidoctor
2566
2643
  end
2567
2644
 
2568
2645
  def convert_inline_quoted node
2569
- theme = load_theme node.document
2570
-
2571
2646
  case node.type
2572
2647
  when :emphasis
2573
2648
  open, close, is_tag = ['<em>', '</em>', true]
@@ -2580,25 +2655,26 @@ module Asciidoctor
2580
2655
  when :subscript
2581
2656
  open, close, is_tag = ['<sub>', '</sub>', true]
2582
2657
  when :double
2583
- open, close, is_tag = [theme.quotes[0], theme.quotes[1], false]
2658
+ open, close = (load_theme node.document).quotes.slice 0, 2
2584
2659
  quotes = true
2585
2660
  when :single
2586
- open, close, is_tag = [theme.quotes[2], theme.quotes[3], false]
2661
+ open, close = (load_theme node.document).quotes.slice 2, 2
2587
2662
  quotes = true
2588
2663
  when :mark
2589
2664
  open, close, is_tag = ['<mark>', '</mark>', true]
2590
2665
  else
2591
- open, close, is_tag = [nil, nil, false]
2666
+ open = close = ''
2592
2667
  end
2593
2668
 
2594
2669
  inner_text = node.text
2595
2670
 
2596
- if quotes && (len = inner_text.length) > 3 &&
2597
- (inner_text.end_with? '...') && !((inner_text_trunc = inner_text.slice 0, len - 3).end_with? ?\\)
2671
+ if quotes && (len = inner_text.length) > 3 && (inner_text.end_with? '...') &&
2672
+ !((inner_text_trunc = inner_text.slice 0, len - 3).end_with? ?\\)
2598
2673
  inner_text = inner_text_trunc + '&#8230;'
2599
2674
  end
2600
2675
 
2601
2676
  if (roles = node.role)
2677
+ theme = load_theme node.document
2602
2678
  roles.split.each do |role|
2603
2679
  if (text_transform = theme[%(role_#{role}_text_transform)])
2604
2680
  inner_text = transform_text inner_text, text_transform
@@ -3838,6 +3914,31 @@ module Asciidoctor
3838
3914
  end
3839
3915
  end
3840
3916
 
3917
+ # Retrieve the intrinsic image dimensions for the specified path in pt.
3918
+ #
3919
+ # Returns a Hash containing :width and :height keys that map to the image's
3920
+ # intrinsic width and height values (in pt).
3921
+ def intrinsic_image_dimensions path, format
3922
+ if format == 'svg'
3923
+ # NOTE: prawn-svg automatically converts intrinsic width and height to pt
3924
+ img_obj = ::Prawn::SVG::Interface.new (::File.read path, mode: 'r:UTF-8'), self, {}
3925
+ img_size = img_obj.document.sizing
3926
+ { width: img_size.output_width, height: img_size.output_height }
3927
+ else
3928
+ # NOTE: build_image_object caches image data previously loaded
3929
+ # NOTE: build_image_object computes intrinsic width and height in px
3930
+ _, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
3931
+ { width: (to_pt img_size.width, :px), height: (to_pt img_size.height, :px) }
3932
+ end
3933
+ rescue
3934
+ # NOTE: image can't be read, so it won't be used anyway
3935
+ { width: 0, height: 0 }
3936
+ end
3937
+
3938
+ def intrinsic_image_width path, format
3939
+ (intrinsic_image_dimensions path, format)[:width]
3940
+ end
3941
+
3841
3942
  # Sends the specified message to the log unless this method is called from the scratch document
3842
3943
  def log severity, message = nil, &block
3843
3944
  logger.send severity, message, &block unless scratch?
@@ -4009,11 +4110,15 @@ module Asciidoctor
4009
4110
  if attrs.key? 'pdfwidth'
4010
4111
  if (width = attrs['pdfwidth']).end_with? '%'
4011
4112
  bounds_width ? (width.to_f / 100) * bounds_width : width
4113
+ elsif width.end_with? 'iw'
4114
+ (width.chomp 'iw').extend ImageWidth
4012
4115
  elsif opts[:support_vw] && (width.end_with? 'vw')
4013
4116
  (width.chomp 'vw').extend ViewportWidth
4014
4117
  else
4015
4118
  str_to_pt width
4016
4119
  end
4120
+ elsif attrs.key? 'scale'
4121
+ attrs['scale'].dup.extend ImageWidth
4017
4122
  elsif attrs.key? 'scaledwidth'
4018
4123
  # NOTE: the parser automatically appends % if value is unitless
4019
4124
  if (width = attrs['scaledwidth']).end_with? '%'
@@ -4046,7 +4151,7 @@ module Asciidoctor
4046
4151
  def resolve_image_options image_path, image_format, image_attrs, opts = {}
4047
4152
  if image_format == 'svg'
4048
4153
  image_opts = {
4049
- enable_file_requests_with_root: (::File.dirname image_path),
4154
+ enable_file_requests_with_root: { base: (::File.dirname image_path), root: @jail_dir },
4050
4155
  enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
4051
4156
  cache_images: cache_uri,
4052
4157
  fallback_font_name: fallback_svg_font_name,
@@ -4133,6 +4238,7 @@ module Asciidoctor
4133
4238
  else
4134
4239
  imagesdir = relative_to
4135
4240
  end
4241
+ @tmp_files ||= {}
4136
4242
  # NOTE: base64 logic currently used for inline images
4137
4243
  if ::Base64 === image_path
4138
4244
  return @tmp_files[image_path] if @tmp_files.key? image_path
@@ -30,4 +30,8 @@ class Asciidoctor::Section
30
30
  end
31
31
  opts[:formal] ? @cached_formal_numbered_title : @cached_numbered_title
32
32
  end unless method_defined? :numbered_title
33
+
34
+ def first_section_of_part?
35
+ (par = @parent).context == :section && par.sectname == 'part' && self == par.blocks.find {|it| it.context == :section }
36
+ end unless method_defined? :first_section_of_part?
33
37
  end
@@ -1,20 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Prawn::Document::ColumnBox.prepend (Module.new do
4
- def absolute_bottom
5
- stretchy? ? @parent.absolute_bottom : super
6
- end
4
+ attr_accessor :current_column
7
5
 
8
6
  def move_past_bottom
9
7
  (doc = @document).y = @y
10
8
  return if (@current_column = (@current_column + 1) % @columns) > 0
11
- @y = (par = @parent).absolute_top if @reflow_margins
9
+ par = @parent
10
+ if (reset_y = @reflow_margins) && (reset_y == true || reset_y > doc.page_number)
11
+ @y = par.absolute_top
12
+ @height = par.height unless stretchy?
13
+ end
12
14
  initial_margins = doc.page.margins
13
15
  par.move_past_bottom
14
16
  if doc.page.margins != initial_margins
15
- doc.bounds = self.class.new doc, par, (margin_box = doc.margin_box).absolute_top_left,
16
- columns: @columns, reflow_margins: true, spacer: @spacer, width: margin_box.width
17
+ doc.bounds = self.class.new doc, par, [(margin_box = doc.margin_box).absolute_left, @y],
18
+ columns: @columns, reflow_margins: @reflow_margins, spacer: @spacer, width: margin_box.width, height: @height
19
+ end
20
+ nil
21
+ end
22
+
23
+ # Rearranges the column box into a single column, where the original columns are in a single file. Used
24
+ # for the purpose of computing the extent of content in a scratch document.
25
+ def single_file
26
+ if @reflow_margins && @parent.absolute_top > @y && @columns > @current_column + 1
27
+ # defer reflow margins until all columns on current page have been exhausted
28
+ @reflow_margins = @document.page_number + (@columns - @current_column)
17
29
  end
30
+ @width = bare_column_width
31
+ @columns = 1
32
+ @current_column = 0
18
33
  nil
19
34
  end
20
35
  end)
@@ -251,16 +251,17 @@ module Asciidoctor
251
251
  # Returns whether the cursor is at the top of the page (i.e., margin box).
252
252
  #
253
253
  def at_page_top?
254
- @y == @margin_box.absolute_top
254
+ @y == (ColumnBox === bounds ? bounds : @margin_box).absolute_top
255
255
  end
256
256
 
257
257
  # Prevents at_page_top? from returning true while yielding to the specified block.
258
258
  #
259
259
  def conceal_page_top
260
- @margin_box.instance_variable_set :@y, (old_top = @margin_box.absolute_top) + 0.0001
260
+ old_top = (outer_bounds = ColumnBox === bounds ? bounds : @margin_box).absolute_top
261
+ outer_bounds.instance_variable_set :@y, old_top + 0.0001
261
262
  yield
262
263
  ensure
263
- @margin_box.instance_variable_set :@y, old_top
264
+ outer_bounds.instance_variable_set :@y, old_top
264
265
  end
265
266
 
266
267
  # Returns whether the current page is the last page in the document.
@@ -560,11 +561,11 @@ module Asciidoctor
560
561
  def float
561
562
  original_page_number = page_number
562
563
  original_y = y
563
- original_column = bounds.instance_variable_get :@current_column if ColumnBox === bounds
564
+ original_column = bounds.current_column if ColumnBox === bounds
564
565
  yield
565
566
  go_to_page original_page_number unless page_number == original_page_number
566
567
  self.y = original_y
567
- bounds.instance_variable_set :@current_column, original_column if original_column
568
+ bounds.current_column = original_column if original_column
568
569
  end
569
570
 
570
571
  # Short-circuits the call to the built-in move_up operation
@@ -721,6 +722,12 @@ module Asciidoctor
721
722
  end
722
723
  end
723
724
 
725
+ # Wraps the column_box method and automatically sets the height unless the :height option is specified.
726
+ def column_box point, options, &block
727
+ options[:height] = cursor unless options.key? :height
728
+ super
729
+ end
730
+
724
731
  # A flowing version of bounding_box. If the content runs to another page, the cursor starts at
725
732
  # the top of the page instead of from the original cursor position. Similar to span, except
726
733
  # the :position option is limited to a numeric value and additional options are passed through
@@ -1146,10 +1153,7 @@ module Asciidoctor
1146
1153
  scratch_pdf.bounds = bounds.dup.tap do |bounds_copy|
1147
1154
  bounds_copy.instance_variable_set :@document, scratch_pdf
1148
1155
  bounds_copy.instance_variable_set :@parent, saved_bounds
1149
- if ColumnBox === bounds_copy
1150
- bounds_copy.instance_variable_set :@width, bounds_copy.bare_column_width
1151
- bounds_copy.instance_variable_set :@current_column, (bounds_copy.instance_variable_set :@columns, 1) - 1
1152
- end
1156
+ bounds_copy.single_file if ColumnBox === bounds_copy
1153
1157
  end
1154
1158
  scratch_pdf.move_cursor_to cursor unless (scratch_start_at_top = keep_together || pages_advanced > 0 || at_page_top?)
1155
1159
  scratch_start_cursor = scratch_pdf.cursor
@@ -33,25 +33,8 @@ module Asciidoctor
33
33
  end
34
34
  end
35
35
 
36
- # Retrieve the intrinsic image dimensions for the specified path in pt.
37
- #
38
- # Returns a Hash containing :width and :height keys that map to the image's
39
- # intrinsic width and height values (in pt).
40
- def intrinsic_image_dimensions path, format
41
- if format == 'svg'
42
- # NOTE: prawn-svg computes intrinsic width and height in pt
43
- img_obj = ::Prawn::SVG::Interface.new (::File.read path, mode: 'r:UTF-8'), self, {}
44
- img_size = img_obj.document.sizing
45
- { width: img_size.output_width, height: img_size.output_height }
46
- else
47
- # NOTE: build_image_object caches image data previously loaded
48
- # NOTE: build_image_object computes intrinsic width and height in px
49
- _, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
50
- { width: (to_pt img_size.width, :px), height: (to_pt img_size.height, :px) }
51
- end
52
- rescue
53
- # NOTE: image cannot be read, so it won't be used anyway
54
- { width: 0, height: 0 }
36
+ def recommend_prawn_gmagick? err, image_format
37
+ ::Prawn::Errors::UnsupportedImageType === err && !(defined? ::GMagick::Image) && ((err.message.include? 'PNG') || (%w(jpg png).none? image_format))
55
38
  end
56
39
  end
57
40
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn::SVG::Elements::Image.prepend (Module.new do
4
+ def image_dimensions data
5
+ unless (handler = find_image_handler data)
6
+ raise ::Prawn::SVG::Elements::Base::SkipElementError, 'Unsupported image type supplied to image tag'
7
+ end
8
+ image = handler.new data
9
+ [image.width.to_f, image.height.to_f]
10
+ end
11
+
12
+ def find_image_handler data
13
+ Prawn.image_handler.find data rescue nil
14
+ end
15
+ end)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn::SVG::Loaders::File.prepend (Module.new do
4
+ attr_reader :jail_path
5
+
6
+ def initialize root_path
7
+ if Hash === root_path
8
+ @jail_path = root_path[:root]
9
+ root_path = root_path[:base]
10
+ super
11
+ else
12
+ super
13
+ @jail_path = self.root_path
14
+ end
15
+ end
16
+
17
+ def assert_valid_path! path
18
+ if jail_path && !(path.start_with? %(#{jail_path}#{File::SEPARATOR}))
19
+ raise Prawn::SVG::UrlLoader::Error, %(file path points to location outside of jail #{jail_path})
20
+ end
21
+ end
22
+ end)
@@ -2,7 +2,9 @@
2
2
 
3
3
  require 'prawn-svg'
4
4
  require_relative 'prawn-svg/calculators/document_sizing'
5
+ require_relative 'prawn-svg/elements/image'
5
6
  require_relative 'prawn-svg/loaders/data'
7
+ require_relative 'prawn-svg/loaders/file'
6
8
  require_relative 'prawn-svg/loaders/web'
7
9
  require_relative 'prawn-svg/url_loader'
8
10
  # NOTE: disable system fonts since they're non-portable
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class FormattedString < String
4
+ attr_reader :fragments
5
+
6
+ def initialize fragments
7
+ super [].tap {|accum| (@fragments = fragments).each {|it| accum << it[:text] } }.join
8
+ end
9
+
10
+ def eql? other
11
+ super && (FormattedString === other ? (@fragments ||= nil) == other.fragments : true)
12
+ end
13
+ end
@@ -48,10 +48,14 @@ module Asciidoctor::PDF::FormattedText
48
48
  drop = scratch
49
49
  end
50
50
  image_path = fragment[:image_path]
51
- image_w = fragment[:image_width] || '100%'
52
-
53
- # NOTE: intrinsic width is stored behind % symbol
54
- if (pctidx = image_w.index '%') && pctidx + 1 < image_w.length
51
+ image_format = fragment[:image_format]
52
+ if (image_w = fragment[:image_width] || '100%') == 'auto'
53
+ image_w = nil # use intrinsic width
54
+ elsif image_w.start_with? 'auto*'
55
+ image_scale = (image_w.slice 5, image_w.length).to_f
56
+ image_w = nil # use intrinsic width
57
+ elsif (pctidx = image_w.index '%') && pctidx + 1 < image_w.length
58
+ # NOTE: intrinsic width is stored behind % symbol
55
59
  pct = (image_w.slice 0, pctidx).to_f / 100
56
60
  intrinsic_w = (image_w.slice pctidx + 1, image_w.length).to_f
57
61
  image_w = [available_w, pct * intrinsic_w].min
@@ -62,26 +66,35 @@ module Asciidoctor::PDF::FormattedText
62
66
  max_image_h = fragment[:image_fit] == 'line' ? [available_h, doc.font.height].min : available_h
63
67
 
64
68
  # TODO: make helper method to calculate width and height of image
65
- if fragment[:image_format] == 'svg'
69
+ if image_format == 'svg'
66
70
  svg_obj = ::Prawn::SVG::Interface.new ::File.read(image_path, mode: 'r:UTF-8'), doc,
67
71
  at: doc.bounds.top_left,
68
72
  width: image_w,
69
73
  fallback_font_name: doc.fallback_svg_font_name,
70
74
  enable_web_requests: doc.allow_uri_read ? (doc.method :load_open_uri).to_proc : false,
71
- enable_file_requests_with_root: (::File.dirname image_path),
75
+ enable_file_requests_with_root: { base: (::File.dirname image_path), root: doc.jail_dir },
72
76
  cache_images: doc.cache_uri
73
77
  svg_size = svg_obj.document.sizing
74
78
  # NOTE: the best we can do is make the image fit within full height of bounds
75
79
  if (image_h = svg_size.output_height) > max_image_h
76
80
  image_w = (svg_obj.resize height: (image_h = max_image_h)).output_width
77
- else
81
+ elsif image_w
78
82
  image_w = svg_size.output_width
83
+ else
84
+ fragment[:image_width] = (image_w = svg_size.output_width).to_s
85
+ image_w *= image_scale if image_scale
86
+ image_w = available_w if image_w > available_w
79
87
  end
80
88
  fragment[:image_obj] = svg_obj
81
89
  else
82
90
  # TODO: cache image info based on path (Prawn caches based on SHA1 of content)
83
91
  # NOTE: image_obj is constrained to image_width by renderer
84
92
  image_obj, image_info = ::File.open(image_path, 'rb') {|fd| doc.build_image_object fd }
93
+ unless image_w
94
+ fragment[:image_width] = (image_w = to_pt image_info.width, :px).to_s
95
+ image_w *= image_scale if image_scale
96
+ image_w = available_w if image_w > available_w
97
+ end
85
98
  if (image_h = image_w * (image_info.height.fdiv image_info.width)) > max_image_h
86
99
  # NOTE: the best we can do is make the image fit within full height of bounds
87
100
  image_w = (image_h = max_image_h) * (image_info.width.fdiv image_info.height)
@@ -117,7 +130,7 @@ module Asciidoctor::PDF::FormattedText
117
130
  fragment[:image_width] = fragment[:width] = image_w
118
131
  fragment[:image_height] = image_h
119
132
  rescue
120
- logger.warn %(could not embed image: #{image_path}; #{$!.message}#{::Prawn::Errors::UnsupportedImageType === $! && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}) unless scratch
133
+ logger.warn %(could not embed image: #{image_path}; #{$!.message}#{(doc.recommend_prawn_gmagick? $!, image_format) ? %(; install prawn-gmagick gem to add support for #{image_format&.upcase || 'unknown'} image format) : ''}) unless scratch
121
134
  drop = true # delegate to cleanup logic in ensure block
122
135
  ensure
123
136
  # NOTE: skip rendering image in scratch document or if image can't be loaded
@@ -69,7 +69,7 @@ module Asciidoctor
69
69
  # NOTE: base theme is loaded "as is" (no post-processing)
70
70
  def self.load_base_theme
71
71
  ::File.open BaseThemePath, mode: 'r:UTF-8' do |io|
72
- (::OpenStruct.new ::YAML.safe_load io, aliases: true, filename: BaseThemePath).tap {|theme| theme.__dir__ = ThemesDir }
72
+ (::OpenStruct.new ::YAML.safe_load io, filename: BaseThemePath).tap {|theme| theme.__dir__ = ThemesDir }
73
73
  end
74
74
  end
75
75
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Asciidoctor
4
4
  module PDF
5
- VERSION = '2.0.6'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: asciidoctor-pdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.6
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Allen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2022-05-30 00:00:00.000000000 Z
12
+ date: 2022-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: asciidoctor
@@ -261,7 +261,9 @@ files:
261
261
  - lib/asciidoctor/pdf/ext/prawn-gmagick.rb
262
262
  - lib/asciidoctor/pdf/ext/prawn-svg.rb
263
263
  - lib/asciidoctor/pdf/ext/prawn-svg/calculators/document_sizing.rb
264
+ - lib/asciidoctor/pdf/ext/prawn-svg/elements/image.rb
264
265
  - lib/asciidoctor/pdf/ext/prawn-svg/loaders/data.rb
266
+ - lib/asciidoctor/pdf/ext/prawn-svg/loaders/file.rb
265
267
  - lib/asciidoctor/pdf/ext/prawn-svg/loaders/web.rb
266
268
  - lib/asciidoctor/pdf/ext/prawn-svg/url_loader.rb
267
269
  - lib/asciidoctor/pdf/ext/prawn-table.rb
@@ -284,6 +286,7 @@ files:
284
286
  - lib/asciidoctor/pdf/ext/rouge.rb
285
287
  - lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb
286
288
  - lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb
289
+ - lib/asciidoctor/pdf/formatted_string.rb
287
290
  - lib/asciidoctor/pdf/formatted_text.rb
288
291
  - lib/asciidoctor/pdf/formatted_text/formatter.rb
289
292
  - lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb