Class: Asciidoctor::BeautifyUri::PdfConverter

Inherits:
Object
  • Object
show all
Defined in:
lib/asciidoctor/beautify_uri/pdf_converter.rb

Overview

Draws the PDF card directly with Prawn — see pdf_card.rb for why this replaced an earlier version that composed the card as a real AsciiDoc table and let asciidoctor-pdf's own table converter render it. Every position here (thumbnail, title, subtitle, link line, frame) is one this method chooses explicitly, in the same coordinate space, rather than routed through a table cell's own layout — that's what fixes the thumbnail/title top-alignment (both start at the same y, full stop) and what makes rounding the thumbnail's corners a plain Prawn clip path instead of an SVG-wrapping detour with its own scale math to get wrong.

Vertical rhythm is anchored to real font metrics, not an arbitrary line pitch: each line of text is placed by its baseline (draw_text, not text_box — see the note below), lines are spaced a full font.height apart (ascender+descender+line gap — a font's own natural single-line height, not asciidoctor-pdf's tighter body-text pitch, which is tuned for paragraphs of running text, not a standalone few-line card) plus theme-configurable slack, the brand icon spans cap-height to baseline of the link line it sits next to, and the thumbnail spans the title's cap-height down to the link line's baseline — i.e. it lines up with the visible top and bottom of the text stack, not the extra headroom/footroom a font reserves for ascenders and descenders that never actually appear in this text.

Every visual knob (spacing, border, corner radii, font styles) reads from a uri_card theme namespace with a || DEFAULT fallback, the same convention asciidoctor-lists-extended uses for its own list-of keys — see doc/modules/ROOT/pages/pdf-theming.adoc for the full list. An unset theme behaves exactly as if these were still hardcoded constants. The one exception is a provider-mandated thumbnail corner radius (e.g. Spotify's design guidelines) — a real brand requirement, not a preference, so it overrides the theme key entirely; see pdf_card.rb and providers/spotify.rb.

Constant Summary collapse

DEFAULT_MARGIN_INNER_PT =
8
DEFAULT_ICON_GAP_PT =
4
DEFAULT_BORDER_COLOR =
'B3B3B3'
DEFAULT_BORDER_WIDTH_PT =
0.5
DEFAULT_BORDER_RADIUS_PT =
6
DEFAULT_THUMBNAIL_BORDER_RADIUS_PT =
4
DEFAULT_LINE_SPACING =

font.height (ascender+descender+line gap) is a font's own natural single-line pitch — already looser than asciidoctor-pdf's own tight body-text line height, but still reads as cramped for a card that's nothing but a few short stacked lines rather than running prose.

1.35
DEFAULT_TITLE_FONT_STYLE =
:bold
DEFAULT_SUBTITLE_FONT_STYLE =
:normal
:bold
ELLIPSIS =
''

Instance Method Summary collapse

Instance Method Details

#convert_uri_card(node) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/asciidoctor/beautify_uri/pdf_converter.rb', line 57

def convert_uri_card node
  add_dest_for_block node if node.id

  title = node.attr 'uri-card-title'
  subtitle = node.attr 'uri-card-subtitle'
  link_text = node.attr 'uri-card-link-text'
  target_url = node.attr 'uri-card-target-url'
  icon_path = node.attr 'uri-card-icon-path'
  thumb_url = node.attr 'uri-card-thumb-url'
  lines = subtitle ? 3 : 2

  margin_inner = theme_num 'margin_inner', DEFAULT_MARGIN_INNER_PT
  icon_gap = theme_num 'icon_gap', DEFAULT_ICON_GAP_PT
  title_style = theme_style 'title_font_style', DEFAULT_TITLE_FONT_STYLE
  subtitle_style = theme_style 'subtitle_font_style', DEFAULT_SUBTITLE_FONT_STYLE
  link_style = theme_style 'link_font_style', DEFAULT_LINK_FONT_STYLE

  line_h = font.height * (theme_num 'line_spacing', DEFAULT_LINE_SPACING)
  title_cap_h = nil
  with_style(title_style) { title_cap_h = cap_height_pt }
  link_cap_h = nil
  with_style(link_style) { link_cap_h = cap_height_pt }

  # No photographic thumbnail (thumbnail=false, or the provider simply
  # didn't resolve one): the provider's own brand mark fills that
  # column at full size instead of sitting small next to the link
  # text, so the card never has an empty thumbnail column with no
  # visual identity at all. The small link-line icon is then
  # redundant — the same mark already anchors the card much larger —
  # and dropped.
  has_photo = !thumb_url.nil?
  link_icon = has_photo ? (build_icon icon_path, link_cap_h) : nil
  link_icon_w = link_icon ? link_icon.document.sizing.output_width : 0

  # The visible span of the meta stack: the title's own cap-height
  # (its capital letters' true top) down to the link line's baseline —
  # not (lines * line_h), which would also count the title's ascender
  # headroom above cap-height and the link line's descender footroom
  # below its baseline, neither of which this stack actually uses.
  text_h = title_cap_h + (line_h * (lines - 1))
  logo_icon = has_photo ? nil : (build_icon icon_path, text_h)
  thumb_w = if has_photo
              thumbnail_width node, text_h
            elsif logo_icon
              logo_icon.document.sizing.output_width
            else
              0
            end

  # The printable area this card must never exceed — at a large
  # enough base font size (or an unusually long title/subtitle/link),
  # text sized only against its own natural width can run past the
  # page margin entirely rather than merely looking a little cramped.
  # Designed to comfortably fit a 25-character playlist/album name, an
  # 18-character artist name, and a 23-character track name (Spotify's
  # own typical metadata lengths) well within this budget at ordinary
  # font sizes; fit_text below is the actual backstop for whatever
  # exceeds it regardless of why.
  max_content_w = bounds.width - (margin_inner * 2)
  thumb_w = [thumb_w, max_content_w * 0.5].min if thumb_w.positive?
  max_text_w = thumb_w.positive? ? max_content_w - thumb_w - margin_inner : max_content_w
  max_link_text_w = [max_text_w - link_icon_w - icon_gap, 1].max

  title_w = nil
  with_style(title_style) { title = fit_text title, max_text_w; title_w = width_of title }
  link_w = nil
  with_style(link_style) { link_text = fit_text link_text, max_link_text_w; link_w = width_of link_text }
  # with_style (like Prawn's own font(name) { block }) returns the font
  # object, not the block's result — assign via an outer local instead
  # of the method's return value.
  subtitle_w = 0.0
  with_style(subtitle_style) { subtitle = fit_text subtitle, max_text_w; subtitle_w = width_of subtitle } if subtitle
  text_col_w = [title_w, link_w + link_icon_w + icon_gap, subtitle_w].max

  # One margin_inner value does double duty, matching how it reads
  # visually: the gap from the frame to the thumbnail (or to the text
  # column, without one) is the same gap as from the thumbnail to the
  # text column — a single consistent inner margin, not two knobs that
  # happen to usually agree.
  content_w = thumb_w.positive? ? thumb_w + margin_inner + text_col_w : text_col_w
  card_w = [content_w + (margin_inner * 2), bounds.width].min
  card_h = text_h + (margin_inner * 2)

  advance_page if card_h > cursor && !at_page_top?

  top = cursor
  left = bounds.left
  content_left = left + margin_inner
  # The line this card's own cap-height/thumbnail-top reference is
  # measured from — see text_h above.
  content_top = top - margin_inner

  draw_frame [left, top], card_w, card_h

  text_left = content_left
  if thumb_w.positive?
    if has_photo
      draw_thumbnail node, thumb_url, target_url, [content_left, content_top], thumb_w, text_h
    else
       logo_icon, target_url, [content_left, content_top], thumb_w, text_h
    end
    text_left += thumb_w + margin_inner
  end

  title_baseline = content_top - title_cap_h
  link_baseline = title_baseline - (line_h * (lines - 1))

  # draw_text (baseline-anchored), not text_box: text_box's own
  # line-fit check needs a touch more box height than a bare line
  # metric to accept a single line at all (confirmed empirically — a
  # box exactly one nominal line tall silently dropped the whole line
  # as unrendered "leftover" instead of drawing it), and every line
  # here is already sized (and, if need be, fit_text-truncated) to fit
  # within text_col_w, so there is nothing left for a box's
  # wrap/truncate machinery to do anyway.
  with_style(title_style) { draw_text title, at: [text_left, title_baseline] }
  with_style(subtitle_style) { draw_text subtitle, at: [text_left, title_baseline - line_h] } if subtitle
  draw_link_line link_icon, link_icon_w, icon_gap, link_text, link_style, target_url, text_left, link_baseline, text_col_w, link_cap_h

  move_cursor_to top - card_h
  theme_margin :block, :bottom, (next_enclosed_block node)
end