diff --git a/lib/pdf/external_font.ex b/lib/pdf/external_font.ex index 1ccbdf3..614604e 100644 --- a/lib/pdf/external_font.ex +++ b/lib/pdf/external_font.ex @@ -1,5 +1,6 @@ defmodule Pdf.ExternalFont do @moduledoc false + @derive {Inspect, only: [:name, :family_name, :weight, :italic_angle]} defstruct name: nil, font_file: nil, dictionary: nil, @@ -49,8 +50,6 @@ defmodule Pdf.ExternalFont do %__MODULE__{ name: font_metrics.name, - font_file: font_file, - dictionary: font_file_dictionary(part1, part2, part3), full_name: font_metrics.full_name, family_name: font_metrics.family_name, weight: font_metrics.weight, @@ -65,9 +64,11 @@ defmodule Pdf.ExternalFont do fixed_pitch: font_metrics.fixed_pitch, bbox: font_metrics.bbox, widths: widths, - glyph_widths: map_widths(font_metrics), + glyph_widths: Metrics.map_widths(font_metrics), glyphs: font_metrics.glyphs, - kern_pairs: font_metrics.kern_pairs + kern_pairs: font_metrics.kern_pairs, + font_file: font_file, + dictionary: font_file_dictionary(part1, part2, part3) } end @@ -113,23 +114,6 @@ defmodule Pdf.ExternalFont do byte_size(to_iolist(font) |> Enum.join()) end - defp map_widths(font) do - Pdf.Encoding.WinAnsi.characters() - |> Enum.map(fn {_, char, name} -> - width = - case font.glyphs[name] do - nil -> - 0 - - %{width: width} -> - width - end - - {char, width} - end) - |> Map.new() - end - @doc """ Returns the width of the specific character diff --git a/lib/pdf/font.ex b/lib/pdf/font.ex index b2a0128..de59193 100644 --- a/lib/pdf/font.ex +++ b/lib/pdf/font.ex @@ -2,159 +2,28 @@ defmodule Pdf.Font do @moduledoc false import Pdf.Utils - alias Pdf.Font.Metrics + # alias Pdf.Font.Metrics alias Pdf.{Array, Dictionary, Font} - font_metrics = - Path.join(__DIR__, "../../fonts/*.afm") - |> Path.wildcard() - |> Enum.map(fn afm_file -> - afm_file - |> File.stream!() - |> Enum.reduce(%Metrics{}, fn line, metrics -> - Metrics.process_line(String.replace_suffix(line, "\n", ""), metrics) - end) - end) - - font_metrics - |> Enum.each(fn metrics -> - font_module = String.to_atom("Elixir.Pdf.Font.#{String.replace(metrics.name, "-", "")}") - - defmodule font_module do - @moduledoc false - @doc "The name of the font" - def name, do: unquote(metrics.name) - @doc "The full name of the font" - def full_name, do: unquote(metrics.full_name) - @doc "The font family of the font" - def family_name, do: unquote(metrics.family_name) - @doc "The font weight" - def weight, do: unquote(metrics.weight) - @doc "The font italic angle" - def italic_angle, do: unquote(metrics.italic_angle) - @doc "The font encoding" - def encoding, do: unquote(metrics.encoding) - @doc "The first character defined in `widths/0`" - def first_char, do: unquote(metrics.first_char) - @doc "The last character defined in `widths/0`" - def last_char, do: unquote(metrics.last_char) - @doc "The font ascender" - def ascender, do: unquote(metrics.ascender || 0) - @doc "The font descender" - def descender, do: unquote(metrics.descender || 0) - @doc "The font cap height" - def cap_height, do: unquote(metrics.cap_height) - @doc "The font x-height" - def x_height, do: unquote(metrics.x_height) - @doc "The font bbox" - def bbox, do: unquote(Macro.escape(metrics.bbox)) - - {_llx, lly, _urx, ury} = metrics.bbox - - def line_gap, - do: unquote(ury - lly - ((metrics.ascender || 0) + (metrics.descender || 0))) - - @doc """ - Returns the character widths of characters beginning from `first_char/0` - """ - def widths, do: unquote(Metrics.widths(metrics)) - - @doc """ - Returns the width of the specific character - - Examples: - - iex> #{inspect(__MODULE__)}.width("A") - 123 - """ - def width(char_code) - - Pdf.Encoding.WinAnsi.characters() - |> Enum.each(fn {char_code, _, name} -> - case metrics.glyphs[name] do - nil -> - def width(unquote(char_code)), do: 0 - - %{width: width} -> - def width(unquote(char_code)), do: unquote(width) - end - end) - - def kern_text(""), do: [""] - - metrics.kern_pairs - |> Enum.each(fn {first, second, amount} -> - def kern_text(<>) do - [ - <>, - unquote(-amount) | kern_text(<>) - ] - end - end) - - def kern_text(<>) do - [head | tail] = kern_text(<>) - [<> | tail] - end - - def kern_text(<<_::integer>> = char), do: [char] - end - end) - - @doc ~S""" - Returns the font module for the named font - - # Example: - - iex> Pdf.Font.lookup("Helvetica-BoldOblique") - Pdf.Font.HelveticaBoldOblique - """ - def lookup(name, opts \\ []) - - font_metrics - |> Enum.each(fn metrics -> - font_module = String.to_atom("Elixir.Pdf.Font.#{String.replace(metrics.name, "-", "")}") - - if metrics.weight == :bold and metrics.italic_angle == 0 do - def lookup(unquote(metrics.family_name), bold: true), do: unquote(font_module) - - def lookup(unquote(metrics.family_name), bold: true, italic: false), - do: unquote(font_module) - - def lookup(unquote(metrics.family_name), italic: false, bold: true), - do: unquote(font_module) - end - - if metrics.weight == :bold and metrics.italic_angle != 0 do - def lookup(unquote(metrics.family_name), bold: true, italic: true), do: unquote(font_module) - def lookup(unquote(metrics.family_name), italic: true, bold: true), do: unquote(font_module) - end - - if metrics.weight != :bold and metrics.italic_angle == 0 do - def lookup(unquote(metrics.family_name), italic: false, bold: false), - do: unquote(font_module) - - def lookup(unquote(metrics.family_name), bold: false, italic: false), - do: unquote(font_module) - - def lookup(unquote(metrics.family_name), []), - do: unquote(font_module) - end - - if metrics.weight != :bold and metrics.italic_angle != 0 do - def lookup(unquote(metrics.family_name), italic: true), do: unquote(font_module) - - def lookup(unquote(metrics.family_name), bold: false, italic: true), - do: unquote(font_module) - - def lookup(unquote(metrics.family_name), italic: true, bold: false), - do: unquote(font_module) - end - - # def lookup(unquote(metrics.name), []), do: unquote(font_module) - end) - - def lookup(_name, _opts), do: nil + @derive {Inspect, only: [:name, :family_name, :weight, :italic_angle]} + defstruct name: nil, + full_name: nil, + family_name: nil, + weight: nil, + italic_angle: nil, + encoding: nil, + first_char: nil, + last_char: nil, + ascender: nil, + descender: nil, + cap_height: nil, + x_height: nil, + fixed_pitch: nil, + bbox: nil, + widths: nil, + glyph_widths: nil, + glyphs: nil, + kern_pairs: nil def to_dictionary(font, id) do Dictionary.new() @@ -176,12 +45,12 @@ defmodule Pdf.Font do iex> Font.width(font, "A") 123 """ - def width(%Pdf.ExternalFont{} = font, char_code) do - Pdf.ExternalFont.width(font, char_code) + def width(font, <> = str) when is_binary(str) do + width(font, char_code) end def width(font, char_code) do - font.width(char_code) + font.glyph_widths[char_code] || 0 end @doc ~S""" @@ -230,6 +99,23 @@ defmodule Pdf.Font do def kern_text(_font, ""), do: [""] + def kern_text(font, <>) do + font.kern_pairs + |> Enum.find(fn {f, s, _amount} -> f == first && s == second end) + |> case do + {f, _s, amount} -> + [<>, -amount | kern_text(font, <>)] + + nil -> + [head | tail] = kern_text(font, <>) + [<> | tail] + end + end + + def kern_text(_font, <<_::integer>> = char), do: [char] + + def kern_text(_font, ""), do: [""] + def kern_text(%Pdf.ExternalFont{} = font, text) do Pdf.ExternalFont.kern_text(font, text) end @@ -237,4 +123,20 @@ defmodule Pdf.Font do def kern_text(font, text) do font.kern_text(text) end + + @doc """ + Lookup a font by family name and attributes [bold: true, italic: true] + """ + def matches_attributes(font, attrs) do + bold = Keyword.get(attrs, :bold, false) + italic = Keyword.get(attrs, :italic, false) + + cond do + bold && !italic && font.weight == :bold && font.italic_angle == 0 -> true + bold && italic && font.weight == :bold && font.italic_angle != 0 -> true + !bold && !italic && font.weight != :bold && font.italic_angle == 0 -> true + !bold && italic && font.weight != :bold && font.italic_angle != 0 -> true + true -> false + end + end end diff --git a/lib/pdf/font/metrics.ex b/lib/pdf/font/metrics.ex index bac4d0b..60aeceb 100644 --- a/lib/pdf/font/metrics.ex +++ b/lib/pdf/font/metrics.ex @@ -36,6 +36,23 @@ defmodule Pdf.Font.Metrics do end) end + def map_widths(font) do + Pdf.Encoding.WinAnsi.characters() + |> Enum.map(fn {_, char, name} -> + width = + case font.glyphs[name] do + nil -> + 0 + + %{width: width} -> + width + end + + {char, width} + end) + |> Map.new() + end + def process_line(<<"FontName ", data::binary>>, metrics), do: %{metrics | name: data} def process_line(<<"FullName ", data::binary>>, metrics), do: %{metrics | full_name: data} def process_line(<<"FamilyName ", data::binary>>, metrics), do: %{metrics | family_name: data} diff --git a/lib/pdf/fonts.ex b/lib/pdf/fonts.ex index 7b84ade..544e3e1 100644 --- a/lib/pdf/fonts.ex +++ b/lib/pdf/fonts.ex @@ -3,6 +3,7 @@ defmodule Pdf.Fonts do import Pdf.Utils alias Pdf.{Font, ExternalFont, ObjectCollection} + alias Pdf.Font.Metrics defmodule FontReference do @moduledoc false @@ -23,6 +24,49 @@ defmodule Pdf.Fonts do GenServer.call(pid, {:add_external_font, path}) end + font_metrics = + Path.join(__DIR__, "../../fonts/*.afm") + |> Path.wildcard() + |> Enum.map(fn afm_file -> + afm_file + |> File.stream!() + |> Enum.reduce(%Pdf.Font.Metrics{}, fn line, metrics -> + Pdf.Font.Metrics.process_line(String.replace_suffix(line, "\n", ""), metrics) + end) + end) + + @internal_fonts font_metrics + |> Enum.map(fn metrics -> + {metrics.name, + %Pdf.Font{ + name: metrics.name, + full_name: metrics.full_name, + family_name: metrics.family_name, + weight: metrics.weight, + italic_angle: metrics.italic_angle, + encoding: metrics.encoding, + first_char: metrics.first_char, + last_char: metrics.last_char, + ascender: metrics.ascender, + descender: metrics.descender, + cap_height: metrics.cap_height, + x_height: metrics.x_height, + bbox: metrics.bbox, + widths: Metrics.widths(metrics), + glyph_widths: Metrics.map_widths(metrics), + glyphs: metrics.glyphs, + kern_pairs: metrics.kern_pairs + }} + end) + |> Map.new() + def get_internal_font(name, opts \\ []) do + @internal_fonts + |> Enum.map(fn {_, font} -> font end) + |> Enum.find(fn font -> + font.family_name == name && Font.matches_attributes(font, opts) + end) + end + defmodule Server do use GenServer @@ -77,31 +121,22 @@ defmodule Pdf.Fonts do end defp lookup_font(state, name, opts) when is_binary(name) do - case Font.lookup(name, opts) do - nil -> - lookup_font(state, name) + case Pdf.Fonts.get_internal_font(name, opts) do + nil -> lookup_font(state, name) + font -> lookup_font(state, font) + end + end - font_module -> - lookup_font(state, font_module) + defp lookup_font(state, %Font{family_name: family_name}, opts) do + case Pdf.Fonts.get_internal_font(family_name, opts) do + nil -> lookup_font(state, family_name) + font -> lookup_font(state, font) end end defp lookup_font(%{fonts: fonts} = state, %ExternalFont{family_name: family_name}, opts) do - bold = Keyword.get(opts, :bold, false) - italic = Keyword.get(opts, :italic, false) - Enum.find(fonts, fn {_, %{module: font}} -> - if font.family_name == family_name do - cond do - bold && !italic && font.weight == :bold && font.italic_angle == 0 -> true - bold && italic && font.weight == :bold && font.italic_angle != 0 -> true - !bold && !italic && font.weight != :bold && font.italic_angle == 0 -> true - !bold && italic && font.weight != :bold && font.italic_angle != 0 -> true - true -> false - end - else - false - end + font.family_name == family_name && Font.matches_attributes(font, opts) end) |> case do nil -> {state, nil} @@ -109,10 +144,6 @@ defmodule Pdf.Fonts do end end - defp lookup_font(state, font, opts) do - lookup_font(state, font.family_name, opts) - end - defp lookup_font(%{fonts: fonts} = state, name) when is_binary(name) do {state, fonts[name]} end diff --git a/lib/pdf/page.ex b/lib/pdf/page.ex index 92f9d8a..6d78244 100644 --- a/lib/pdf/page.ex +++ b/lib/pdf/page.ex @@ -541,8 +541,8 @@ defmodule Pdf.Page do defp kern_text(text, font, true) do text = - text - |> font.kern_text() + font + |> Font.kern_text(text) |> Text.escape() |> Enum.map(fn str when is_binary(str) -> s(str) diff --git a/test/pdf/font_test.exs b/test/pdf/font_test.exs index b1e65de..df856dd 100644 --- a/test/pdf/font_test.exs +++ b/test/pdf/font_test.exs @@ -1,10 +1,12 @@ defmodule Pdf.FontTest do use ExUnit.Case, async: true + alias Pdf.Font + alias Pdf.Fonts describe "text_width/3" do test "It calculates the width of a line of text" do - font = Pdf.Font.Helvetica + font = Fonts.get_internal_font("Helvetica") assert Font.text_width(font, "VA") == 1334 assert Font.text_width(font, "VA", 10) == 13.34 assert Font.text_width(font, "VA", kerning: true) == 1254 @@ -15,18 +17,11 @@ defmodule Pdf.FontTest do end test "It calculates the width of a blank string" do - font = Pdf.Font.Helvetica + font = Fonts.get_internal_font("Helvetica") assert Font.text_width(font, "") == 0 assert Font.text_width(font, "", 10) == 0 assert Font.text_width(font, "", kerning: true) == 0 assert Font.text_width(font, "", 10, kerning: true) == 0 end end - - describe "lookup" do - assert Pdf.Font.Helvetica == Pdf.Font.lookup("Helvetica") - assert Pdf.Font.HelveticaBold == Pdf.Font.lookup("Helvetica", bold: true) - assert Pdf.Font.HelveticaBoldOblique == Pdf.Font.lookup("Helvetica", bold: true, italic: true) - assert Pdf.Font.HelveticaOblique == Pdf.Font.lookup("Helvetica", bold: false, italic: true) - end end diff --git a/test/pdf/fonts_test.exs b/test/pdf/fonts_test.exs index 61984d9..247ce65 100644 --- a/test/pdf/fonts_test.exs +++ b/test/pdf/fonts_test.exs @@ -8,35 +8,28 @@ defmodule Pdf.FontsTest do test "looking up an internal font by name" do document = Document.new() - assert %Fonts.FontReference{module: Pdf.Font.Helvetica} = + assert %Fonts.FontReference{module: %Pdf.Font{name: "Helvetica"}} = Fonts.get_font(document.fonts, "Helvetica", []) end - test "looking up an internal font by font, bold" do - document = Document.new() - - assert %Fonts.FontReference{module: Pdf.Font.HelveticaBold} = - Fonts.get_font(document.fonts, Pdf.Font.Helvetica, bold: true) - end - test "looking up an internal font by name, bold" do document = Document.new() - assert %Fonts.FontReference{module: Pdf.Font.HelveticaBold} = + assert %Fonts.FontReference{module: %Pdf.Font{name: "Helvetica-Bold"}} = Fonts.get_font(document.fonts, "Helvetica", bold: true) end test "looking up an internal font by name, italic" do document = Document.new() - assert %Fonts.FontReference{module: Pdf.Font.HelveticaOblique} = + assert %Fonts.FontReference{module: %Pdf.Font{name: "Helvetica-Oblique"}} = Fonts.get_font(document.fonts, "Helvetica", italic: true) end test "looking up an internal font by name, bold, italic" do document = Document.new() - assert %Fonts.FontReference{module: Pdf.Font.HelveticaBoldOblique} = + assert %Fonts.FontReference{module: %Pdf.Font{name: "Helvetica-BoldOblique"}} = Fonts.get_font(document.fonts, "Helvetica", italic: true, bold: true) end diff --git a/test/pdf/text_test.exs b/test/pdf/text_test.exs index fb6bade..d2ddedc 100644 --- a/test/pdf/text_test.exs +++ b/test/pdf/text_test.exs @@ -4,7 +4,7 @@ defmodule Pdf.TextTest do alias Pdf.Text setup do - font = Pdf.Font.Helvetica + font = Pdf.Fonts.get_internal_font("Helvetica") font_size = 10 {:ok, font: font, font_size: font_size} end