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

Enable embedding of OpenType fonts #11

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bd5e5af
Update dependencies to latest
jbowtie Oct 31, 2016
1322805
Fix link to helpful PDF
jbowtie Nov 6, 2016
3958e5e
Add basic TrueType parser and supporting test
jbowtie Nov 20, 2016
04aea66
Add support to register fonts with context and (in theory) write out …
jbowtie Dec 6, 2016
863af16
Use byte_size for correct lengths and offsets
jbowtie Dec 12, 2016
43ceb5a
Tweak serialization for clarity
jbowtie Dec 12, 2016
6207871
Register and embed fonts
jbowtie Dec 12, 2016
36f2ba8
Generate correct glyph widths
jbowtie Dec 24, 2016
101f574
Tweak layout text flow to enable future implementation of GSUB/GPOS f…
jbowtie Dec 24, 2016
c6aa453
Add very rough ligature support
jbowtie Jan 14, 2017
597509c
Start looking at GPOS parsing
jbowtie Jan 27, 2017
a7164e4
Add Noto Italic used to visually inspect ligature output
jbowtie Jan 27, 2017
79b74d4
Move OpenType fonts into a GenServer
jbowtie Feb 8, 2017
4ea84a8
Eliminate compiler warnings
jbowtie Feb 8, 2017
70e2918
Partial cleanup of some hard-coded bits; more GSUB formats
jbowtie Feb 8, 2017
b9c814d
Cleanup and improve existing GSUB/GPOS lookups
jbowtie Feb 9, 2017
be6b3f5
Move write_positioned_glyphs into correct module
jbowtie Feb 10, 2017
e4824d1
Implement chaining context substitution
jbowtie Feb 10, 2017
536617f
Bump depedencies
jbowtie Feb 10, 2017
4c0b3a6
Build correct ToUnicode map to enable basic copy/paste
jbowtie Feb 11, 2017
5ebbd27
Better test names
jbowtie Feb 11, 2017
755e433
Embed the whole OTF font instead of just the CFF table
jbowtie Feb 11, 2017
0fe96a7
Capture familyClass from OS/2 table
jbowtie Feb 11, 2017
39a74ab
Implement class-based kerning
jbowtie Feb 12, 2017
c936e7e
Fix parsing and application of kerning
jbowtie Feb 13, 2017
62d5e8c
Update Travis config to use OTP 17.4 or later
jbowtie Feb 13, 2017
4c9154a
Do better and specify elixir language release (which will select appr…
jbowtie Feb 13, 2017
400a8fc
Use sFamilyClass, change embed subtype
jbowtie Feb 14, 2017
4f6c089
Only parse one cmap and one set of names
jbowtie Feb 16, 2017
b4a0db9
Make things fast enough to handle a CJK font
jbowtie Feb 16, 2017
b7191f3
Use actual leading value instead of hardcoded test value
jbowtie Feb 16, 2017
b197c10
Implement GPOS type 1 (single glyph adjustment)
jbowtie Feb 17, 2017
0ea1d4e
Allow script and lang to be passed in with sensible fallback policy
jbowtie Feb 17, 2017
e9e1721
Add API to enable/disable individual OpenType features
jbowtie Feb 18, 2017
162319f
Add support for parsing GDEF and handling mark positioning
jbowtie Feb 27, 2017
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
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: elixir
otp_release:
- 17.0
elixir:
- 1.4
env:
- MIX_ENV=test
73 changes: 69 additions & 4 deletions lib/gutenex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule Gutenex do
alias Gutenex.PDF
alias Gutenex.PDF.Context
alias Gutenex.PDF.Text
alias Gutenex.PDF.OpenTypeFont
alias Gutenex.PDF.Font

alias Gutenex.Geometry
Expand Down Expand Up @@ -99,6 +100,22 @@ defmodule Gutenex do
pid
end

@doc """
Activate an OpenType feature
"""
def activate_feature(pid, tag) do
GenServer.cast(pid, {:text, :add_feature, tag})
pid
end

@doc """
Deactivate an OpenType feature
"""
def deactivate_feature(pid, tag) do
GenServer.cast(pid, {:text, :remove_feature, tag})
pid
end

@doc """
Set line space
"""
Expand Down Expand Up @@ -135,6 +152,14 @@ defmodule Gutenex do
pid
end

@doc """
Register a font for embedding
"""
def register_font(pid, font_name, font_data) do
GenServer.cast(pid, {:font, :register, {font_name, font_data}})
pid
end

@doc """
Gets the current stream
"""
Expand Down Expand Up @@ -302,23 +327,40 @@ defmodule Gutenex do
Write some text!
"""
def handle_cast({:text, :write, text_to_write}, [context, stream]) do
stream = stream <> Text.write_text(text_to_write)
stream = if is_pid(context.current_font) do
output = OpenTypeFont.layout(context.current_font, text_to_write, context.features)
|>Text.write_positioned_glyphs(context.current_font_size)
stream <> output
else
stream <> Text.write_text(text_to_write)
end

{:noreply, [context, stream]}
end

@doc """
Write some text more break line!
"""
def handle_cast({:text, :write_br, text_to_write}, [context, stream]) do
stream = stream <> Text.write_text_br(text_to_write)
new_y = context.current_text_y - context.current_leading
stream = if is_pid(context.current_font) do
output = OpenTypeFont.layout(context.current_font, text_to_write, context.features)
|>Text.write_positioned_glyphs(context.current_font_size)
stream <> output <> " 1 0 0 1 #{context.current_text_x} #{new_y} Tm\n"
else
stream <> Text.write_text_br(text_to_write)
end
context = %Context{context | current_text_y: new_y}
{:noreply, [context, stream]}
end


@doc """
Set line space
"""
def handle_cast({:text, :line_spacing, size}, [context, stream]) do
stream = stream <> Text.line_spacing(size)
context = %Context {context | current_leading: size}
{:noreply, [context, stream]}
end

Expand All @@ -327,6 +369,7 @@ defmodule Gutenex do
"""
def handle_cast({:text, :position, {x_coordinate, y_coordinate}}, [context, stream]) do
stream = stream <> Text.text_position(x_coordinate, y_coordinate)
context = %Context{context | current_text_x: x_coordinate, current_text_y: y_coordinate}
{:noreply, [context, stream]}
end

Expand Down Expand Up @@ -376,17 +419,39 @@ defmodule Gutenex do
"""
def handle_cast({:font, :set, {font_name, font_size}}, [context, stream]) do
stream = stream <> Font.set_font(context.fonts, font_name, font_size)
{:noreply, [context, stream]}
new_context = Context.set_current_font(context, font_name, font_size)
{:noreply, [new_context, stream]}
end

@doc """
Set the font
"""
def handle_cast({:font, :set, font_name}, [context, stream]) do
stream = stream <> Font.set_font(context.fonts, font_name)
{:noreply, [context, stream]}
new_context = Context.set_current_font(context, font_name)
{:noreply, [new_context, stream]}
end

@doc """
Register a font for usage
"""
def handle_cast({:font, :register, {font_name, font_data}}, [context, stream]) do
new_context = Gutenex.PDF.Context.register_font(context, font_name, font_data)
{:noreply, [new_context, stream]}
end

def handle_cast({:text, :add_feature, tag}, [context, stream]) do
new_context = %Context {context | features: [tag | context.features]}
{:noreply, [new_context, stream]}
end

def handle_cast({:text, :remove_feature, tag}, [context, stream]) do
features = context.features |> Enum.filter(fn t -> t != tag end)
new_context = %Context {context | features: features}
{:noreply, [new_context, stream]}
end


#####################################
# Geometry #
#####################################
Expand Down
189 changes: 189 additions & 0 deletions lib/gutenex/pdf/builders/font_builder.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Gutenex.PDF.Builders.FontBuilder do
alias Gutenex.PDF.Context
alias Gutenex.PDF.RenderContext
alias Gutenex.PDF.OpenTypeFont

# Builds each font object, adding the font objects and references to the
# render context. Returns {render_context, context}
Expand All @@ -16,6 +17,88 @@ defmodule Gutenex.PDF.Builders.FontBuilder do
}
end

# This handles embedding a Type0 composite font per the 1.7 spec
defp build_fonts(%RenderContext{}=render_context, [{font_alias, pid } | fonts]) when is_pid(pid) do
ttf = OpenTypeFont.font_structure(pid)
# add stream, add descriptor, add descfont, add tounicodemap, add font
# font = {:dict, font_definition}
fo = RenderContext.current_object(render_context)
fr = RenderContext.current_reference(render_context)
# CIDFont = {:dict, CID}
cidc = RenderContext.next_index(render_context)
cido = RenderContext.current_object(cidc)
cidr = RenderContext.current_reference(cidc)
# descriptor = {:dict, desc}
dec = RenderContext.next_index(cidc)
deo = RenderContext.current_object(dec)
der = RenderContext.current_reference(dec)
# embedded = {:stream, CFF}
ec = RenderContext.next_index(dec)
eo = RenderContext.current_object(ec)
er = RenderContext.current_reference(ec)
# CMap = {:stream, details}
cmapc = RenderContext.next_index(ec)
cmapo = RenderContext.current_object(cmapc)
cmapr = RenderContext.current_reference(cmapc)
# set up info
base_font = %{
"Type" => {:name, "Font"},
"Encoding" => {:name, "Identity-H"},
"Subtype" => {:name, "Type0"},
"BaseFont" => {:name, ttf.name},
"DescendantFonts" => {:array, [cidr]},
"ToUnicode" => cmapr
}
subtype = if ttf.isCFF, do: "CIDFontType0", else: "CIDFontType2"
cid_font = %{
"Type" => {:name, "Font"},
"Subtype" => {:name, subtype},
"BaseFont" => {:name, ttf.name},
"CIDSystemInfo" => {:dict, %{"Ordering" => "Identity", "Registry" => "Adobe", "Supplement" => 0} },
"FontDescriptor" => der,
"DW" => ttf.defaultWidth,
"W" => glyph_widths(ttf)
}
ffkey = if ttf.isCFF, do: "FontFile3", else: "FontFile2"
metrics = %{
"Type" => {:name, "FontDescriptor"},
"FontName" => {:name, ttf.name},
"FontWeight" => ttf.usWeightClass,
"Flags" => ttf.flags,
"FontBBox" => {:rect, ttf.bbox},
"ItalicAngle" => ttf.italicAngle,
"Ascent" => ttf.ascent,
"Descent" => ttf.descent,
"CapHeight" => ttf.capHeight,
"StemV" => ttf.stemV,
ffkey => er
}

z = :zlib.open()
:zlib.deflateInit(z)
zout = :zlib.deflate(z, ttf.embed)
compressed = IO.iodata_to_binary(zout)
:zlib.close(z)

#embed_subtype = "CIDFontType0C"
embed_subtype = "OpenType"
embed_bytes = {:stream, {:dict, %{"Subtype" => {:name, embed_subtype}, "Length" => byte_size(compressed), "Filter" => {:name, "FlateDecode"}}}, compressed}

rc = %RenderContext{RenderContext.next_index(cmapc) |
font_aliases: Map.put(ec.font_aliases, font_alias, fr),
font_objects: [
{ fo, {:dict, base_font} },
{ cido, {:dict, cid_font} },
{ deo, {:dict, metrics} },
{ eo, embed_bytes },
{ cmapo, identity_tounicode_cmap(ttf) }
| ec.font_objects
]
}
build_fonts(rc, fonts)
end


defp build_fonts(%RenderContext{}=render_context, [{font_alias, font_definition} | fonts]) do
render_context = %RenderContext{
RenderContext.next_index(render_context) |
Expand All @@ -40,4 +123,110 @@ defmodule Gutenex.PDF.Builders.FontBuilder do
]
end

defp glyph_widths(ttf) do
{b, _, :end} = ttf.glyphWidths ++ [:end]
|> Enum.with_index
|> Enum.reduce({[], 0, 0}, fn({w, gid}, {buckets, lg, lw}) -> bucketWidth(gid, w, buckets, lg, lw) end)
cw = b |> Enum.reduce([], fn(x, acc) -> fmtb(x, acc) end)
{:array, cw}
end

def fmtb({s,w}, output) do
[s, {:array, w}] ++ output
end
def fmtb({s,e,w}, output) do
[s,e,w] ++ output
end

def bucketWidth(gid, width, [], 0, _) do
{[], gid, width}
end
def bucketWidth(gid, width, [], 1, width) do
{[{1, 1, width}], gid, width}
end
def bucketWidth(gid, width, [], 1, lw) do
{[{1, [lw]}], gid, width}
end
def bucketWidth(gid, width, [{start, widths} | tail], lastgid, width) do
{[{lastgid, lastgid, width} | [{start, widths} | tail]], gid, width}
end
def bucketWidth(gid, width, [{start, widths} | tail], _lastgid, lastWidth) do
{[{start, widths ++ [lastWidth]} | tail], gid, width}
end
def bucketWidth(gid, width, [{s, _, w} | tail], lastgid, w) do
{[{s, lastgid, w} | tail], gid, width}
end
def bucketWidth(gid, width, [{s, e, w} | tail], lastgid, width) do
{[{lastgid, lastgid, width} | [{s, e, w} | tail]], gid, width}
end
def bucketWidth(gid, width, [{s, e, w} | tail], lastgid, lw) do
{[{lastgid, [lw]} | [{s, e, w} | tail]], gid, width}
end

def addR(n, []) do
[[n]]
end
def addR(n, [range | ranges]) do
if hd(range) + 1 == n do
[[n | range] | ranges]
else
[[n], range | ranges]
end
end

defp hexify(g), do: Integer.to_string(g, 16) |> String.pad_leading(4, "0")
defp identity_tounicode_cmap(ttf) do
keys = Map.keys(ttf.gid2cid)
|> Enum.sort
|> Enum.reduce([], &addR/2)
|> Enum.reverse
ranges = keys
|> Stream.filter(fn x -> length(x) > 1 end)
|> Stream.map(fn x -> {List.last(x), List.first(x)} end)
|> Enum.map(fn {first, last} ->
cids = first..last
|> Stream.map(fn n -> Map.get(ttf.gid2cid, n) |> hexify end)
|> Enum.map_join(" ", fn s -> "<#{s}>" end)
"<#{hexify(first)}> <#{hexify(last)}> [#{cids}]\n"
end)
singles = keys
|> Stream.filter(fn x -> length(x) == 1 end)
|> Stream.map(&hd/1)
|> Stream.map(fn x -> {hexify(x), Map.get(ttf.gid2cid, x) |> hexify} end)
|> Enum.map(fn {k, s} -> "<#{k}> <#{s}>\n" end)
charblock = if length(singles) > 0 do
"""
#{length(singles)} beginbfchar
#{Enum.join(singles)}
endbfchar
"""
else
""
end
{:stream, """
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (Adobe)
/Ordering (UCS)
/Supplement 0
>> def
/CMapName /Adobe−Identity−UCS def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
#{length(ranges)} beginbfrange
#{Enum.join(ranges)}
endbfrange
#{charblock}
endcmap
CMapName currentdict /CMap defineresource pop
end
end
"""}
end
end


34 changes: 34 additions & 0 deletions lib/gutenex/pdf/context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,40 @@ defmodule Gutenex.PDF.Context do
scripts: [],
convert_mode: "utf8_to_latin2",
current_page: 1,
current_font: %{},
current_font_size: 10,
current_leading: 12,
current_text_x: 0,
current_text_y: 0,
features: [
"ccmp", "locl", # preprocess (compose/decompose, local forms)
#"mark", "mkmk", # marks (mark-to-base, mark-to-mark)
"clig", "liga", "rlig", # ligatures (contextual, standard, required)
"calt", "rclt", # contextual alts (standard, required)
"kern", # the "palt" feature will enable automatically if "kern" is on
#"opbd", "lfbd", "rtbd", # optical bounds -- requires app support to identify bounding glyphs?
"curs", # cursive (required? for Arabic, useful for cursive latin)
],
media_box: Page.page_size(:letter),
generation_number: 0)

def register_font(context, font_alias, font_def) do
%Gutenex.PDF.Context{
context |
fonts: Map.put(context.fonts, font_alias, font_def)
}
end
def set_current_font(context, font_alias) do
%Gutenex.PDF.Context{
context |
current_font: Map.get(context.fonts, font_alias)
}
end
def set_current_font(context, font_alias, font_size) do
%Gutenex.PDF.Context{
context |
current_font: Map.get(context.fonts, font_alias),
current_font_size: font_size
}
end
end
Loading