asciidoctor-pdf 1.5.0.alpha.18 → 1.5.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,13 +3,13 @@ module Image
3
3
  DataUriRx = /^data:image\/(?<fmt>png|jpe?g|gif|pdf|bmp|tiff);base64,(?<data>.*)$/
4
4
 
5
5
  class << self
6
- def format path, node = nil
7
- (node && (node.attr 'format', nil, false)) || (::File.extname path).downcase[1..-1]
6
+ def format path, attributes = nil
7
+ (attributes && attributes['format']) || ((ext = ::File.extname path).downcase.slice 1, ext.length)
8
8
  end
9
9
  end
10
10
 
11
11
  def format
12
- (attr 'format', nil, false) || ::File.extname(inline? ? target : (attr 'target')).downcase[1..-1]
12
+ (attr 'format', nil, false) || ((ext = ::File.extname(inline? ? target : (attr 'target'))).downcase.slice 1, ext.length)
13
13
  end
14
14
 
15
15
  def target_and_format
@@ -17,7 +17,7 @@ module Image
17
17
  if (image_path.start_with? 'data:') && (m = DataUriRx.match image_path)
18
18
  [(m[:data].extend ::Base64), m[:fmt]]
19
19
  else
20
- [image_path, (attr 'format', nil, false) || (::File.extname image_path).downcase[1..-1]]
20
+ [image_path, (attr 'format', nil, false) || ((ext = ::File.extname image_path).downcase.slice 1, ext.length)]
21
21
  end
22
22
  end
23
23
  end
@@ -57,6 +57,7 @@ class Converter < ::Prawn::Document
57
57
  ColumnPositions = [:left, :center, :right]
58
58
  PageLayouts = [:portrait, :landscape]
59
59
  PageSides = [:recto, :verso]
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
60
61
  LF = %(\n)
61
62
  DoubleLF = %(\n\n)
62
63
  TAB = %(\t)
@@ -96,7 +97,7 @@ class Converter < ::Prawn::Document
96
97
  UriBreakCharRepl = %(\\&#{ZeroWidthSpace})
97
98
  UriSchemeBoundaryRx = /(?<=:\/\/)/
98
99
  LineScanRx = /\n|.+/
99
- BlankLineRx = /\n[[:blank:]]*\n/
100
+ BlankLineRx = /\n{2,}/
100
101
  WhitespaceChars = %( \t\n)
101
102
  SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
102
103
  PygmentsBgColorRx = /^\.highlight +{ *background: *#([^;]+);/
@@ -182,16 +183,14 @@ class Converter < ::Prawn::Document
182
183
  set_page_margin next_page_margin
183
184
  end
184
185
  # TODO implement as a watermark (on top)
185
- if @page_bg_image
186
- # FIXME implement fitting and centering for SVG
187
- # TODO implement image scaling (numeric value or "fit")
188
- canvas { image @page_bg_image, position: :center, fit: [bounds.width, bounds.height] }
186
+ if (bg_image = @page_bg_image[page_side])
187
+ canvas { image bg_image[0], ({ position: :center, vposition: :center }.merge bg_image[1]) }
189
188
  elsif @page_bg_color && @page_bg_color != 'FFFFFF'
190
189
  fill_absolute_bounds @page_bg_color
191
190
  end
192
191
  end if respond_to? :on_page_create
193
192
 
194
- layout_cover_page :front, doc
193
+ layout_cover_page doc, :front
195
194
  if (insert_title_page = doc.doctype == 'book' || (doc.attr? 'title-page'))
196
195
  layout_title_page doc
197
196
  # NOTE a new page will already be started if the cover image is a PDF
@@ -291,7 +290,7 @@ class Converter < ::Prawn::Document
291
290
  catalog.data[:OpenAction] = dest_fit_horizontally((page_height + 1), state.pages[0]) if state.pages.size > 0
292
291
  catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }
293
292
 
294
- layout_cover_page :back, doc
293
+ layout_cover_page doc, :back
295
294
  nil
296
295
  end
297
296
 
@@ -302,11 +301,12 @@ class Converter < ::Prawn::Document
302
301
 
303
302
  # TODO only allow method to be called once (or we need a reset)
304
303
  def init_pdf doc
305
- theme = load_theme doc
306
- pdf_opts = build_pdf_options doc, theme
307
- # QUESTION should page options be preserved (otherwise, not readily available)
304
+ @allow_uri_read = doc.attr? 'allow-uri-read'
305
+ pdf_opts = build_pdf_options doc, (theme = load_theme doc)
306
+ # QUESTION should page options be preserved? (otherwise, not readily available)
308
307
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
309
308
  ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
309
+ renderer.min_version PDFVersions[doc.attr 'pdf-version']
310
310
  @page_margin_by_side = { recto: page_margin, verso: page_margin }
311
311
  if (@media = doc.attr 'media', 'screen') == 'prepress'
312
312
  @ppbook = doc.doctype == 'book'
@@ -324,15 +324,21 @@ class Converter < ::Prawn::Document
324
324
  end
325
325
  # QUESTION should ThemeLoader register fonts?
326
326
  register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
327
- if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image != 'none'
328
- @page_bg_image = bg_image
327
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image[0]
328
+ @page_bg_image = { verso: bg_image, recto: bg_image }
329
329
  else
330
- @page_bg_image = nil
330
+ @page_bg_image = { verso: nil, recto: nil }
331
+ end
332
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image-verso')
333
+ @page_bg_image[:verso] = bg_image[0] ? bg_image : nil
334
+ 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
331
337
  end
332
338
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
333
339
  @fallback_fonts = [*theme.font_fallbacks]
334
340
  @font_color = theme.base_font_color
335
- @base_align = (align = doc.attr 'text-alignment') && (TextAlignmentNames.include? align) ? align : theme.base_align
341
+ @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
336
342
  @text_transform = nil
337
343
  @list_numerals = []
338
344
  @list_bullets = []
@@ -345,7 +351,16 @@ class Converter < ::Prawn::Document
345
351
  end
346
352
 
347
353
  def load_theme doc
348
- @theme ||= doc.options[:pdf_theme] || ThemeLoader.load_theme((doc.attr 'pdf-style'), (doc.attr 'pdf-stylesdir'))
354
+ @theme ||= begin
355
+ if (theme = doc.options[:pdf_theme])
356
+ @themesdir = theme.__dir__ || (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir')
357
+ 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'))
360
+ @themesdir = theme.__dir__
361
+ end
362
+ theme
363
+ end
349
364
  end
350
365
 
351
366
  def build_pdf_options doc, theme
@@ -637,7 +652,7 @@ class Converter < ::Prawn::Document
637
652
  label_text = node.caption
638
653
  theme_font :admonition_label do
639
654
  theme_font %(admonition_label_#{type}) do
640
- if (transform = @text_transform) && transform != 'none'
655
+ if (transform = @text_transform)
641
656
  label_text = transform_text label_text, transform
642
657
  end
643
658
  label_width = rendered_width_of_string label_text
@@ -689,15 +704,15 @@ class Converter < ::Prawn::Document
689
704
  color: icon_data[:stroke_color],
690
705
  size: icon_size
691
706
  elsif icons
692
- if icon_path.end_with? '.svg'
707
+ if (::Asciidoctor::Image.format icon_path) == 'svg'
693
708
  begin
694
709
  svg_obj = ::Prawn::SVG::Interface.new ::File.read(icon_path), self,
695
- position: label_align,
710
+ position: label_align,
696
711
  vposition: label_valign,
697
712
  width: label_width,
698
713
  height: box_height,
699
714
  fallback_font_name: default_svg_font,
700
- enable_web_requests: (doc.attr? 'allow-uri-read'),
715
+ enable_web_requests: allow_uri_read,
701
716
  enable_file_requests_with_root: (::File.dirname icon_path)
702
717
  if (icon_height = (svg_size = svg_obj.document.sizing).output_height) > box_height
703
718
  icon_width = (svg_obj.resize height: (icon_height = box_height)).output_width
@@ -1026,10 +1041,10 @@ class Converter < ::Prawn::Document
1026
1041
  def convert_olist node
1027
1042
  add_dest_for_block node if node.id
1028
1043
  @list_numerals ||= []
1029
- # TODO move list_number resolve to a method
1030
- list_number = case node.style
1044
+ # TODO move list_numeral resolve to a method
1045
+ list_numeral = case node.style
1031
1046
  when 'arabic'
1032
- '1'
1047
+ 1
1033
1048
  when 'decimal'
1034
1049
  '01'
1035
1050
  when 'loweralpha'
@@ -1047,13 +1062,17 @@ class Converter < ::Prawn::Document
1047
1062
  when 'none'
1048
1063
  ''
1049
1064
  else
1050
- '1'
1065
+ 1
1051
1066
  end
1052
- # TODO support start values < 1 (issue #498)
1053
- if (start = ((node.attr 'start', nil, false) || ((node.option? 'reversed') ? node.items.size : 1)).to_i) > 1
1054
- (start - 1).times { list_number = list_number.next }
1067
+ if list_numeral && list_numeral != '' &&
1068
+ (start = (node.attr 'start', nil, false) || ((node.option? 'reversed') ? node.items.size : nil))
1069
+ if (start = start.to_i) > 1
1070
+ (start - 1).times { list_numeral = list_numeral.next }
1071
+ elsif start < 1 && !(::String === list_numeral)
1072
+ (start - 1).abs.times { list_numeral = list_numeral.pred }
1073
+ end
1055
1074
  end
1056
- @list_numerals << list_number
1075
+ @list_numerals << list_numeral
1057
1076
  convert_outline_list node
1058
1077
  @list_numerals.pop
1059
1078
  end
@@ -1167,7 +1186,7 @@ class Converter < ::Prawn::Document
1167
1186
  when :olist
1168
1187
  complex = node.complex?
1169
1188
  if (index = @list_numerals.pop)
1170
- if index.empty?
1189
+ if index == ''
1171
1190
  marker = ''
1172
1191
  else
1173
1192
  marker = %(#{index}.)
@@ -1238,19 +1257,25 @@ class Converter < ::Prawn::Document
1238
1257
 
1239
1258
  if image_format == 'gif' && !(defined? ::GMagick::Image)
1240
1259
  logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
1241
- image_path = false
1260
+ image_path = nil
1242
1261
  elsif ::Base64 === target
1243
1262
  image_path = target
1244
- elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format) &&
1245
- (::File.readable? image_path)
1246
- # NOTE import_page automatically advances to next page afterwards
1247
- # QUESTION should we add destination to top of imported page?
1248
- return import_page image_path, replace: page_is_empty? if image_format == 'pdf'
1249
- else
1250
- logger.warn %(image to embed not found or not readable: #{image_path || target}) unless scratch?
1263
+ elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format)
1264
+ if ::File.readable? image_path
1265
+ # NOTE import_page automatically advances to next page afterwards
1266
+ # QUESTION should we add destination to top of imported page?
1267
+ return import_page image_path, replace: page_is_empty? if image_format == 'pdf'
1268
+ elsif image_format == 'pdf'
1269
+ logger.warn %(pdf to insert not found or not readable: #{image_path}) unless scratch?
1270
+ # QUESTION should we use alt text in this case?
1271
+ return
1272
+ else
1273
+ logger.warn %(image to embed not found or not readable: #{image_path}) unless scratch?
1274
+ image_path = nil
1275
+ end
1276
+ elsif image_format == 'pdf'
1251
1277
  # QUESTION should we use alt text in this case?
1252
- return if image_format == 'pdf'
1253
- image_path = false
1278
+ return
1254
1279
  end
1255
1280
 
1256
1281
  theme_margin :block, :top unless (pinned = opts[:pinned])
@@ -1266,7 +1291,7 @@ class Converter < ::Prawn::Document
1266
1291
  end if node.title?
1267
1292
 
1268
1293
  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
1269
- width = resolve_explicit_width node.attributes, (available_w = bounds.width), support_vw: true, use_fallback: true
1294
+ width = resolve_explicit_width node.attributes, (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
1270
1295
  # TODO add `to_pt page_width` method to ViewportWidth type
1271
1296
  width = (width.to_f / 100) * page_width if ViewportWidth === width
1272
1297
 
@@ -1287,8 +1312,7 @@ class Converter < ::Prawn::Document
1287
1312
  position: alignment,
1288
1313
  width: width,
1289
1314
  fallback_font_name: default_svg_font,
1290
- enable_web_requests: (node.document.attr? 'allow-uri-read'),
1291
- # TODO enforce jail in safe mode
1315
+ enable_web_requests: allow_uri_read,
1292
1316
  enable_file_requests_with_root: file_request_root
1293
1317
  rendered_w = (svg_size = svg_obj.document.sizing).output_width
1294
1318
  if !width && (svg_obj.document.root.attributes.key? 'width')
@@ -1392,11 +1416,11 @@ class Converter < ::Prawn::Document
1392
1416
  when 'youtube'
1393
1417
  video_path = %(https://www.youtube.com/watch?v=#{video_id = node.attr 'target'})
1394
1418
  # see http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
1395
- poster = (node.document.attr? 'allow-uri-read') ? %(https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg) : nil
1419
+ poster = allow_uri_read ? %(https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg) : nil
1396
1420
  type = 'YouTube video'
1397
1421
  when 'vimeo'
1398
1422
  video_path = %(https://vimeo.com/#{video_id = node.attr 'target'})
1399
- if node.document.attr? 'allow-uri-read'
1423
+ if allow_uri_read
1400
1424
  if node.document.attr? 'cache-uri'
1401
1425
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
1402
1426
  else
@@ -1534,14 +1558,24 @@ class Converter < ::Prawn::Document
1534
1558
  fragments = restore_conums fragments, conum_mapping, num_trailing_spaces, linenums if conum_mapping
1535
1559
  fragments = guard_indentation fragments
1536
1560
  when 'rouge'
1537
- lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
1538
- lexer_opts = lexer.tag == 'php' ? { start_inline: !(node.option? 'mixed') } : {}
1561
+ if (srclang = node.attr 'language', nil, false)
1562
+ if srclang.include? '?'
1563
+ if (lexer = ::Rouge::Lexer.find_fancy srclang)
1564
+ unless lexer.tag != 'php' || (node.option? 'mixed') || ((lexer_opts = lexer.options).key? 'start_inline')
1565
+ lexer = lexer.class.new lexer_opts.merge 'start_inline' => true
1566
+ end
1567
+ end
1568
+ elsif (lexer = ::Rouge::Lexer.find srclang)
1569
+ lexer = lexer.new start_inline: true if lexer.tag == 'php' && !(node.option? 'mixed')
1570
+ end
1571
+ end
1572
+ lexer ||= ::Rouge::Lexers::PlainText
1539
1573
  formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap)
1540
1574
  formatter_opts = (node.attr? 'linenums') ? { line_numbers: true, start_line: (node.attr 'start', 1, false).to_i } : {}
1541
1575
  # QUESTION allow border color to be set by theme for highlighted block?
1542
1576
  bg_color_override = formatter.background_color
1543
1577
  source_string, conum_mapping = extract_conums source_string
1544
- fragments = formatter.format((lexer.lex source_string, lexer_opts), formatter_opts)
1578
+ fragments = formatter.format((lexer.lex source_string), formatter_opts)
1545
1579
  # NOTE cleanup trailing endline (handled in rouge_ext/formatters/prawn instead)
1546
1580
  #fragments[-1][:text] == LF ? fragments.pop : fragments[-1][:text].chop!
1547
1581
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
@@ -1727,9 +1761,7 @@ class Converter < ::Prawn::Document
1727
1761
  table_data = []
1728
1762
  node.rows[:head].each do |row|
1729
1763
  table_header = true
1730
- if (head_transform = theme.table_head_text_transform)
1731
- head_transform = nil if head_transform == 'none'
1732
- end
1764
+ head_transform = resolve_text_transform :table_head_text_transform, nil
1733
1765
  row_data = []
1734
1766
  row.each do |cell|
1735
1767
  row_data << {
@@ -1743,7 +1775,8 @@ class Converter < ::Prawn::Document
1743
1775
  colspan: cell.colspan || 1,
1744
1776
  rowspan: cell.rowspan || 1,
1745
1777
  align: (cell.attr 'halign', nil, false).to_sym,
1746
- valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
1778
+ valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym,
1779
+ padding: theme.table_head_cell_padding || theme.table_cell_padding,
1747
1780
  }
1748
1781
  end
1749
1782
  table_data << row_data
@@ -1791,9 +1824,7 @@ class Converter < ::Prawn::Document
1791
1824
  end
1792
1825
  end
1793
1826
  header_cell_data = header_cell_data_cache.dup
1794
- if (cell_transform = header_cell_data.delete :text_transform) == 'none'
1795
- cell_transform = nil
1796
- end
1827
+ cell_transform = resolve_text_transform header_cell_data, nil
1797
1828
  cell_data.update header_cell_data unless header_cell_data.empty?
1798
1829
  cell_line_metrics = calc_line_metrics theme.base_line_height
1799
1830
  when :monospaced
@@ -1871,6 +1902,11 @@ class Converter < ::Prawn::Document
1871
1902
  table_border_color = theme.table_border_color || theme.table_grid_color || theme.base_border_color
1872
1903
  table_border_style = (theme.table_border_style || :solid).to_sym
1873
1904
  table_border_width = theme.table_border_width
1905
+ if table_header
1906
+ head_border_bottom_color = theme.table_head_border_bottom_color || table_border_color
1907
+ head_border_bottom_style = (theme.table_head_border_bottom_style || table_border_style).to_sym
1908
+ head_border_bottom_width = theme.table_head_border_bottom_width || table_border_width
1909
+ end
1874
1910
  [:top, :bottom, :left, :right].each {|edge| border_width[edge] = table_border_width }
1875
1911
  table_grid_color = theme.table_grid_color || table_border_color
1876
1912
  table_grid_style = (theme.table_grid_style || table_border_style).to_sym
@@ -1920,6 +1956,7 @@ class Converter < ::Prawn::Document
1920
1956
  end
1921
1957
 
1922
1958
  caption_side = (theme.table_caption_side || :top).to_sym
1959
+ caption_max_width = (theme.table_caption_max_width || 'fit-content').to_s
1923
1960
 
1924
1961
  table_settings = {
1925
1962
  header: table_header,
@@ -1952,14 +1989,14 @@ class Converter < ::Prawn::Document
1952
1989
  table table_data, table_settings do
1953
1990
  # NOTE call width to capture resolved table width
1954
1991
  table_width = width
1955
- @pdf.layout_table_caption node, table_width, alignment if node.title? && caption_side == :top
1992
+ caption_max_width = caption_max_width == 'fit-content' ? table_width : nil
1993
+ @pdf.layout_table_caption node, alignment, caption_max_width if node.title? && caption_side == :top
1956
1994
  if grid == 'none' && frame == 'none'
1957
1995
  if table_header
1958
- # FIXME allow header border bottom width and style to be set by theme
1959
1996
  rows(0).tap do |r|
1960
- r.border_bottom_line, r.border_bottom_width = :solid, 1.25
1961
- # QUESTION should we use the table border color for the bottom border color of the header row?
1962
- #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
1997
+ r.border_bottom_color = head_border_bottom_color
1998
+ r.border_bottom_line = head_border_bottom_style
1999
+ r.border_bottom_width = head_border_bottom_width
1963
2000
  end
1964
2001
  end
1965
2002
  else
@@ -1967,16 +2004,15 @@ class Converter < ::Prawn::Document
1967
2004
  cells.border_width = [border_width[:rows], border_width[:cols], border_width[:rows], border_width[:cols]]
1968
2005
 
1969
2006
  if table_header
1970
- # FIXME allow header border bottom width and style to be set by theme
1971
2007
  rows(0).tap do |r|
1972
- r.border_bottom_line, r.border_bottom_width = :solid, 1.25
1973
- # QUESTION should we use the table border color for the bottom border color of the header row?
1974
- #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
2008
+ r.border_bottom_color = head_border_bottom_color
2009
+ r.border_bottom_line = head_border_bottom_style
2010
+ r.border_bottom_width = head_border_bottom_width
1975
2011
  end
1976
2012
  rows(1).tap do |r|
1977
- r.border_top_line, r.border_top_width = :solid, 1.25
1978
- # QUESTION should we use the table border color for the top border color of the first row?
1979
- #r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, :solid, 1.25
2013
+ r.border_top_color = head_border_bottom_color
2014
+ r.border_top_line = head_border_bottom_style
2015
+ r.border_top_width = head_border_bottom_width
1980
2016
  end if num_rows > 1
1981
2017
  end
1982
2018
 
@@ -2008,12 +2044,12 @@ class Converter < ::Prawn::Document
2008
2044
  foot_row.font = theme.table_foot_font_family if theme.table_foot_font_family
2009
2045
  foot_row.font_style = theme.table_foot_font_style.to_sym if theme.table_foot_font_style
2010
2046
  # HACK we should do this transformation when creating the cell
2011
- #if (foot_transform = theme.table_foot_text_transform) && foot_transform != 'none'
2047
+ #if (foot_transform = resolve_text_transform :table_foot_text_transform, nil)
2012
2048
  # foot_row.each {|c| c.content = (transform_text c.content, foot_transform) if c.content }
2013
2049
  #end
2014
2050
  end
2015
2051
  end
2016
- layout_table_caption node, table_width, alignment, :bottom if node.title? && caption_side == :bottom
2052
+ layout_table_caption node, alignment, caption_max_width, caption_side if node.title? && caption_side == :bottom
2017
2053
  theme_margin :block, :bottom
2018
2054
  end
2019
2055
 
@@ -2033,18 +2069,23 @@ class Converter < ::Prawn::Document
2033
2069
 
2034
2070
  # NOTE to insert sequential page breaks, you must put {nbsp} between page breaks
2035
2071
  def convert_page_break node
2036
- unless at_page_top?
2037
- if (page_layout = node.attr 'page-layout').nil_or_empty?
2038
- if node.role? && (page_layout = (node.roles.map(&:to_sym) & PageLayouts)[-1])
2039
- advance_page layout: page_layout
2040
- else
2041
- advance_page
2042
- end
2043
- elsif PageLayouts.include?(page_layout = page_layout.to_sym)
2072
+ if (page_layout = node.attr 'page-layout').nil_or_empty?
2073
+ unless node.role? && (page_layout = (node.roles.map(&:to_sym) & PageLayouts)[-1])
2074
+ page_layout = nil
2075
+ end
2076
+ elsif !PageLayouts.include?(page_layout = page_layout.to_sym)
2077
+ page_layout = nil
2078
+ end
2079
+
2080
+ if at_page_top?
2081
+ if page_layout && page_layout != page.layout && page_is_empty?
2082
+ delete_page
2044
2083
  advance_page layout: page_layout
2045
- else
2046
- advance_page
2047
2084
  end
2085
+ elsif page_layout
2086
+ advance_page layout: page_layout
2087
+ else
2088
+ advance_page
2048
2089
  end
2049
2090
  end
2050
2091
 
@@ -2161,8 +2202,7 @@ class Converter < ::Prawn::Document
2161
2202
  end
2162
2203
 
2163
2204
  def convert_inline_button node
2164
- %(<strong>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</strong>)
2165
- #%(<strong>[#{NoBreakSpace}#{node.text}#{NoBreakSpace}]</strong>)
2205
+ %(<button>#{(@theme.button_content || '%s').sub '%s', node.text}</button>)
2166
2206
  end
2167
2207
 
2168
2208
  def convert_inline_callout node
@@ -2238,11 +2278,15 @@ class Converter < ::Prawn::Document
2238
2278
  logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
2239
2279
  img = %([#{node.attr 'alt'}])
2240
2280
  # NOTE an image with a data URI is handled using a temporary file
2241
- elsif (image_path = resolve_image_path node, target, true, image_format) && (::File.readable? image_path)
2242
- width_attr = (width = preresolve_explicit_width node.attributes) ? %( width="#{width}") : nil
2243
- img = %(<img src="#{image_path}" format="#{image_format}" alt="[#{encode_quotes node.attr 'alt'}]"#{width_attr} tmp="#{TemporaryPath === image_path}">)
2281
+ elsif (image_path = resolve_image_path node, target, true, image_format)
2282
+ if ::File.readable? image_path
2283
+ width_attr = (width = preresolve_explicit_width node.attributes) ? %( width="#{width}") : nil
2284
+ img = %(<img src="#{image_path}" format="#{image_format}" alt="[#{encode_quotes node.attr 'alt'}]"#{width_attr} tmp="#{TemporaryPath === image_path}">)
2285
+ else
2286
+ logger.warn %(image to embed not found or not readable: #{image_path}) unless scratch?
2287
+ img = %([#{node.attr 'alt'}])
2288
+ end
2244
2289
  else
2245
- logger.warn %(image to embed not found or not readable: #{image_path || target}) unless scratch?
2246
2290
  img = %([#{node.attr 'alt'}])
2247
2291
  end
2248
2292
  (node.attr? 'link', nil, false) ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
@@ -2328,19 +2372,16 @@ class Converter < ::Prawn::Document
2328
2372
  def layout_title_page doc
2329
2373
  return unless doc.header? && !doc.notitle
2330
2374
 
2331
- prev_bg_image = @page_bg_image
2375
+ prev_bg_image = @page_bg_image[side = page_side]
2332
2376
  prev_bg_color = @page_bg_color
2333
-
2334
- if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
2335
- @page_bg_image = (bg_image == 'none' ? nil : bg_image)
2336
- end
2377
+ @page_bg_image[side] = (bg_image = resolve_background_image doc, @theme, 'title-page-background-image') && bg_image[0] ? bg_image : nil
2337
2378
  if (bg_color = resolve_theme_color :title_page_background_color)
2338
2379
  @page_bg_color = bg_color
2339
2380
  end
2340
2381
  # NOTE a new page will already be started if the cover image is a PDF
2341
2382
  start_new_page unless page_is_empty?
2342
2383
  start_new_page if @ppbook && verso_page?
2343
- @page_bg_image = prev_bg_image if bg_image
2384
+ @page_bg_image[side] = prev_bg_image if prev_bg_image
2344
2385
  @page_bg_color = prev_bg_color if bg_color
2345
2386
 
2346
2387
  # IMPORTANT this is the first page created, so we need to set the base font
@@ -2360,7 +2401,7 @@ class Converter < ::Prawn::Document
2360
2401
  relative_to_imagesdir = false
2361
2402
  end
2362
2403
  # HACK quick fix to resolve image path relative to theme
2363
- logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, (doc.attr 'pdf-stylesdir') unless doc.attr? 'title-logo-image'
2404
+ logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.attr? 'title-logo-image'
2364
2405
  logo_image_attrs['target'] = logo_image_path
2365
2406
  logo_image_attrs['align'] ||= (@theme.title_page_logo_align || title_align.to_s)
2366
2407
  # QUESTION should we allow theme to turn logo image off?
@@ -2448,30 +2489,33 @@ class Converter < ::Prawn::Document
2448
2489
  end
2449
2490
  end
2450
2491
 
2451
- def layout_cover_page face, doc
2492
+ def layout_cover_page doc, face
2452
2493
  # TODO turn processing of attribute with inline image a utility function in Asciidoctor
2453
- if (cover_image = (doc.attr %(#{face}-cover-image)))
2454
- if (cover_image.include? ':') && cover_image =~ ImageAttributeValueRx
2455
- # TODO support explicit image format
2456
- cover_image = resolve_image_path doc, $1
2494
+ if (image_path = (doc.attr %(#{face}-cover-image)))
2495
+ if (image_path.include? ':') && image_path =~ ImageAttributeValueRx
2496
+ image_attrs = (AttributeList.new $2).parse ['alt', 'width']
2497
+ image_path = resolve_image_path doc, $1, true, (image_format = image_attrs['format'])
2457
2498
  else
2458
- cover_image = resolve_image_path doc, cover_image, false
2499
+ image_path = resolve_image_path doc, image_path, false
2459
2500
  end
2460
2501
 
2461
- if ::File.readable? cover_image
2462
- go_to_page page_count if face == :back
2463
- if cover_image.downcase.end_with? '.pdf'
2464
- # NOTE import_page automatically advances to next page afterwards (can we change this behavior?)
2465
- import_page cover_image, advance: face != :back
2466
- else
2467
- image_page cover_image, canvas: true
2468
- end
2502
+ return unless image_path
2503
+
2504
+ unless ::File.readable? image_path
2505
+ logger.warn %(#{face} cover image not found or readable: #{image_path})
2506
+ return
2507
+ end
2508
+
2509
+ go_to_page page_count if face == :back
2510
+ if image_path.downcase.end_with? '.pdf'
2511
+ import_page image_path, advance: face != :back
2469
2512
  else
2470
- logger.warn %(#{face} cover image not found or readable: #{cover_image})
2513
+ image_opts = resolve_image_options image_path, image_attrs, background: true, format: image_format
2514
+ image_page image_path, (image_opts.merge canvas: true)
2471
2515
  end
2472
2516
  end
2473
2517
  ensure
2474
- unlink_tmp_file cover_image if cover_image
2518
+ unlink_tmp_file image_path if image_path
2475
2519
  end
2476
2520
 
2477
2521
  def start_new_chapter chapter
@@ -2492,7 +2536,7 @@ class Converter < ::Prawn::Document
2492
2536
  def layout_heading string, opts = {}
2493
2537
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme[%(heading_h#{opts[:level]}_margin_top)] || @theme.heading_margin_top
2494
2538
  bot_margin = margin || (opts.delete :margin_bottom) || @theme[%(heading_h#{opts[:level]}_margin_bottom)] || @theme.heading_margin_bottom
2495
- if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
2539
+ if (transform = resolve_text_transform opts)
2496
2540
  string = transform_text string, transform
2497
2541
  end
2498
2542
  margin_top top_margin
@@ -2508,7 +2552,7 @@ class Converter < ::Prawn::Document
2508
2552
  def layout_prose string, opts = {}
2509
2553
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top
2510
2554
  bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
2511
- if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
2555
+ if (transform = resolve_text_transform opts)
2512
2556
  string = transform_text string, transform
2513
2557
  end
2514
2558
  # NOTE used by extensions; ensures linked text gets formatted using the link styles
@@ -2581,14 +2625,19 @@ class Converter < ::Prawn::Document
2581
2625
  end
2582
2626
 
2583
2627
  # Render the caption for a table and return the height of the rendered content
2584
- def layout_table_caption node, width, alignment = :left, side = :top
2585
- # QUESTION should we confine width of title to width of table?
2586
- if alignment == :left || (excess = bounds.width - width) == 0
2587
- layout_caption node, side: side
2588
- else
2589
- indent excess * (alignment == :center ? 0.5 : 1) do
2590
- layout_caption node, side: side
2628
+ def layout_table_caption node, table_alignment = :left, max_width = nil, side = :top
2629
+ if max_width && (remainder = bounds.width - max_width) > 0
2630
+ case table_alignment
2631
+ when :right
2632
+ indent(remainder) { layout_caption node, side: side }
2633
+ when :center
2634
+ side_margin = remainder * 0.5
2635
+ indent(side_margin, side_margin) { layout_caption node, side: side }
2636
+ else # :left
2637
+ indent(0, remainder) { layout_caption node, side: side }
2591
2638
  end
2639
+ else
2640
+ layout_caption node, side: side
2592
2641
  end
2593
2642
  end
2594
2643
 
@@ -2639,8 +2688,7 @@ class Converter < ::Prawn::Document
2639
2688
  end
2640
2689
  sections.each do |sect|
2641
2690
  theme_font :toc, level: (sect.level + 1) do
2642
- sect_title = (transform = @text_transform) && transform != 'none' ?
2643
- (transform_text sect.numbered_title, transform) : sect.numbered_title
2691
+ sect_title = (transform = @text_transform) ? (transform_text sect.numbered_title, transform) : sect.numbered_title
2644
2692
  # NOTE only write section title (excluding dots and page number) if this is a dry run
2645
2693
  if scratch?
2646
2694
  # FIXME use layout_prose
@@ -2728,8 +2776,8 @@ class Converter < ::Prawn::Document
2728
2776
  # FIXME probably need to treat doctypes differently
2729
2777
  is_book = doc.doctype == 'book'
2730
2778
  header = doc.header? ? doc.header : nil
2731
- # TODO make this section threshold configurable (perhaps in theme?)
2732
- sections = doc.find_by(context: :section) {|sect| sect.level < 3 && sect != header } || []
2779
+ sectlevels = (@theme[%(#{periphery}_sectlevels)] || 2).to_i
2780
+ sections = doc.find_by(context: :section) {|sect| sect.level <= sectlevels && sect != header } || []
2733
2781
 
2734
2782
  # FIXME we need a proper model for all this page counting
2735
2783
  # FIXME we make a big assumption that part & chapter start on new pages
@@ -2810,168 +2858,6 @@ class Converter < ::Prawn::Document
2810
2858
  doc.set_attr 'document-title', doctitle.main
2811
2859
  doc.set_attr 'document-subtitle', doctitle.subtitle
2812
2860
  doc.set_attr 'page-count', num_pages
2813
- allow_uri_read = doc.attr? 'allow-uri-read'
2814
- svg_fallback_font = default_svg_font
2815
-
2816
- if periphery == :header
2817
- trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height)
2818
- trim_top = page_height
2819
- # NOTE height is required atm
2820
- trim_height = @theme.header_height || page_margin_top
2821
- trim_padding = inflate_padding @theme.header_padding || 0
2822
- trim_bg_color = resolve_theme_color :header_background_color
2823
- trim_border_width = @theme.header_border_width || @theme.base_border_width
2824
- trim_border_style = (@theme.header_border_style || :solid).to_sym
2825
- trim_border_color = resolve_theme_color :header_border_color
2826
- trim_valign = (@theme.header_vertical_align || :middle).to_sym
2827
- trim_img_valign = @theme.header_image_vertical_align
2828
- else
2829
- trim_line_metrics = calc_line_metrics(@theme.footer_line_height || @theme.base_line_height)
2830
- # NOTE height is required atm
2831
- trim_top = trim_height = @theme.footer_height || page_margin_bottom
2832
- trim_padding = inflate_padding @theme.footer_padding || 0
2833
- trim_bg_color = resolve_theme_color :footer_background_color
2834
- trim_border_width = @theme.footer_border_width || @theme.base_border_width
2835
- trim_border_style = (@theme.footer_border_style || :solid).to_sym
2836
- trim_border_color = resolve_theme_color :footer_border_color
2837
- trim_valign = (@theme.footer_vertical_align || :middle).to_sym
2838
- trim_img_valign = @theme.footer_image_vertical_align
2839
- end
2840
-
2841
- trim_stamp_name = {
2842
- recto: %(#{periphery}_recto),
2843
- verso: %(#{periphery}_verso)
2844
- }
2845
- trim_left = {
2846
- recto: @page_margin_by_side[:recto][3],
2847
- verso: @page_margin_by_side[:verso][3]
2848
- }
2849
- trim_width = {
2850
- recto: page_width - trim_left[:recto] - @page_margin_by_side[:recto][1],
2851
- verso: page_width - trim_left[:verso] - @page_margin_by_side[:verso][1]
2852
- }
2853
- trim_content_left = {
2854
- recto: trim_left[:recto] + trim_padding[3],
2855
- verso: trim_left[:verso] + trim_padding[3]
2856
- }
2857
- trim_content_width = {
2858
- recto: trim_width[:recto] - trim_padding[3] - trim_padding[1],
2859
- verso: trim_width[:verso] - trim_padding[3] - trim_padding[1]
2860
- }
2861
- trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom
2862
- trim_border_color = nil if trim_border_width == 0
2863
- trim_valign = :center if trim_valign == :middle
2864
- case trim_img_valign
2865
- when nil
2866
- trim_img_valign = trim_valign
2867
- when 'middle'
2868
- trim_img_valign = :center
2869
- when 'top', 'center', 'bottom'
2870
- trim_img_valign = trim_img_valign.to_sym
2871
- end
2872
-
2873
- colspec_dict = PageSides.inject({}) do |acc, side|
2874
- side_trim_content_width = trim_content_width[side]
2875
- if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
2876
- case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split[0..2]).size
2877
- when 3
2878
- colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
2879
- when 2
2880
- colspecs = { left: colspecs[0], center: '0', right: colspecs[1] }
2881
- when 0, 1
2882
- colspecs = { left: '0', center: colspecs[0] || '100', right: '0' }
2883
- end
2884
- tot_width = 0
2885
- side_colspecs = colspecs.map {|col, spec|
2886
- if (alignment_char = spec.chr).to_i.to_s != alignment_char
2887
- alignment = AlignmentTable[alignment_char] || :left
2888
- rel_width = spec[1..-1].to_f
2889
- else
2890
- alignment = :left
2891
- rel_width = spec.to_f
2892
- end
2893
- tot_width += rel_width
2894
- [col, { align: alignment, width: rel_width, x: 0 }]
2895
- }.to_h
2896
- # QUESTION should we allow the columns to overlap (capping width at 100%)?
2897
- side_colspecs.each {|_, colspec| colspec[:width] = (colspec[:width] / tot_width) * side_trim_content_width }
2898
- side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
2899
- acc[side] = side_colspecs
2900
- else
2901
- acc[side] = {
2902
- left: { align: :left, width: side_trim_content_width, x: 0 },
2903
- center: { align: :center, width: side_trim_content_width, x: 0 },
2904
- right: { align: :right, width: side_trim_content_width, x: 0 }
2905
- }
2906
- end
2907
- acc
2908
- end
2909
-
2910
- # TODO move this to a method so it can be reused; cache results
2911
- content_dict = PageSides.inject({}) do |acc, side|
2912
- side_content = {}
2913
- ColumnPositions.each do |position|
2914
- unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
2915
- # TODO support image URL (using resolve_image_path)
2916
- if (val.include? ':') && val =~ ImageAttributeValueRx
2917
- if ::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
2918
- attrs = (AttributeList.new $2).parse
2919
- col_width = colspec_dict[side][position][:width]
2920
- if (fit = attrs['fit']) == 'contain'
2921
- width = col_width
2922
- else
2923
- unless (width = resolve_explicit_width attrs, col_width)
2924
- # QUESTION should we lookup and scale intrinsic width if explicit width is not given?
2925
- # NOTE failure message will be reported later when image is rendered
2926
- width = (to_pt intrinsic_image_dimensions(path)[:width], :px) rescue 0
2927
- end
2928
- width = col_width if fit == 'scale-down' && width > col_width
2929
- end
2930
- side_content[position] = { path: path, width: width, fit: !!fit }
2931
- else
2932
- logger.warn %(image to embed not found or not readable: #{path})
2933
- side_content[position] = val
2934
- end
2935
- else
2936
- side_content[position] = val
2937
- end
2938
- end
2939
- end
2940
- # NOTE set fallbacks if not explicitly disabled
2941
- if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
2942
- side_content = { side == :recto ? :right : :left => '{page-number}' }
2943
- end
2944
-
2945
- acc[side] = side_content
2946
- acc
2947
- end
2948
-
2949
- stamps = {}
2950
- if trim_bg_color || trim_border_color
2951
- PageSides.each do |side|
2952
- create_stamp trim_stamp_name[side] do
2953
- canvas do
2954
- if trim_bg_color
2955
- bounding_box [0, trim_top], width: bounds.width, height: trim_height do
2956
- fill_bounds trim_bg_color
2957
- if trim_border_color
2958
- # TODO stroke_horizontal_rule should support :at
2959
- move_down bounds.height if periphery == :header
2960
- stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
2961
- end
2962
- end
2963
- else
2964
- bounding_box [trim_left[side], trim_top], width: trim_width[side], height: trim_height do
2965
- # TODO stroke_horizontal_rule should support :at
2966
- move_down bounds.height if periphery == :header
2967
- stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
2968
- end
2969
- end
2970
- end
2971
- end
2972
- end
2973
- stamps[periphery] = true
2974
- end
2975
2861
 
2976
2862
  pagenums_enabled = doc.attr? 'pagenums'
2977
2863
  attribute_missing_doc = doc.attr 'attribute-missing'
@@ -2985,12 +2871,15 @@ class Converter < ::Prawn::Document
2985
2871
  else
2986
2872
  folio_basis, invert_folio = :virtual, false
2987
2873
  end
2874
+ periphery_layout_cache = {}
2988
2875
  repeat((content_start_page..page_count), dynamic: true) do
2989
2876
  # NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
2990
2877
  next if page.imported_page?
2991
2878
  pgnum_label = page_number - skip_pagenums
2992
2879
  pgnum_label = (RomanNumeral.new page_number, :lower) if pgnum_label < 1
2993
2880
  side = page_side((folio_basis == :physical ? page_number : pgnum_label), invert_folio)
2881
+ # QUESTION should allocation be per side?
2882
+ trim_styles, colspec_dict, content_dict, stamp_names = allocate_running_content_layout page, periphery, periphery_layout_cache
2994
2883
  # FIXME we need to have a content setting for chapter pages
2995
2884
  content_by_position, colspec_by_position = content_dict[side], colspec_dict[side]
2996
2885
  # TODO populate chapter-number
@@ -3002,77 +2891,73 @@ class Converter < ::Prawn::Document
3002
2891
  doc.set_attr 'section-title', (sections_by_page[pgnum_label] || '')
3003
2892
  doc.set_attr 'section-or-chapter-title', (sections_by_page[pgnum_label] || chapters_by_page[pgnum_label] || '')
3004
2893
 
3005
- stamp trim_stamp_name[side] if stamps[periphery]
2894
+ stamp stamp_names[side] if stamp_names
3006
2895
 
3007
2896
  theme_font periphery do
3008
2897
  canvas do
3009
- bounding_box [trim_content_left[side], trim_top], width: trim_content_width[side], height: trim_height do
2898
+ bounding_box [trim_styles[:content_left][side], trim_styles[:top]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
2899
+ if (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
2900
+ trim_column_rule_spacing = trim_styles[:column_rule_spacing]
2901
+ else
2902
+ trim_column_rule_width = nil
2903
+ end
2904
+ prev_position = nil
3010
2905
  ColumnPositions.each do |position|
3011
2906
  next unless (content = content_by_position[position])
3012
2907
  next unless (colspec = colspec_by_position[position])[:width] > 0
2908
+ left, colwidth = colspec[:x], colspec[:width]
2909
+ if trim_column_rule_width && colwidth < bounds.width
2910
+ if (trim_column_rule = prev_position)
2911
+ left += (trim_column_rule_spacing * 0.5)
2912
+ colwidth -= trim_column_rule_spacing
2913
+ else
2914
+ colwidth -= (trim_column_rule_spacing * 0.5)
2915
+ end
2916
+ end
3013
2917
  # FIXME we need to have a content setting for chapter pages
3014
2918
  case content
3015
- when ::Hash
3016
- # NOTE image vposition respects padding; use negative image_vertical_align value to revert
3017
- trim_v_padding = trim_padding[0] + trim_padding[2]
2919
+ when ::Array
3018
2920
  # NOTE float ensures cursor position is restored and returns us to current page if we overrun
3019
2921
  float do
3020
- # NOTE bounding_box is redundant if trim_v_padding is 0
3021
- bounding_box [colspec[:x], cursor - trim_padding[0]], width: colspec[:width], height: (bounds.height - trim_v_padding) do
3022
- begin
3023
- if (img_path = content[:path]).downcase.end_with? '.svg'
3024
- svg_data = ::File.read img_path
3025
- svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
3026
- position: colspec[:align],
3027
- vposition: trim_img_valign,
3028
- width: content[:width],
3029
- # TODO enforce jail in safe mode
3030
- enable_file_requests_with_root: (::File.dirname img_path),
3031
- enable_web_requests: allow_uri_read,
3032
- fallback_font_name: svg_fallback_font
3033
- if content[:fit] && svg_obj.document.sizing.output_height > (available_h = bounds.height)
3034
- svg_obj.resize height: available_h
3035
- end
3036
- svg_obj.draw
3037
- else
3038
- img_opts = { position: colspec[:align], vposition: trim_img_valign }
3039
- if content[:fit]
3040
- img_opts[:fit] = [content[:width], bounds.height]
3041
- else
3042
- img_opts[:width] = content[:width]
3043
- end
3044
- image img_path, img_opts
3045
- end
3046
- rescue
3047
- logger.warn %(could not embed image in running content: #{img_path}; #{$!.message})
3048
- end
2922
+ # NOTE bounding_box is redundant if both vertical padding and border width are 0
2923
+ bounding_box [left, bounds.top - trim_styles[:padding][0] - trim_styles[:content_offset]], width: colwidth, height: trim_styles[:content_height] do
2924
+ # NOTE image vposition respects padding; use negative image_vertical_align value to revert
2925
+ 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})
3049
2927
  end
3050
2928
  end
3051
2929
  when ::String
3052
- # NOTE minor optimization
3053
- if content == '{page-number}'
3054
- content = pagenums_enabled ? pgnum_label.to_s : nil
3055
- else
3056
- # FIXME get apply_subs to handle drop-line w/o a warning
3057
- doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip'
3058
- if (content = doc.apply_subs content).include? '{'
3059
- # NOTE must use &#123; in place of {, not \{, to escape attribute reference
3060
- content = content.split(LF).delete_if {|line| SimpleAttributeRefRx.match? line } * LF
3061
- end
3062
- doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip'
3063
- end
3064
2930
  theme_font %(#{periphery}_#{side}_#{position}) do
2931
+ # NOTE minor optimization
2932
+ if content == '{page-number}'
2933
+ content = pagenums_enabled ? pgnum_label.to_s : nil
2934
+ else
2935
+ # FIXME get apply_subs to handle drop-line w/o a warning
2936
+ doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip'
2937
+ if (content = doc.apply_subs content).include? '{'
2938
+ # NOTE must use &#123; in place of {, not \{, to escape attribute reference
2939
+ content = content.split(LF).delete_if {|line| SimpleAttributeRefRx.match? line } * LF
2940
+ end
2941
+ doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip'
2942
+ if (transform = @text_transform) && transform != 'none'
2943
+ content = transform_text content, @text_transform
2944
+ end
2945
+ end
3065
2946
  formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]),
3066
- at: [colspec[:x], trim_top - trim_padding[0] + (trim_valign == :center ? font.descender * 0.5 : 0)],
3067
- width: colspec[:width],
3068
- height: trim_content_height,
2947
+ at: [left, bounds.top - trim_styles[:padding][0] - trim_styles[:content_offset] + (trim_styles[:valign] == :center ? font.descender * 0.5 : 0)],
2948
+ width: colwidth,
2949
+ height: trim_styles[:prose_content_height],
3069
2950
  align: colspec[:align],
3070
- valign: trim_valign,
3071
- leading: trim_line_metrics.leading,
2951
+ valign: trim_styles[:valign],
2952
+ leading: trim_styles[:line_metrics].leading,
3072
2953
  final_gap: false,
3073
2954
  overflow: :truncate
3074
2955
  end
3075
2956
  end
2957
+ bounding_box [colspec[:x], bounds.top - trim_styles[:padding][0] - trim_styles[:content_offset]], width: colspec[:width], height: trim_styles[:content_height] do
2958
+ stroke_vertical_rule trim_styles[:column_rule_color], at: bounds.left, line_style: trim_styles[:column_rule_style], line_width: trim_column_rule_width
2959
+ end if trim_column_rule
2960
+ prev_position = position
3076
2961
  end
3077
2962
  end
3078
2963
  end
@@ -3083,6 +2968,150 @@ class Converter < ::Prawn::Document
3083
2968
  nil
3084
2969
  end
3085
2970
 
2971
+ def allocate_running_content_layout page, periphery, cache
2972
+ layout = page.layout
2973
+ cache[layout] ||= begin
2974
+ trim_styles = {
2975
+ line_metrics: (trim_line_metrics = calc_line_metrics @theme[%(#{periphery}_line_height)] || @theme.base_line_height),
2976
+ # NOTE we've already verified this property is set
2977
+ height: (trim_height = @theme[%(#{periphery}_height)]),
2978
+ top: periphery == :header ? page_height : trim_height,
2979
+ padding: (trim_padding = inflate_padding @theme[%(#{periphery}_padding)] || 0),
2980
+ bg_color: (resolve_theme_color %(#{periphery}_background_color).to_sym),
2981
+ border_color: (trim_border_color = resolve_theme_color %(#{periphery}_border_color).to_sym),
2982
+ border_style: (@theme[%(#{periphery}_border_style)] || :solid).to_sym,
2983
+ border_width: (trim_border_width = trim_border_color ? @theme[%(#{periphery}_border_width)] || @theme.base_border_width || 0 : 0),
2984
+ column_rule_color: (trim_column_rule_color = resolve_theme_color %(#{periphery}_column_rule_color).to_sym),
2985
+ column_rule_style: (@theme[%(#{periphery}_column_rule_style)] || :solid).to_sym,
2986
+ column_rule_width: (trim_column_rule_color ? @theme[%(#{periphery}_column_rule_width)] || 0 : 0),
2987
+ column_rule_spacing: (trim_column_rule_spacing = @theme[%(#{periphery}_column_rule_spacing)] || 0),
2988
+ valign: (val = (@theme[%(#{periphery}_vertical_align)] || :middle).to_sym) == :middle ? :center : val,
2989
+ img_valign: @theme[%(#{periphery}_image_vertical_align)],
2990
+ left: {
2991
+ recto: (trim_left_recto = @page_margin_by_side[:recto][3]),
2992
+ verso: (trim_left_verso = @page_margin_by_side[:verso][3]),
2993
+ },
2994
+ width: {
2995
+ recto: (trim_width_recto = page_width - trim_left_recto - @page_margin_by_side[:recto][1]),
2996
+ verso: (trim_width_verso = page_width - trim_left_verso - @page_margin_by_side[:verso][1]),
2997
+ },
2998
+ content_left: {
2999
+ recto: trim_left_recto + trim_padding[3],
3000
+ verso: trim_left_verso + trim_padding[3],
3001
+ },
3002
+ content_width: (trim_content_width = {
3003
+ recto: trim_width_recto - trim_padding[1] - trim_padding[3],
3004
+ verso: trim_width_verso - trim_padding[1] - trim_padding[3],
3005
+ }),
3006
+ content_height: (content_height = trim_height - trim_padding[0] - trim_padding[2] - (trim_border_width * 0.5)),
3007
+ prose_content_height: content_height - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom,
3008
+ # NOTE content offset adjusts y position to account for border
3009
+ content_offset: (periphery == :footer ? trim_border_width * 0.5 : 0),
3010
+ }
3011
+ case trim_styles[:img_valign]
3012
+ when nil
3013
+ trim_styles[:img_valign] = trim_styles[:valign]
3014
+ when 'middle'
3015
+ trim_styles[:img_valign] = :center
3016
+ when 'top', 'center', 'bottom'
3017
+ trim_styles[:img_valign] = trim_styles[:img_valign].to_sym
3018
+ end
3019
+
3020
+ colspec_dict = PageSides.inject({}) do |acc, side|
3021
+ side_trim_content_width = trim_content_width[side]
3022
+ if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
3023
+ case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split[0..2]).size
3024
+ when 3
3025
+ colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
3026
+ when 2
3027
+ colspecs = { left: colspecs[0], center: '0', right: colspecs[1] }
3028
+ when 0, 1
3029
+ colspecs = { left: '0', center: colspecs[0] || '100', right: '0' }
3030
+ end
3031
+ tot_width = 0
3032
+ side_colspecs = colspecs.map {|col, spec|
3033
+ if (alignment_char = spec.chr).to_i.to_s != alignment_char
3034
+ alignment = AlignmentTable[alignment_char] || :left
3035
+ rel_width = spec[1..-1].to_f
3036
+ else
3037
+ alignment = :left
3038
+ rel_width = spec.to_f
3039
+ end
3040
+ tot_width += rel_width
3041
+ [col, { align: alignment, width: rel_width, x: 0 }]
3042
+ }.to_h
3043
+ # QUESTION should we allow the columns to overlap (capping width at 100%)?
3044
+ side_colspecs.each {|_, colspec| colspec[:width] = (colspec[:width] / tot_width) * side_trim_content_width }
3045
+ side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
3046
+ acc[side] = side_colspecs
3047
+ else
3048
+ acc[side] = {
3049
+ left: { align: :left, width: side_trim_content_width, x: 0 },
3050
+ center: { align: :center, width: side_trim_content_width, x: 0 },
3051
+ right: { align: :right, width: side_trim_content_width, x: 0 }
3052
+ }
3053
+ end
3054
+ acc
3055
+ end
3056
+
3057
+ content_dict = PageSides.inject({}) do |acc, side|
3058
+ side_content = {}
3059
+ ColumnPositions.each do |position|
3060
+ unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
3061
+ if (val.include? ':') && val =~ ImageAttributeValueRx
3062
+ # TODO support image URL
3063
+ if ::File.readable? (image_path = (ThemeLoader.resolve_theme_asset $1, @themesdir))
3064
+ image_attrs = (AttributeList.new $2).parse ['alt', 'width']
3065
+ 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]
3067
+ else
3068
+ # NOTE allows inline image handler to report invalid reference and replace with alt text
3069
+ side_content[position] = %(image:#{image_path}[#{$2}])
3070
+ end
3071
+ else
3072
+ side_content[position] = val
3073
+ end
3074
+ end
3075
+ end
3076
+ # NOTE set fallbacks if not explicitly disabled
3077
+ if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
3078
+ side_content = { side == :recto ? :right : :left => '{page-number}' }
3079
+ end
3080
+
3081
+ acc[side] = side_content
3082
+ acc
3083
+ end
3084
+
3085
+ if trim_styles[:bg_color] || trim_styles[:border_width] > 0
3086
+ stamp_names = { recto: %(#{layout}_#{periphery}_recto), verso: %(#{layout}_#{periphery}_verso) }
3087
+ PageSides.each do |side|
3088
+ create_stamp stamp_names[side] do
3089
+ canvas do
3090
+ if trim_styles[:bg_color]
3091
+ bounding_box [0, trim_styles[:top]], width: bounds.width, height: trim_styles[:height] do
3092
+ fill_bounds trim_styles[:bg_color]
3093
+ if trim_styles[:border_width] > 0
3094
+ # TODO stroke_horizontal_rule should support :at
3095
+ move_down bounds.height if periphery == :header
3096
+ stroke_horizontal_rule trim_styles[:border_color], line_width: trim_styles[:border_width], line_style: trim_styles[:border_style]
3097
+ end
3098
+ end
3099
+ else
3100
+ bounding_box [trim_styles[:left][side], trim_styles[:top]], width: trim_styles[:width][side], height: trim_styles[:height] do
3101
+ # TODO stroke_horizontal_rule should support :at
3102
+ move_down bounds.height if periphery == :header
3103
+ stroke_horizontal_rule trim_styles[:border_color], line_width: trim_styles[:border_width], line_style: trim_styles[:border_style]
3104
+ end
3105
+ end
3106
+ end
3107
+ end
3108
+ end
3109
+ end
3110
+
3111
+ [trim_styles, colspec_dict, content_dict, stamp_names]
3112
+ end
3113
+ end
3114
+
3086
3115
  def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0
3087
3116
  front_matter_counter = RomanNumeral.new 0, :lower
3088
3117
  pagenum_labels = {}
@@ -3099,7 +3128,7 @@ class Converter < ::Prawn::Document
3099
3128
  outline.define do
3100
3129
  # FIXME use sanitize: :plain_text once available
3101
3130
  if (doctitle = document.sanitize(doc.doctitle use_fallback: true))
3102
- # FIXME link to title page if there's a cover page (skip cover page and ensuing blank page)
3131
+ # FIXME link to title page if there's a cover page (skip cover page and ensure blank page)
3103
3132
  page title: doctitle, destination: (document.dest_top 1)
3104
3133
  end
3105
3134
  page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) unless toc_page_nums.none?
@@ -3160,6 +3189,16 @@ class Converter < ::Prawn::Document
3160
3189
  @theme.svg_font_family || @theme.base_font_family
3161
3190
  end
3162
3191
 
3192
+ attr_reader :allow_uri_read
3193
+
3194
+ def resolve_text_transform key, use_fallback = true
3195
+ if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key.to_s])
3196
+ transform == 'none' ? nil : transform
3197
+ elsif use_fallback
3198
+ @text_transform
3199
+ end
3200
+ end
3201
+
3163
3202
  # QUESTION should we pass a category as an argument?
3164
3203
  # QUESTION should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
3165
3204
  def resolve_theme_color key, fallback_color = nil
@@ -3233,7 +3272,7 @@ class Converter < ::Prawn::Document
3233
3272
  end
3234
3273
 
3235
3274
  prev_color, @font_color = @font_color, color if color
3236
- prev_transform, @text_transform = @text_transform, transform if transform
3275
+ prev_transform, @text_transform = @text_transform, (transform == 'none' ? nil : transform) if transform
3237
3276
 
3238
3277
  font family, size: size, style: (style && style.to_sym) do
3239
3278
  result = yield
@@ -3486,7 +3525,7 @@ class Converter < ::Prawn::Document
3486
3525
  doc = node.document
3487
3526
  imagesdir = relative_to_imagesdir ? (resolve_imagesdir doc) : nil
3488
3527
  image_path ||= node.attr 'target'
3489
- image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node : nil)
3528
+ image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node.attributes : nil)
3490
3529
  # NOTE currently used for inline images
3491
3530
  if ::Base64 === image_path
3492
3531
  tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
@@ -3502,7 +3541,7 @@ class Converter < ::Prawn::Document
3502
3541
  # handle case when image is a URI
3503
3542
  elsif (node.is_uri? image_path) || (imagesdir && (node.is_uri? imagesdir) &&
3504
3543
  (image_path = (node.normalize_web_path image_path, imagesdir, false)))
3505
- unless doc.attr? 'allow-uri-read'
3544
+ unless allow_uri_read
3506
3545
  logger.warn %(allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
3507
3546
  return
3508
3547
  end
@@ -3527,34 +3566,89 @@ class Converter < ::Prawn::Document
3527
3566
  end
3528
3567
  end
3529
3568
 
3530
- # Resolve the path to the background image either from a document attribute or theme key.
3569
+ # Resolve the path and sizing of the background image either from a document attribute or theme key.
3531
3570
  #
3532
- # Returns The string "none" if the background image value is none, otherwise the resolved
3533
- # path to the image. If neither the document attribute or theme key are specified, or
3534
- # the image path cannot be resolved, return nil.
3571
+ # Returns the argument list for the image method if the document attribute or theme key is found. Otherwise,
3572
+ # nothing. The first argument in the argument list is the image path. If that value is nil, the background
3573
+ # image is disabled. The second argument is the options hash to specify the dimensions, such as width and fit.
3535
3574
  def resolve_background_image doc, theme, key
3536
- if (bg_image = (doc_attr_val = (doc.attr key)) || theme[(key.tr '-', '_').to_sym])
3537
- return bg_image if bg_image == 'none'
3538
-
3539
- if (bg_image.include? ':') && bg_image =~ ImageAttributeValueRx
3540
- # QUESTION should we support width and height in this case?
3541
- # TODO support explicit format
3542
- bg_image = $1
3543
- relative_to_imagesdir = true
3575
+ if (image_path = (doc.attr key) || (from_theme = theme[(key.tr '-', '_').to_sym]))
3576
+ if image_path == 'none'
3577
+ return []
3578
+ elsif (image_path.include? ':') && image_path =~ ImageAttributeValueRx
3579
+ image_attrs = (AttributeList.new $2).parse ['alt', 'width']
3580
+ # TODO support remote image when loaded from theme
3581
+ image_path = from_theme ? (ThemeLoader.resolve_theme_asset $1, @themesdir) : (resolve_image_path doc, $1, true, (image_format = image_attrs['format']))
3544
3582
  else
3545
- relative_to_imagesdir = false
3583
+ image_path = from_theme ? (ThemeLoader.resolve_theme_asset image_path, @themesdir) : (resolve_image_path doc, image_path, false)
3546
3584
  end
3547
3585
 
3548
- if (bg_image = doc_attr_val ? (resolve_image_path doc, bg_image, relative_to_imagesdir) :
3549
- (ThemeLoader.resolve_theme_asset bg_image, (doc.attr 'pdf-stylesdir')))
3550
- if ::File.readable? bg_image
3551
- bg_image
3552
- else
3553
- logger.warn %(#{key.tr '-', ' '} not found or readable: #{bg_image})
3554
- nil
3586
+ return unless image_path
3587
+
3588
+ unless ::File.readable? image_path
3589
+ logger.warn %(#{key.tr '-', ' '} not found or readable: #{image_path})
3590
+ return
3591
+ end
3592
+
3593
+ [image_path, (resolve_image_options image_path, image_attrs, background: true, format: image_format)]
3594
+ end
3595
+ end
3596
+
3597
+ def resolve_image_options image_path, image_attrs, opts = {}
3598
+ if (image_format = opts[:format] || (::Asciidoctor::Image.format image_path)) == 'svg'
3599
+ image_opts = {
3600
+ enable_file_requests_with_root: (::File.dirname image_path),
3601
+ enable_web_requests: allow_uri_read,
3602
+ fallback_font_name: default_svg_font,
3603
+ format: 'svg',
3604
+ }
3605
+ else
3606
+ image_opts = {}
3607
+ end
3608
+ background = opts[:background]
3609
+ container_size = opts.fetch :container_size, (background ? [page_width, page_height] : [bounds.width, bounds.height])
3610
+ if image_attrs
3611
+ if background && (image_pos = image_attrs['position']) && (image_pos = resolve_background_position image_pos, nil)
3612
+ image_opts.update image_pos
3613
+ end
3614
+ if (image_fit = image_attrs['fit'])
3615
+ container_width, container_height = container_size
3616
+ case image_fit
3617
+ when 'none'
3618
+ if (image_width = resolve_explicit_width image_attrs, container_width)
3619
+ image_opts[:width] = image_width
3620
+ end
3621
+ when 'scale-down'
3622
+ # NOTE if width and height aren't set in SVG, real width and height are computed after stretching viewbox to fit page
3623
+ if (image_width = resolve_explicit_width image_attrs, container_width) && image_width > container_width
3624
+ image_opts[:fit] = container_size
3625
+ elsif (image_size = intrinsic_image_dimensions image_path, image_format) &&
3626
+ (image_width ? image_width * (image_size[:height] / image_size[:width]) > container_height : (to_pt image_size[:width], :px) > container_width || (to_pt image_size[:height], :px) > container_height)
3627
+ image_opts[:fit] = container_size
3628
+ elsif image_width
3629
+ image_opts[:width] = image_width
3630
+ end
3631
+ when 'cover'
3632
+ # QUESTION should we take explicit width into account?
3633
+ if (image_size = intrinsic_image_dimensions image_path, image_format)
3634
+ if container_width * (image_size[:height] / image_size[:width]) < container_height
3635
+ image_opts[:height] = container_height
3636
+ else
3637
+ image_opts[:width] = container_width
3638
+ end
3639
+ end
3640
+ else # contain
3641
+ image_opts[:fit] = container_size
3555
3642
  end
3643
+ elsif (image_width = resolve_explicit_width image_attrs, container_size[0])
3644
+ image_opts[:width] = image_width
3645
+ else # default to fit=contain if sizing is not specified
3646
+ image_opts[:fit] = container_size
3556
3647
  end
3648
+ else
3649
+ image_opts[:fit] = container_size
3557
3650
  end
3651
+ image_opts
3558
3652
  end
3559
3653
 
3560
3654
  # Resolves the explicit width as a PDF pt value if the value is specified in
@@ -3609,7 +3703,39 @@ class Converter < ::Prawn::Document
3609
3703
  end
3610
3704
  elsif attrs.key? 'width'
3611
3705
  # QUESTION should we honor percentage width value?
3612
- [max_width, (to_pt attrs['width'].to_f, :px)].min
3706
+ width = to_pt attrs['width'].to_f, :px
3707
+ opts[:constrain_to_bounds] ? [max_width, width].min : width
3708
+ end
3709
+ end
3710
+
3711
+ def resolve_background_position value, default_value = {}
3712
+ if value.include? ' '
3713
+ result = {}
3714
+ center = nil
3715
+ (value.split ' ', 2).each do |keyword|
3716
+ if keyword == 'left' || keyword == 'right'
3717
+ result[:position] = keyword.to_sym
3718
+ elsif keyword == 'top' || keyword == 'bottom'
3719
+ result[:vposition] = keyword.to_sym
3720
+ elsif keyword == 'center'
3721
+ center = true
3722
+ end
3723
+ end
3724
+ if center
3725
+ result[:position] ||= :center
3726
+ result[:vposition] ||= :center
3727
+ result
3728
+ elsif (result.key? :position) && (result.key? :vposition)
3729
+ result
3730
+ else
3731
+ default_value
3732
+ end
3733
+ elsif value == 'left' || value == 'right' || value == 'center'
3734
+ { position: value.to_sym, vposition: :center }
3735
+ elsif value == 'top' || value == 'bottom'
3736
+ { position: :center, vposition: value.to_sym }
3737
+ else
3738
+ default_value
3613
3739
  end
3614
3740
  end
3615
3741