stationery 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -68
- data/README.md +775 -78
- data/examples/accessible_report.rb +133 -0
- data/examples/article.rb +93 -0
- data/examples/assets/bay.png +0 -0
- data/examples/assets/dunes.png +0 -0
- data/examples/assets/hills.png +0 -0
- data/examples/assets/logo.png +0 -0
- data/examples/assets/ridge.png +0 -0
- data/examples/assets/sea.png +0 -0
- data/examples/assets/stone.png +0 -0
- data/examples/assets/wall.png +0 -0
- data/examples/certificate.rb +104 -0
- data/examples/contract.rb +159 -0
- data/examples/e_invoice.rb +161 -0
- data/examples/flyer.rb +336 -0
- data/examples/form.rb +113 -0
- data/examples/invoice.rb +163 -0
- data/examples/letter.rb +121 -0
- data/examples/menu.rb +133 -0
- data/examples/multilingual_notice.rb +121 -0
- data/examples/newsletter.rb +108 -0
- data/examples/packing_slip.rb +120 -0
- data/examples/postcard.rb +83 -0
- data/examples/price_list.rb +107 -0
- data/examples/receipt.rb +176 -0
- data/examples/report.rb +294 -0
- data/examples/resume.rb +121 -0
- data/examples/shaping/extraction_matrix/pdfium.py +8 -0
- data/examples/shaping/extraction_matrix/pdfjs.mjs +18 -0
- data/examples/shaping/extraction_matrix/pdfkit.swift +6 -0
- data/examples/shaping/extraction_matrix.rb +208 -0
- data/examples/shaping/harfbuzz_shaper.rb +132 -0
- data/examples/shaping/rtl_letter.rb +106 -0
- data/examples/shipping_label.rb +93 -0
- data/lib/stationery/barcode/code128.rb +96 -0
- data/lib/stationery/barcode/data_matrix.rb +111 -0
- data/lib/stationery/barcode/data_matrix_placement.rb +118 -0
- data/lib/stationery/barcode/ean13.rb +66 -0
- data/lib/stationery/barcode/qr.rb +120 -0
- data/lib/stationery/barcode/qr_matrix.rb +189 -0
- data/lib/stationery/barcode/reed_solomon.rb +61 -0
- data/lib/stationery/barcode.rb +33 -0
- data/lib/stationery/builder.rb +12 -2
- data/lib/stationery/canvas/glyphs.rb +40 -0
- data/lib/stationery/canvas/interface.rb +169 -0
- data/lib/stationery/canvas/marking.rb +32 -3
- data/lib/stationery/canvas/path.rb +32 -70
- data/lib/stationery/canvas/surfacing.rb +67 -0
- data/lib/stationery/canvas/text.rb +4 -25
- data/lib/stationery/canvas.rb +24 -75
- data/lib/stationery/cli/examples.rb +66 -0
- data/lib/stationery/cli/inspect.rb +65 -0
- data/lib/stationery/cli/layout_text.rb +132 -0
- data/lib/stationery/cli/pictures.rb +50 -0
- data/lib/stationery/cli/render.rb +33 -6
- data/lib/stationery/cli/skill.rb +166 -0
- data/lib/stationery/cli.rb +5 -0
- data/lib/stationery/color.rb +27 -13
- data/lib/stationery/component.rb +1 -0
- data/lib/stationery/css/stylesheet.rb +9 -1
- data/lib/stationery/document.rb +135 -27
- data/lib/stationery/elements/forms.rb +2 -1
- data/lib/stationery/elements/lists.rb +6 -2
- data/lib/stationery/elements.rb +43 -7
- data/lib/stationery/examples.rb +22 -0
- data/lib/stationery/fonts/cff.rb +46 -0
- data/lib/stationery/fonts/charstring.rb +297 -0
- data/lib/stationery/fonts/fallback.rb +22 -1
- data/lib/stationery/fonts/font.rb +58 -3
- data/lib/stationery/fonts/font_book.rb +7 -3
- data/lib/stationery/fonts/glyf_outline.rb +230 -0
- data/lib/stationery/fonts/glyph_run.rb +37 -10
- data/lib/stationery/fonts/outline.rb +102 -0
- data/lib/stationery/fonts/shaped_run.rb +22 -4
- data/lib/stationery/fonts/true_type.rb +7 -0
- data/lib/stationery/forms/typeface.rb +2 -4
- data/lib/stationery/html/css.rb +31 -6
- data/lib/stationery/html/tree_builder/blocks.rb +7 -3
- data/lib/stationery/images/images.rb +10 -0
- data/lib/stationery/images/jpeg/bit_reader.rb +92 -0
- data/lib/stationery/images/jpeg/colors.rb +133 -0
- data/lib/stationery/images/jpeg/decoder.rb +253 -0
- data/lib/stationery/images/jpeg/huffman.rb +59 -0
- data/lib/stationery/images/jpeg/idct.rb +150 -0
- data/lib/stationery/images/jpeg/progressive.rb +128 -0
- data/lib/stationery/images/jpeg/reduced_idct.rb +106 -0
- data/lib/stationery/images/jpeg/scan.rb +116 -0
- data/lib/stationery/images/jpeg/upsampler.rb +81 -0
- data/lib/stationery/images/jpeg.rb +59 -1
- data/lib/stationery/images/png.rb +10 -7
- data/lib/stationery/images/resampled.rb +10 -2
- data/lib/stationery/images/webp.rb +6 -5
- data/lib/stationery/layout/barcode.rb +75 -0
- data/lib/stationery/layout/box/wrapping.rb +56 -0
- data/lib/stationery/layout/box.rb +41 -15
- data/lib/stationery/layout/columns/leveller.rb +65 -0
- data/lib/stationery/layout/columns/pour.rb +7 -0
- data/lib/stationery/layout/columns.rb +32 -15
- data/lib/stationery/layout/field.rb +3 -2
- data/lib/stationery/layout/floated.rb +12 -1
- data/lib/stationery/layout/flow/floating.rb +23 -5
- data/lib/stationery/layout/flow/placement.rb +16 -12
- data/lib/stationery/layout/flow/splitter.rb +56 -10
- data/lib/stationery/layout/flow.rb +1 -0
- data/lib/stationery/layout/image.rb +2 -2
- data/lib/stationery/layout/list_item.rb +41 -11
- data/lib/stationery/layout/mark.rb +2 -0
- data/lib/stationery/layout/node.rb +16 -2
- data/lib/stationery/layout/opening.rb +61 -0
- data/lib/stationery/layout/paginator.rb +24 -5
- data/lib/stationery/layout/svg.rb +1 -1
- data/lib/stationery/layout/table/cell.rb +39 -10
- data/lib/stationery/layout/table/row_splitter.rb +6 -2
- data/lib/stationery/layout/table/selection.rb +9 -3
- data/lib/stationery/layout/table/stream.rb +123 -0
- data/lib/stationery/layout/table/widths.rb +26 -0
- data/lib/stationery/layout/table.rb +151 -35
- data/lib/stationery/layout/table_of_contents/entry.rb +3 -3
- data/lib/stationery/layout/table_of_contents.rb +1 -1
- data/lib/stationery/layout/text.rb +22 -2
- data/lib/stationery/minitest.rb +11 -3
- data/lib/stationery/monochrome/bitmap.rb +78 -0
- data/lib/stationery/monochrome/canvas.rb +19 -0
- data/lib/stationery/monochrome/dither.rb +74 -0
- data/lib/stationery/monochrome/grid.rb +131 -0
- data/lib/stationery/monochrome/painting.rb +140 -0
- data/lib/stationery/monochrome/rules.rb +141 -0
- data/lib/stationery/monochrome.rb +87 -0
- data/lib/stationery/page/format.rb +84 -0
- data/lib/stationery/page.rb +11 -7
- data/lib/stationery/page_templates.rb +13 -10
- data/lib/stationery/path.rb +138 -0
- data/lib/stationery/pdf/assembler.rb +14 -3
- data/lib/stationery/pdf/canvases.rb +51 -0
- data/lib/stationery/pdf/conformance.rb +43 -13
- data/lib/stationery/pdf/print_hints.rb +137 -0
- data/lib/stationery/pdf/serializer.rb +41 -0
- data/lib/stationery/raster/adjust.rb +67 -0
- data/lib/stationery/raster/canvas.rb +108 -0
- data/lib/stationery/raster/canvases.rb +49 -0
- data/lib/stationery/raster/flattener.rb +75 -0
- data/lib/stationery/raster/glyphs.rb +81 -0
- data/lib/stationery/raster/painter.rb +184 -0
- data/lib/stationery/raster/picture.rb +69 -0
- data/lib/stationery/raster/png.rb +32 -0
- data/lib/stationery/raster/render.rb +134 -0
- data/lib/stationery/raster/scanner.rb +164 -0
- data/lib/stationery/raster/shading.rb +77 -0
- data/lib/stationery/raster/spans.rb +66 -0
- data/lib/stationery/raster/stroker.rb +208 -0
- data/lib/stationery/raster/surface.rb +70 -0
- data/lib/stationery/raster.rb +44 -0
- data/lib/stationery/rich/nodes.rb +4 -4
- data/lib/stationery/rich/renderer/boxes.rb +37 -16
- data/lib/stationery/rich/renderer.rb +3 -2
- data/lib/stationery/skill/SKILL.md.erb +244 -0
- data/lib/stationery/skill/recipes/flyer.md.erb +76 -0
- data/lib/stationery/skill/recipes/form.md.erb +78 -0
- data/lib/stationery/skill/recipes/invoice.md.erb +142 -0
- data/lib/stationery/skill/recipes/label.md.erb +116 -0
- data/lib/stationery/skill/recipes/receipt.md.erb +79 -0
- data/lib/stationery/skill/recipes/report.md.erb +113 -0
- data/lib/stationery/skill/reference/conformance.md.erb +45 -0
- data/lib/stationery/skill/reference/elements.md.erb +19 -0
- data/lib/stationery/skill/reference/fonts_and_images.md.erb +7 -0
- data/lib/stationery/skill/reference/forms.md.erb +5 -0
- data/lib/stationery/skill/reference/html_and_markdown.md.erb +5 -0
- data/lib/stationery/skill/reference/pages.md.erb +15 -0
- data/lib/stationery/skill/reference/printing.md.erb +33 -0
- data/lib/stationery/skill/reference/testing.md.erb +33 -0
- data/lib/stationery/skill.rb +101 -0
- data/lib/stationery/structure.rb +7 -6
- data/lib/stationery/svg/gradient.rb +12 -0
- data/lib/stationery/svg/painter.rb +1 -1
- data/lib/stationery/tagging/element.rb +9 -2
- data/lib/stationery/tagging/tree.rb +98 -2
- data/lib/stationery/testing/field_page.rb +89 -0
- data/lib/stationery/testing/inspector.rb +80 -2
- data/lib/stationery/testing/layout.rb +166 -0
- data/lib/stationery/testing/marked_text.rb +124 -18
- data/lib/stationery/testing/matchers.rb +63 -14
- data/lib/stationery/testing/page_reader.rb +146 -0
- data/lib/stationery/text/exclusions.rb +11 -0
- data/lib/stationery/text/paragraph.rb +13 -8
- data/lib/stationery/text/wrapper/one_word.rb +33 -0
- data/lib/stationery/text/wrapper.rb +62 -27
- data/lib/stationery/units.rb +45 -0
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery/warnings.rb +67 -7
- data/lib/stationery/zpl/native.rb +51 -0
- data/lib/stationery/zpl/render.rb +56 -0
- data/lib/stationery/zpl.rb +88 -0
- data/lib/stationery.rb +48 -0
- metadata +118 -1
data/examples/letter.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A one-page business letter with a vector letterhead. Run it to write
|
|
4
|
+
# examples/letter.pdf:
|
|
5
|
+
#
|
|
6
|
+
# ruby -Ilib examples/letter.rb
|
|
7
|
+
# ruby -Ilib exe/stationery render examples/letter.rb
|
|
8
|
+
require "stationery"
|
|
9
|
+
|
|
10
|
+
class ExampleLetter < Stationery::Document
|
|
11
|
+
ACCENT = "#B45309"
|
|
12
|
+
INK = "#1C1917"
|
|
13
|
+
MUTED = "#78716C"
|
|
14
|
+
HAIRLINE = "#E7E5E4"
|
|
15
|
+
LOGO = <<~SVG
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
|
17
|
+
<rect width="40" height="40" rx="9" fill="#B45309"/>
|
|
18
|
+
<path d="M11 28V12l9 10 9-10v16" fill="none" stroke="#FFFFFF" stroke-width="3.2"
|
|
19
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
20
|
+
<circle cx="20" cy="31" r="2.2" fill="#FDE68A"/>
|
|
21
|
+
</svg>
|
|
22
|
+
SVG
|
|
23
|
+
|
|
24
|
+
page size: :a4, margin: [48, 64, 40, 64]
|
|
25
|
+
default_text size: 10.5, color: INK
|
|
26
|
+
metadata title: "Letter to Hallberg Bygg AB", author: "Mälaren Studio AB", creator: "stationery example"
|
|
27
|
+
|
|
28
|
+
footer do
|
|
29
|
+
rule height: 0.5, color: HAIRLINE
|
|
30
|
+
spacer 6
|
|
31
|
+
text "Mälaren Studio AB · Org. nr 559123-4567 · VAT SE559123456701 · Registered office Västerås, Sweden",
|
|
32
|
+
size: 7.5, color: MUTED, align: :center
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.preview
|
|
36
|
+
new(recipient: ["Ms Anna Hallberg", "Hallberg Bygg AB", "Kungsgatan 12", "753 21 Uppsala"],
|
|
37
|
+
date: "27 September 2026", reference: "Proposal MS-2026-118")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(recipient:, date:, reference:)
|
|
41
|
+
super()
|
|
42
|
+
@recipient = recipient
|
|
43
|
+
@date = date
|
|
44
|
+
@reference = reference
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def view_template
|
|
48
|
+
letterhead
|
|
49
|
+
spacer 56
|
|
50
|
+
row do
|
|
51
|
+
column(width: 0.6) { text @recipient.join("\n"), leading: 2 }
|
|
52
|
+
column(width: 0.4) { text "Västerås, #{@date}\nOur ref. #{@reference}", color: MUTED, align: :right, leading: 2 }
|
|
53
|
+
end
|
|
54
|
+
spacer 40
|
|
55
|
+
text "Proposal for the redesign of your customer portal", size: 12, weight: :bold
|
|
56
|
+
spacer 16
|
|
57
|
+
body
|
|
58
|
+
spacer 28
|
|
59
|
+
signature
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def letterhead
|
|
65
|
+
row(align: :middle, gap: 12) do
|
|
66
|
+
column(width: 40) { svg LOGO, width: 40 }
|
|
67
|
+
column do
|
|
68
|
+
text "Mälaren Studio", size: 15, weight: :bold
|
|
69
|
+
text "Stora Gatan 21, 722 12 Västerås", size: 8.5, color: MUTED
|
|
70
|
+
end
|
|
71
|
+
column(width: 160) do
|
|
72
|
+
text_style(size: 8.5, color: MUTED, align: :right, leading: 1.5) do
|
|
73
|
+
text "+46 21 123 45 67"
|
|
74
|
+
text "hello@malaren.studio"
|
|
75
|
+
text "malaren.studio"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
spacer 12
|
|
80
|
+
rule height: 2, color: ACCENT
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def body
|
|
84
|
+
text_style(align: :justify, leading: 4) do
|
|
85
|
+
[
|
|
86
|
+
"Dear Ms Hallberg,",
|
|
87
|
+
"Thank you for meeting us in Uppsala last week and for walking us through the way your site managers " \
|
|
88
|
+
"use the customer portal today. It was clear that the portal holds the right information but makes it " \
|
|
89
|
+
"hard to find: most of the questions your office answers by phone are already answered somewhere in it.",
|
|
90
|
+
"We propose a redesign in <b>three phases over twelve weeks</b>. The first phase maps the ten most " \
|
|
91
|
+
"common tasks with your customers and staff; the second turns them into a clickable prototype that we " \
|
|
92
|
+
"test on site; the third delivers the finished design and supports your developers while they build it.",
|
|
93
|
+
"The fixed price for all three phases is <b>SEK 480,000</b> excluding VAT, invoiced at the end of each " \
|
|
94
|
+
"phase. The full proposal, with the schedule and the people involved, is available at " \
|
|
95
|
+
"<link href='https://malaren.studio/p/MS-2026-118'><color rgb='#{ACCENT}'><u>malaren.studio/p/MS-2026-118</u>" \
|
|
96
|
+
"</color></link>.",
|
|
97
|
+
"We would be glad to start in the week of 2 November. If you have any questions, or would like to " \
|
|
98
|
+
"adjust the scope, please call me directly on the number above."
|
|
99
|
+
].each_with_index do |paragraph, index|
|
|
100
|
+
spacer 10 if index.positive?
|
|
101
|
+
text paragraph, markup: true
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def signature
|
|
107
|
+
group(keep_together: true) do
|
|
108
|
+
text "Kind regards,"
|
|
109
|
+
spacer 30
|
|
110
|
+
text "Erik Sandberg", weight: :bold
|
|
111
|
+
text "Creative Director, Mälaren Studio AB", color: MUTED, size: 9.5
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if $PROGRAM_NAME == __FILE__
|
|
117
|
+
letter = ExampleLetter.preview
|
|
118
|
+
letter.to_pdf(File.expand_path("letter.pdf", __dir__))
|
|
119
|
+
warn letter.warnings.map(&:message) if letter.warnings.any?
|
|
120
|
+
puts "wrote examples/letter.pdf"
|
|
121
|
+
end
|
data/examples/menu.rb
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A restaurant menu on one page: an introduction wrapped around a floated
|
|
4
|
+
# photo, the dishes poured through two balanced columns, a floated box for
|
|
5
|
+
# the dish of the day, and headings set in the heavier weights of Inter. Run
|
|
6
|
+
# it to write examples/menu.pdf:
|
|
7
|
+
#
|
|
8
|
+
# ruby -Ilib examples/menu.rb
|
|
9
|
+
# ruby -Ilib exe/stationery render examples/menu.rb
|
|
10
|
+
#
|
|
11
|
+
# The type is the bundled Inter. For a font pack, install it
|
|
12
|
+
# (`stationery fonts install noto_serif`) and name it:
|
|
13
|
+
# `font_family "Noto Serif"`, then `default_text font: "Noto Serif"`.
|
|
14
|
+
require "stationery"
|
|
15
|
+
|
|
16
|
+
class ExampleMenu < Stationery::Document
|
|
17
|
+
PHOTO = File.expand_path("assets/bay.png", __dir__)
|
|
18
|
+
INK = "#1C1917"
|
|
19
|
+
MUTED = "#78716C"
|
|
20
|
+
ACCENT = "#9A3412"
|
|
21
|
+
CREAM = "#FFF7ED"
|
|
22
|
+
LINE = "#E7E5E4"
|
|
23
|
+
|
|
24
|
+
COURSES = {
|
|
25
|
+
"Starters" => [
|
|
26
|
+
["Smoked mackerel", "Rye crisp, pickled cucumber, horseradish cream", 145],
|
|
27
|
+
["Beetroot tartare", "Goat's cheese, walnuts, dill oil", 135],
|
|
28
|
+
["Fish soup", "Saffron broth, mussels, aioli, toasted sourdough", 165],
|
|
29
|
+
["Chanterelles on toast", "Brown butter, thyme, aged cheese", 155]
|
|
30
|
+
],
|
|
31
|
+
"Mains" => [
|
|
32
|
+
["Pan-fried cod", "Brown butter, capers, new potatoes, peas", 295],
|
|
33
|
+
["Slow-cooked lamb", "Root vegetables, rosemary jus, crispy kale", 325],
|
|
34
|
+
["Barley risotto", "Mushrooms, parsley, pecorino, lemon", 245],
|
|
35
|
+
["Char with sorrel sauce", "Fennel, dill potatoes, roe", 310],
|
|
36
|
+
["Harbour burger", "Aged beef, cheddar, onions, fries", 225]
|
|
37
|
+
],
|
|
38
|
+
"Desserts" => [
|
|
39
|
+
["Cloudberry parfait", "Almond crumble, warm cloudberries", 125],
|
|
40
|
+
["Chocolate cake", "Sea salt, crème fraîche", 115],
|
|
41
|
+
["Cheese from the island", "Three cheeses, crispbread, quince", 145]
|
|
42
|
+
],
|
|
43
|
+
"To drink" => [
|
|
44
|
+
["House lemonade", "Elderflower or rhubarb", 55],
|
|
45
|
+
["Local cider", "Dry, 33 cl", 85],
|
|
46
|
+
["Coffee or tea", "Refills are free", 40]
|
|
47
|
+
],
|
|
48
|
+
"Wine by the glass" => [
|
|
49
|
+
["Grüner Veltliner", "Crisp and peppery, with the fish", 125],
|
|
50
|
+
["Pinot noir", "Light and red-fruited, with the lamb", 135],
|
|
51
|
+
["Sparkling cider", "Méthode traditionnelle, from the island", 110]
|
|
52
|
+
]
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
page size: :a4, margin: [56, 56, 48, 56]
|
|
56
|
+
default_text size: 9.5, color: INK, leading: 2
|
|
57
|
+
metadata title: "Menu · The Harbour Kitchen", author: "The Harbour Kitchen", creator: "stationery example"
|
|
58
|
+
|
|
59
|
+
def self.preview = new(special: ["Grilled herring", "Mustard sauce, mashed potatoes, lingonberries", 185])
|
|
60
|
+
|
|
61
|
+
def initialize(special:)
|
|
62
|
+
super()
|
|
63
|
+
@special = special
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def view_template
|
|
67
|
+
masthead
|
|
68
|
+
introduction
|
|
69
|
+
spacer 16
|
|
70
|
+
# One flow of courses poured through two columns: column one top to
|
|
71
|
+
# bottom, then column two, ending level where the dishes run out.
|
|
72
|
+
columns(count: 2, gap: 28, rule: { color: LINE }) do
|
|
73
|
+
COURSES.each { |course, dishes| course(course, dishes) }
|
|
74
|
+
end
|
|
75
|
+
spacer 14
|
|
76
|
+
rule color: LINE
|
|
77
|
+
spacer 6
|
|
78
|
+
text "Tell us about allergies before you order. All prices in SEK, service included.",
|
|
79
|
+
size: 8, color: MUTED, align: :center
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def masthead
|
|
85
|
+
text "THE HARBOUR KITCHEN", size: 10, weight: :bold, color: ACCENT, letter_spacing: 3, align: :center
|
|
86
|
+
spacer 4
|
|
87
|
+
text "Autumn menu", size: 34, weight: :bold, align: :center
|
|
88
|
+
spacer 14
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The photo floats left and the text wraps beside it; the dish of the day
|
|
92
|
+
# floats right, between them, in a box of its own.
|
|
93
|
+
def introduction
|
|
94
|
+
image PHOTO, float: :left, width: 0.34, height: 104, fit: :cover, radius: 6, margin: 14
|
|
95
|
+
box(float: :right, width: 150, margin: { left: 14, bottom: 6 }, background: CREAM, radius: 6, padding: 10) do
|
|
96
|
+
text "DISH OF THE DAY", size: 7, weight: :bold, color: ACCENT, letter_spacing: 1
|
|
97
|
+
spacer 3
|
|
98
|
+
name, description, price = @special
|
|
99
|
+
text name, size: 11, weight: :bold
|
|
100
|
+
text description, size: 8.5, color: MUTED
|
|
101
|
+
spacer 3
|
|
102
|
+
text price.to_s, weight: :bold, color: ACCENT
|
|
103
|
+
end
|
|
104
|
+
text "Everything on this menu comes from within a day's drive of the harbour: the fish from the boats " \
|
|
105
|
+
"you can see from your table, the vegetables from two farms on the island and the cheese from the " \
|
|
106
|
+
"dairy up the hill. The menu changes with the catch and the season, so some dishes may run out " \
|
|
107
|
+
"before the evening does.", align: :justify, hyphenate: true, size: 10.5, leading: 3
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A course is kept together, so a column never starts in the middle of one.
|
|
111
|
+
def course(title, dishes)
|
|
112
|
+
group(keep_together: true) do
|
|
113
|
+
text title, size: 18, weight: :bold, color: ACCENT
|
|
114
|
+
spacer 8
|
|
115
|
+
dishes.each do |name, description, price|
|
|
116
|
+
row(gap: 8) do
|
|
117
|
+
column { text name, size: 10.5, weight: :bold }
|
|
118
|
+
column(width: :auto) { text price.to_s, size: 10.5, weight: :bold }
|
|
119
|
+
end
|
|
120
|
+
text description, size: 9, color: MUTED, style: :italic
|
|
121
|
+
spacer 10
|
|
122
|
+
end
|
|
123
|
+
spacer 14
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if $PROGRAM_NAME == __FILE__
|
|
129
|
+
menu = ExampleMenu.preview
|
|
130
|
+
menu.to_pdf(File.expand_path("menu.pdf", __dir__))
|
|
131
|
+
warn menu.warnings.map(&:message) if menu.warnings.any?
|
|
132
|
+
puts "wrote examples/menu.pdf"
|
|
133
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A notice to residents in English, German and Swedish side by side, each
|
|
4
|
+
# column justified and hyphenated by the patterns of its own language, with
|
|
5
|
+
# a Japanese paragraph when a CJK font is at hand. Run it to write
|
|
6
|
+
# examples/multilingual_notice.pdf:
|
|
7
|
+
#
|
|
8
|
+
# ruby -Ilib examples/multilingual_notice.rb
|
|
9
|
+
# ruby -Ilib exe/stationery render examples/multilingual_notice.rb
|
|
10
|
+
#
|
|
11
|
+
# Inter, the font the gem bundles, has Latin, Greek and Cyrillic but no CJK
|
|
12
|
+
# ideographs, and no font pack has them either. Name a font that has them to
|
|
13
|
+
# add the Japanese paragraph; it is registered as a fallback, so it draws only
|
|
14
|
+
# the characters Inter lacks, and the lines break between ideographs:
|
|
15
|
+
#
|
|
16
|
+
# CJK_FONT=/path/to/NotoSansJP-Regular.ttf ruby -Ilib examples/multilingual_notice.rb
|
|
17
|
+
require "stationery"
|
|
18
|
+
|
|
19
|
+
class ExampleMultilingualNotice < Stationery::Document
|
|
20
|
+
ACCENT = "#0E7490"
|
|
21
|
+
INK = "#111827"
|
|
22
|
+
MUTED = "#6B7280"
|
|
23
|
+
PANEL = "#ECFEFF"
|
|
24
|
+
CJK_FONT = ENV.fetch("CJK_FONT", nil)
|
|
25
|
+
|
|
26
|
+
# Each language with the hyphenation patterns to break its words by:
|
|
27
|
+
# `true` is American English, "de" German and "sv" Swedish.
|
|
28
|
+
NOTICES = [
|
|
29
|
+
["English", true, "Water will be turned off",
|
|
30
|
+
["The water supply to every flat in the building will be turned off on Tuesday 14 October between " \
|
|
31
|
+
"08:00 and 16:00 while the main distribution pipes in the basement are replaced.",
|
|
32
|
+
"Please fill a few containers with drinking water beforehand and keep the taps closed during the " \
|
|
33
|
+
"work. Discoloured water may run for a short while afterwards; let the cold tap run until it is clear."]],
|
|
34
|
+
["Deutsch", "de", "Das Wasser wird abgestellt",
|
|
35
|
+
["Am Dienstag, dem 14. Oktober, wird die Wasserversorgung aller Wohnungen im Gebäude zwischen 08:00 " \
|
|
36
|
+
"und 16:00 Uhr unterbrochen, da die Hauptverteilungsleitungen im Keller erneuert werden.",
|
|
37
|
+
"Bitte füllen Sie vorher einige Behälter mit Trinkwasser und lassen Sie die Wasserhähne während der " \
|
|
38
|
+
"Arbeiten geschlossen. Danach kann das Wasser kurzzeitig verfärbt sein; lassen Sie das kalte Wasser " \
|
|
39
|
+
"laufen, bis es klar ist."]],
|
|
40
|
+
["Svenska", "sv", "Vattnet stängs av",
|
|
41
|
+
["Tisdagen den 14 oktober stängs vattnet av i alla lägenheter i huset mellan klockan 08.00 och 16.00, " \
|
|
42
|
+
"medan huvudledningarna i källaren byts ut.",
|
|
43
|
+
"Fyll gärna några kärl med dricksvatten i förväg och håll kranarna stängda under arbetet. Efteråt kan " \
|
|
44
|
+
"vattnet vara missfärgat en kort stund; spola kallvatten tills det är klart."]]
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
JAPANESE = "10月14日(火)8時から16時まで、地下の給水管の交換工事のため、建物内のすべての住戸で断水します。" \
|
|
48
|
+
"事前に飲料水をご用意いただき、工事中は蛇口を閉めておいてください。工事後、しばらく濁った水が出ることが" \
|
|
49
|
+
"ありますので、透明になるまで水を流してください。"
|
|
50
|
+
|
|
51
|
+
page size: :a4, margin: [52, 48, 44, 48]
|
|
52
|
+
default_text font: "Inter", size: 10.5, color: INK, leading: 3
|
|
53
|
+
metadata title: "Notice to residents", author: "Example Housing Cooperative", creator: "stationery example"
|
|
54
|
+
|
|
55
|
+
# A font for what Inter lacks: drawn only where Inter has no glyph.
|
|
56
|
+
if CJK_FONT
|
|
57
|
+
font_family "CJK", regular: CJK_FONT
|
|
58
|
+
font_fallbacks "CJK"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.preview = new
|
|
62
|
+
|
|
63
|
+
def view_template
|
|
64
|
+
text "NOTICE · HINWEIS · MEDDELANDE", size: 9, weight: :bold, color: ACCENT, letter_spacing: 2
|
|
65
|
+
spacer 6
|
|
66
|
+
text "Harbour View Housing Cooperative, 1–5 Example Street", size: 10, color: MUTED
|
|
67
|
+
spacer 8
|
|
68
|
+
rule height: 2, color: ACCENT
|
|
69
|
+
spacer 16
|
|
70
|
+
row(gap: 18) do
|
|
71
|
+
NOTICES.each do |language, hyphenate, title, paragraphs|
|
|
72
|
+
column { notice(language, hyphenate, title, paragraphs) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
spacer 18
|
|
76
|
+
japanese
|
|
77
|
+
spacer 18
|
|
78
|
+
contact
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def notice(language, hyphenate, title, paragraphs)
|
|
84
|
+
text language.upcase, size: 7.5, weight: :bold, color: MUTED, letter_spacing: 1
|
|
85
|
+
spacer 4
|
|
86
|
+
text title, size: 15, weight: :bold, color: ACCENT
|
|
87
|
+
spacer 6
|
|
88
|
+
paragraphs.each do |paragraph|
|
|
89
|
+
text paragraph, align: :justify, hyphenate: hyphenate
|
|
90
|
+
spacer 6
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def japanese
|
|
95
|
+
box(background: PANEL, radius: 6, padding: 12) do
|
|
96
|
+
text "JAPANESE", size: 7.5, weight: :bold, color: MUTED, letter_spacing: 1
|
|
97
|
+
spacer 4
|
|
98
|
+
if CJK_FONT
|
|
99
|
+
text JAPANESE, leading: 4 # Inter for the digits, the CJK font for the rest
|
|
100
|
+
else
|
|
101
|
+
text "Japanese: set CJK_FONT to a font with Japanese glyphs to include this paragraph. Inter, the " \
|
|
102
|
+
"font in the gem, has none; see the comment at the top of examples/multilingual_notice.rb.",
|
|
103
|
+
size: 8.5, color: MUTED
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def contact
|
|
109
|
+
rule color: "#E5E7EB"
|
|
110
|
+
spacer 6
|
|
111
|
+
text "Questions · Fragen · Frågor: caretaker@harbour-view.example · +46 31 000 00 00",
|
|
112
|
+
size: 8.5, color: MUTED, align: :center
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
if $PROGRAM_NAME == __FILE__
|
|
117
|
+
notice = ExampleMultilingualNotice.preview
|
|
118
|
+
notice.to_pdf(File.expand_path("multilingual_notice.pdf", __dir__))
|
|
119
|
+
warn notice.warnings.map(&:message) if notice.warnings.any?
|
|
120
|
+
puts "wrote examples/multilingual_notice.pdf"
|
|
121
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A one-page newsletter: a masthead across the page, an article poured
|
|
4
|
+
# through two balanced columns with headings and a photo, and a footer note
|
|
5
|
+
# across the page below the columns.
|
|
6
|
+
# Run it to write examples/newsletter.pdf:
|
|
7
|
+
#
|
|
8
|
+
# ruby -Ilib examples/newsletter.rb
|
|
9
|
+
# ruby -Ilib exe/stationery render examples/newsletter.rb
|
|
10
|
+
require "stationery"
|
|
11
|
+
|
|
12
|
+
class ExampleNewsletter < Stationery::Document
|
|
13
|
+
ASSETS = File.expand_path("assets", __dir__)
|
|
14
|
+
INK = "#1F2937"
|
|
15
|
+
MUTED = "#6B7280"
|
|
16
|
+
ACCENT = "#0F766E"
|
|
17
|
+
LINE = "#D1D5DB"
|
|
18
|
+
|
|
19
|
+
ARTICLE = [
|
|
20
|
+
[nil, ["The harbour wall that has kept the winter swell out of the village since 1911 was reopened on " \
|
|
21
|
+
"Saturday, eighteen months after a storm took forty metres of it into the bay. Two hundred people " \
|
|
22
|
+
"walked its length behind the brass band, and the first boat through the gap was the oldest in the " \
|
|
23
|
+
"fleet.",
|
|
24
|
+
"The rebuilt section stands a metre higher than the old one. Its core is concrete, but the face is " \
|
|
25
|
+
"the original sandstone: divers recovered nine blocks in ten from the seabed, and the quarry above " \
|
|
26
|
+
"the village was opened for a month to cut the rest."]],
|
|
27
|
+
["What it cost", ["The work came to 2.4 million, a third of it raised by the village itself. The harbour " \
|
|
28
|
+
"dues will rise by four per cent a year until 2031 to repay the loan, a rise the " \
|
|
29
|
+
"fishing cooperative voted for without a single vote against.",
|
|
30
|
+
"Nothing was spent on the lighthouse, which needs a new lantern. The council has asked " \
|
|
31
|
+
"for tenders and expects to decide in the spring."]],
|
|
32
|
+
["What comes next", ["The inner quay is next. Its timber piles date from the fifties and the survey gives " \
|
|
33
|
+
"them ten years at most. Plans go on show in the old customs house in March, and " \
|
|
34
|
+
"the council wants comments before the summer.",
|
|
35
|
+
"Until then the wall is open to walkers from dawn to dusk. The gate at the landward " \
|
|
36
|
+
"end is locked in any wind above force seven."]]
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
page size: :a4, margin: 48
|
|
40
|
+
default_text size: 10, color: INK, leading: 2.5
|
|
41
|
+
metadata title: "The Harbour Letter", author: "stationery example", lang: "en"
|
|
42
|
+
tagged
|
|
43
|
+
|
|
44
|
+
def self.preview = new
|
|
45
|
+
|
|
46
|
+
def view_template
|
|
47
|
+
masthead
|
|
48
|
+
columns(count: 2, gap: 22, rule: { color: LINE }) { article }
|
|
49
|
+
note
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def masthead
|
|
55
|
+
text "THE HARBOUR LETTER", size: 9, weight: :bold, color: ACCENT, letter_spacing: 1.5
|
|
56
|
+
spacer 6
|
|
57
|
+
text "The sea wall is whole again", size: 30, weight: :bold, leading: 0, heading: 1
|
|
58
|
+
spacer 6
|
|
59
|
+
text "Issue 42 · October 2026", size: 9, color: MUTED
|
|
60
|
+
spacer 10
|
|
61
|
+
rule height: 2, color: ACCENT
|
|
62
|
+
spacer 16
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def article
|
|
66
|
+
ARTICLE.each_with_index do |(title, paragraphs), index|
|
|
67
|
+
section_heading(title) if title
|
|
68
|
+
paragraphs.each { |paragraph| body(paragraph) }
|
|
69
|
+
photo if index.zero?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def section_heading(title)
|
|
74
|
+
text title, size: 13, weight: :bold, color: ACCENT, leading: 0, heading: 2, keep_with_next: true
|
|
75
|
+
spacer 5
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def body(paragraph)
|
|
79
|
+
text paragraph, align: :justify, hyphenate: true, orphans: 2, widows: 2
|
|
80
|
+
spacer 8
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def photo
|
|
84
|
+
group(keep_together: true) do
|
|
85
|
+
image File.join(ASSETS, "bay.png"), width: 1.0, height: 120, fit: :cover, radius: 6,
|
|
86
|
+
alt: "The bay seen from the rebuilt wall"
|
|
87
|
+
spacer 4
|
|
88
|
+
text "The bay from the new section of the wall.", size: 8, color: MUTED, style: :italic
|
|
89
|
+
end
|
|
90
|
+
spacer 8
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def note
|
|
94
|
+
spacer 16
|
|
95
|
+
rule color: LINE
|
|
96
|
+
spacer 8
|
|
97
|
+
text "The Harbour Letter is written by volunteers and printed by the cooperative. Letters to the " \
|
|
98
|
+
"editor are welcome at the customs house, where back issues are kept.",
|
|
99
|
+
size: 8.5, color: MUTED, align: :center
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if $PROGRAM_NAME == __FILE__
|
|
104
|
+
newsletter = ExampleNewsletter.preview
|
|
105
|
+
newsletter.to_pdf(File.expand_path("newsletter.pdf", __dir__))
|
|
106
|
+
warn newsletter.warnings.map(&:message) if newsletter.warnings.any?
|
|
107
|
+
puts "wrote examples/newsletter.pdf"
|
|
108
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A landscape packing slip: a long table that repeats its header on every
|
|
4
|
+
# page and a barcode drawn on a canvas. Run it to write examples/packing_slip.pdf:
|
|
5
|
+
#
|
|
6
|
+
# ruby -Ilib examples/packing_slip.rb
|
|
7
|
+
# ruby -Ilib exe/stationery render examples/packing_slip.rb
|
|
8
|
+
require "stationery"
|
|
9
|
+
|
|
10
|
+
class ExamplePackingSlip < Stationery::Document
|
|
11
|
+
ACCENT = "#1D4ED8"
|
|
12
|
+
INK = "#111827"
|
|
13
|
+
MUTED = "#6B7280"
|
|
14
|
+
HAIRLINE = "#E5E7EB"
|
|
15
|
+
ZEBRA = "#F3F4F6"
|
|
16
|
+
PANEL = "#F9FAFB"
|
|
17
|
+
|
|
18
|
+
PRODUCTS = ["Hex bolt M8 × 40, zinc plated", "Washer M8, stainless A2", "Cable tie 200 mm, black",
|
|
19
|
+
"Wall plug 8 mm", "Wood screw 5 × 60, Torx", "Hinge 90 mm, brushed steel",
|
|
20
|
+
"Shelf bracket 200 × 250 mm, white powder coat, including screws and wall plugs for concrete",
|
|
21
|
+
"Drawer runner 450 mm, soft close", "Threaded rod M10 × 1000", "Nylon lock nut M10",
|
|
22
|
+
"Corner brace 40 mm", "Cabinet handle 128 mm, matt black aluminium, for doors up to 22 mm thick"].freeze
|
|
23
|
+
|
|
24
|
+
page size: :a4, layout: :landscape, margin: [36, 40, 32, 40]
|
|
25
|
+
default_text size: 9, color: INK
|
|
26
|
+
metadata title: "Packing slip", creator: "stationery example"
|
|
27
|
+
|
|
28
|
+
header(gap: 14) do |page|
|
|
29
|
+
row(align: :bottom) do
|
|
30
|
+
text "Packing slip · Order NW-2026-004817", size: 13, weight: :bold
|
|
31
|
+
text "Page #{page.number} of #{page.count}", size: 8, color: MUTED, align: :right
|
|
32
|
+
end
|
|
33
|
+
spacer 6
|
|
34
|
+
rule height: 1.5, color: ACCENT
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
footer { text "Check the contents against this slip within 5 days of delivery.", size: 7, color: MUTED }
|
|
38
|
+
|
|
39
|
+
def self.preview
|
|
40
|
+
items = Array.new(120) do |i|
|
|
41
|
+
sku = "NW-#{10_000 + (i * 37)}"
|
|
42
|
+
bin = "#{"ABCDEF"[i % 6]}-#{((i % 14) + 1).to_s.rjust(2, "0")}"
|
|
43
|
+
[sku, PRODUCTS[i % PRODUCTS.size], bin, ((i * 7) % 24) + 1, (((i * 13) % 40) + 5) / 10.0]
|
|
44
|
+
end
|
|
45
|
+
new(order: "NW-2026-004817", items:,
|
|
46
|
+
ship_to: ["Hallberg Bygg AB", "Goods reception, gate 4", "Industrigatan 8", "753 23 Uppsala", "Sweden"],
|
|
47
|
+
bill_to: ["Hallberg Bygg AB", "Accounts payable", "Box 1204", "751 42 Uppsala", "Sweden"])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initialize(order:, items:, ship_to:, bill_to:)
|
|
51
|
+
super()
|
|
52
|
+
@order = order
|
|
53
|
+
@items = items
|
|
54
|
+
@ship_to = ship_to
|
|
55
|
+
@bill_to = bill_to
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def view_template
|
|
59
|
+
row(gap: 16) do
|
|
60
|
+
column(width: 0.35) { address("SHIP TO", @ship_to) }
|
|
61
|
+
column(width: 0.35) { address("BILL TO", @bill_to) }
|
|
62
|
+
column { barcode }
|
|
63
|
+
end
|
|
64
|
+
spacer 18
|
|
65
|
+
items_table
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def address(label, lines)
|
|
71
|
+
box(background: PANEL, border: { color: HAIRLINE, width: 1 }, radius: 6, padding: [10, 12]) do
|
|
72
|
+
text label, size: 7.5, weight: :bold, color: MUTED, letter_spacing: 0.6
|
|
73
|
+
spacer 4
|
|
74
|
+
text "<b>#{lines.first}</b>\n#{lines.drop(1).join("\n")}", markup: true, leading: 1.5
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def barcode
|
|
79
|
+
digits = @order.delete("^0-9").chars.map(&:to_i)
|
|
80
|
+
canvas(height: 40) do |canvas, rect|
|
|
81
|
+
x = rect.x
|
|
82
|
+
digits.cycle(3).each_with_index do |digit, index|
|
|
83
|
+
width = 1 + (digit % 3)
|
|
84
|
+
canvas.fill_rect(x, rect.y, width, rect.height, color: INK) if index.even?
|
|
85
|
+
x += width + 1
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
spacer 4
|
|
89
|
+
text @order, size: 8, color: MUTED, letter_spacing: 2
|
|
90
|
+
spacer 8
|
|
91
|
+
text "Shipped 27 September 2026 · 3 parcels · Example Freight", size: 8, color: MUTED
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def items_table
|
|
95
|
+
rows = [["SKU", "Description", "Bin", "Qty", "Unit (kg)", "Weight (kg)"]]
|
|
96
|
+
@items.each do |sku, name, bin, qty, weight|
|
|
97
|
+
rows << [sku, name, bin, qty.to_s, kilograms(weight), kilograms(qty * weight)]
|
|
98
|
+
end
|
|
99
|
+
rows << [{ content: "#{@items.size} lines", colspan: 3 }, @items.sum { |item| item[3] }.to_s, "",
|
|
100
|
+
kilograms(@items.sum { |*, qty, weight| qty * weight })]
|
|
101
|
+
|
|
102
|
+
table(rows, header: true, split_rows: true, width: :full, widths: [80, 300, nil, 60, 70, 80],
|
|
103
|
+
cell: { padding: [2.5, 8], borders: [], size: 8.5 }) do |t|
|
|
104
|
+
t.row(0).set(background: ACCENT, color: "#FFFFFF", weight: :bold)
|
|
105
|
+
t.columns(0).rows(1..).color = MUTED
|
|
106
|
+
t.columns(3..).align = :right
|
|
107
|
+
t.zebra(from: 1, to: @items.size, color: ZEBRA)
|
|
108
|
+
t.row(-1).set(weight: :bold, color: INK, borders: [:top], border_color: INK, border_width: 1)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def kilograms(value) = format("%.1f", value)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
if $PROGRAM_NAME == __FILE__
|
|
116
|
+
slip = ExamplePackingSlip.preview
|
|
117
|
+
slip.to_pdf(File.expand_path("packing_slip.pdf", __dir__))
|
|
118
|
+
warn slip.warnings.map(&:message) if slip.warnings.any?
|
|
119
|
+
puts "wrote examples/packing_slip.pdf"
|
|
120
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# An A5 landscape postcard: a photo collage of one large picture with two
|
|
4
|
+
# tilted, white-framed snapshots overlapping its corners, beside a greeting.
|
|
5
|
+
# Run it to write examples/postcard.pdf:
|
|
6
|
+
#
|
|
7
|
+
# ruby -Ilib examples/postcard.rb
|
|
8
|
+
# ruby -Ilib exe/stationery render examples/postcard.rb
|
|
9
|
+
require "stationery"
|
|
10
|
+
|
|
11
|
+
class ExamplePostcard < Stationery::Document
|
|
12
|
+
ASSETS = File.expand_path("assets", __dir__)
|
|
13
|
+
PAPER = "#FFFBEB"
|
|
14
|
+
INK = "#1F2937"
|
|
15
|
+
MUTED = "#6B7280"
|
|
16
|
+
ACCENT = "#B45309"
|
|
17
|
+
OVERHANG = 18
|
|
18
|
+
|
|
19
|
+
page size: :a5, layout: :landscape, margin: 36
|
|
20
|
+
default_text size: 10.5, color: INK
|
|
21
|
+
metadata title: "Greetings from Sardinia", author: "stationery example", lang: "en"
|
|
22
|
+
tagged
|
|
23
|
+
|
|
24
|
+
# Cream card stock: a background layer painted under every page.
|
|
25
|
+
page_template(layer: :background) do |page|
|
|
26
|
+
canvas(at: [0, 0], width: page.width, height: page.height) do |c, rect|
|
|
27
|
+
c.fill_rect(rect.x, rect.y, rect.width, rect.height, color: PAPER)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.preview = new
|
|
32
|
+
|
|
33
|
+
def view_template
|
|
34
|
+
spacer 30
|
|
35
|
+
row(gap: 20, align: :middle) do
|
|
36
|
+
column(width: 0.55) { collage }
|
|
37
|
+
column { greeting }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The site-style collage: a base photo with rounded corners and a shadow, a
|
|
44
|
+
# 4:3 snapshot hanging off the bottom-left corner and a square one off the
|
|
45
|
+
# top-right, each tilted in its white frame. The stack's layers overhang by
|
|
46
|
+
# OVERHANG points, so the box around it keeps them on the page.
|
|
47
|
+
def collage
|
|
48
|
+
box(padding: OVERHANG) do
|
|
49
|
+
stack do
|
|
50
|
+
box(width: :auto, radius: 16, shadow: true) do
|
|
51
|
+
image photo("sea"), width: 228, height: 171, fit: :cover, radius: 16, alt: "The bay at dawn"
|
|
52
|
+
end
|
|
53
|
+
layer(bottom: -OVERHANG, left: -OVERHANG, width: 0.4, rotate: -3, **frame) do
|
|
54
|
+
image photo("hills"), width: 83, height: 62, fit: :cover, radius: 8, alt: "Hills above the village"
|
|
55
|
+
end
|
|
56
|
+
layer(top: -OVERHANG, right: -OVERHANG, width: 1/3r, rotate: 2, **frame) do
|
|
57
|
+
image photo("stone"), width: 68, height: 68, fit: :cover, radius: 8, alt: "A sandstone wall"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def frame = { padding: 4, background: "#FFFFFF", radius: 12, shadow: true }
|
|
64
|
+
def photo(name) = File.join(ASSETS, "#{name}.png")
|
|
65
|
+
|
|
66
|
+
def greeting
|
|
67
|
+
text "Greetings from Sardinia", size: 22, weight: :bold, color: ACCENT, heading: 1
|
|
68
|
+
spacer 6
|
|
69
|
+
text "October 2026", size: 9, color: MUTED
|
|
70
|
+
spacer 14
|
|
71
|
+
text "The sea is still warm enough to swim before breakfast, the hills smell of myrtle and the village " \
|
|
72
|
+
"square fills up every evening. Wish you were here.", leading: 3
|
|
73
|
+
spacer 14
|
|
74
|
+
text "With love, the whole crew", style: :italic, color: MUTED
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if $PROGRAM_NAME == __FILE__
|
|
79
|
+
postcard = ExamplePostcard.preview
|
|
80
|
+
postcard.to_pdf(File.expand_path("postcard.pdf", __dir__))
|
|
81
|
+
warn postcard.warnings.map(&:message) if postcard.warnings.any?
|
|
82
|
+
puts "wrote examples/postcard.pdf"
|
|
83
|
+
end
|