diff --git a/lib/mudbrick/text_block.ex b/lib/mudbrick/text_block.ex index 4e9c9f5..7845fda 100644 --- a/lib/mudbrick/text_block.ex +++ b/lib/mudbrick/text_block.ex @@ -26,6 +26,7 @@ defmodule Mudbrick.TextBlock do defstruct operations: [] alias Mudbrick.ContentStream.{BT, ET} + alias Mudbrick.ContentStream.Rg alias Mudbrick.ContentStream.Td alias Mudbrick.ContentStream.Tf alias Mudbrick.ContentStream.{Tj, Apostrophe} @@ -47,15 +48,41 @@ defmodule Mudbrick.TextBlock do |> add(%TL{leading: leading(tb)}) |> add(%Td{tx: x, ty: y}) |> then(fn output -> - [%{parts: [first_part]} | rest] = tb.lines + [%Line{parts: first_parts} | other_lines] = tb.lines |> Enum.reverse() output - |> Output.add(%Tj{font: font, text: first_part.text}) |> then( - &for %{parts: [part]} <- rest, reduce: &1 do - acc -> Output.add(acc, %Apostrophe{font: font, text: part.text}) - end + &List.foldr(first_parts, &1, fn %Line.Part{text: text, colour: colour}, acc -> + acc + |> colour(colour) + |> add(%Tj{font: font, text: text}) + end) ) + |> then(fn output -> + for %Line{parts: parts} <- other_lines, reduce: output do + acc -> + {_, acc} = + List.foldr(parts, {false, acc}, fn + %Line.Part{text: text, colour: colour}, {false, inner_acc} -> + { + true, + inner_acc + |> colour(colour) + |> add(%Apostrophe{font: font, text: text}) + } + + %Line.Part{text: text, colour: colour}, {true, inner_acc} -> + { + true, + inner_acc + |> colour(colour) + |> add(%Tj{font: font, text: text}) + } + end) + + acc + end + end) end) |> end_block() @@ -75,11 +102,11 @@ defmodule Mudbrick.TextBlock do |> add(%Tf{font: font, size: font_size}) |> add(%TL{leading: leading(tb)}) |> then(fn output -> - [%{parts: [first_part]} | rest] = tb.lines + [%{parts: [first_part]} | rest] = Enum.reverse(tb.lines) output |> right_offset(tb, first_part.text, 1) - |> Output.add(%Tj{font: font, text: first_part.text}) + |> add(%Tj{font: font, text: first_part.text}) |> end_block() |> then(fn output -> {_line, output} = @@ -90,7 +117,7 @@ defmodule Mudbrick.TextBlock do acc |> start_block() |> right_offset(tb, part.text, line) - |> Output.add(%Tj{font: font, text: part.text}) + |> add(%Tj{font: font, text: part.text}) |> end_block() } end @@ -102,28 +129,39 @@ defmodule Mudbrick.TextBlock do output.operations end - def add(%__MODULE__{} = output, op) do - Map.update!(output, :operations, &[op | &1]) - end - - def start_block(output) do + defp start_block(output) do add(output, %BT{}) end - def end_block(output) do + defp end_block(output) do add(output, %ET{}) end - def right_offset(output, tb, text, line) do + defp right_offset(output, tb, text, line) do n = line - 1 {x, y} = tb.position - Output.add(output, %Td{ + add(output, %Td{ tx: x - Font.width(tb.font, tb.font_size, text), ty: y - leading(tb) * n }) end + defp colour(output, {r, g, b}) do + new_colour = %Rg{r: r, g: g, b: b} + latest_colour = Enum.find(output.operations, &match?(%Rg{}, &1)) || %Rg{r: 0, g: 0, b: 0} + + if latest_colour != new_colour do + add(output, new_colour) + else + output + end + end + + defp add(%__MODULE__{} = output, op) do + Map.update!(output, :operations, &[op | &1]) + end + defp leading(tb) do tb.font_size * 1.2 end @@ -144,9 +182,9 @@ defmodule Mudbrick.TextBlock do def write(tb, text, opts \\ []) do Map.update!(tb, :lines, fn [] -> - text - |> String.split("\n") - |> Enum.map(&Line.wrap(&1, opts)) + for text <- String.split(text, "\n"), reduce: [] do + acc -> [Line.wrap(text, opts) | acc] + end [%Line{} = previous_line | existing_lines] -> [first_new_line_text | new_line_texts] = String.split(text, "\n") diff --git a/test/mudbrick/text_block_test.exs b/test/mudbrick/text_block_test.exs index 4d98f89..fd2a2b2 100644 --- a/test/mudbrick/text_block_test.exs +++ b/test/mudbrick/text_block_test.exs @@ -9,6 +9,21 @@ defmodule Mudbrick.TextBlockTest do alias Mudbrick.TextBlock.Line.Part alias Mudbrick.TextBlock.Output + test "single write is divided into lines" do + block = + TextBlock.new( + font_size: 10, + position: {400, 500} + ) + |> TextBlock.write("first\nsecond\nthird", colour: {0, 0, 0}) + + assert block.lines == [ + %Line{parts: [%Part{text: "third"}]}, + %Line{parts: [%Part{text: "second"}]}, + %Line{parts: [%Part{text: "first"}]} + ] + end + test "writes get divided into lines and parts" do block = TextBlock.new( @@ -23,74 +38,123 @@ defmodule Mudbrick.TextBlockTest do |> TextBlock.write(""" third line\ """) + |> TextBlock.write("\nfourth", colour: {0, 1, 0}) assert block.lines == [ - %Line{parts: [%Part{text: "third line"}, %Part{text: ""}]}, + %Line{parts: [%Part{text: "fourth", colour: {0, 1, 0}}]}, + %Line{ + parts: [ + %Part{text: "", colour: {0, 1, 0}}, + %Part{text: "third line"}, + %Part{text: ""} + ] + }, %Line{parts: [%Part{text: "second line"}]}, %Line{parts: [%Part{text: "line"}, %Part{text: "first ", colour: {1, 0, 0}}]} ] end - test "left-aligned newlines become apostrophes" do - assert [ - "BT", - "/F1 10 Tf", - "12.0 TL", - "400 500 Td", - "<014C010F0116011D01B700ED00D900F400C0> Tj", - "<011600C000B500FC00F400BB01B700ED00D900F400C0> '", - "() '", - "ET" - ] = - output(fn font -> - TextBlock.new( - font: font, - font_size: 10, - position: {400, 500} - ) - |> TextBlock.write(""" - first line - second line - """) - end) - |> operations() + describe "left-aligned" do + test "newlines become apostrophes" do + assert [ + "BT", + "/F1 10 Tf", + "12.0 TL", + "400 500 Td", + "<014C010F0116011D01B700ED00D900F400C0> Tj", + "<011600C000B500FC00F400BB01B700ED00D900F400C0> '", + "() '", + "ET" + ] = + output(fn font -> + TextBlock.new( + font: font, + font_size: 10, + position: {400, 500} + ) + |> TextBlock.write(""" + first line + second line + """) + end) + |> operations() + end + + test "inline colours are written with Tjs" do + assert [ + "BT", + "/F1 10 Tf", + "12.0 TL", + "400 500 Td", + "<00A5> Tj", + "1 0 0 rg", + "<00B4> Tj", + "0 1 0 rg", + "() Tj", + "<00B5> '", + "<00BB> '", + "0 0 1 rg", + "<00C0> Tj", + "ET" + ] = + output(fn font -> + TextBlock.new( + font: font, + font_size: 10, + position: {400, 500} + ) + |> TextBlock.write("a") + |> TextBlock.write("b", colour: {1, 0, 0}) + |> TextBlock.write("\nc\nd", colour: {0, 1, 0}) + |> TextBlock.write("e", colour: {0, 0, 1}) + end) + |> operations() + end end - test "right-aligned newlines become Tjs with offsets" do - assert [ - "BT", - "/F1 10 Tf", - "12.0 TL", - "384.82 500.0 Td", - "<00A500A500A5> Tj", - "ET", - "BT", - "379.42 488.0 Td", - "<013801380138> Tj", - "ET", - "BT", - "306.48 476.0 Td", - "<00880055008800550088005500880055008800550088> Tj", - "ET", - "BT", - "400 464.0 Td", - "() Tj", - "ET" - ] = - output(fn font -> - Mudbrick.TextBlock.new( - font: font, - font_size: 10, - position: {400, 500}, - align: :right - ) - |> Mudbrick.TextBlock.write(""" - aaa - www - WOWOWOWOWOW - """) - end) - |> operations() + describe "right-aligned" do + test "newlines become Tjs with offsets" do + assert [ + "BT", + "/F1 10 Tf", + "12.0 TL", + "384.82 500.0 Td", + "<00A500A500A5> Tj", + "ET", + "BT", + "379.42 488.0 Td", + "<013801380138> Tj", + "ET", + "BT", + "306.48 476.0 Td", + "<00880055008800550088005500880055008800550088> Tj", + "ET", + "BT", + "400 464.0 Td", + "() Tj", + "ET", + "BT", + "390.74 452.0 Td", + "<00D500D9> Tj", + "ET" + ] = + output(fn font -> + Mudbrick.TextBlock.new( + font: font, + font_size: 10, + position: {400, 500}, + align: :right + ) + |> Mudbrick.TextBlock.write(""" + aaa + www + WOWOWOWOWOW + + hi\ + """) + end) + |> operations() + end end defp operations(tb) do