defmodule Pdf.Text do @moduledoc false def escape(parts) when is_list(parts) do Enum.map(parts, &escape/1) end def escape(string) when is_binary(string) do string |> String.replace("(", "\\(") |> String.replace(")", "\\)") end def escape(other), do: other def chunk_text(string, font, font_size, opts \\ []) do zero_width_space = "\u200B" soft_hyphen = "\u00AD" hyphen = "-" whitespace = "\s\t#{zero_width_space}\n" break_chars = " \t\n\v\r#{zero_width_space}#{soft_hyphen}#{hyphen}" [ "([^#{break_chars}]+)(#{soft_hyphen})", "([^#{break_chars}]+#{hyphen}+)", "([^#{break_chars}]+)", "([#{whitespace}])", "(#{hyphen}+[^#{break_chars}]*)" ] |> Enum.join("|") |> Regex.compile!("u") |> Regex.scan(string, capture: :all_but_first) |> Enum.flat_map(& &1) |> Enum.reject(&(&1 == "")) |> Enum.map(&{&1, Pdf.Font.text_width(font, &1, font_size, opts), opts}) end def chunk_attributed_text(attributed_text, opts) do attributed_text |> Enum.flat_map(fn {text, _width, text_opts} -> chunk_text( text, Keyword.get(text_opts, :font).module, Keyword.get(text_opts, :font_size), Keyword.merge(opts, text_opts) ) end) end def wrap_chunks(chunks, width) do fit_chunks(chunks, width) end defp fit_chunks(chunks, wrap_width, acc_width \\ 0, acc \\ []) defp fit_chunks([{"\n", width, opts} | tail], _wrap_width, _acc_width, acc) do {Enum.reverse(remove_wrapped_whitespace(acc)), [{"", width, opts} | remove_wrapped_whitespace(tail)]} end defp fit_chunks([{_, width, _} = chunk | tail], wrap_width, acc_width, [ {"\u00AD", hyphen_width, _} | acc ]) when width + acc_width - hyphen_width <= wrap_width do fit_chunks(tail, wrap_width, acc_width + width, [chunk | acc]) end defp fit_chunks([{_, width, _} = chunk | tail], wrap_width, acc_width, acc) when width + acc_width <= wrap_width do fit_chunks(tail, wrap_width, acc_width + width, [chunk | acc]) end defp fit_chunks(chunks, _wrap_width, _acc_width, acc) do {Enum.reverse(remove_wrapped_whitespace(acc)), remove_wrapped_whitespace(chunks)} end defp remove_wrapped_whitespace([{" ", _, _} | tail]), do: remove_wrapped_whitespace(tail) defp remove_wrapped_whitespace([{"\u200B", _, _} | tail]), do: remove_wrapped_whitespace(tail) defp remove_wrapped_whitespace(chunks), do: chunks def wrap(string, width, font, font_size, opts \\ []) do string |> chunk_text(font, font_size, opts) |> wrap_chunks(width) |> Enum.map(fn chunks -> chunks |> Enum.reject(&(elem(&1, 1) == 0.00)) |> Enum.map_join("", &elem(&1, 0)) end) end def wrap_all_chunks(chunks, width) def wrap_all_chunks([], _width), do: [] def wrap_all_chunks(chunks, width) do case wrap_chunks(chunks, width) do {[], [chunk | tail]} -> [{:line, [chunk]} | wrap_all_chunks(tail, width)] {chunks, tail} -> [{:line, chunks} | wrap_all_chunks(tail, width)] end end def normalize_string(string, replace_with \\ :raise) when is_binary(string) do string |> normalize_unicode_characters() |> Pdf.Encoding.WinAnsi.encode(replace_with) end # Only available from OTP 20.0 if Kernel.function_exported?(:unicode, :characters_to_nfc_binary, 1) do defp normalize_unicode_characters(string) do :unicode.characters_to_nfc_binary(string) end else defp normalize_unicode_characters(string), do: string end end