diff --git a/data/themes/default-theme.yml b/data/themes/default-theme.yml index 301a21fa3..ac3f781d8 100644 --- a/data/themes/default-theme.yml +++ b/data/themes/default-theme.yml @@ -25,6 +25,9 @@ page: background_color: ffffff layout: portrait margin: [0.5in, 0.67in, 0.67in, 0.67in] + # margin_inner and margin_outer keys are used for recto/verso print margins when media=press + margin_inner: 0.75in + margin_outer: 0.59in size: A4 base: align: justify diff --git a/docs/theming-guide.adoc b/docs/theming-guide.adoc index 1071602fc..51de9cf8a 100644 --- a/docs/theming-guide.adoc +++ b/docs/theming-guide.adoc @@ -18,6 +18,7 @@ endif::[] // Aliases: :conum-guard-yaml: # ifndef::icons[:conum-guard-yaml: # #] +ifdef::backend-pdf[:conum-guard-yaml: # #] //// Topics remaining to document: @@ -264,7 +265,7 @@ Like programming languages, multiple and divide take precedence over add and sub The following table lists the supported operations and the corresponding operator for each. -[%header%autowidth] +[width=25%] |=== |Operation |Operator @@ -330,7 +331,7 @@ You can let the theme do this conversion for you automatically by adding a unit The following units are supported: -[%header%autowidth] +[width=25%] |=== |Unit |Suffix @@ -419,17 +420,16 @@ Regardless, we recommend that you always use either a leading `+#+` or surroundi The following are all equivalent values for the color red: -[%autowidth,cols=4] +[cols="8*m"] |=== -|#f00 -|'f00' |#ff0000 +|#FF0000 |'ff0000' - +|'FF0000' +|#f00 |#F00 +|'f00' |'F00' -|#FF0000 -|'FF0000' |=== Here's how a hex color value appears in the theme file: @@ -541,7 +541,7 @@ There's nothing Asciidoctor can do to convince PDF to work without the right fon The names of the built-in fonts (for general-purpose text) are as follows: -[%header%autowidth] +[width=33.33%] |=== |Font Name |Font Family @@ -788,6 +788,16 @@ See <> for details. |page: margin: [0.5in, 0.67in, 1in, 0.67in] +|margin_inner^[3]^ +|<<measurement-units,Measurement>> +|page: + margin_inner: 0.75in + +|margin_outer^[3]^ +|<<measurement-units,Measurement>> +|page: + margin_outer: 0.59in + |size |/~https://github.com/prawnpdf/pdf-core/blob/0.6.0/lib/pdf/core/page_geometry.rb#L16-L68[Named size^] {vbar} <<measurement-units,Measurement[width,height]>> + (default: A4) @@ -800,6 +810,8 @@ This limitation is due to a bug in Prawn 1.3.1. The limitation will remain until AsciidoctorJ PDF upgrades to Prawn 2.x (an upgrade that is waiting on AsciidoctorJ to migrate to JRuby 9000). For more details, see http://discuss.asciidoctor.org/Asciidoctor-YAML-style-file-for-PDF-and-maven-td3849.html[this thread]. . Target may be an absolute path or a path relative to the value of the `pdf-stylesdir` attribute. +. The margins for `recto` (right-hand, odd-numbered) and `verso` (left-hand, even-numbered) pages are calculated automatically from the margin_inner and margin_outer values. +These margins and used when the value `prepress` is assigned to the `media` document attribute. === Base @@ -1331,7 +1343,7 @@ The keys in this category control the spacing around block elements when a more Block styles are applied to the following block types: -[cols="1a,1a,1a", grid=none, frame=none] +[cols="3*a",grid=none,frame=none] |=== | * admonition @@ -2350,7 +2362,7 @@ The keys in this category control the arrangement and style of running header an |=== . The background color spans the width of the page, as does the border when a background color is specified. -. `<side>` can be `recto` (right-hand, odd pages) or `verso` (left-hand, even pages). +. `<side>` can be `recto` (right-hand, odd-numbered pages) or `verso` (left-hand, even-numbered pages). . `<placement>` can be `left`, `center` or `right`. IMPORTANT: You must define a height for the running header or footer, respectively, or it will not be shown. @@ -2517,12 +2529,12 @@ The only thing you need to add to an existing build is the attributes mentioned There are various settings in the theme you control using document attributes. These settings override equivalent keys defined in the theme file, where applicable. -[cols="2,3,6m"] +[cols="2,3,6l"] |=== |Attribute |Value Type |Example |autofit-option -|flag (default: _not set_) +|flag (default: _off_) |:autofit-option: |chapter-label @@ -2533,16 +2545,20 @@ These settings override equivalent keys defined in the theme file, where applica |inline image macro (target is relative to `imagesdir`; can be image or PDF file) |+:front-cover-image: image:front-cover.pdf[]+ +|media +|screen {vbar} print {vbar} prepress +|:media: prepress + |pagenums^[2]^ -|flag (default: _set_) +|flag (default: _on_) |:pagenums: //|pdf-page-layout -//|portrait, landscape +//|portrait {vbar} landscape //|:pdf-page-layout: landscape |pdf-page-size -|/~https://github.com/prawnpdf/pdf-core/blob/0.6.0/lib/pdf/core/page_geometry.rb#L16-L68[named size^], <<measurement-units,measurement dimensions>> (width x height) +|/~https://github.com/prawnpdf/pdf-core/blob/0.6.0/lib/pdf/core/page_geometry.rb#L16-L68[Named size^] {vbar} <<measurement-units,Measurement[width, height]>> |:pdf-page-size: 6in x 9in |title-logo-image @@ -2554,6 +2570,54 @@ These settings override equivalent keys defined in the theme file, where applica . Controls whether the `page-number` attribute is accessible to the running header and footer content specified in the theme file. Use the `noheader` and `nofooter` attributes to disable the running header and footer, respectively, from the document. +== Publishing Mode + +Asciidoctor PDF provides the following features to assist with publishing: + +* Double-sided (mirror) page margins +* Automatic facing pages + +These features are activated when you set the `media` attribute to `prepress` in the header of your AsciiDoc document or from the CLI or API. +The following sections describe the behaviors that this setting activates. + +=== Double-Sided Page Margins + +The page margins for the recto (right-hand, odd-numbered) and verso (left-hand, even-numbered) pages are automatically calculated by replacing the side page margins with the values of the `page_margin_inner` and `page_margin_outer` keys. + +For example, let's assume you've defined the following settings in your theme: + +[source,yaml] +---- +page: + margin: [0.5in, 0.67in, 0.67in, 0.67in] + margin_inner: 0.75in + margin_outer: 0.59in +---- + +The page margins for the recto and verso pages will be resolved as follows: + +recto page margin:: [0.5in, *0.59in*, 0.67in, *0.75in*] +verso page margin:: [0.5in, *0.75in*, 0.67in, *0.59in*] + +The page margins alternate between recto and verso. +The first page in the document is a recto page. + +=== Automatic Facing Pages + +If a document uses the book doctype, a blank page will be inserted, if necessary, to ensure the following pages are recto-facing pages: + +* Title page +* Table of contents +* First page of body content + +Other facing pages may be added in the future. + +For documents that use the article doctype, Asciidoctor PDF incorrectly places the document title and table of contents on their own pages. +This can result in the page numbering and the page facing to be out of sync. +As a workaround, Asciidoctor PDF inserts a blank page, if necessary, to ensure the first page of body content is a recto-facing page. + +You can check on the status of this defect by following /~https://github.com/asciidoctor/asciidoctor-pdf/issues/95[issue #95]. + //// == Resources for Extending Asciidoctor PDF diff --git a/lib/asciidoctor-pdf/converter.rb b/lib/asciidoctor-pdf/converter.rb index 529b1b109..bd168ed3a 100644 --- a/lib/asciidoctor-pdf/converter.rb +++ b/lib/asciidoctor-pdf/converter.rb @@ -41,6 +41,7 @@ class Converter < ::Prawn::Document AlignmentNames = ['left', 'center', 'right'] AlignmentTable = { '<' => :left, '=' => :center, '>' => :right } ColumnPlacements = [:left, :center, :right] + PageSides = [:recto, :verso] LF = %(\n) DoubleLF = %(\n\n) TAB = %(\t) @@ -133,8 +134,13 @@ def convert_document doc end #assign_missing_section_ids doc - # NOTE the on_page_create callback is called within a float context + # NOTE on_page_create is called within a float context + # NOTE on_page_create is not called for imported pages, front and back cover pages, and other image pages on_page_create do + # NOTE we assume here that physical page number reflects page side + if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin + set_page_margin next_page_margin + end # TODO implement as a watermark (on top) if @page_bg_image # FIXME implement fitting and centering for SVG @@ -151,19 +157,19 @@ def convert_document doc # NOTE a new page will already be started if the cover image is a PDF start_new_page unless page_is_empty? - toc_start_page_num = page_number num_toc_levels = (doc.attr 'toclevels', 2).to_i if (include_toc = doc.attr? 'toc') - toc_page_nums = () - dry_run do - toc_page_nums = layout_toc doc, num_toc_levels, 1 - end - # NOTE reserve pages for the toc - toc_page_nums.each do - start_new_page - end + start_new_page if @ppbook && verso_page? + toc_page_nums = page_number + dry_run { toc_page_nums = layout_toc doc, num_toc_levels, toc_page_nums } + # NOTE reserve pages for the toc; leaves cursor on page after last page in toc + toc_page_nums.each { start_new_page } end + # FIXME only apply to book doctype once title and toc are moved to start page when using article doctype + #start_new_page if @ppbook && verso_page? + start_new_page if @media == 'prepress' && verso_page? + num_front_matter_pages = page_number - 1 font @theme.base_font_family, size: @theme.base_font_size, style: @theme.base_font_style.to_sym doc.set_attr 'pdf-anchor', (doc_anchor = derive_anchor_from_id doc.id, 'top') @@ -174,11 +180,7 @@ def convert_document doc # QUESTION should we delete page if document is empty? (leaving no pages?) delete_page if page_is_empty? && page_count > 1 - toc_page_nums = if include_toc - layout_toc doc, num_toc_levels, toc_start_page_num, num_front_matter_pages - else - (0..-1) - end + toc_page_nums = include_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages) : [] if page_count > num_front_matter_pages layout_running_content :header, doc, skip: num_front_matter_pages unless doc.noheader @@ -201,23 +203,37 @@ def convert_document doc # TODO only allow method to be called once (or we need a reset) def init_pdf doc - theme = ThemeLoader.load_theme((doc.attr 'pdf-style'), (doc.attr 'pdf-stylesdir')) - @theme = theme + @theme = theme = ThemeLoader.load_theme((doc.attr 'pdf-style'), (doc.attr 'pdf-stylesdir')) pdf_opts = build_pdf_options doc, theme # QUESTION should page options be preserved (otherwise, not readily available) #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] } ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts + @page_margin_by_side = { recto: page_margin, verso: page_margin } + if (@media = doc.attr 'media', 'screen') == 'prepress' + @ppbook = doc.doctype == 'book' + page_margin_recto = @page_margin_by_side[:recto] + if (page_margin_outer = theme.page_margin_outer) + page_margin_recto[1] = @page_margin_by_side[:verso][3] = page_margin_outer + end + if (page_margin_inner = theme.page_margin_inner) + page_margin_recto[3] = @page_margin_by_side[:verso][1] = page_margin_inner + end + # NOTE prepare scratch document to use page margin from recto side + set_page_margin page_margin_recto unless page_margin_recto == page_margin + else + @ppbook = false + end # QUESTION should ThemeLoader register fonts? register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir) - @page_bg_image = nil - if (bg_image = resolve_background_image doc, theme, 'page-background-image') - @page_bg_image = (bg_image == 'none' ? nil : bg_image) + if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image != 'none' + @page_bg_image = bg_image + else + @page_bg_image = nil end @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF' @fallback_fonts = [*theme.font_fallbacks] @font_color = theme.base_font_color @text_transform = nil - @stamps = {} # NOTE we have to init pdfmarks here while we have a reference to the doc @pdfmarks = (doc.attr? 'pdfmarks') ? (Pdfmarks.new doc) : nil init_scratch_prototype @@ -325,10 +341,10 @@ def convert_section sect, opts = {} start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + (@theme.base_line_height_length * 1.5) end # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map? - sect.set_attr 'pdf-page-start', (start_page_num = page_number) + sect.set_attr 'pdf-page-start', (start_pgnum = page_number) # QUESTION should we just assign the section this generated id? # NOTE section must have pdf-anchor in order to be listed in the TOC - sect.set_attr 'pdf-anchor', (sect_anchor = derive_anchor_from_id sect.id, %(#{start_page_num}-#{y.ceil})) + sect.set_attr 'pdf-anchor', (sect_anchor = derive_anchor_from_id sect.id, %(#{start_pgnum}-#{y.ceil})) add_dest_for_block sect, sect_anchor if type == :part layout_part_title sect, title, align: align @@ -1438,7 +1454,7 @@ def convert_inline_anchor node end #attrs << %( title="#{node.attr 'title'}") if node.attr? 'title' attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false - if (((doc = node.document).attr? 'media', 'print') || (doc.attr? 'show-link-uri')) && !(node.has_role? 'bare') + if (@media != 'screen' || (node.document.attr? 'show-link-uri')) && !(node.has_role? 'bare') # TODO allow style of visible link to be controlled by theme %(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> <font size="0.9em">[#{target}]</font>) else @@ -1625,6 +1641,7 @@ def layout_title_page doc end # NOTE a new page will already be started if the cover image is a PDF start_new_page unless page_is_empty? + start_new_page if @ppbook && verso_page? @page_bg_image = prev_bg_image if bg_image @page_bg_color = prev_bg_color if bg_color @@ -1729,10 +1746,9 @@ def layout_cover_page face, doc if (cover_image.include? ':') && cover_image =~ ImageAttributeValueRx cover_image = resolve_image_path doc, $1 end - # QUESTION should we go to page 1 when face == :front? go_to_page page_count if face == :back if cover_image.downcase.end_with? '.pdf' - # NOTE import_page automatically advances to next page afterwards + # NOTE import_page automatically advances to next page afterwards (can we change this behavior?) import_page cover_image, advance: face != :back else image_page cover_image, canvas: true @@ -1847,6 +1863,7 @@ def layout_table_caption node, width, alignment = :left, side = :top end end + # NOTE num_front_matter_pages is not used during a dry run def layout_toc doc, num_levels = 2, toc_page_number = 2, num_front_matter_pages = 0 go_to_page toc_page_number unless (page_number == toc_page_number) || scratch? start_page_number = page_number @@ -1887,17 +1904,17 @@ def layout_toc_level sections, num_levels, line_metrics, dot_width, num_front_ma # TODO it would be convenient to have a cursor mark / placement utility that took page number into account go_to_page start_page_number if start_page_number != end_page_number move_cursor_to start_cursor - sect_page_num = (sect.attr 'pdf-page-start') - num_front_matter_pages + sect_explicit_pgnum = (sect.attr 'pdf-page-start') - num_front_matter_pages spacer_width = (width_of NoBreakSpace) * 0.75 # FIXME this calculation will be wrong if a style is set per level - num_dots = ((bounds.width - (width_of %(#{sect_title}#{sect_page_num}), inline_format: true) - spacer_width) / dot_width).floor + num_dots = ((bounds.width - (width_of %(#{sect_title}#{sect_explicit_pgnum}), inline_format: true) - spacer_width) / dot_width).floor num_dots = 0 if num_dots < 0 # FIXME dots don't line up if width of page numbers differ typeset_formatted_text [ { text: %(#{(@theme.toc_dot_leader_content || DotLeaderDefault) * num_dots}), color: toc_dot_color }, # FIXME this spacing doesn't always work out; should we use graphics instead? { text: NoBreakSpace, size: (@font_size * 0.5) }, - { text: sect_page_num.to_s, anchor: sect_anchor, color: @font_color }], line_metrics, align: :right + { text: sect_explicit_pgnum.to_s, anchor: sect_anchor, color: @font_color }], line_metrics, align: :right go_to_page end_page_number if start_page_number != end_page_number move_cursor_to end_cursor end @@ -1925,9 +1942,9 @@ def admonition_icon_data key end # TODO delegate to layout_page_header and layout_page_footer per page - def layout_running_content position, doc, opts = {} + def layout_running_content periphery, doc, opts = {} # QUESTION should we short-circuit if setting not specified and if so, which setting? - return unless (position == :header && @theme.header_height) || (position == :footer && @theme.footer_height) + return unless (periphery == :header && @theme.header_height) || (periphery == :footer && @theme.footer_height) skip = opts[:skip] || 1 start = skip + 1 num_pages = page_count - skip @@ -1972,10 +1989,10 @@ def layout_running_content position, doc, opts = {} doc.set_attr 'page-count', num_pages # TODO move this to a method so it can be reused; cache results - content_dict = [:recto, :verso].inject({}) do |acc, side| + content_dict = PageSides.inject({}) do |acc, side| side_content = {} ColumnPlacements.each do |placement| - if (val = @theme[%(#{position}_#{side}_#{placement}_content)]) + if (val = @theme[%(#{periphery}_#{side}_#{placement}_content)]) # TODO support image URL (using resolve_image_path) if (val.include? ':') && val =~ ImageAttributeValueRx && ::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir'))) @@ -1992,7 +2009,7 @@ def layout_running_content position, doc, opts = {} end end # NOTE set fallbacks if not explicitly disabled - if side_content.empty? && position == :footer && @theme[%(footer_#{side}_content)] != 'none' + if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none' side_content = { side == :recto ? :right : :left => '{page-number}' } end @@ -2000,14 +2017,12 @@ def layout_running_content position, doc, opts = {} acc end - if position == :header + if periphery == :header trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height) trim_top = page_height # NOTE height is required atm trim_height = @theme.header_height || page_margin_top trim_padding = @theme.header_padding || [0, 0, 0, 0] - trim_left = page_margin_left - trim_width = page_width - trim_left - page_margin_right trim_bg_color = resolve_theme_color :header_background_color trim_border_width = @theme.header_border_width || @theme.base_border_width trim_border_style = (@theme.header_border_style || :solid).to_sym @@ -2019,8 +2034,6 @@ def layout_running_content position, doc, opts = {} # NOTE height is required atm trim_top = trim_height = @theme.footer_height || page_margin_bottom trim_padding = @theme.footer_padding || [0, 0, 0, 0] - trim_left = page_margin_left - trim_width = page_width - trim_left - page_margin_right trim_bg_color = resolve_theme_color :footer_background_color trim_border_width = @theme.footer_border_width || @theme.base_border_width trim_border_style = (@theme.footer_border_style || :solid).to_sym @@ -2029,10 +2042,27 @@ def layout_running_content position, doc, opts = {} trim_img_valign = @theme.footer_image_vertical_align end - trim_stamp = position.to_s - trim_content_left = trim_left + trim_padding[3] + trim_stamp_name = { + recto: %(#{periphery}_recto), + verso: %(#{periphery}_verso) + } + trim_left = { + recto: @page_margin_by_side[:recto][3], + verso: @page_margin_by_side[:verso][3] + } + trim_width = { + recto: page_width - trim_left[:recto] - @page_margin_by_side[:recto][1], + verso: page_width - trim_left[:verso] - @page_margin_by_side[:verso][1] + } + trim_content_left = { + recto: trim_left[:recto] + trim_padding[3], + verso: trim_left[:verso] + trim_padding[3] + } + trim_content_width = { + recto: trim_width[:recto] - trim_padding[3] - trim_padding[1], + verso: trim_width[:verso] - trim_padding[3] - trim_padding[1] + } trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom - trim_content_width = trim_width - trim_padding[3] - trim_padding[1] trim_border_color = nil if trim_border_width == 0 trim_valign = :center if trim_valign == :middle case trim_img_valign @@ -2044,8 +2074,9 @@ def layout_running_content position, doc, opts = {} trim_img_valign = trim_img_valign.to_sym end - colspec_dict = [:recto, :verso].inject({}) do |acc, side| - if (custom_colspecs = @theme[%(#{position}_#{side}_columns)]) + colspec_dict = PageSides.inject({}) do |acc, side| + side_trim_content_width = trim_content_width[side] + if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)]) colspecs = %w(<40% =20% >40%) (custom_colspecs.tr ',', ' ').split[0..2].each_with_index {|c, idx| colspecs[idx] = c } colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] } @@ -2059,8 +2090,8 @@ def layout_running_content position, doc, opts = {} pcwidth = spec.to_f end # QUESTION should we allow the columns to overlap (capping width at 100%)? - if (w = trim_content_width * (pcwidth / 100.0)) + cml_width > trim_content_width - w = trim_content_width - cml_width + if (w = side_trim_content_width * (pcwidth / 100.0)) + cml_width > side_trim_content_width + w = side_trim_content_width - cml_width end cml_width += w [col, { align: alignment, width: w, x: 0 }] @@ -2069,39 +2100,42 @@ def layout_running_content position, doc, opts = {} acc[side] = side_colspecs else acc[side] = { - left: { align: :left, width: trim_content_width, x: 0 }, - center: { align: :center, width: trim_content_width, x: 0 }, - right: { align: :right, width: trim_content_width, x: 0 } + left: { align: :left, width: side_trim_content_width, x: 0 }, + center: { align: :center, width: side_trim_content_width, x: 0 }, + right: { align: :right, width: side_trim_content_width, x: 0 } } end acc end + stamps = {} if trim_bg_color || trim_border_color # NOTE switch to first content page so stamp will get created properly (can't create on imported page) prev_page_number = page_number go_to_page start - create_stamp trim_stamp do - canvas do - if trim_bg_color - bounding_box [0, trim_top], width: bounds.width, height: trim_height do - fill_bounds trim_bg_color - if trim_border_color + PageSides.each do |side| + create_stamp trim_stamp_name[side] do + canvas do + if trim_bg_color + bounding_box [0, trim_top], width: bounds.width, height: trim_height do + fill_bounds trim_bg_color + if trim_border_color + # TODO stroke_horizontal_rule should support :at + move_down bounds.height if periphery == :header + stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style + end + end + else + bounding_box [trim_left[side], trim_top], width: trim_width[side], height: trim_height do # TODO stroke_horizontal_rule should support :at - move_down bounds.height if position == :header + move_down bounds.height if periphery == :header stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style end end - else - bounding_box [trim_left, trim_top], width: trim_width, height: trim_height do - # TODO stroke_horizontal_rule should support :at - move_down bounds.height if position == :header - stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style - end end end end - @stamps[position] = true + stamps[periphery] = true go_to_page prev_page_number end @@ -2110,30 +2144,32 @@ def layout_running_content position, doc, opts = {} repeat (start..page_count), dynamic: true do # NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF) next if page.imported_page? - visual_pgnum = page_number - skip + explicit_pgnum = page_number - skip + # QUESTION should we respect physical page number or just look at the content page number? + side = page_side explicit_pgnum # FIXME we need to have a content setting for chapter pages - content_by_placement = content_dict[side = visual_pgnum.odd? ? :recto : :verso] + content_by_placement = content_dict[side] colspec_by_placement = colspec_dict[side] # TODO populate chapter-number # TODO populate numbered and unnumbered chapter and section titles # FIXME leave page-number attribute unset once we filter lines with unresolved attributes (see below) - doc.set_attr 'page-number', (pagenums_enabled ? visual_pgnum : '') - doc.set_attr 'chapter-title', (chapters_by_page[visual_pgnum] || '') - doc.set_attr 'section-title', (sections_by_page[visual_pgnum] || '') - doc.set_attr 'section-or-chapter-title', (sections_by_page[visual_pgnum] || chapters_by_page[visual_pgnum] || '') + doc.set_attr 'page-number', (pagenums_enabled ? explicit_pgnum : '') + doc.set_attr 'chapter-title', (chapters_by_page[explicit_pgnum] || '') + doc.set_attr 'section-title', (sections_by_page[explicit_pgnum] || '') + doc.set_attr 'section-or-chapter-title', (sections_by_page[explicit_pgnum] || chapters_by_page[explicit_pgnum] || '') - stamp trim_stamp if @stamps[position] + stamp trim_stamp_name[side] if stamps[periphery] - theme_font position do + theme_font periphery do canvas do - bounding_box [trim_content_left, trim_top], width: trim_content_width, height: trim_height do + bounding_box [trim_content_left[side], trim_top], width: trim_content_width[side], height: trim_height do ColumnPlacements.each do |placement| next unless (content = content_by_placement[placement]) next unless (colspec = colspec_by_placement[placement])[:width] > 0 # FIXME we need to have a content setting for chapter pages case content when ::Hash - # NOTE image position respects padding; use negative image_vertical_align value to revert + # NOTE image vposition respects padding; use negative image_vertical_align value to revert trim_v_padding = trim_padding[0] + trim_padding[2] # NOTE float ensures cursor position is restored and returns us to current page if we overrun float do @@ -2146,7 +2182,7 @@ def layout_running_content position, doc, opts = {} end when ::String if content == '{page-number}' - content = pagenums_enabled ? visual_pgnum.to_s : nil + content = pagenums_enabled ? explicit_pgnum.to_s : nil else # FIXME get apply_subs to handle drop-line w/o a warning doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip' @@ -2156,7 +2192,7 @@ def layout_running_content position, doc, opts = {} end doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip' end - theme_font %(#{position}_#{side}_#{placement}) do + theme_font %(#{periphery}_#{side}_#{placement}) do formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]), at: [colspec[:x], trim_content_height + trim_padding[2] + trim_line_metrics.padding_bottom], width: colspec[:width], @@ -2176,44 +2212,26 @@ def layout_running_content position, doc, opts = {} nil end - # FIXME we are assuming we always have exactly one title page - def add_outline doc, num_levels = 2, toc_page_nums = (0..-1), num_front_matter_pages = 0 + def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0 front_matter_counter = RomanNumeral.new 0, :lower - page_num_labels = {} - # FIXME account for cover page - # cover page (i) - #front_matter_counter.next! - - # title page (i) - # TODO same conditional logic as in layout_title_page; consolidate - if doc.header? && !doc.notitle - page_num_labels[0] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) } - end - - # toc pages (ii..?) - toc_page_nums.each do + num_front_matter_pages.times do page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) } end - # credits page - #page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) } - - # number of front matter pages aside from the document title to skip in page number index - numbering_offset = front_matter_counter.to_i - 1 + # placeholder for first page of content, in case it's not the destination of an outline entry + page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new('1') } outline.define do # FIXME use sanitize: :plain_text once available if (doctitle = document.sanitize(doc.doctitle use_fallback: true)) + # FIXME link to title page if there's a cover page (skip cover page and ensuing blank page) page title: doctitle, destination: (document.dest_top 1) end - if doc.attr? 'toc' - page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) - end - #page title: 'Credits', destination: (document.dest_top toc_page_nums.first + 1) + page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) if toc_page_nums.first # QUESTION any way to get add_outline_level to invoke in the context of the outline? - document.add_outline_level self, doc.sections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages + document.add_outline_level self, doc.sections, num_levels, page_num_labels, num_front_matter_pages end catalog.data[:PageLabels] = state.store.ref Nums: page_num_labels.flatten @@ -2222,17 +2240,17 @@ def add_outline doc, num_levels = 2, toc_page_nums = (0..-1), num_front_matter_p end # TODO only nest inside root node if doctype=article - def add_outline_level outline, sections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages + def add_outline_level outline, sections, num_levels, page_num_labels, num_front_matter_pages sections.each do |sect| sect_title = sanitize sect.numbered_title formal: true sect_destination = sect.attr 'pdf-destination' - sect_page_num = (sect.attr 'pdf-page-start') - num_front_matter_pages - page_num_labels[sect_page_num + numbering_offset] = { P: ::PDF::Core::LiteralString.new(sect_page_num.to_s) } + sect_explicit_pgnum = (sect_pgnum = sect.attr 'pdf-page-start') - num_front_matter_pages + page_num_labels[sect_pgnum - 1] = { P: ::PDF::Core::LiteralString.new(sect_explicit_pgnum.to_s) } if (subsections = sect.sections).empty? || sect.level == num_levels outline.page title: sect_title, destination: sect_destination elsif sect.level < num_levels + 1 outline.section sect_title, { destination: sect_destination } do - add_outline_level outline, subsections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages + add_outline_level outline, subsections, num_levels, page_num_labels, num_front_matter_pages end end end @@ -2666,7 +2684,7 @@ def init_scratch_prototype # IMPORTANT don't set font before using Marshal, it causes serialization to fail @prototype = ::Marshal.load ::Marshal.dump self @prototype.state.store.info.data[:Scratch] = true - # we're now starting a new page each time, so no need to do it here + # NOTE we're now starting a new page each time, so no need to do it here #@prototype.start_new_page if @prototype.page_number == 0 end diff --git a/lib/asciidoctor-pdf/prawn_ext/extensions.rb b/lib/asciidoctor-pdf/prawn_ext/extensions.rb index 0197a7492..4a3c210d9 100644 --- a/lib/asciidoctor-pdf/prawn_ext/extensions.rb +++ b/lib/asciidoctor-pdf/prawn_ext/extensions.rb @@ -11,6 +11,7 @@ module Extensions IconSets = ['fa', 'fi', 'octicon', 'pf'].to_set MeasurementValueRx = /(\d+|\d*\.\d+)(in|mm|cm|px|pt)?$/ + InitialPageContent = %(q\n) # - :height is the height of a line # - :leading is spacing between adjacent lines @@ -53,6 +54,20 @@ def effective_page_height reference_bounds.height end + # Set the margins for the current page. + # + def set_page_margin margin + # FIXME is there a cleaner way to set margins? does it make sense to override create_new_page? + apply_margin_options margin: margin + generate_margin_box + end + + # Returns the margins for the current page as a 4 element array (top, right, bottom, left) + # + def page_margin + [page.margins[:top], page.margins[:right], page.margins[:bottom], page.margins[:left]] + end + # Returns the width of the left margin for the current page # def page_margin_left @@ -93,20 +108,38 @@ def bounds_margin_right page.dimensions[2] - bounds.absolute_right end - # Returns whether the cursor is at the top of the page (i.e., margin box) + # Returns the side the current page is facing, :recto or :verso. + # + def page_side pgnum = nil + (recto_page? pgnum) ? :recto : :verso + end + + # Returns whether the page is a recto page. + # + def recto_page? pgnum = nil + (pgnum || page_number).odd? + end + + # Returns whether the page is a verso page. + # + def verso_page? pgnum = nil + (pgnum || page_number).even? + end + + # Returns whether the cursor is at the top of the page (i.e., margin box). # def at_page_top? @y == @margin_box.absolute_top end - # Returns whether the current page is empty (no content is written). - # If at least one page has not yet been created, returns false. + # Returns whether the current page is empty (i.e., no content has been written). + # Returns false if a page has not yet been created. # def empty_page? # if we are at the page top, assume we didn't write anything to the page #at_page_top? - # ...or use low-level check (initial value is "q\n") - (page.content.stream || []).length <= 2 && page_number > 0 + # ...or use more robust, low-level check (initial value of content is "q\n") + page_number > 0 && page.content.stream.filtered_stream == InitialPageContent end alias :page_is_empty? :empty_page? @@ -799,7 +832,7 @@ def keep_together_if verdict, &block =begin def run_with_trial &block available_space = cursor - whole_pages, remainder = dry_run(&block) + total_height, whole_pages, remainder = dry_run(&block) if whole_pages > 0 || remainder > available_space started_new_page = true else diff --git a/lib/asciidoctor-pdf/theme_loader.rb b/lib/asciidoctor-pdf/theme_loader.rb index 7a412c6ee..91b16916d 100644 --- a/lib/asciidoctor-pdf/theme_loader.rb +++ b/lib/asciidoctor-pdf/theme_loader.rb @@ -81,10 +81,10 @@ def load hash, theme_data = nil theme_data ||= ::OpenStruct.new hash.inject(theme_data) {|data, (key, val)| process_entry key, val, data } # NOTE remap legacy running content keys (e.g., header_recto_content_left => header_recto_left_content) - %w(header_recto header_verso footer_recto footer_verso).each do |position_face| + %w(header_recto header_verso footer_recto footer_verso).each do |periphery_face| %w(left center right).each do |align| - if (val = theme_data.delete %(#{position_face}_content_#{align})) - theme_data[%(#{position_face}_#{align}_content)] = val + if (val = theme_data.delete %(#{periphery_face}_content_#{align})) + theme_data[%(#{periphery_face}_#{align}_content)] = val end end end