Skip to content

Commit e410077

Browse files
committed
Underlines for left-aligned text
1 parent 6d422bd commit e410077

File tree

5 files changed

+148
-23
lines changed

5 files changed

+148
-23
lines changed

lib/mudbrick.ex

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ defmodule Mudbrick do
236236
...> new(fonts: %{regular: [file: bodoni_regular()], bold: [file: bodoni_bold()]})
237237
...> |> page()
238238
...> |> text(["I am ", {"bold", font: :bold}], font: :regular, position: {200, 200})
239+
240+
Underlined text.
241+
242+
iex> import Mudbrick.TestHelper
243+
...> import Mudbrick
244+
...> new(fonts: %{bodoni: [file: bodoni_regular()]})
245+
...> |> page(size: {100, 40})
246+
...> |> text(["nounderline\\n", {"underline!", underline: [width: 1]}], position: {8, 20}, font: :bodoni, font_size: 8)
247+
...> |> render()
248+
...> |> then(&File.write("examples/underlined_text.pdf", &1))
249+
250+
Produces [this PDF](examples/underlined_text.pdf?#navpanes=0).
251+
252+
<object width="400" height="115" data="examples/underlined_text.pdf?#navpanes=0" type="application/pdf"></object>
239253
"""
240254

241255
@spec text(context(), Mudbrick.TextBlock.write(), Mudbrick.TextBlock.options()) :: context()
@@ -256,7 +270,7 @@ defmodule Mudbrick do
256270

257271
context
258272
|> ContentStream.update_operations(fn ops ->
259-
Enum.reverse(TextBlock.Output.from(text_block).operations) ++ ops
273+
TextBlock.Output.from(text_block).operations ++ ops
260274
end)
261275
end
262276

lib/mudbrick/text_block.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
defmodule Mudbrick.TextBlock do
22
@type alignment :: :left | :right
33

4+
@type underline_option ::
5+
{:width, number()}
6+
| {:colour, Mudbrick.colour()}
7+
@type underline_options :: [underline_option()]
8+
49
@type option ::
510
{:align, alignment()}
611
| {:colour, Mudbrick.colour()}
@@ -16,6 +21,7 @@ defmodule Mudbrick.TextBlock do
1621
| {:font, atom()}
1722
| {:font_size, number()}
1823
| {:leading, number()}
24+
| {:underline, underline_options()}
1925

2026
@type part_options :: [part_option()]
2127

@@ -40,9 +46,9 @@ defmodule Mudbrick.TextBlock do
4046
colour: {0, 0, 0},
4147
font: nil,
4248
font_size: 12,
49+
leading: nil,
4350
lines: [],
44-
position: {0, 0},
45-
leading: nil
51+
position: {0, 0}
4652

4753
alias Mudbrick.TextBlock.Line
4854

lib/mudbrick/text_block/line.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ defmodule Mudbrick.TextBlock.Line do
1717
font: nil,
1818
font_size: nil,
1919
left_offset: nil,
20-
text: ""
20+
text: "",
21+
underline: nil
2122

2223
def width(part) do
2324
Mudbrick.Font.width(

lib/mudbrick/text_block/output.ex

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
defmodule Mudbrick.TextBlock.Output do
22
@moduledoc false
33

4-
defstruct font: nil, font_size: nil, operations: []
4+
defstruct position: nil,
5+
font: nil,
6+
font_size: nil,
7+
operations: [],
8+
drawings: []
59

610
alias Mudbrick.ContentStream.{BT, ET}
711
alias Mudbrick.ContentStream.Rg
812
alias Mudbrick.ContentStream.Td
913
alias Mudbrick.ContentStream.Tf
1014
alias Mudbrick.ContentStream.{Apostrophe, Tj}
1115
alias Mudbrick.ContentStream.TL
16+
alias Mudbrick.Path
1217
alias Mudbrick.TextBlock.Line
1318

1419
defmodule LeftAlign do
1520
@moduledoc false
1621

1722
alias Mudbrick.TextBlock.Output
1823

19-
defp leading(output, %Line{leading: nil}) do
20-
output
21-
end
22-
2324
defp leading(output, line) do
24-
Output.add(output, %TL{leading: line.leading})
25+
output
26+
|> Output.add(%TL{leading: line.leading})
2527
end
2628

2729
def reduce_lines(output, [line]) do
@@ -42,7 +44,9 @@ defmodule Mudbrick.TextBlock.Output do
4244
end
4345

4446
defp reduce_parts(output, %Line{parts: [part]}, _operator, :first_line) do
45-
Output.add_part(output, part, Tj)
47+
output
48+
|> Output.add_part(part, Tj)
49+
|> underline(part)
4650
end
4751

4852
defp reduce_parts(output, %Line{parts: []}, _operator, nil) do
@@ -52,13 +56,40 @@ defmodule Mudbrick.TextBlock.Output do
5256

5357
defp reduce_parts(output, %Line{parts: [part]}, _operator, nil) do
5458
Output.add_part(output, part, Apostrophe)
59+
|> underline(part)
5560
end
5661

5762
defp reduce_parts(output, %Line{parts: [part | parts]} = line, operator, line_kind) do
5863
output
5964
|> Output.add_part(part, operator)
65+
|> underline(part)
6066
|> reduce_parts(%{line | parts: parts}, Tj, line_kind)
6167
end
68+
69+
defp underline(output, part) do
70+
output
71+
|> then(fn output ->
72+
if part.underline do
73+
{x, y} = output.position
74+
{offset_x, offset_y} = part.left_offset
75+
76+
x = x + offset_x
77+
y = y + offset_y - 2
78+
79+
path_output =
80+
Path.new()
81+
|> Path.move(to: {x, y})
82+
|> Path.line(to: {x + Line.Part.width(part), y})
83+
|> Path.Output.from()
84+
85+
Map.update!(output, :drawings, fn drawings ->
86+
[path_output | drawings]
87+
end)
88+
else
89+
output
90+
end
91+
end)
92+
end
6293
end
6394

6495
defmodule RightAlign do
@@ -98,26 +129,37 @@ defmodule Mudbrick.TextBlock.Output do
98129
end
99130
end
100131

132+
defp drawings(output) do
133+
Map.update!(output, :operations, fn ops ->
134+
for drawing <- output.drawings, reduce: ops do
135+
ops ->
136+
Enum.reverse(drawing.operations) ++ ops
137+
end
138+
end)
139+
end
140+
101141
def from(
102142
%Mudbrick.TextBlock{
103143
align: :left,
104144
font: font,
105145
font_size: font_size,
106-
position: {x, y}
146+
position: position = {x, y}
107147
} = tb
108148
) do
109149
tl = %TL{leading: leading(tb)}
110150
tf = %Tf{font: font, size: font_size}
111151

112-
%__MODULE__{font: font, font_size: font_size}
152+
%__MODULE__{position: position, font: font, font_size: font_size}
113153
|> end_block()
114154
|> LeftAlign.reduce_lines(tb.lines)
115155
|> add(%Td{tx: x, ty: y})
116156
|> add(tl)
117157
|> add(tf)
118158
|> start_block()
159+
|> drawings()
119160
|> deduplicate(tl)
120161
|> deduplicate(tf)
162+
|> Map.update!(:operations, &Enum.reverse/1)
121163
end
122164

123165
def from(
@@ -136,6 +178,7 @@ defmodule Mudbrick.TextBlock.Output do
136178
|> add(%TL{leading: leading(tb)})
137179
|> add(tf)
138180
|> deduplicate(tf)
181+
|> Map.update!(:operations, &Enum.reverse/1)
139182
end
140183

141184
def add(%__MODULE__{} = output, op) do
@@ -145,21 +188,17 @@ defmodule Mudbrick.TextBlock.Output do
145188
def add_part(output, part, operator) do
146189
output
147190
|> with_font(
148-
struct!(operator, font: part.font || output.font, text: part.text),
191+
struct!(operator, font: part.font, text: part.text),
149192
part
150193
)
151194
|> colour(part.colour)
152195
end
153196

154197
def with_font(output, op, part) do
155-
if part.font in [nil, output.font] and part.font_size == nil do
156-
add(output, op)
157-
else
158-
output
159-
|> add(%Tf{font: output.font, size: output.font_size})
160-
|> add(op)
161-
|> add(%Tf{font: part.font || output.font, size: part.font_size || output.font_size})
162-
end
198+
output
199+
|> add(%Tf{font: output.font, size: output.font_size})
200+
|> add(op)
201+
|> add(%Tf{font: part.font, size: part.font_size})
163202
end
164203

165204
def colour(output, {r, g, b}) do

test/mudbrick/text_block_test.exs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,34 @@ defmodule Mudbrick.TextBlockTest do
135135
] = block.lines
136136
end
137137

138+
describe "underline" do
139+
test "can be set on a single line" do
140+
block =
141+
TextBlock.new(
142+
font_size: 10,
143+
position: {400, 500}
144+
)
145+
|> TextBlock.write("this is ")
146+
|> TextBlock.write("underlined", underline: [width: 1])
147+
148+
assert block.lines == [
149+
%Line{
150+
leading: 12,
151+
parts: [
152+
%Part{
153+
colour: {0, 0, 0},
154+
font: nil,
155+
font_size: 10,
156+
text: "underlined",
157+
underline: [width: 1]
158+
},
159+
%Part{colour: {0, 0, 0}, font: nil, font_size: 10, text: "this is "}
160+
]
161+
}
162+
]
163+
end
164+
end
165+
138166
describe "leading" do
139167
test "can be set per line" do
140168
block =
@@ -267,6 +295,41 @@ defmodule Mudbrick.TextBlockTest do
267295
end)
268296
|> operations()
269297
end
298+
299+
test "underlines happen" do
300+
assert [
301+
"q",
302+
"0.0 469.2 m",
303+
"0 0 0 RG",
304+
"1 w",
305+
"91.53599999999999 469.2 l",
306+
"S",
307+
"Q",
308+
"q",
309+
"0.0 498.0 m",
310+
"0 0 0 RG",
311+
"1 w",
312+
"62.064 498.0 l",
313+
"S",
314+
"Q",
315+
"BT",
316+
"/F1 12 Tf",
317+
"14.399999999999999 TL",
318+
"0 500 Td",
319+
"0 0 0 rg",
320+
"<012100F400BB00C0010F00ED00D900F400C000BB01B7> Tj",
321+
"<00F400FC011D01B7012100F400BB00C0010F00ED00D900F400C000BB01B7> '",
322+
"<012100F400BB00C0010F00ED00D900F400C000BB01B700A500CF00A500D900F4> '",
323+
"ET"
324+
] =
325+
output(fn %{fonts: fonts} ->
326+
TextBlock.new(font: fonts.regular, position: {0, 500})
327+
|> TextBlock.write("underlined ", underline: [width: 1])
328+
|> TextBlock.write("\nnot underlined ")
329+
|> TextBlock.write("\nunderlined again", underline: [width: 1])
330+
end)
331+
|> operations()
332+
end
270333
end
271334

272335
describe "right-aligned" do
@@ -387,7 +450,9 @@ defmodule Mudbrick.TextBlockTest do
387450
end
388451
end
389452

390-
defp output(f), do: output(f, Mudbrick.TextBlock.Output)
453+
defp output(f) do
454+
output(f, Mudbrick.TextBlock.Output) |> Enum.reverse()
455+
end
391456

392457
defp operations(ops) do
393458
Enum.map(ops, &Mudbrick.TestHelper.show/1)

0 commit comments

Comments
 (0)