Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Font refactor #52

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 5 additions & 21 deletions lib/pdf/external_font.ex
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
210 changes: 56 additions & 154 deletions lib/pdf/font.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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(<<unquote(first)::integer, unquote(second)::integer, rest::binary>>) do
[
<<unquote(first)>>,
unquote(-amount) | kern_text(<<unquote(second)::integer, rest::binary>>)
]
end
end)

def kern_text(<<first::integer, second::integer, rest::binary>>) do
[head | tail] = kern_text(<<second::integer, rest::binary>>)
[<<first::integer, head::binary>> | 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()
Expand All @@ -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, <<char_code::integer>> = 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"""
Expand Down Expand Up @@ -230,11 +99,44 @@ defmodule Pdf.Font do

def kern_text(_font, ""), do: [""]

def kern_text(font, <<first::integer, second::integer, rest::binary>>) do
font.kern_pairs
|> Enum.find(fn {f, s, _amount} -> f == first && s == second end)
|> case do
{f, _s, amount} ->
[<<f>>, -amount | kern_text(font, <<second::integer, rest::binary>>)]

nil ->
[head | tail] = kern_text(font, <<second::integer, rest::binary>>)
[<<first::integer, head::binary>> | 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

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
17 changes: 17 additions & 0 deletions lib/pdf/font/metrics.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading
Loading