asciidoctor-pdf 1.5.2 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +68 -1
  3. data/{LICENSE.adoc → LICENSE} +3 -4
  4. data/NOTICE.adoc +4 -6
  5. data/README.adoc +139 -46
  6. data/asciidoctor-pdf.gemspec +11 -14
  7. data/bin/asciidoctor-pdf-optimize +1 -1
  8. data/docs/theming-guide.adoc +26 -22
  9. data/lib/asciidoctor/pdf/converter.rb +88 -78
  10. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +3 -2
  11. data/lib/asciidoctor/pdf/ext/core/file.rb +4 -5
  12. data/lib/asciidoctor/pdf/ext/core.rb +0 -1
  13. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +1 -1
  14. data/lib/asciidoctor/pdf/ext/pdf-core.rb +15 -0
  15. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +4 -13
  16. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +28 -2
  17. data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +1 -1
  18. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/arranger.rb +15 -0
  19. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +28 -7
  20. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +0 -1
  21. data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
  22. data/lib/asciidoctor/pdf/ext/pygments.rb +11 -9
  23. data/lib/asciidoctor/pdf/ext.rb +0 -1
  24. data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +5 -1
  25. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +9 -2
  26. data/lib/asciidoctor/pdf/formatted_text/transform.rb +6 -4
  27. data/lib/asciidoctor/pdf/index_catalog.rb +1 -1
  28. data/lib/asciidoctor/pdf/optimizer.rb +40 -12
  29. data/lib/asciidoctor/pdf/pdfmark.rb +1 -2
  30. data/lib/asciidoctor/pdf/text_transformer.rb +10 -71
  31. data/lib/asciidoctor/pdf/theme_loader.rb +1 -1
  32. data/lib/asciidoctor/pdf/version.rb +1 -1
  33. data/lib/asciidoctor/pdf.rb +3 -0
  34. metadata +21 -63
  35. data/lib/asciidoctor/pdf/ext/core/numeric.rb +0 -26
  36. data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +0 -14
  37. data/lib/asciidoctor/pdf/ext/prawn-templates.rb +0 -7
@@ -8,10 +8,11 @@ class Asciidoctor::Section
8
8
  if @numbered && !@caption && slevel <= (@document.attr 'sectnumlevels', 3).to_i
9
9
  @is_numbered = true
10
10
  if @document.doctype == 'book'
11
- if slevel == 0
11
+ case slevel
12
+ when 0
12
13
  @cached_numbered_title = %(#{sectnum nil, ':'} #{title})
13
14
  @cached_formal_numbered_title = %(#{@document.attr 'part-signifier', 'Part'} #{@cached_numbered_title}).lstrip
14
- elsif slevel == 1
15
+ when 1
15
16
  @cached_numbered_title = %(#{sectnum} #{title})
16
17
  @cached_formal_numbered_title = %(#{@document.attr 'chapter-signifier', (@document.attr 'chapter-label', 'Chapter')} #{@cached_numbered_title}).lstrip
17
18
  else
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class File
4
- class << self
5
- def absolute_path? path
6
- (path.start_with? '/') || (ALT_SEPARATOR && (path.start_with? (absolute_path path).slice 0, 3))
7
- end unless method_defined? :absolute_path?
8
- end
4
+ # NOTE: remove once minimum required Ruby version is at least 2.7
5
+ def self.absolute_path? path
6
+ (::Pathname.new path).absolute?
7
+ end unless respond_to? :absolute_path?
9
8
  end
@@ -4,7 +4,6 @@ require_relative 'core/object'
4
4
  require_relative 'core/array'
5
5
  require_relative 'core/file'
6
6
  require_relative 'core/hash'
7
- require_relative 'core/numeric'
8
7
  require_relative 'core/string'
9
8
  require_relative 'core/regexp'
10
9
  require_relative 'core/quantifiable_stdout'
@@ -3,6 +3,6 @@
3
3
  unless (defined? PDF::Core.pdf_object) == 'method'
4
4
  module PDF::Core
5
5
  alias pdf_object PdfObject
6
- module_function :pdf_object # rubocop:disable Style/AccessModifierDeclarations
6
+ module_function :pdf_object
7
7
  end
8
8
  end
@@ -1,4 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ module PDF::Core
4
+ class << self
5
+ alias _initial_real real
6
+ def real num
7
+ num.to_f.round 4
8
+ end
9
+
10
+ alias _initial_real_params real_params
11
+ def real_params array
12
+ return array.map {|it| it.to_f.round 5 }.join ' ' if (caller_locations 1, 1)[0].base_label == 'transformation_matrix'
13
+ _initial_real_params array
14
+ end
15
+ end
16
+ end
17
+
3
18
  require_relative 'pdf-core/pdf_object'
4
19
  require_relative 'pdf-core/page'
@@ -5,21 +5,12 @@
5
5
  # This file was copied from Prawn (manual/syntax_highlight.rb) and
6
6
  # modified for use with Asciidoctor PDF.
7
7
  #
8
- # Prawn is free software: you can redistribute it and/or modify
9
- # it under the terms of the GNU General Public License as published by
10
- # the Free Software Foundation, either version 3 of the License, or
11
- # (at your option) any later version.
12
- #
13
- # Prawn is distributed in the hope that it will be useful,
14
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
- # GNU General Public License for more details.
17
- #
18
- # You should have received a copy of the GNU General Public License
19
- # along with Prawn. If not, see <http://www.gnu.org/licenses/>.
8
+ # Since the file originates from the Prawn project, it shares the Prawn
9
+ # license. Thus, the file may be used under Matz's original licensing terms for
10
+ # Ruby, the GPLv2 license, or the GPLv3 license.
20
11
  #
21
12
  # Copyright (C) Felipe Doria
22
- # Copyright (C) 2014 OpenDevise Inc. and the Asciidoctor Project
13
+ # Copyright (C) 2014-present OpenDevise Inc. and the Asciidoctor Project
23
14
  #
24
15
  ######################################################################
25
16
 
@@ -76,6 +76,27 @@ module Asciidoctor
76
76
  reference_bounds.height
77
77
  end
78
78
 
79
+ # workaround for https://github.com/prawnpdf/prawn/issues/1121
80
+ def generate_margin_box
81
+ page_w, page_h = (page = state.page).dimensions.slice 2, 2
82
+ page_m = page.margins
83
+ prev_margin_box, @margin_box = @margin_box, (::Prawn::Document::BoundingBox.new self, nil, [page_m[:left], page_h - page_m[:top]], width: page_w - page_m[:left] - page_m[:right], height: page_h - page_m[:top] - page_m[:bottom])
84
+
85
+ # update bounding box if not flowing from the previous page
86
+ unless @bounding_box&.parent
87
+ prev_margin_box = @bounding_box
88
+ @bounding_box = @margin_box
89
+ end
90
+
91
+ # maintains indentation settings across page breaks
92
+ if prev_margin_box
93
+ @margin_box.add_left_padding prev_margin_box.total_left_padding
94
+ @margin_box.add_right_padding prev_margin_box.total_right_padding
95
+ end
96
+
97
+ nil
98
+ end
99
+
79
100
  # Set the margins for the current page.
80
101
  #
81
102
  def set_page_margin margin
@@ -190,7 +211,7 @@ module Asciidoctor
190
211
  # }
191
212
  #
192
213
  def register_font data
193
- font_families.update data.each_with_object({}) {|(key, val), accum| accum[key.to_s] = val }
214
+ font_families.update data.transform_keys(&:to_s)
194
215
  end
195
216
 
196
217
  # Enhances the built-in font method to allow the font
@@ -553,7 +574,7 @@ module Asciidoctor
553
574
 
554
575
  # TODO: memoize the result
555
576
  def inflate_padding padding
556
- padding = [*(padding || 0)].slice 0, 4
577
+ padding = (Array padding || 0).slice 0, 4
557
578
  case padding.size
558
579
  when 1
559
580
  [padding[0], padding[0], padding[0], padding[0]]
@@ -882,6 +903,11 @@ module Asciidoctor
882
903
  [(whole_pages * full_page_height + partial_page_height), whole_pages, partial_page_height]
883
904
  end
884
905
 
906
+ def with_dry_run &block
907
+ total_height, = dry_run(&block)
908
+ instance_exec total_height, &block
909
+ end
910
+
885
911
  # Attempt to keep the objects generated in the block on the same page
886
912
  #
887
913
  # TODO: short-circuit nested usage
@@ -6,4 +6,4 @@ class Prawn::FontMetricCache::CacheEntry
6
6
  font = font.hash
7
7
  super
8
8
  end
9
- end
9
+ end if Prawn::FontMetricCache::CacheEntry.members == [:font, :options, :string]
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn::Text::Formatted::Arranger.prepend (Module.new do
4
+ def initialize *_args
5
+ super
6
+ @dummy_text = ?\u0000
7
+ end
8
+
9
+ def next_string
10
+ if (string = super) == @dummy_text
11
+ def string.lstrip!; end
12
+ end
13
+ string
14
+ end
15
+ end)
@@ -17,9 +17,32 @@ Prawn::Text::Formatted::Box.prepend (Module.new do
17
17
  end
18
18
  end
19
19
 
20
- def find_font_for_this_glyph char, current_font, fallback_fonts_to_check, original_font = current_font
21
- (doc = @document).font current_font
22
- if fallback_fonts_to_check.empty?
20
+ def analyze_glyphs_for_fallback_font_support fragment_hash
21
+ fragment_font = fragment_hash[:font] || (original_font = @document.font.family)
22
+ if (fragment_font_styles = fragment_hash[:styles])
23
+ if fragment_font_styles.include? :bold
24
+ fragment_font_opts = { style: (fragment_font_styles.include? :italic) ? :bold_italic : :bold }
25
+ elsif fragment_font_styles.include? :italic
26
+ fragment_font_opts = { style: :italic }
27
+ end
28
+ end
29
+ fallback_fonts = @fallback_fonts.dup
30
+ font_glyph_pairs = []
31
+ @document.save_font do
32
+ fragment_hash[:text].each_char do |char|
33
+ font_glyph_pairs << [(find_font_for_this_glyph char, fragment_font, fragment_font_opts || {}, fallback_fonts.dup), char]
34
+ end
35
+ end
36
+ # NOTE: don't add a :font to fragment if it wasn't there originally
37
+ font_glyph_pairs.each {|pair| pair[0] = nil if pair[0] == original_font } if original_font
38
+ form_fragments_from_like_font_glyph_pairs font_glyph_pairs, fragment_hash
39
+ end
40
+
41
+ def find_font_for_this_glyph char, current_font, current_font_opts = {}, fallback_fonts_to_check = [], original_font = current_font
42
+ (doc = @document).font current_font, current_font_opts
43
+ if doc.font.glyph_present? char
44
+ current_font
45
+ elsif fallback_fonts_to_check.empty?
23
46
  if logger.info? && !doc.scratch?
24
47
  fonts_checked = @fallback_fonts.dup.unshift original_font
25
48
  missing_chars = (doc.instance_variable_defined? :@missing_chars) ?
@@ -30,11 +53,9 @@ Prawn::Text::Formatted::Box.prepend (Module.new do
30
53
  previous_fonts_checked << fonts_checked
31
54
  end
32
55
  end
33
- current_font
34
- elsif doc.font.glyph_present? char
35
- current_font
56
+ original_font
36
57
  else
37
- find_font_for_this_glyph char, fallback_fonts_to_check.shift, fallback_fonts_to_check, original_font
58
+ find_font_for_this_glyph char, fallback_fonts_to_check.shift, current_font_opts, fallback_fonts_to_check, original_font
38
59
  end
39
60
  end
40
61
 
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'prawn-svg' unless defined? Prawn::SVG::Interface
4
- require_relative 'prawn-svg/interface'
5
4
  # NOTE disable system fonts since they're non-portable
6
5
  Prawn::SVG::Interface.font_path.clear
@@ -4,6 +4,7 @@
4
4
  require_relative 'prawn/font_metric_cache'
5
5
  require_relative 'prawn/font/afm'
6
6
  require_relative 'prawn/images'
7
+ require_relative 'prawn/formatted_text/arranger'
7
8
  require_relative 'prawn/formatted_text/box'
8
9
  require_relative 'prawn/formatted_text/fragment'
9
10
  require_relative 'prawn/extensions'
@@ -1,23 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pygments.rb'
4
-
5
3
  module Pygments
6
4
  module Ext
7
5
  module BlockStyles
8
6
  BlockSelectorRx = /^\.highlight *\{([^}]+?)\}/
9
7
  HighlightBackgroundColorRx = /^\.highlight +\.hll +{ *background(?:-color)?: *#([a-fA-F0-9]{6})/
10
- HexColorRx = /^#[a-fA-F0-9]{6}$/
8
+ ColorPropertiesRx = /(?:^|;) *(background(?:-color)?|color): *#?([a-fA-F0-9]{6}) *(?=$|;)/
11
9
 
12
10
  @cache = ::Hash.new do |cache, key|
13
11
  styles = {}
14
12
  if BlockSelectorRx =~ (css = ::Pygments.css '.highlight', style: key)
15
- ($1.strip.split ';').each do |style|
16
- pname, pval = (style.split ':', 2).map(&:strip)
17
- if pname == 'background' || pname == 'background-color'
18
- styles[:background_color] = pval.slice 1, pval.length if HexColorRx.match? pval
19
- elsif pname == 'color'
20
- styles[:font_color] = pval.slice 1, pval.length if HexColorRx.match? pval
13
+ $1.scan ColorPropertiesRx do |pname, pval|
14
+ case pname
15
+ when 'background', 'background-color'
16
+ styles[:background_color] = pval
17
+ when 'color'
18
+ styles[:font_color] = pval
21
19
  end
22
20
  end
23
21
  end
@@ -26,6 +24,10 @@ module Pygments
26
24
  styles
27
25
  end
28
26
 
27
+ def self.available? style
28
+ (@available ||= ::Pygments.styles.to_set).include? style
29
+ end
30
+
29
31
  def self.for style
30
32
  @cache[style]
31
33
  end
@@ -6,4 +6,3 @@ require_relative 'ext/pdf-core'
6
6
  require_relative 'ext/prawn'
7
7
  require_relative 'ext/prawn-svg'
8
8
  require_relative 'ext/prawn-table'
9
- require_relative 'ext/prawn-templates'
@@ -2,7 +2,11 @@
2
2
 
3
3
  module Asciidoctor::PDF::FormattedText
4
4
  class FragmentPositionRenderer
5
- attr_reader :top, :right, :bottom, :left, :page_number
5
+ attr_reader :top
6
+ attr_reader :right
7
+ attr_reader :bottom
8
+ attr_reader :left
9
+ attr_reader :page_number
6
10
 
7
11
  def render_behind fragment
8
12
  @top = fragment.top
@@ -39,8 +39,14 @@ module Asciidoctor::PDF::FormattedText
39
39
  scratch = doc.scratch?
40
40
  available_w = doc.bounds.width
41
41
  available_h = doc.page.empty? ? doc.cursor : doc.bounds.height
42
+ last_fragment = {}
42
43
  raw_image_fragments.each do |fragment|
43
- drop = scratch
44
+ if fragment[:object_id] == last_fragment[:object_id]
45
+ fragments.delete fragment
46
+ next
47
+ else
48
+ drop = scratch
49
+ end
44
50
  begin
45
51
  image_path = fragment[:image_path]
46
52
 
@@ -115,7 +121,7 @@ module Asciidoctor::PDF::FormattedText
115
121
  fragment[:image_width] = fragment[:width] = image_w
116
122
  fragment[:image_height] = image_h
117
123
  rescue
118
- logger.warn %(could not embed image: #{image_path}; #{$!.message}#{::Prawn::Errors::UnsupportedImageType === $! ? '; install prawn-gmagick gem to add support' : ''}) unless scratch
124
+ logger.warn %(could not embed image: #{image_path}; #{$!.message}#{::Prawn::Errors::UnsupportedImageType === $! && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}) unless scratch
119
125
  drop = true # delegate to cleanup logic in ensure block
120
126
  ensure
121
127
  # NOTE skip rendering image in scratch document or if image can't be loaded
@@ -125,6 +131,7 @@ module Asciidoctor::PDF::FormattedText
125
131
  # NOTE retain key to indicate we've visited fragment already
126
132
  fragment[:image_obj] = nil
127
133
  end
134
+ last_fragment = fragment
128
135
  end
129
136
  end
130
137
  end
@@ -172,9 +172,10 @@ module Asciidoctor
172
172
  fragment = {
173
173
  image_path: attributes[:src],
174
174
  image_format: attributes[:format],
175
- # a zero-width space in the text will cause the image to be duplicated
176
- text: (attributes[:alt].delete ZeroWidthSpace),
175
+ # NOTE: add enclosing square brackets here to avoid errors in parsing
176
+ text: %([#{attributes[:alt].delete ZeroWidthSpace}]),
177
177
  callback: [InlineImageRenderer],
178
+ object_id: node.object_id, # used to deduplicate if fragment gets split up
178
179
  }
179
180
  if inherited && (link = inherited[:link])
180
181
  fragment[:link] = link
@@ -379,9 +380,10 @@ module Asciidoctor
379
380
 
380
381
  def update_fragment fragment, props
381
382
  fragment.update props do |k, oval, nval|
382
- if k == :styles
383
+ case k
384
+ when :styles
383
385
  nval ? (oval.merge nval) : oval.clear
384
- elsif k == :callback
386
+ when :callback
385
387
  oval | nval
386
388
  else
387
389
  nval
@@ -32,7 +32,7 @@ module Asciidoctor
32
32
 
33
33
  def store_primary_term name, dest = nil
34
34
  store_dest dest if dest
35
- (init_category uppercase_mb name.chr).store_term name, dest
35
+ (init_category name.chr.upcase).store_term name, dest
36
36
  end
37
37
 
38
38
  def store_secondary_term primary_name, secondary_name, dest = nil
@@ -2,11 +2,35 @@
2
2
 
3
3
  require 'pathname'
4
4
  require 'rghost'
5
+ require 'rghost/gs_alone'
5
6
  require 'tmpdir'
6
7
 
8
+ RGhost::GSAlone.prepend (Module.new do
9
+ WindowsRx = /win|ming/
10
+
11
+ def initialize params, debug
12
+ (@params = params.dup).push(*(@params.pop.split File::PATH_SEPARATOR))
13
+ @debug = debug
14
+ end
15
+
16
+ def run
17
+ RGhost::Config.config_platform unless File.exist? RGhost::Config::GS[:path].to_s
18
+ (cmd = @params.slice 1, @params.length).unshift RGhost::Config::GS[:path].to_s
19
+ #puts cmd if @debug
20
+ system(*cmd)
21
+ end
22
+ end)
23
+
24
+ RGhost::Engine.prepend (Module.new do
25
+ def shellescape str
26
+ str
27
+ end
28
+ end)
29
+
7
30
  module Asciidoctor
8
31
  module PDF
9
32
  class Optimizer
33
+ # see https://www.ghostscript.com/doc/current/VectorDevices.htm#PSPDF_IN for details
10
34
  (QUALITY_NAMES = {
11
35
  'default' => :default,
12
36
  'screen' => :screen,
@@ -15,27 +39,31 @@ module Asciidoctor
15
39
  'prepress' => :prepress,
16
40
  }).default = :default
17
41
 
42
+ attr_reader :quality
43
+ attr_reader :compatibility_level
44
+
18
45
  def initialize quality = 'default', compatibility_level = '1.4'
19
46
  @quality = QUALITY_NAMES[quality]
20
47
  @compatibility_level = compatibility_level
48
+ if (gs_path = ::ENV['GS'])
49
+ ::RGhost::Config::GS[:path] = gs_path
50
+ end
21
51
  end
22
52
 
23
- def generate_file target
53
+ def optimize_file target
24
54
  ::Dir::Tmpname.create ['asciidoctor-pdf-', '.pdf'] do |tmpfile|
25
- filename = Pathname.new target
26
- filename_o = Pathname.new tmpfile
27
- pdfmark = filename.sub_ext '.pdfmark'
28
- inputs = pdfmark.file? ? [target, pdfmark.to_s] : target
55
+ filename_o = ::Pathname.new target
56
+ filename_tmp = ::Pathname.new tmpfile
57
+ if (pdfmark = filename_o.sub_ext '.pdfmark').file?
58
+ inputs = [target, pdfmark.to_s].join ::File::PATH_SEPARATOR
59
+ else
60
+ inputs = target
61
+ end
29
62
  (::RGhost::Convert.new inputs).to :pdf,
30
- filename: filename_o.to_s,
63
+ filename: filename_tmp.to_s,
31
64
  quality: @quality,
32
65
  d: { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
33
- begin
34
- filename_o.rename target
35
- rescue ::Errno::EXDEV
36
- filename.binwrite filename_o.binread
37
- filename_o.unlink
38
- end
66
+ filename_o.binwrite filename_tmp.binread
39
67
  end
40
68
  nil
41
69
  end