asciidoctor-pdf 1.5.2 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
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