1534 lines
46 KiB
Odin
1534 lines
46 KiB
Odin
package pdf
|
|||
|
|
|
||
|
|
import "core:mem"
|
||
|
|
import "core:slice"
|
||
|
|
|
||
|
|
// Type 1 font programs (Adobe, "Adobe Type 1 Font Format", 1990).
|
||
|
|
//
|
||
|
|
// Bytes in, outlines out. Nothing here knows what a PDF is; a Type 1 program
|
||
|
|
// is the same file whether it arrived as a /FontFile stream, a .pfb on disk or
|
||
|
|
// a .pfa pasted into a PostScript job, and keeping this file ignorant of the
|
||
|
|
// difference is what makes it testable from byte literals.
|
||
|
|
//
|
||
|
|
// SHAPE OF THE FILE
|
||
|
|
//
|
||
|
|
// %!FontType1-1.0 ... cleartext PostScript: /FontMatrix, /Encoding
|
||
|
|
// currentfile eexec
|
||
|
|
// <encrypted> the Private dict: /lenIV, /Subrs, /CharStrings
|
||
|
|
// 512 zeros, cleartomark trailer
|
||
|
|
//
|
||
|
|
// The encrypted portion is enciphered with the "eexec" stream cipher keyed at
|
||
|
|
// r = 55665, whose first four plaintext bytes are random padding. It arrives
|
||
|
|
// either as raw binary (.pfb, and PDF /FontFile streams) or as hex ASCII
|
||
|
|
// (.pfa); both are accepted, told apart by whether the first four bytes are hex
|
||
|
|
// digits -- which works because Adobe's own encryptor is required to choose
|
||
|
|
// padding that keeps the two forms distinguishable. Each charstring inside is
|
||
|
|
// enciphered again with the same cipher keyed at r = 4330, with /lenIV bytes
|
||
|
|
// (4 unless the font says otherwise) of padding to drop.
|
||
|
|
//
|
||
|
|
// A .pfb wraps the three parts in segment headers -- 0x80, a type byte, then a
|
||
|
|
// little-endian length -- which are stripped and the payloads concatenated
|
||
|
|
// before anything else looks at the bytes.
|
||
|
|
//
|
||
|
|
// WHAT SUBSETTERS DO TO THESE FILES
|
||
|
|
//
|
||
|
|
// Font programs are the most reliably damaged data in a PDF, because every
|
||
|
|
// producer rewrites them. So: a /Subrs or /CharStrings count that disagrees
|
||
|
|
// with the entries that follow is ignored in favour of the entries; a
|
||
|
|
// charstring shorter than /lenIV yields an empty glyph rather than an error; an
|
||
|
|
// eexec block that stops in the middle yields the glyphs decoded up to that
|
||
|
|
// point; and a font with no eexec section at all -- some are shipped with that
|
||
|
|
// layer already stripped -- is read as though its Private dict were the
|
||
|
|
// cleartext it is.
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Limits. Every one of these bounds something an attacker controls.
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// Tokens read from one cleartext or Private section. These are data, not
|
||
|
|
// programs, so the cap only ever fires on garbage -- which is exactly what the
|
||
|
|
// tail of a truncated eexec block decrypts to.
|
||
|
|
@(private)
|
||
|
|
MAX_T1_TOKENS :: 1 << 20
|
||
|
|
// Glyph indices are u16 everywhere in this layer, so the dictionary stops one
|
||
|
|
// short of where the index would stop being representable.
|
||
|
|
@(private)
|
||
|
|
MAX_T1_GLYPHS :: 0xFFFF
|
||
|
|
@(private)
|
||
|
|
MAX_T1_SUBRS :: 1 << 16
|
||
|
|
// The charstring interpreter's bounds. The format allows 24 operands and a
|
||
|
|
// call depth of 10; the margin absorbs the fonts that exceed the spec without
|
||
|
|
// letting a hostile one run away.
|
||
|
|
@(private)
|
||
|
|
T1_STACK_MAX :: 48
|
||
|
|
@(private)
|
||
|
|
T1_PS_MAX :: 32
|
||
|
|
@(private)
|
||
|
|
T1_MAX_CALL_DEPTH :: 30
|
||
|
|
// Charstring bytes executed for one glyph, subroutine calls included.
|
||
|
|
@(private)
|
||
|
|
T1_MAX_FUEL :: 1 << 17
|
||
|
|
// Points a FLEX collects: a reference point plus the six that make two curves.
|
||
|
|
@(private)
|
||
|
|
T1_FLEX_POINTS :: 7
|
||
|
|
|
||
|
|
// The two cipher keys, and the constants they share (Type 1 spec, 7.3).
|
||
|
|
@(private)
|
||
|
|
T1_EEXEC_KEY :: 55665
|
||
|
|
@(private)
|
||
|
|
T1_CHARSTRING_KEY :: 4330
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The parsed font
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// T1_Name_Entry is one glyph name and the glyph index it belongs to, in a table
|
||
|
|
// sorted by name so that a lookup is a binary search. A simple Type 1 font is
|
||
|
|
// addressed entirely by name, so this is the hot path.
|
||
|
|
@(private)
|
||
|
|
T1_Name_Entry :: struct {
|
||
|
|
name: string,
|
||
|
|
gid: u16,
|
||
|
|
}
|
||
|
|
|
||
|
|
// Type1_Font is the parsed form of a font program. Declared here so that Font can
|
||
|
|
// reference it; the reader that fills it in owns this file and adds the fields
|
||
|
|
// it needs. Nothing in this file may depend on Document or on any PDF type --
|
||
|
|
// a font program is bytes in and outlines out, and stays separately testable.
|
||
|
|
Type1_Font :: struct {
|
||
|
|
// The whole program with any PFB segment headers removed. When the input
|
||
|
|
// was not PFB-wrapped this aliases the caller's bytes rather than copying
|
||
|
|
// them, so `data` passed to `t1_parse` must outlive the font: the built-in
|
||
|
|
// encoding's glyph names point into it.
|
||
|
|
data: []byte,
|
||
|
|
// The decrypted Private dictionary, always a buffer of this font's own.
|
||
|
|
// Glyph names, charstrings and subroutines point into it.
|
||
|
|
priv: []byte,
|
||
|
|
|
||
|
|
// /FontName, for diagnostics only.
|
||
|
|
name: string,
|
||
|
|
// /FontMatrix, defaulting to the 1000-units-per-em [0.001 0 0 0.001 0 0].
|
||
|
|
mtx: Matrix,
|
||
|
|
|
||
|
|
// The program's built-in encoding: 256 glyph names, "" where the font
|
||
|
|
// leaves a code undefined. Meaningful only when `has_encoding` is set.
|
||
|
|
encoding: [256]string,
|
||
|
|
has_encoding: bool,
|
||
|
|
|
||
|
|
// /lenIV: bytes of padding at the head of every decrypted charstring.
|
||
|
|
// Negative means the charstrings are not enciphered at all.
|
||
|
|
len_iv: int,
|
||
|
|
|
||
|
|
subrs: [][]byte,
|
||
|
|
names: []string,
|
||
|
|
chars: [][]byte,
|
||
|
|
// `names` sorted, for t1_gid_for_name.
|
||
|
|
by_name: []T1_Name_Entry,
|
||
|
|
|
||
|
|
// Advances are memoised: laying out a page asks for the same handful of
|
||
|
|
// glyphs thousands of times, and each answer costs a charstring run.
|
||
|
|
advances: []f64,
|
||
|
|
adv_known: []bool,
|
||
|
|
|
||
|
|
// Set when the eexec block or a charstring ran off the end of the input.
|
||
|
|
// The font is still usable; it simply holds fewer glyphs than it claims.
|
||
|
|
truncated: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Parsing
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
// t1_parse reads a Type 1 font program: PFB-wrapped, raw binary or PFA.
|
||
|
|
//
|
||
|
|
// The returned font and everything it points at are allocated with `allocator`
|
||
|
|
// and belong to the caller; nothing is freed individually. `data` is referenced,
|
||
|
|
// not copied, so it must outlive the font.
|
||
|
|
//
|
||
|
|
// An error comes back only when there is genuinely nothing to hand over -- no
|
||
|
|
// charstring could be read at all. A font that is damaged but still holds one
|
||
|
|
// glyph parses successfully, because a page that draws that glyph should draw
|
||
|
|
// it.
|
||
|
|
@(private)
|
||
|
|
t1_parse :: proc(data: []byte, allocator := context.allocator) -> (^Type1_Font, Error) {
|
||
|
|
if len(data) == 0 {
|
||
|
|
return nil, .Bad_Type1
|
||
|
|
}
|
||
|
|
|
||
|
|
f := new(Type1_Font, allocator)
|
||
|
|
if f == nil {
|
||
|
|
return nil, .Out_Of_Memory
|
||
|
|
}
|
||
|
|
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
|
||
|
|
f.len_iv = 4
|
||
|
|
|
||
|
|
raw, raw_owned := t1_unwrap_pfb(data, allocator)
|
||
|
|
f.data = raw
|
||
|
|
|
||
|
|
// The cleartext is everything before `eexec`. For a font that has none,
|
||
|
|
// stop at the first thing that introduces binary data instead, so the
|
||
|
|
// tokeniser never walks over charstring bytes.
|
||
|
|
clear_end := len(raw)
|
||
|
|
eexec_at := t1_find_token(raw, "eexec")
|
||
|
|
if eexec_at >= 0 {
|
||
|
|
clear_end = eexec_at
|
||
|
|
}
|
||
|
|
if p := t1_find_token(raw[:clear_end], "/CharStrings"); p >= 0 {
|
||
|
|
clear_end = p
|
||
|
|
}
|
||
|
|
if p := t1_find_token(raw[:clear_end], "/Subrs"); p >= 0 {
|
||
|
|
clear_end = p
|
||
|
|
}
|
||
|
|
t1_parse_cleartext(f, raw[:clear_end])
|
||
|
|
|
||
|
|
// The encrypted region: everything after `eexec` and the whitespace that
|
||
|
|
// follows it. Adobe requires the first byte of the ciphertext not to be
|
||
|
|
// whitespace precisely so that skipping it is unambiguous (Type 1 spec,
|
||
|
|
// 7.2).
|
||
|
|
body := raw
|
||
|
|
encrypted := false
|
||
|
|
if eexec_at >= 0 {
|
||
|
|
i := eexec_at + len("eexec")
|
||
|
|
for i < len(raw) && t1_is_space(raw[i]) {
|
||
|
|
i += 1
|
||
|
|
}
|
||
|
|
body = raw[i:]
|
||
|
|
encrypted = true
|
||
|
|
}
|
||
|
|
// A font whose eexec layer has already been stripped still says `eexec`.
|
||
|
|
// Twelve plaintext bytes surviving encipherment is not a thing that
|
||
|
|
// happens, so finding the token means the region is not enciphered.
|
||
|
|
if encrypted && t1_find_token(body, "/CharStrings") >= 0 {
|
||
|
|
encrypted = false
|
||
|
|
}
|
||
|
|
|
||
|
|
// `f.priv` is always the whole allocation, so it can be handed back to the
|
||
|
|
// allocator; `priv_body` is the part after the four bytes of random
|
||
|
|
// padding at the head of the eexec plaintext.
|
||
|
|
priv_body: []byte
|
||
|
|
switch {
|
||
|
|
case encrypted && t1_is_hex_eexec(body):
|
||
|
|
buf := make([]byte, len(body) / 2 + 1, allocator)
|
||
|
|
n := t1_hex_decode(buf, body)
|
||
|
|
t1_decipher(buf[:n], T1_EEXEC_KEY)
|
||
|
|
f.priv = buf
|
||
|
|
priv_body = n >= 4 ? buf[4:n] : buf[n:n]
|
||
|
|
case encrypted:
|
||
|
|
f.priv = t1_copy(body, allocator)
|
||
|
|
t1_decipher(f.priv, T1_EEXEC_KEY)
|
||
|
|
priv_body = len(f.priv) >= 4 ? f.priv[4:] : f.priv[len(f.priv):]
|
||
|
|
case:
|
||
|
|
f.priv = t1_copy(body, allocator)
|
||
|
|
priv_body = f.priv
|
||
|
|
}
|
||
|
|
|
||
|
|
ctx := T1_Parse_Ctx{
|
||
|
|
f = f,
|
||
|
|
subrs = make([dynamic][]byte, allocator),
|
||
|
|
names = make([dynamic]string, allocator),
|
||
|
|
chars = make([dynamic][]byte, allocator),
|
||
|
|
}
|
||
|
|
t1_parse_private(&ctx, priv_body)
|
||
|
|
|
||
|
|
if len(ctx.chars) == 0 {
|
||
|
|
delete(ctx.subrs)
|
||
|
|
delete(ctx.names)
|
||
|
|
delete(ctx.chars)
|
||
|
|
delete(f.priv, allocator)
|
||
|
|
if raw_owned {
|
||
|
|
delete(raw, allocator)
|
||
|
|
}
|
||
|
|
free(f, allocator)
|
||
|
|
return nil, .Bad_Type1
|
||
|
|
}
|
||
|
|
|
||
|
|
f.subrs = ctx.subrs[:]
|
||
|
|
f.names = ctx.names[:]
|
||
|
|
f.chars = ctx.chars[:]
|
||
|
|
|
||
|
|
n := len(f.names)
|
||
|
|
f.by_name = make([]T1_Name_Entry, n, allocator)
|
||
|
|
for i in 0 ..< n {
|
||
|
|
f.by_name[i] = T1_Name_Entry{name = f.names[i], gid = u16(i)}
|
||
|
|
}
|
||
|
|
slice.sort_by(f.by_name, proc(a, b: T1_Name_Entry) -> bool {
|
||
|
|
return a.name < b.name
|
||
|
|
})
|
||
|
|
|
||
|
|
f.advances = make([]f64, n, allocator)
|
||
|
|
f.adv_known = make([]bool, n, allocator)
|
||
|
|
|
||
|
|
return f, .None
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_glyph_count is the number of charstrings the font holds. Glyph indices
|
||
|
|
// are positions in the /CharStrings dictionary, in the order it was written.
|
||
|
|
@(private)
|
||
|
|
t1_glyph_count :: proc(f: ^Type1_Font) -> int {
|
||
|
|
if f == nil {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
return len(f.chars)
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_font_matrix maps glyph space to text space. Outlines from t1_glyph_path
|
||
|
|
// are in glyph space and have not been through it.
|
||
|
|
@(private)
|
||
|
|
t1_font_matrix :: proc(f: ^Type1_Font) -> Matrix {
|
||
|
|
if f == nil {
|
||
|
|
return Matrix{0.001, 0, 0, 0.001, 0, 0}
|
||
|
|
}
|
||
|
|
return f.mtx
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_builtin_encoding is the font program's own encoding, or nil when it
|
||
|
|
// declares none -- which the caller must read as "use StandardEncoding",
|
||
|
|
// not as "this font has no glyphs at those codes".
|
||
|
|
@(private)
|
||
|
|
t1_builtin_encoding :: proc(f: ^Type1_Font) -> ^[256]string {
|
||
|
|
if f == nil || !f.has_encoding {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return &f.encoding
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_gid_for_name resolves a glyph name against the /CharStrings dictionary.
|
||
|
|
@(private)
|
||
|
|
t1_gid_for_name :: proc(f: ^Type1_Font, name: string) -> (u16, bool) {
|
||
|
|
if f == nil || len(name) == 0 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
lo, hi := 0, len(f.by_name)
|
||
|
|
for lo < hi {
|
||
|
|
mid := lo + (hi - lo) / 2
|
||
|
|
if f.by_name[mid].name < name {
|
||
|
|
lo = mid + 1
|
||
|
|
} else {
|
||
|
|
hi = mid
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if lo < len(f.by_name) && f.by_name[lo].name == name {
|
||
|
|
return f.by_name[lo].gid, true
|
||
|
|
}
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_glyph_advance is the width `hsbw` or `sbw` declares, in glyph space.
|
||
|
|
// Zero for a glyph that declares none, which is what a viewer advances by.
|
||
|
|
@(private)
|
||
|
|
t1_glyph_advance :: proc(f: ^Type1_Font, gid: u16) -> f64 {
|
||
|
|
if f == nil || int(gid) >= len(f.chars) {
|
||
|
|
return 0
|
||
|
|
}
|
||
|
|
i := int(gid)
|
||
|
|
if i < len(f.adv_known) && f.adv_known[i] {
|
||
|
|
return f.advances[i]
|
||
|
|
}
|
||
|
|
|
||
|
|
ip := T1_Interp{width_only = true}
|
||
|
|
t1_run(f, i, nil, Point{0, 0}, &ip)
|
||
|
|
w := ip.have_width ? ip.width : 0
|
||
|
|
if !is_finite(w) {
|
||
|
|
w = 0
|
||
|
|
}
|
||
|
|
if i < len(f.adv_known) {
|
||
|
|
f.advances[i] = w
|
||
|
|
f.adv_known[i] = true
|
||
|
|
}
|
||
|
|
return w
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_glyph_path appends one glyph's outline, in glyph space, to `out`.
|
||
|
|
//
|
||
|
|
// A glyph that cannot be interpreted appends whatever it managed to draw and
|
||
|
|
// still returns .None: a broken glyph is a blank, never a failed page. Only an
|
||
|
|
// index that names no glyph is an error.
|
||
|
|
@(private)
|
||
|
|
t1_glyph_path :: proc(f: ^Type1_Font, gid: u16, out: ^Path) -> Error {
|
||
|
|
if f == nil || out == nil {
|
||
|
|
return .Invalid_Argument
|
||
|
|
}
|
||
|
|
if int(gid) >= len(f.chars) {
|
||
|
|
return .No_Glyph
|
||
|
|
}
|
||
|
|
|
||
|
|
ip := T1_Interp{allow_seac = true}
|
||
|
|
t1_run(f, int(gid), out, Point{0, 0}, &ip)
|
||
|
|
if !ip.seac {
|
||
|
|
return .None
|
||
|
|
}
|
||
|
|
|
||
|
|
// seac composes an accented glyph out of two others named by their
|
||
|
|
// StandardEncoding codes -- the one place a font program refers to an
|
||
|
|
// encoding it does not carry. The table is font_standard.odin's, which is
|
||
|
|
// the same table the format's Annex D defines.
|
||
|
|
asb := ip.seac_args[0]
|
||
|
|
adx := ip.seac_args[1]
|
||
|
|
ady := ip.seac_args[2]
|
||
|
|
if !is_finite(asb) || !is_finite(adx) || !is_finite(ady) {
|
||
|
|
return .None
|
||
|
|
}
|
||
|
|
bcode := int(ip.seac_args[3])
|
||
|
|
acode := int(ip.seac_args[4])
|
||
|
|
std := encoding_standard()
|
||
|
|
|
||
|
|
if bcode >= 0 && bcode < 256 {
|
||
|
|
if bgid, ok := t1_gid_for_name(f, std[bcode]); ok {
|
||
|
|
base := T1_Interp{}
|
||
|
|
t1_run(f, int(bgid), out, Point{0, 0}, &base)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if acode >= 0 && acode < 256 {
|
||
|
|
if agid, ok := t1_gid_for_name(f, std[acode]); ok {
|
||
|
|
// adx/ady position the accent's left sidebearing point relative to
|
||
|
|
// the COMPOSITE's left sidebearing point, not its origin -- so the
|
||
|
|
// composite's own sidebearing, which `ip.sbx` holds, is part of the
|
||
|
|
// offset, and `asb` (the accent's sidebearing, which the accent's
|
||
|
|
// own hsbw is about to apply again) is taken back out.
|
||
|
|
//
|
||
|
|
// Getting this wrong is invisible in the many fonts whose accented
|
||
|
|
// glyphs have a zero sidebearing and glaring in the ones that do
|
||
|
|
// not: dropping `ip.sbx` moves the dieresis off the I by the width
|
||
|
|
// of the sidebearing in 104 of the 448 seac composites in the
|
||
|
|
// Bitstream and URW faces this was checked against.
|
||
|
|
acc := T1_Interp{}
|
||
|
|
t1_run(f, int(agid), out, Point{ip.sbx + adx - asb, ady}, &acc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return .None
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// Container: PFB segments, eexec, and the two ciphers
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_copy :: proc(src: []byte, allocator: mem.Allocator) -> []byte {
|
||
|
|
if len(src) == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
out := make([]byte, len(src), allocator)
|
||
|
|
if len(out) != len(src) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
copy(out, src)
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_unwrap_pfb strips .pfb segment headers and concatenates the payloads.
|
||
|
|
// Returns the input untouched, and owned = false, for anything else.
|
||
|
|
@(private = "file")
|
||
|
|
t1_unwrap_pfb :: proc(data: []byte, allocator: mem.Allocator) -> (out: []byte, owned: bool) {
|
||
|
|
if len(data) < 6 || data[0] != 0x80 {
|
||
|
|
return data, false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Two passes so the join is exactly sized: a declared length is a file
|
||
|
|
// value and must not be believed as an allocation size.
|
||
|
|
total := 0
|
||
|
|
for pass in 0 ..< 2 {
|
||
|
|
pos := 0
|
||
|
|
n := 0
|
||
|
|
for pos + 6 <= len(data) {
|
||
|
|
if data[pos] != 0x80 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
kind := data[pos + 1]
|
||
|
|
// 3 is the end-of-file marker; anything else is not a segment.
|
||
|
|
if kind != 1 && kind != 2 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
seg := int(u32(data[pos + 2]) |
|
||
|
|
u32(data[pos + 3]) << 8 |
|
||
|
|
u32(data[pos + 4]) << 16 |
|
||
|
|
u32(data[pos + 5]) << 24)
|
||
|
|
pos += 6
|
||
|
|
if seg > len(data) - pos {
|
||
|
|
seg = len(data) - pos
|
||
|
|
}
|
||
|
|
if seg <= 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if pass == 0 {
|
||
|
|
total += seg
|
||
|
|
} else {
|
||
|
|
copy(out[n:], data[pos:pos + seg])
|
||
|
|
n += seg
|
||
|
|
}
|
||
|
|
pos += seg
|
||
|
|
}
|
||
|
|
if pass == 0 {
|
||
|
|
if total == 0 {
|
||
|
|
return data, false
|
||
|
|
}
|
||
|
|
out = make([]byte, total, allocator)
|
||
|
|
if len(out) != total {
|
||
|
|
return data, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return out, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_decipher runs the eexec cipher over `buf` in place (Type 1 spec, 7.3).
|
||
|
|
@(private = "file")
|
||
|
|
t1_decipher :: proc(buf: []byte, key: u16) {
|
||
|
|
r := u32(key)
|
||
|
|
for i in 0 ..< len(buf) {
|
||
|
|
c := u32(buf[i])
|
||
|
|
buf[i] = u8(c) ~ u8(r >> 8)
|
||
|
|
r = ((c + r) * 52845 + 22719) & 0xFFFF
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_is_hex_eexec tells the .pfa form from the binary one. Adobe requires the
|
||
|
|
// random padding to be chosen so that the first four ciphertext bytes are not
|
||
|
|
// all hex digits, which is what makes this test safe rather than a guess.
|
||
|
|
@(private = "file")
|
||
|
|
t1_is_hex_eexec :: proc(body: []byte) -> bool {
|
||
|
|
if len(body) < 4 {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
for i in 0 ..< 4 {
|
||
|
|
if _, ok := hex_value(body[i]); !ok {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_hex_decode decodes the hex-ASCII eexec form into `dst`, returning how many
|
||
|
|
// bytes it wrote. Decoding stops at the first character that is neither hex nor
|
||
|
|
// whitespace, which in a .pfa is the `cleartomark` trailer.
|
||
|
|
@(private = "file")
|
||
|
|
t1_hex_decode :: proc(dst, src: []byte) -> int {
|
||
|
|
n := 0
|
||
|
|
hi: u8
|
||
|
|
have_hi := false
|
||
|
|
for c in src {
|
||
|
|
if t1_is_space(c) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
v, ok := hex_value(c)
|
||
|
|
if !ok {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if !have_hi {
|
||
|
|
hi = v
|
||
|
|
have_hi = true
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if n >= len(dst) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
dst[n] = hi << 4 | v
|
||
|
|
n += 1
|
||
|
|
have_hi = false
|
||
|
|
}
|
||
|
|
return n
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_decrypt_charstring deciphers one charstring in place and drops its /lenIV
|
||
|
|
// bytes of padding. A charstring shorter than the padding yields an empty
|
||
|
|
// glyph, which draws nothing -- the alternative is a slice out of range.
|
||
|
|
@(private = "file")
|
||
|
|
t1_decrypt_charstring :: proc(buf: []byte, len_iv: int) -> []byte {
|
||
|
|
if len_iv < 0 {
|
||
|
|
return buf
|
||
|
|
}
|
||
|
|
t1_decipher(buf, T1_CHARSTRING_KEY)
|
||
|
|
if len(buf) <= len_iv {
|
||
|
|
return buf[len(buf):]
|
||
|
|
}
|
||
|
|
return buf[len_iv:]
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// A PostScript-shaped tokeniser
|
||
|
|
//
|
||
|
|
// Only enough of the syntax to find the handful of constructs that matter.
|
||
|
|
// Strings and hex strings come back whole so that their contents can never be
|
||
|
|
// mistaken for keywords: a /Notice is allowed to contain the word "eexec".
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
T1_Lexer :: struct {
|
||
|
|
src: []byte,
|
||
|
|
pos: int,
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_is_space :: proc "contextless" (c: byte) -> bool {
|
||
|
|
return c == 0 || c == 9 || c == 10 || c == 12 || c == 13 || c == 32
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_is_special :: proc "contextless" (c: byte) -> bool {
|
||
|
|
switch c {
|
||
|
|
case '(', ')', '<', '>', '[', ']', '{', '}', '/', '%':
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_next :: proc(lx: ^T1_Lexer) -> (tok: []byte, ok: bool) {
|
||
|
|
for {
|
||
|
|
for lx.pos < len(lx.src) && t1_is_space(lx.src[lx.pos]) {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
if lx.pos >= len(lx.src) {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
if lx.src[lx.pos] != '%' {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
for lx.pos < len(lx.src) && lx.src[lx.pos] != '\n' && lx.src[lx.pos] != '\r' {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
start := lx.pos
|
||
|
|
switch lx.src[lx.pos] {
|
||
|
|
case '/':
|
||
|
|
lx.pos += 1
|
||
|
|
for lx.pos < len(lx.src) &&
|
||
|
|
!t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
case '(':
|
||
|
|
depth := 0
|
||
|
|
for lx.pos < len(lx.src) {
|
||
|
|
c := lx.src[lx.pos]
|
||
|
|
lx.pos += 1
|
||
|
|
switch c {
|
||
|
|
case '\\':
|
||
|
|
lx.pos += 1
|
||
|
|
case '(':
|
||
|
|
depth += 1
|
||
|
|
case ')':
|
||
|
|
depth -= 1
|
||
|
|
}
|
||
|
|
if depth == 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if lx.pos > len(lx.src) {
|
||
|
|
lx.pos = len(lx.src)
|
||
|
|
}
|
||
|
|
case '<':
|
||
|
|
lx.pos += 1
|
||
|
|
for lx.pos < len(lx.src) && lx.src[lx.pos] != '>' {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
if lx.pos < len(lx.src) {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
case ')', '>', '[', ']', '{', '}':
|
||
|
|
lx.pos += 1
|
||
|
|
case:
|
||
|
|
for lx.pos < len(lx.src) &&
|
||
|
|
!t1_is_space(lx.src[lx.pos]) && !t1_is_special(lx.src[lx.pos]) {
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if lx.pos == start {
|
||
|
|
// Nothing consumed would stall the caller's loop; a delimiter with no
|
||
|
|
// body is still one byte of progress.
|
||
|
|
lx.pos += 1
|
||
|
|
}
|
||
|
|
return lx.src[start:lx.pos], true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_eq :: proc "contextless" (tok: []byte, s: string) -> bool {
|
||
|
|
return string(tok) == s
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_tok_int reads a plain decimal integer, which is all the constructs here
|
||
|
|
// ever need. Values far past anything meaningful are rejected rather than
|
||
|
|
// wrapped, because every one of them is a length or a count.
|
||
|
|
@(private = "file")
|
||
|
|
t1_tok_int :: proc "contextless" (tok: []byte) -> (int, bool) {
|
||
|
|
if len(tok) == 0 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
i := 0
|
||
|
|
neg := false
|
||
|
|
if tok[0] == '+' || tok[0] == '-' {
|
||
|
|
neg = tok[0] == '-'
|
||
|
|
i = 1
|
||
|
|
}
|
||
|
|
if i >= len(tok) {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
v := 0
|
||
|
|
for ; i < len(tok); i += 1 {
|
||
|
|
c := tok[i]
|
||
|
|
if c < '0' || c > '9' {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
if v > 1 << 30 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
v = v * 10 + int(c - '0')
|
||
|
|
}
|
||
|
|
return neg ? -v : v, true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_next_int :: proc(lx: ^T1_Lexer) -> (int, bool) {
|
||
|
|
tok, ok := t1_next(lx)
|
||
|
|
if !ok {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return t1_tok_int(tok)
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_find_token locates a whitespace-delimited keyword. Delimiting matters:
|
||
|
|
// `/Subrs` is not to be found inside `/SubrsCount`, and a bare substring search
|
||
|
|
// finds `eexec` inside the middle of a word.
|
||
|
|
@(private = "file")
|
||
|
|
t1_find_token :: proc(hay: []byte, needle: string) -> int {
|
||
|
|
if len(needle) == 0 || len(hay) < len(needle) {
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
for i in 0 ..= len(hay) - len(needle) {
|
||
|
|
if hay[i] != needle[0] {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if string(hay[i:i + len(needle)]) != needle {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if i > 0 {
|
||
|
|
p := hay[i - 1]
|
||
|
|
if !t1_is_space(p) && p != ')' && p != ']' && p != '}' && p != '>' {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
j := i + len(needle)
|
||
|
|
if j < len(hay) {
|
||
|
|
c := hay[j]
|
||
|
|
if !t1_is_space(c) && !t1_is_special(c) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return i
|
||
|
|
}
|
||
|
|
return -1
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The cleartext portion
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_parse_cleartext :: proc(f: ^Type1_Font, src: []byte) {
|
||
|
|
lx := T1_Lexer{src = src}
|
||
|
|
for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 {
|
||
|
|
tok, ok := t1_next(&lx)
|
||
|
|
if !ok {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case t1_eq(tok, "/FontMatrix"):
|
||
|
|
t1_read_matrix(f, &lx)
|
||
|
|
case t1_eq(tok, "/Encoding"):
|
||
|
|
t1_read_encoding(f, &lx)
|
||
|
|
case t1_eq(tok, "/FontName"):
|
||
|
|
if name, nok := t1_next(&lx); nok && len(name) > 1 && name[0] == '/' {
|
||
|
|
f.name = string(name[1:])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_read_matrix reads `[a b c d e f]`. A matrix that is not invertible, or
|
||
|
|
// that carries a NaN, is discarded in favour of the 1000-per-em default: it
|
||
|
|
// would otherwise collapse every glyph in the font to a point.
|
||
|
|
@(private = "file")
|
||
|
|
t1_read_matrix :: proc(f: ^Type1_Font, lx: ^T1_Lexer) {
|
||
|
|
open, ok := t1_next(lx)
|
||
|
|
if !ok || (!t1_eq(open, "[") && !t1_eq(open, "{")) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
vals: [6]f64
|
||
|
|
n := 0
|
||
|
|
for n < 6 {
|
||
|
|
tok, tok_ok := t1_next(lx)
|
||
|
|
if !tok_ok || t1_eq(tok, "]") || t1_eq(tok, "}") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
num, num_ok := parse_number(string(tok))
|
||
|
|
v, v_ok := to_number(num)
|
||
|
|
if !num_ok || !v_ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
vals[n] = v
|
||
|
|
n += 1
|
||
|
|
}
|
||
|
|
m := Matrix{vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]}
|
||
|
|
if !matrix_is_finite(m) || matrix_determinant(m) == 0 {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
f.mtx = m
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_read_encoding reads both spellings the format allows: the
|
||
|
|
// `StandardEncoding` shorthand, and the `256 array ... dup <code> /<name> put`
|
||
|
|
// form that every subsetter emits.
|
||
|
|
@(private = "file")
|
||
|
|
t1_read_encoding :: proc(f: ^Type1_Font, lx: ^T1_Lexer) {
|
||
|
|
tok, ok := t1_next(lx)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if t1_eq(tok, "StandardEncoding") {
|
||
|
|
f.encoding = encoding_standard()^
|
||
|
|
f.has_encoding = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
depth := 0
|
||
|
|
for fuel := MAX_T1_TOKENS; fuel > 0; fuel -= 1 {
|
||
|
|
t, tok_ok := t1_next(lx)
|
||
|
|
if !tok_ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case t1_eq(t, "{"):
|
||
|
|
depth += 1
|
||
|
|
continue
|
||
|
|
case t1_eq(t, "}"):
|
||
|
|
depth -= 1
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if depth <= 0 && (t1_eq(t, "def") || t1_eq(t, "readonly") || t1_eq(t, "eexec")) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if depth != 0 || !t1_eq(t, "dup") {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
// `dup <code> /<name> put`. The three tokens are read together, and
|
||
|
|
// anything that does not fit the shape is stepped over rather than
|
||
|
|
// consumed: a font is allowed to `dup` for other reasons.
|
||
|
|
save := lx.pos
|
||
|
|
code, code_ok := t1_next_int(lx)
|
||
|
|
name, name_ok := t1_next(lx)
|
||
|
|
put, put_ok := t1_next(lx)
|
||
|
|
if !code_ok || !name_ok || !put_ok ||
|
||
|
|
code < 0 || code > 255 || len(name) < 2 || name[0] != '/' ||
|
||
|
|
!t1_eq(put, "put") {
|
||
|
|
lx.pos = save
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
f.encoding[code] = string(name[1:])
|
||
|
|
f.has_encoding = true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The Private dictionary
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
T1_Parse_Ctx :: struct {
|
||
|
|
f: ^Type1_Font,
|
||
|
|
subrs: [dynamic][]byte,
|
||
|
|
names: [dynamic]string,
|
||
|
|
chars: [dynamic][]byte,
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_parse_private :: proc(ctx: ^T1_Parse_Ctx, src: []byte) {
|
||
|
|
ctx.f.len_iv = t1_scan_len_iv(src)
|
||
|
|
|
||
|
|
lx := T1_Lexer{src = src}
|
||
|
|
subrs_open := false
|
||
|
|
fuel := MAX_T1_TOKENS
|
||
|
|
for fuel > 0 {
|
||
|
|
fuel -= 1
|
||
|
|
tok, ok := t1_next(&lx)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
switch {
|
||
|
|
case t1_eq(tok, "/CharStrings"):
|
||
|
|
t1_skip_to_begin(&lx)
|
||
|
|
t1_read_charstrings(ctx, &lx, &fuel)
|
||
|
|
return
|
||
|
|
case t1_eq(tok, "/Subrs"):
|
||
|
|
// The count that follows is not read at all: the array grows to fit
|
||
|
|
// the entries that turn up, which is what a subsetter that dropped
|
||
|
|
// some without rewriting the count needs, and what stops a count of
|
||
|
|
// a billion from being an allocation.
|
||
|
|
subrs_open = true
|
||
|
|
case subrs_open && t1_eq(tok, "dup"):
|
||
|
|
t1_read_subr(ctx, &lx)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_scan_len_iv finds /lenIV before any binary data can be reached, so that a
|
||
|
|
// charstring byte pattern cannot be mistaken for the key.
|
||
|
|
@(private = "file")
|
||
|
|
t1_scan_len_iv :: proc(src: []byte) -> int {
|
||
|
|
limit := len(src)
|
||
|
|
if p := t1_find_token(src, "/Subrs"); p >= 0 {
|
||
|
|
limit = min(limit, p)
|
||
|
|
}
|
||
|
|
if p := t1_find_token(src, "/CharStrings"); p >= 0 {
|
||
|
|
limit = min(limit, p)
|
||
|
|
}
|
||
|
|
at := t1_find_token(src[:limit], "/lenIV")
|
||
|
|
if at < 0 {
|
||
|
|
return 4
|
||
|
|
}
|
||
|
|
lx := T1_Lexer{src = src[:limit], pos = at}
|
||
|
|
if _, ok := t1_next(&lx); !ok {
|
||
|
|
return 4
|
||
|
|
}
|
||
|
|
v, ok := t1_next_int(&lx)
|
||
|
|
// 0..16 is the padding a font can mean. A negative value is how the tools
|
||
|
|
// that strip the charstring cipher record that they did.
|
||
|
|
if !ok || v > 16 || v < -1 {
|
||
|
|
return 4
|
||
|
|
}
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_skip_to_begin steps over the `<n> dict dup begin` that introduces the
|
||
|
|
// /CharStrings dictionary. A font that writes it differently is not followed
|
||
|
|
// off a cliff: the lexer is put back where it was and the entries are read from
|
||
|
|
// there.
|
||
|
|
@(private = "file")
|
||
|
|
t1_skip_to_begin :: proc(lx: ^T1_Lexer) {
|
||
|
|
save := lx.pos
|
||
|
|
for _ in 0 ..< 32 {
|
||
|
|
tok, ok := t1_next(lx)
|
||
|
|
if !ok {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if t1_eq(tok, "begin") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if len(tok) > 0 && tok[0] == '/' {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
lx.pos = save
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_read_binary takes the `<len>` bytes that follow an RD token. Exactly one
|
||
|
|
// delimiter separates the two, and the byte after it is data even when it looks
|
||
|
|
// like whitespace.
|
||
|
|
//
|
||
|
|
// A length that runs past the end of the input is clamped rather than rejected:
|
||
|
|
// that is a truncated eexec block, and the partial charstring it yields draws
|
||
|
|
// what it can. The clamp always leaves the lexer at the end of the input, so
|
||
|
|
// the truncation is recorded by the token read that follows, in
|
||
|
|
// t1_read_charstrings.
|
||
|
|
@(private = "file")
|
||
|
|
t1_read_binary :: proc(lx: ^T1_Lexer, n: int) -> ([]byte, bool) {
|
||
|
|
if lx.pos >= len(lx.src) || n < 0 {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
lx.pos += 1
|
||
|
|
take := min(n, len(lx.src) - lx.pos)
|
||
|
|
out := lx.src[lx.pos:lx.pos + take]
|
||
|
|
lx.pos += take
|
||
|
|
return out, true
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_read_subr reads one `dup <index> <len> RD <bytes> NP`. The RD token's name
|
||
|
|
// is whatever the font defined it as -- RD, -|, or anything else -- so it is
|
||
|
|
// read and discarded rather than matched.
|
||
|
|
@(private = "file")
|
||
|
|
t1_read_subr :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer) {
|
||
|
|
save := lx.pos
|
||
|
|
idx, idx_ok := t1_next_int(lx)
|
||
|
|
n, n_ok := t1_next_int(lx)
|
||
|
|
if !idx_ok || !n_ok || idx < 0 || n < 0 || idx >= MAX_T1_SUBRS {
|
||
|
|
lx.pos = save
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if _, ok := t1_next(lx); !ok {
|
||
|
|
lx.pos = save
|
||
|
|
return
|
||
|
|
}
|
||
|
|
blob, blob_ok := t1_read_binary(lx, n)
|
||
|
|
if !blob_ok {
|
||
|
|
lx.pos = save
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if idx >= len(ctx.subrs) {
|
||
|
|
resize(&ctx.subrs, idx + 1)
|
||
|
|
}
|
||
|
|
ctx.subrs[idx] = t1_decrypt_charstring(blob, ctx.f.len_iv)
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_read_charstrings reads `/<name> <len> RD <bytes> ND` until `end`.
|
||
|
|
@(private = "file")
|
||
|
|
t1_read_charstrings :: proc(ctx: ^T1_Parse_Ctx, lx: ^T1_Lexer, fuel: ^int) {
|
||
|
|
for fuel^ > 0 {
|
||
|
|
fuel^ -= 1
|
||
|
|
tok, ok := t1_next(lx)
|
||
|
|
if !ok {
|
||
|
|
ctx.f.truncated = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if t1_eq(tok, "end") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if len(tok) < 2 || tok[0] != '/' {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
save := lx.pos
|
||
|
|
n, n_ok := t1_next_int(lx)
|
||
|
|
if !n_ok || n < 0 {
|
||
|
|
lx.pos = save
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
// Both of these mean the dictionary stopped in the middle of an
|
||
|
|
// entry, which is what a truncated eexec block looks like from here.
|
||
|
|
if _, rd_ok := t1_next(lx); !rd_ok {
|
||
|
|
ctx.f.truncated = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
blob, blob_ok := t1_read_binary(lx, n)
|
||
|
|
if !blob_ok {
|
||
|
|
ctx.f.truncated = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if len(ctx.names) >= MAX_T1_GLYPHS {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
append(&ctx.names, string(tok[1:]))
|
||
|
|
append(&ctx.chars, t1_decrypt_charstring(blob, ctx.f.len_iv))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
// The charstring interpreter (Type 1 spec, chapter 6)
|
||
|
|
// ---------------------------------------------------------------------------
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
T1_Interp :: struct {
|
||
|
|
f: ^Type1_Font,
|
||
|
|
out: ^Path,
|
||
|
|
|
||
|
|
stack: [T1_STACK_MAX]f64,
|
||
|
|
sp: int,
|
||
|
|
// Values an OtherSubr left behind for the `pop`s that follow it. The
|
||
|
|
// format models this as the PostScript operand stack, which a charstring
|
||
|
|
// can only reach through `pop`.
|
||
|
|
res: [T1_PS_MAX]f64,
|
||
|
|
nres: int,
|
||
|
|
ires: int,
|
||
|
|
|
||
|
|
// Where the glyph is being drawn. `origin` is non-zero only for the accent
|
||
|
|
// half of a seac, and translates the whole outline because every Type 1
|
||
|
|
// path operator is relative.
|
||
|
|
cur: Point,
|
||
|
|
origin: Point,
|
||
|
|
|
||
|
|
sbx: f64,
|
||
|
|
width: f64,
|
||
|
|
have_width: bool,
|
||
|
|
// A subpath has been begun and not yet closed.
|
||
|
|
open: bool,
|
||
|
|
|
||
|
|
in_flex: bool,
|
||
|
|
// Where the pen stood when the FLEX began; nothing is emitted while its
|
||
|
|
// points are collected, so this is still the subpath's live end.
|
||
|
|
flex_start: Point,
|
||
|
|
flex: [T1_FLEX_POINTS]Point,
|
||
|
|
nflex: int,
|
||
|
|
|
||
|
|
fuel: int,
|
||
|
|
stop: bool,
|
||
|
|
|
||
|
|
allow_seac: bool,
|
||
|
|
seac: bool,
|
||
|
|
seac_args: [5]f64,
|
||
|
|
width_only: bool,
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_run :: proc(f: ^Type1_Font, gid: int, out: ^Path, origin: Point, ip: ^T1_Interp) {
|
||
|
|
if gid < 0 || gid >= len(f.chars) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ip.f = f
|
||
|
|
ip.out = out
|
||
|
|
ip.origin = origin
|
||
|
|
ip.cur = origin
|
||
|
|
ip.fuel = T1_MAX_FUEL
|
||
|
|
t1_exec(ip, f.chars[gid], 0)
|
||
|
|
// Glyph outlines are closed shapes, so whatever the charstring left open --
|
||
|
|
// whether it ended at `endchar`, ran out of bytes, or hit a bound -- is
|
||
|
|
// closed here.
|
||
|
|
t1_close_subpath(ip)
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_push :: proc(ip: ^T1_Interp, v: f64) {
|
||
|
|
if ip.sp >= len(ip.stack) {
|
||
|
|
// The format allows 24 operands. Past our margin the charstring is
|
||
|
|
// corrupt, and carrying on would mis-align every operator after it.
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ip.stack[ip.sp] = is_finite(v) ? v : 0
|
||
|
|
ip.sp += 1
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_args :: proc(ip: ^T1_Interp, n: int) -> ([]f64, bool) {
|
||
|
|
if ip.sp < n {
|
||
|
|
return nil, false
|
||
|
|
}
|
||
|
|
return ip.stack[ip.sp - n:ip.sp], true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_ensure_open :: proc(ip: ^T1_Interp) {
|
||
|
|
if ip.open {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if ip.out != nil {
|
||
|
|
path_move_to(ip.out, ip.cur.x, ip.cur.y)
|
||
|
|
}
|
||
|
|
ip.open = true
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_close_subpath ends the current subpath. Type 1 `closepath` leaves the
|
||
|
|
// current point where it was, unlike the content stream's `h`, so `cur` is
|
||
|
|
// untouched here.
|
||
|
|
@(private = "file")
|
||
|
|
t1_close_subpath :: proc(ip: ^T1_Interp) {
|
||
|
|
if !ip.open {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if ip.out != nil {
|
||
|
|
path_close(ip.out)
|
||
|
|
}
|
||
|
|
ip.open = false
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_rmoveto :: proc(ip: ^T1_Interp, dx, dy: f64) {
|
||
|
|
nx := ip.cur.x + dx
|
||
|
|
ny := ip.cur.y + dy
|
||
|
|
if !is_finite(nx) || !is_finite(ny) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ip.cur = Point{nx, ny}
|
||
|
|
|
||
|
|
if ip.in_flex {
|
||
|
|
// Inside a FLEX the seven movetos are control points being collected,
|
||
|
|
// not seven separate subpaths. A reader that draws them as movetos
|
||
|
|
// loses the curve entirely and the glyph goes visibly lumpy.
|
||
|
|
if ip.nflex < len(ip.flex) {
|
||
|
|
ip.flex[ip.nflex] = ip.cur
|
||
|
|
}
|
||
|
|
ip.nflex += 1
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Glyph outlines are always closed, and fonts that leave the `closepath`
|
||
|
|
// out are common enough that closing here rather than trusting them is the
|
||
|
|
// only way a stroked text render mode looks right.
|
||
|
|
t1_close_subpath(ip)
|
||
|
|
if ip.out != nil {
|
||
|
|
path_move_to(ip.out, nx, ny)
|
||
|
|
}
|
||
|
|
ip.open = true
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_rlineto :: proc(ip: ^T1_Interp, dx, dy: f64) {
|
||
|
|
nx := ip.cur.x + dx
|
||
|
|
ny := ip.cur.y + dy
|
||
|
|
if !is_finite(nx) || !is_finite(ny) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
t1_ensure_open(ip)
|
||
|
|
if ip.out != nil {
|
||
|
|
path_line_to(ip.out, nx, ny)
|
||
|
|
}
|
||
|
|
ip.cur = Point{nx, ny}
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_rrcurveto :: proc(ip: ^T1_Interp, dx1, dy1, dx2, dy2, dx3, dy3: f64) {
|
||
|
|
x1 := ip.cur.x + dx1
|
||
|
|
y1 := ip.cur.y + dy1
|
||
|
|
x2 := x1 + dx2
|
||
|
|
y2 := y1 + dy2
|
||
|
|
x3 := x2 + dx3
|
||
|
|
y3 := y2 + dy3
|
||
|
|
t1_curve_abs(ip, Point{x1, y1}, Point{x2, y2}, Point{x3, y3})
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_curve_abs :: proc(ip: ^T1_Interp, c1, c2, e: Point) {
|
||
|
|
if !is_finite(c1.x) || !is_finite(c1.y) || !is_finite(c2.x) ||
|
||
|
|
!is_finite(c2.y) || !is_finite(e.x) || !is_finite(e.y) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
t1_ensure_open(ip)
|
||
|
|
if ip.out != nil {
|
||
|
|
path_curve_to(ip.out, c1.x, c1.y, c2.x, c2.y, e.x, e.y)
|
||
|
|
}
|
||
|
|
ip.cur = e
|
||
|
|
}
|
||
|
|
|
||
|
|
@(private = "file")
|
||
|
|
t1_exec :: proc(ip: ^T1_Interp, code: []byte, depth: int) {
|
||
|
|
if depth > T1_MAX_CALL_DEPTH {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
i := 0
|
||
|
|
for i < len(code) {
|
||
|
|
if ip.stop {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ip.fuel -= 1
|
||
|
|
if ip.fuel <= 0 {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
b := code[i]
|
||
|
|
i += 1
|
||
|
|
|
||
|
|
// 32..255 begin a number; 0..31 are the operators (Type 1 spec, 6.2).
|
||
|
|
if b >= 32 {
|
||
|
|
v: f64
|
||
|
|
switch {
|
||
|
|
case b <= 246:
|
||
|
|
v = f64(int(b) - 139)
|
||
|
|
case b <= 250:
|
||
|
|
if i >= len(code) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
v = f64((int(b) - 247) * 256 + int(code[i]) + 108)
|
||
|
|
i += 1
|
||
|
|
case b <= 254:
|
||
|
|
if i >= len(code) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
v = f64(-(int(b) - 251) * 256 - int(code[i]) - 108)
|
||
|
|
i += 1
|
||
|
|
case:
|
||
|
|
if i + 4 > len(code) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
u := u32(code[i]) << 24 | u32(code[i + 1]) << 16 |
|
||
|
|
u32(code[i + 2]) << 8 | u32(code[i + 3])
|
||
|
|
v = f64(i32(u))
|
||
|
|
i += 4
|
||
|
|
}
|
||
|
|
t1_push(ip, v)
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
switch b {
|
||
|
|
case 1, 3:
|
||
|
|
// hstem, vstem: hints, which this reader does not use.
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 4: // vmoveto
|
||
|
|
if a, ok := t1_args(ip, 1); ok {
|
||
|
|
t1_rmoveto(ip, 0, a[0])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 5: // rlineto
|
||
|
|
if a, ok := t1_args(ip, 2); ok {
|
||
|
|
t1_rlineto(ip, a[0], a[1])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 6: // hlineto
|
||
|
|
if a, ok := t1_args(ip, 1); ok {
|
||
|
|
t1_rlineto(ip, a[0], 0)
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 7: // vlineto
|
||
|
|
if a, ok := t1_args(ip, 1); ok {
|
||
|
|
t1_rlineto(ip, 0, a[0])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 8: // rrcurveto
|
||
|
|
if a, ok := t1_args(ip, 6); ok {
|
||
|
|
t1_rrcurveto(ip, a[0], a[1], a[2], a[3], a[4], a[5])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 9: // closepath
|
||
|
|
t1_close_subpath(ip)
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 10: // callsubr
|
||
|
|
if ip.sp < 1 {
|
||
|
|
ip.sp = 0
|
||
|
|
break
|
||
|
|
}
|
||
|
|
ip.sp -= 1
|
||
|
|
idx := int(ip.stack[ip.sp])
|
||
|
|
if idx < 0 || idx >= len(ip.f.subrs) || len(ip.f.subrs[idx]) == 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
t1_exec(ip, ip.f.subrs[idx], depth + 1)
|
||
|
|
|
||
|
|
case 11: // return
|
||
|
|
return
|
||
|
|
|
||
|
|
case 13: // hsbw
|
||
|
|
if a, ok := t1_args(ip, 2); ok {
|
||
|
|
ip.sbx = a[0]
|
||
|
|
ip.width = a[1]
|
||
|
|
ip.have_width = true
|
||
|
|
// The left sidebearing is where the outline starts, so setting
|
||
|
|
// the current point here is what translates the whole glyph.
|
||
|
|
ip.cur = Point{ip.origin.x + a[0], ip.origin.y}
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
if ip.width_only && ip.have_width {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
case 14: // endchar
|
||
|
|
// The open subpath is closed by t1_run rather than here, because a
|
||
|
|
// charstring that simply runs out of bytes has to be closed too.
|
||
|
|
ip.sp = 0
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
|
||
|
|
case 21: // rmoveto
|
||
|
|
if a, ok := t1_args(ip, 2); ok {
|
||
|
|
t1_rmoveto(ip, a[0], a[1])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 22: // hmoveto
|
||
|
|
if a, ok := t1_args(ip, 1); ok {
|
||
|
|
t1_rmoveto(ip, a[0], 0)
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 30: // vhcurveto
|
||
|
|
if a, ok := t1_args(ip, 4); ok {
|
||
|
|
t1_rrcurveto(ip, 0, a[0], a[1], a[2], a[3], 0)
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 31: // hvcurveto
|
||
|
|
if a, ok := t1_args(ip, 4); ok {
|
||
|
|
t1_rrcurveto(ip, a[0], 0, a[1], a[2], 0, a[3])
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 12:
|
||
|
|
if i >= len(code) {
|
||
|
|
ip.stop = true
|
||
|
|
return
|
||
|
|
}
|
||
|
|
b2 := code[i]
|
||
|
|
i += 1
|
||
|
|
if t1_escape(ip, b2) {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
case:
|
||
|
|
// An operator this reader does not know is a byte of corruption,
|
||
|
|
// not a reason to abandon the glyph: clear the operands it would
|
||
|
|
// have taken and carry on.
|
||
|
|
ip.sp = 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_escape runs the two-byte operators. Returns true when the caller must
|
||
|
|
// stop executing this charstring.
|
||
|
|
@(private = "file")
|
||
|
|
t1_escape :: proc(ip: ^T1_Interp, op: byte) -> bool {
|
||
|
|
switch op {
|
||
|
|
case 0, 1, 2:
|
||
|
|
// dotsection, vstem3, hstem3: hints.
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case 6: // seac
|
||
|
|
if a, ok := t1_args(ip, 5); ok && ip.allow_seac {
|
||
|
|
ip.seac = true
|
||
|
|
for k in 0 ..< 5 {
|
||
|
|
ip.seac_args[k] = a[k]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
ip.stop = true
|
||
|
|
return true
|
||
|
|
|
||
|
|
case 7: // sbw
|
||
|
|
if a, ok := t1_args(ip, 4); ok {
|
||
|
|
ip.sbx = a[0]
|
||
|
|
ip.width = a[2]
|
||
|
|
ip.have_width = true
|
||
|
|
ip.cur = Point{ip.origin.x + a[0], ip.origin.y + a[1]}
|
||
|
|
}
|
||
|
|
ip.sp = 0
|
||
|
|
if ip.width_only && ip.have_width {
|
||
|
|
ip.stop = true
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
case 12: // div
|
||
|
|
if ip.sp < 2 {
|
||
|
|
ip.sp = 0
|
||
|
|
break
|
||
|
|
}
|
||
|
|
den := ip.stack[ip.sp - 1]
|
||
|
|
num := ip.stack[ip.sp - 2]
|
||
|
|
ip.sp -= 2
|
||
|
|
// A zero denominator gives an infinity or a NaN, which t1_push turns
|
||
|
|
// into a zero: this is the only arithmetic in the interpreter, so it
|
||
|
|
// is the only place a non-finite number can enter the operand stack.
|
||
|
|
t1_push(ip, num / den)
|
||
|
|
|
||
|
|
case 16: // callothersubr
|
||
|
|
t1_othersubr(ip)
|
||
|
|
|
||
|
|
case 17: // pop
|
||
|
|
v := 0.0
|
||
|
|
if ip.ires < ip.nres {
|
||
|
|
v = ip.res[ip.ires]
|
||
|
|
ip.ires += 1
|
||
|
|
}
|
||
|
|
t1_push(ip, v)
|
||
|
|
|
||
|
|
case 33: // setcurrentpoint
|
||
|
|
// The operands restate a current point the interpreter already holds:
|
||
|
|
// the FLEX sequence ends `pop pop setcurrentpoint`, handing back the
|
||
|
|
// two values OtherSubr 0 left. Honouring them could only move the pen
|
||
|
|
// somewhere the outline never went, so they are dropped.
|
||
|
|
ip.in_flex = false
|
||
|
|
ip.sp = 0
|
||
|
|
|
||
|
|
case:
|
||
|
|
ip.sp = 0
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_othersubr implements `callothersubr`, which is where the font program
|
||
|
|
// calls out to PostScript procedures the interpreter is expected to know.
|
||
|
|
//
|
||
|
|
// Skipping it is not an option. OtherSubrs 0-2 are FLEX, which every serif face
|
||
|
|
// uses on the shallow curves of stems and bowls; a reader that treats the seven
|
||
|
|
// collected movetos as movetos draws those curves as a stack of disconnected
|
||
|
|
// points. OtherSubr 3 is hint replacement, whose one argument has to come back
|
||
|
|
// out through the `pop` that follows so the `callsubr` after it reaches the
|
||
|
|
// right subroutine.
|
||
|
|
@(private = "file")
|
||
|
|
t1_othersubr :: proc(ip: ^T1_Interp) {
|
||
|
|
if ip.sp < 2 {
|
||
|
|
ip.sp = 0
|
||
|
|
return
|
||
|
|
}
|
||
|
|
othersubr := int(ip.stack[ip.sp - 1])
|
||
|
|
n := int(ip.stack[ip.sp - 2])
|
||
|
|
ip.sp -= 2
|
||
|
|
if n < 0 {
|
||
|
|
n = 0
|
||
|
|
}
|
||
|
|
if n > ip.sp {
|
||
|
|
n = ip.sp
|
||
|
|
}
|
||
|
|
args := ip.stack[ip.sp - n:ip.sp]
|
||
|
|
ip.sp -= n
|
||
|
|
|
||
|
|
ip.nres = 0
|
||
|
|
ip.ires = 0
|
||
|
|
|
||
|
|
switch othersubr {
|
||
|
|
case 0:
|
||
|
|
t1_end_flex(ip)
|
||
|
|
case 1:
|
||
|
|
ip.in_flex = true
|
||
|
|
ip.nflex = 0
|
||
|
|
ip.flex_start = ip.cur
|
||
|
|
case 2:
|
||
|
|
// Collect one FLEX point. The rmoveto that precedes it has already
|
||
|
|
// recorded it, so there is nothing left to do.
|
||
|
|
case 3:
|
||
|
|
// Hint replacement: the subroutine number is handed straight back. A
|
||
|
|
// reader that answered 3 instead -- the value the non-interpreting
|
||
|
|
// fallback pushes -- would run the do-nothing subroutine every font
|
||
|
|
// ships as Subrs[3] and lose the replacement.
|
||
|
|
ip.nres = 1
|
||
|
|
ip.res[0] = len(args) > 0 ? args[0] : 3
|
||
|
|
case:
|
||
|
|
// An OtherSubr this reader does not know: leave its arguments for the
|
||
|
|
// pops that follow, which is what the PostScript procedure it stands in
|
||
|
|
// for would have left on the stack.
|
||
|
|
cnt := min(len(args), len(ip.res))
|
||
|
|
for k in 0 ..< cnt {
|
||
|
|
ip.res[k] = args[k]
|
||
|
|
}
|
||
|
|
ip.nres = cnt
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// t1_end_flex turns the seven collected points into the two curves they
|
||
|
|
// describe. The first is a reference point the hinting machinery uses and the
|
||
|
|
// outline ignores; the other six are two cubics' control and end points.
|
||
|
|
@(private = "file")
|
||
|
|
t1_end_flex :: proc(ip: ^T1_Interp) {
|
||
|
|
if !ip.in_flex {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
ip.in_flex = false
|
||
|
|
|
||
|
|
// Nothing was emitted while the points were collected, so the subpath is
|
||
|
|
// still sitting where the FLEX began.
|
||
|
|
if !ip.open {
|
||
|
|
if ip.out != nil {
|
||
|
|
path_move_to(ip.out, ip.flex_start.x, ip.flex_start.y)
|
||
|
|
}
|
||
|
|
ip.open = true
|
||
|
|
}
|
||
|
|
|
||
|
|
if ip.nflex >= T1_FLEX_POINTS {
|
||
|
|
t1_curve_abs(ip, ip.flex[1], ip.flex[2], ip.flex[3])
|
||
|
|
t1_curve_abs(ip, ip.flex[4], ip.flex[5], ip.flex[6])
|
||
|
|
} else if ip.nflex > 0 {
|
||
|
|
// A FLEX that collected the wrong number of points is malformed, but
|
||
|
|
// the pen did move: joining the ends with a straight segment keeps the
|
||
|
|
// outline in one piece instead of leaving a gap across the glyph.
|
||
|
|
if ip.out != nil {
|
||
|
|
path_line_to(ip.out, ip.cur.x, ip.cur.y)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// The two values `pop pop setcurrentpoint` is about to consume.
|
||
|
|
ip.res[0] = ip.cur.x
|
||
|
|
ip.res[1] = ip.cur.y
|
||
|
|
ip.nres = 2
|
||
|
|
ip.ires = 0
|
||
|
|
}
|