1091 lines
37 KiB
Odin
1091 lines
37 KiB
Odin
package pdf
|
|
|
|
import "core:math"
|
|
import "core:strings"
|
|
import "core:testing"
|
|
|
|
// The text device is tested the way the interpreter will drive it: by handing
|
|
// it Text runs whose items carry a `trm`. Nothing here needs a Document -- a
|
|
// Font is a plain struct, and `trm` is the only geometry the device reads --
|
|
// which keeps these tests independent of content.odin.
|
|
//
|
|
// Every run is at size 10 with 500/1000 glyphs unless it says otherwise, so
|
|
// one glyph advances 5 device units, an em is 10, and the space threshold
|
|
// (0.20 em) is 2.
|
|
|
|
@(private)
|
|
tdt_font_simple :: proc(f: ^Font, width: f64 = 500) {
|
|
f.kind = .Type1
|
|
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
|
|
for c in 0 ..< FONT_CODES {
|
|
f.code_width[c] = width
|
|
}
|
|
}
|
|
|
|
// tdt_tm is the text matrix a page's base matrix produces for upright text:
|
|
// x to the right, y down the device, which is the flip render_page installs.
|
|
@(private)
|
|
tdt_tm :: proc(x, y, size: f64) -> Matrix {
|
|
return Matrix{size, 0, 0, -size, x, y}
|
|
}
|
|
|
|
@(private)
|
|
tdt_item :: proc(f: ^Font, tm: Matrix, r: rune) -> Text_Item {
|
|
return Text_Item{
|
|
trm = matrix_mul(font_matrix(f), tm),
|
|
code = 65,
|
|
cid = 65,
|
|
unicode = r,
|
|
advance = 5,
|
|
}
|
|
}
|
|
|
|
@(private)
|
|
tdt_show :: proc(td: ^Text_Device, f: ^Font, items: []Text_Item, wmode := 0) {
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = f
|
|
txt.wmode = wmode
|
|
for it in items {
|
|
append(&txt.items, it)
|
|
}
|
|
dev_fill_text(text_device_as_device(td), &txt, MATRIX_IDENTITY, Paint{})
|
|
}
|
|
|
|
// tdt_line shows an ASCII string with one x position per byte.
|
|
@(private)
|
|
tdt_line :: proc(td: ^Text_Device, f: ^Font, s: string, xs: []f64, y, size: f64) {
|
|
items := make([dynamic]Text_Item, 0, len(s))
|
|
defer delete(items)
|
|
for i in 0 ..< len(s) {
|
|
x := i < len(xs) ? xs[i] : f64(i) * size * 0.5
|
|
append(&items, tdt_item(f, tdt_tm(x, y, size), rune(s[i])))
|
|
}
|
|
tdt_show(td, f, items[:])
|
|
}
|
|
|
|
// tdt_run shows an ASCII string laid out on one baseline at a fixed step.
|
|
@(private)
|
|
tdt_run :: proc(td: ^Text_Device, f: ^Font, s: string, x0, y, size, step: f64) {
|
|
xs := make([dynamic]f64, 0, len(s))
|
|
defer delete(xs)
|
|
for i in 0 ..< len(s) {
|
|
append(&xs, x0 + f64(i) * step)
|
|
}
|
|
tdt_line(td, f, s, xs[:], y, size)
|
|
}
|
|
|
|
@(private)
|
|
tdt_text :: proc(t: ^testing.T, td: ^Text_Device) -> string {
|
|
s := text_device_result(td)
|
|
return s
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(test)
|
|
test_text_device_lifecycle_and_hints :: proc(t: ^testing.T) {
|
|
td, err := text_device_make()
|
|
testing.expect_value(t, err, Error.None)
|
|
testing.expect(t, td != nil, "a text device must be built")
|
|
defer text_device_destroy(td)
|
|
|
|
dev := text_device_as_device(td)
|
|
testing.expect(t, dev != nil, "")
|
|
// The whole point of the hints: a page whose text sits on a 4000x4000
|
|
// photograph must not decode it.
|
|
testing.expect(t, !dev_wants_images(dev), "the text device must not want images")
|
|
testing.expect(t, !dev_wants_shades(dev), "the text device must not want shadings")
|
|
testing.expect(t, .No_Glyph_Outlines in dev.hints, "outlines are never drawn here")
|
|
// Every path and image hook stays nil; the interpreter's dev_* wrappers
|
|
// are what make that free.
|
|
testing.expect(t, dev.fill_path == nil && dev.stroke_path == nil, "")
|
|
testing.expect(t, dev.fill_image == nil && dev.fill_shade == nil, "")
|
|
testing.expect(t, dev.pop_clip == nil, "a device with no clip stack pops nothing")
|
|
testing.expect(t, dev.fill_text != nil && dev.stroke_text != nil && dev.ignore_text != nil, "")
|
|
|
|
// An empty page extracts to an empty string, not to a newline.
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "")
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect_value(t, len(blocks), 0)
|
|
|
|
text_device_destroy(nil)
|
|
testing.expect(t, text_device_as_device(nil) == nil, "")
|
|
}
|
|
|
|
// Glyphs emitted left to right, each advancing by its own width, are one word:
|
|
// no spaces invented between letters.
|
|
@(test)
|
|
test_text_device_joins_glyphs_into_a_word :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "Hello", 0, 100, 10, 5)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "Hello")
|
|
}
|
|
|
|
// The device reads positions, not the order the producer chose to emit them
|
|
// in. This is the same word with its glyphs shown in a shuffled order.
|
|
@(test)
|
|
test_text_device_sorts_out_of_order_glyphs :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
order := [5]int{2, 0, 4, 1, 3}
|
|
word := "Hello"
|
|
items := make([dynamic]Text_Item, 0, 5)
|
|
defer delete(items)
|
|
for k in order {
|
|
append(&items, tdt_item(&f, tdt_tm(f64(k) * 5, 100, 10), rune(word[k])))
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "Hello")
|
|
}
|
|
|
|
// Producers routinely position words instead of emitting a space character.
|
|
// A gap wider than 0.20 em is a word break; anything narrower is kerning.
|
|
@(test)
|
|
test_text_device_wide_gap_becomes_a_space :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// "the" ends at 15; "quick" starts at 18, so the gap is 3 = 0.30 em.
|
|
xs := [8]f64{0, 5, 10, 18, 23, 28, 33, 38}
|
|
tdt_line(td, &f, "thequick", xs[:], 100, 10)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "the quick")
|
|
}
|
|
|
|
@(test)
|
|
test_text_device_kerning_gap_is_not_a_space :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
|
|
// 0.8 device units is 0.08 em: a wide positive kern, and the largest gap
|
|
// that must NOT become a space.
|
|
{
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
xs := [2]f64{0, 5.8}
|
|
tdt_line(td, &f, "AV", xs[:], 100, 10)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "AV")
|
|
}
|
|
// 2.5 device units is 0.25 em, a word space, and must.
|
|
{
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
xs := [2]f64{0, 7.5}
|
|
tdt_line(td, &f, "AV", xs[:], 100, 10)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "A V")
|
|
}
|
|
}
|
|
|
|
// Two baselines a normal leading apart are two lines of one paragraph.
|
|
@(test)
|
|
test_text_device_two_baselines_are_two_lines :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "abc", 0, 100, 10, 5)
|
|
tdt_run(td, &f, "def", 0, 112, 10, 5)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abc\ndef")
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect_value(t, len(blocks), 1)
|
|
testing.expect_value(t, len(blocks[0].lines), 2)
|
|
}
|
|
|
|
// Baselines that drift by less than the step tolerance would otherwise chain
|
|
// glyph to glyph until a whole page was one line. The total spread of a line
|
|
// is bounded as well as the step.
|
|
@(test)
|
|
test_text_device_drifting_baselines_do_not_chain :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// 0.3 em of drift per glyph: under the 0.35 em step, over the 0.80 em a
|
|
// line is allowed to span in total.
|
|
items := make([dynamic]Text_Item, 0, 5)
|
|
defer delete(items)
|
|
word := "abcde"
|
|
for i in 0 ..< len(word) {
|
|
append(&items, tdt_item(&f, tdt_tm(f64(i) * 5, 100 + f64(i) * 3, 10), rune(word[i])))
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abc\nde")
|
|
}
|
|
|
|
// A page rotated 90 degrees carries the rotation in the text matrix, so
|
|
// "left to right" is not device x and "top to bottom" is not device y. The
|
|
// same code has to read it without a special case.
|
|
@(test)
|
|
test_text_device_rotated_text_reads_in_order :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// Writing direction is device -y (up the page); the glyph's own up axis
|
|
// points at device -x. Successive lines therefore advance along +x.
|
|
tm :: proc(x, y: f64) -> Matrix { return Matrix{0, -10, -10, 0, x, y} }
|
|
items := make([dynamic]Text_Item, 0, 6)
|
|
defer delete(items)
|
|
append(&items, tdt_item(&f, tm(100, 0), 'a'))
|
|
append(&items, tdt_item(&f, tm(100, -5), 'b'))
|
|
append(&items, tdt_item(&f, tm(100, -10), 'c'))
|
|
append(&items, tdt_item(&f, tm(112, 0), 'd'))
|
|
append(&items, tdt_item(&f, tm(112, -5), 'e'))
|
|
append(&items, tdt_item(&f, tm(112, -10), 'f'))
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
// Read along the baseline, and the columns in the order the rotation puts
|
|
// them; reading device x and y instead would give "ad\nbe\ncf".
|
|
testing.expect_value(t, s, "abc\ndef")
|
|
}
|
|
|
|
// Render mode 3 paints nothing at all, which is exactly how a scanned page
|
|
// stores the OCR layer a user most wants to select. It reaches the device
|
|
// through ignore_text and must come out whole.
|
|
@(test)
|
|
test_text_device_invisible_text_is_extracted :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &f
|
|
txt.render_mode = .Invisible
|
|
ocr := "OCR"
|
|
for i in 0 ..< len(ocr) {
|
|
append(&txt.items, tdt_item(&f, tdt_tm(f64(i) * 5, 100, 10), rune(ocr[i])))
|
|
}
|
|
dev_ignore_text(text_device_as_device(td), &txt, MATRIX_IDENTITY)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "OCR")
|
|
}
|
|
|
|
// Mode 1 is stroked text and is real text; mode 7 is a clip and is still text
|
|
// on the page. Both arrive on their own hook.
|
|
@(test)
|
|
test_text_device_stroked_and_clipped_text_is_extracted :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
stroke := STROKE_STATE_DEFAULT
|
|
a := text_make()
|
|
defer text_destroy(&a)
|
|
a.font = &f
|
|
append(&a.items, tdt_item(&f, tdt_tm(0, 100, 10), 'S'))
|
|
dev_stroke_text(text_device_as_device(td), &a, &stroke, MATRIX_IDENTITY, Paint{})
|
|
|
|
b := text_make()
|
|
defer text_destroy(&b)
|
|
b.font = &f
|
|
append(&b.items, tdt_item(&f, tdt_tm(5, 100, 10), 'C'))
|
|
dev_clip_text(text_device_as_device(td), &b, MATRIX_IDENTITY)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "SC")
|
|
}
|
|
|
|
// Render mode 2 fills and strokes, and modes 4 to 6 also clip, so one run
|
|
// reaches a device that implements every text hook two or three times. It is
|
|
// one piece of text.
|
|
@(test)
|
|
test_text_device_run_seen_twice_is_counted_once :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
stroke := STROKE_STATE_DEFAULT
|
|
txt := text_make()
|
|
defer text_destroy(&txt)
|
|
txt.font = &f
|
|
txt.render_mode = .Fill_Stroke_Clip
|
|
append(&txt.items, tdt_item(&f, tdt_tm(0, 100, 10), 'A'))
|
|
append(&txt.items, tdt_item(&f, tdt_tm(5, 100, 10), 'B'))
|
|
|
|
dev := text_device_as_device(td)
|
|
dev_fill_text(dev, &txt, MATRIX_IDENTITY, Paint{})
|
|
dev_stroke_text(dev, &txt, &stroke, MATRIX_IDENTITY, Paint{})
|
|
dev_clip_text(dev, &txt, MATRIX_IDENTITY)
|
|
|
|
// A different run at a different place is not a repeat.
|
|
other := text_make()
|
|
defer text_destroy(&other)
|
|
other.font = &f
|
|
append(&other.items, tdt_item(&f, tdt_tm(10, 100, 10), 'C'))
|
|
dev_fill_text(dev, &other, MATRIX_IDENTITY, Paint{})
|
|
|
|
// The glyphs are dropped on arrival, not tidied up at the end: three
|
|
// passes over a two-glyph run must not cost six glyphs of memory.
|
|
testing.expect_value(t, len(td.glyphs), 3)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "ABC")
|
|
}
|
|
|
|
// A glyph the font resolves no Unicode for must leave a mark. Dropping it
|
|
// silently joins the words either side of it and the caller cannot tell.
|
|
@(test)
|
|
test_text_device_missing_unicode_becomes_replacement :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 3)
|
|
defer delete(items)
|
|
append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'A'))
|
|
it := tdt_item(&f, tdt_tm(5, 100, 10), 'x')
|
|
it.unicode = -1 // font.odin's "no Unicode at all"
|
|
append(&items, it)
|
|
append(&items, tdt_item(&f, tdt_tm(10, 100, 10), 'B'))
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "A�B")
|
|
testing.expect_value(t, len(s), 5) // three characters, one of them 3 bytes
|
|
}
|
|
|
|
// More than a line height between two baselines is a paragraph break.
|
|
@(test)
|
|
test_text_device_blank_line_between_blocks :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "abc", 0, 100, 10, 5)
|
|
tdt_run(td, &f, "def", 0, 112, 10, 5) // 1.2 em: same paragraph
|
|
tdt_run(td, &f, "ghi", 0, 152, 10, 5) // 4.0 em: a new one
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abc\ndef\n\nghi")
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect_value(t, len(blocks), 2)
|
|
testing.expect_value(t, len(blocks[0].lines), 2)
|
|
testing.expect_value(t, len(blocks[1].lines), 1)
|
|
}
|
|
|
|
// The structured form is what a viewer needs to draw a selection rectangle.
|
|
@(test)
|
|
test_text_device_blocks_carry_spans_and_rectangles :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
g: Font
|
|
tdt_font_simple(&g)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 4)
|
|
defer delete(items)
|
|
append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'a'))
|
|
append(&items, tdt_item(&f, tdt_tm(5, 100, 10), 'b'))
|
|
tdt_show(td, &f, items[:])
|
|
clear(&items)
|
|
// A second font on the same baseline, at a different size.
|
|
append(&items, tdt_item(&g, tdt_tm(10, 100, 20), 'C'))
|
|
tdt_show(td, &g, items[:])
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect_value(t, len(blocks), 1)
|
|
testing.expect_value(t, len(blocks[0].lines), 1)
|
|
|
|
line := blocks[0].lines[0]
|
|
testing.expect_value(t, len(line.spans), 2)
|
|
testing.expect_value(t, line.spans[0].text, "ab")
|
|
testing.expect_value(t, line.spans[0].size, 10)
|
|
testing.expect(t, line.spans[0].font == &f, "a span names the font it was set in")
|
|
testing.expect_value(t, line.spans[1].text, "C")
|
|
testing.expect_value(t, line.spans[1].size, 20)
|
|
testing.expect(t, line.spans[1].font == &g, "")
|
|
|
|
// "ab" spans x 0..10 and, at 0.8 em up and 0.2 em down from a baseline at
|
|
// y=100, y 92..102.
|
|
r := line.spans[0].rect
|
|
testing.expect(t, abs(r.x0 - 0) < 1e-9 && abs(r.x1 - 10) < 1e-9, "span x")
|
|
testing.expect(t, abs(r.y0 - 92) < 1e-9 && abs(r.y1 - 102) < 1e-9, "span y")
|
|
// The line and block rectangles cover every span.
|
|
testing.expect(t, line.rect.x1 >= 20 && line.rect.y0 <= 92, "line rect covers its spans")
|
|
testing.expect_value(t, blocks[0].rect, line.rect)
|
|
}
|
|
|
|
// Letter-spacing (Tc) widens every gap in a run equally. Without
|
|
// compensation a tracked heading extracts as "S P A C E D".
|
|
@(test)
|
|
test_text_device_tracking_is_not_word_spaces :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// Advance 5 plus 2.5 of tracking: every gap is 0.25 em, over the space
|
|
// threshold, and none of them is a word break.
|
|
xs := [7]f64{0, 7.5, 15, 22.5, 30, 37.5, 50.5}
|
|
// ... except the last, which is 8 further on than the tracking explains.
|
|
tdt_line(td, &f, "SPACEDX", xs[:], 100, 10)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "SPACED X")
|
|
}
|
|
|
|
// The cap on that compensation is what keeps a table row -- one TJ with wide
|
|
// gaps between the cells -- from having every space compensated away.
|
|
@(test)
|
|
test_text_device_tracking_cap_keeps_table_cells_apart :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// Five cells, 8 units (0.8 em) of clear space between each.
|
|
xs := [5]f64{0, 13, 26, 39, 52}
|
|
tdt_line(td, &f, "ABCDE", xs[:], 100, 10)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "A B C D E")
|
|
}
|
|
|
|
// Producers fake bold by drawing the same glyph twice a hair apart. It is one
|
|
// letter.
|
|
@(test)
|
|
test_text_device_fake_bold_is_not_doubled :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 4)
|
|
defer delete(items)
|
|
append(&items, tdt_item(&f, tdt_tm(0, 100, 10), 'A'))
|
|
append(&items, tdt_item(&f, tdt_tm(0.3, 100, 10), 'A'))
|
|
append(&items, tdt_item(&f, tdt_tm(5, 100, 10), 'B'))
|
|
append(&items, tdt_item(&f, tdt_tm(5.3, 100, 10), 'B'))
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "AB")
|
|
}
|
|
|
|
// A real repeated letter is not fake bold: it is a whole advance away.
|
|
@(test)
|
|
test_text_device_repeated_letter_survives :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "aardvark", 0, 100, 10, 5)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "aardvark")
|
|
}
|
|
|
|
// A composite font's advances come from /W and /DW, not from the 256-entry
|
|
// table a simple font uses, and a Type3's come through its /FontMatrix. Get
|
|
// either wrong and a line of text turns into a line of spaced letters.
|
|
@(test)
|
|
test_text_device_composite_and_type3_advances :: proc(t: ^testing.T) {
|
|
{
|
|
f: Font
|
|
f.kind = .Type0
|
|
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
|
|
f.default_width = 1000 // a full em per glyph
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
tdt_run(td, &f, "CJK", 0, 100, 10, 10)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "CJK")
|
|
}
|
|
{
|
|
// A Type3 whose glyph space is hundredths: /Widths of 50 is half an em.
|
|
f: Font
|
|
f.kind = .Type3
|
|
f.mtx = Matrix{0.01, 0, 0, 0.01, 0, 0}
|
|
for c in 0 ..< FONT_CODES {
|
|
f.code_width[c] = 50
|
|
}
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
tdt_run(td, &f, "abc", 0, 100, 10, 5)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abc")
|
|
}
|
|
}
|
|
|
|
// A vertical CMap writes down text space's -y, and its columns advance
|
|
// leftwards, so the rightmost column is read first (ISO 32000-2, 9.7.4.3).
|
|
@(test)
|
|
test_text_device_vertical_writing :: proc(t: ^testing.T) {
|
|
f: Font
|
|
f.kind = .Type0
|
|
f.mtx = Matrix{0.001, 0, 0, 0.001, 0, 0}
|
|
f.vertical = true
|
|
f.default_width = 1000
|
|
f.default_w1 = -1000
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 6)
|
|
defer delete(items)
|
|
append(&items, tdt_item(&f, tdt_tm(100, 0, 10), 'a'))
|
|
append(&items, tdt_item(&f, tdt_tm(100, 10, 10), 'b'))
|
|
append(&items, tdt_item(&f, tdt_tm(100, 20, 10), 'c'))
|
|
append(&items, tdt_item(&f, tdt_tm(88, 0, 10), 'd'))
|
|
append(&items, tdt_item(&f, tdt_tm(88, 10, 10), 'e'))
|
|
append(&items, tdt_item(&f, tdt_tm(88, 20, 10), 'f'))
|
|
tdt_show(td, &f, items[:], 1)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abc\ndef")
|
|
}
|
|
|
|
// The device reads the matrices, not Text_Item.advance, precisely so that it
|
|
// cannot be desynchronised from where the glyphs actually landed.
|
|
@(test)
|
|
test_text_device_ignores_the_advance_field :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 8)
|
|
defer delete(items)
|
|
xs := [8]f64{0, 5, 10, 18, 23, 28, 33, 38}
|
|
word := "thequick"
|
|
for i in 0 ..< 8 {
|
|
it := tdt_item(&f, tdt_tm(xs[i], 100, 10), rune(word[i]))
|
|
it.advance = f64(i) * -1e6 // nonsense, in nonsense units
|
|
append(&items, it)
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "the quick")
|
|
}
|
|
|
|
// Content streams are the most malformed data in a PDF. None of this may
|
|
// crash, hang or leak, and whatever is still readable must still be read.
|
|
@(test)
|
|
test_text_device_survives_degenerate_input :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
dev := text_device_as_device(td)
|
|
|
|
stroke := STROKE_STATE_DEFAULT
|
|
dev_fill_text(dev, nil, MATRIX_IDENTITY, Paint{})
|
|
dev_stroke_text(dev, nil, &stroke, MATRIX_IDENTITY, Paint{})
|
|
dev_ignore_text(dev, nil, MATRIX_IDENTITY)
|
|
|
|
empty := text_make()
|
|
defer text_destroy(&empty)
|
|
dev_fill_text(dev, &empty, MATRIX_IDENTITY, Paint{})
|
|
|
|
// A run with no font at all: font_matrix falls back, widths are zero.
|
|
nofont := text_make()
|
|
defer text_destroy(&nofont)
|
|
append(&nofont.items, Text_Item{trm = Matrix{0.01, 0, 0, -0.01, 0, 0}, unicode = 'Z'})
|
|
dev_fill_text(dev, &nofont, MATRIX_IDENTITY, Paint{})
|
|
|
|
bad := text_make()
|
|
defer text_destroy(&bad)
|
|
bad.font = &f
|
|
nan := math.QNAN_F64
|
|
inf := math.INF_F64
|
|
append(&bad.items, Text_Item{trm = Matrix{nan, 0, 0, nan, 0, 0}, unicode = 'a'})
|
|
append(&bad.items, Text_Item{trm = Matrix{1, 0, 0, 1, inf, 0}, unicode = 'b'})
|
|
append(&bad.items, Text_Item{trm = Matrix{0, 0, 0, 0, 5, 5}, unicode = 'c'})
|
|
append(&bad.items, tdt_item(&f, tdt_tm(0, 100, 10), 'K'))
|
|
dev_fill_text(dev, &bad, MATRIX_IDENTITY, Paint{})
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
// The one placeable glyph in that soup is the one that comes back.
|
|
testing.expect(t, strings.contains(s, "K"), "a readable glyph must survive its neighbours")
|
|
testing.expect(t, !strings.contains(s, "a"), "a NaN matrix places nothing")
|
|
testing.expect(t, !strings.contains(s, "b"), "an infinite position places nothing")
|
|
testing.expect(t, !strings.contains(s, "c"), "a collapsed matrix places nothing")
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect(t, len(blocks) >= 1, "")
|
|
for b in blocks {
|
|
testing.expect(t, rect_is_finite(b.rect), "no NaN may reach a caller's rectangle")
|
|
for l in b.lines {
|
|
testing.expect(t, rect_is_finite(l.rect), "")
|
|
for sp in l.spans {
|
|
testing.expect(t, rect_is_finite(sp.rect) && is_finite(sp.size), "")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Both outputs are rendered from one layout, so they cannot disagree about
|
|
// what the page says.
|
|
@(test)
|
|
test_text_device_result_and_blocks_agree :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "alpha", 0, 100, 10, 5)
|
|
tdt_run(td, &f, "beta", 0, 112, 10, 5)
|
|
tdt_run(td, &f, "gamma", 0, 160, 10, 5)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
|
|
b := strings.builder_make()
|
|
defer strings.builder_destroy(&b)
|
|
for blk, bi in blocks {
|
|
if bi > 0 { strings.write_string(&b, "\n\n") }
|
|
for ln, li in blk.lines {
|
|
if li > 0 { strings.write_byte(&b, '\n') }
|
|
for sp in ln.spans { strings.write_string(&b, sp.text) }
|
|
}
|
|
}
|
|
testing.expect_value(t, s, strings.to_string(b))
|
|
testing.expect_value(t, s, "alpha\nbeta\n\ngamma")
|
|
}
|
|
|
|
// Extracting twice must give the same answer: nothing is consumed.
|
|
@(test)
|
|
test_text_device_result_is_repeatable :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
tdt_run(td, &f, "twice", 0, 100, 10, 5)
|
|
|
|
a := text_device_result(td)
|
|
defer delete(a)
|
|
b := text_device_result(td)
|
|
defer delete(b)
|
|
testing.expect_value(t, a, b)
|
|
testing.expect_value(t, a, "twice")
|
|
}
|
|
|
|
// Text_Item.trm is contracted to map glyph space to device space. A producer
|
|
// of Text_Items that handed over the text-space matrix instead would leave
|
|
// every em a thousand times too large, and the whole page would collapse into
|
|
// one line with no spaces in it. The guard is a page whose text cannot
|
|
// possibly fit inside one em.
|
|
@(test)
|
|
test_text_device_rescales_impossible_matrices :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
word := "abcdefghij klmnopqrst"
|
|
items := make([dynamic]Text_Item, 0, len(word))
|
|
defer delete(items)
|
|
x := 0.0
|
|
for i in 0 ..< len(word) {
|
|
if word[i] == ' ' {
|
|
x += 3
|
|
continue
|
|
}
|
|
// Note: no font matrix folded in, which is the mistake being guarded.
|
|
append(&items, Text_Item{
|
|
trm = tdt_tm(x, 100, 10),
|
|
code = 65,
|
|
cid = 65,
|
|
unicode = rune(word[i]),
|
|
})
|
|
x += 5
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abcdefghij klmnopqrst")
|
|
}
|
|
|
|
// Trailing space glyphs are producer noise; a space glyph at the start of a
|
|
// line carries nothing the rectangle does not.
|
|
@(test)
|
|
test_text_device_trims_edge_spaces :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, " hi ", 0, 100, 10, 5)
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "hi")
|
|
}
|
|
|
|
// A run that is emitted twice with a gap between the two halves still reads
|
|
// as one line: runs are joined by position, not by operator.
|
|
@(test)
|
|
test_text_device_joins_runs_on_one_baseline :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
tdt_run(td, &f, "one", 0, 100, 10, 5)
|
|
tdt_run(td, &f, "two", 15, 100, 10, 5) // butts straight on
|
|
tdt_run(td, &f, "three", 33, 100, 10, 5) // 3 units clear: a word break
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "onetwo three")
|
|
}
|
|
|
|
// A text matrix with a flattened axis -- a producer hiding text, or a scale
|
|
// that rounded to zero -- still says where every glyph is. The surviving axis
|
|
// stands in for the other rather than the run being thrown away.
|
|
@(test)
|
|
test_text_device_flattened_matrix_still_reads :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 4)
|
|
defer delete(items)
|
|
word := "flat"
|
|
for i in 0 ..< len(word) {
|
|
// No y axis at all: {size, 0, 0, 0, x, y}.
|
|
append(&items, tdt_item(&f, Matrix{10, 0, 0, 0, f64(i) * 5, 100}, rune(word[i])))
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "flat")
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect_value(t, len(blocks), 1)
|
|
// The borrowed axis has to give the glyph box a real height, or a
|
|
// selection rectangle is a zero-height line.
|
|
r := blocks[0].lines[0].spans[0].rect
|
|
testing.expect(t, rect_height(r) > 5, "the em box must survive a flattened matrix")
|
|
testing.expect(t, rect_width(r) > 15, "")
|
|
}
|
|
|
|
// Fake italics are a shear in the text matrix: the y axis leans but the
|
|
// baseline does not. Lines still group by their baselines.
|
|
@(test)
|
|
test_text_device_sheared_matrix_reads_in_order :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
// A hard lean, and lines long enough that measuring baselines along the
|
|
// leaning y axis rather than along the true normal would tear each line
|
|
// into pieces.
|
|
shear :: proc(x, y: f64) -> Matrix { return Matrix{10, 0, 8, -10, x, y} }
|
|
items := make([dynamic]Text_Item, 0, 20)
|
|
defer delete(items)
|
|
top := "abcdefghij"
|
|
bot := "klmnopqrst"
|
|
for i in 0 ..< len(top) {
|
|
append(&items, tdt_item(&f, shear(f64(i) * 5, 100), rune(top[i])))
|
|
}
|
|
for i in 0 ..< len(bot) {
|
|
append(&items, tdt_item(&f, shear(f64(i) * 5, 112), rune(bot[i])))
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "abcdefghij\nklmnopqrst")
|
|
}
|
|
|
|
// The glyph budget is a bound on what a hostile content stream can make the
|
|
// device hold, and it has to bite without losing what came before it.
|
|
@(test)
|
|
test_text_device_glyph_budget_bites :: proc(t: ^testing.T) {
|
|
f: Font
|
|
tdt_font_simple(&f)
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
|
|
items := make([dynamic]Text_Item, 0, 1000)
|
|
defer delete(items)
|
|
for line in 0 ..< 400 {
|
|
clear(&items)
|
|
for i in 0 ..< 1000 {
|
|
append(&items, tdt_item(&f, tdt_tm(f64(i) * 5, f64(line) * 12, 10), 'x'))
|
|
}
|
|
tdt_show(td, &f, items[:])
|
|
if td.truncated {
|
|
break
|
|
}
|
|
}
|
|
testing.expect(t, td.truncated, "400 000 glyphs must not all be kept")
|
|
testing.expect_value(t, len(td.glyphs), MAX_TEXT_GLYPHS)
|
|
|
|
blocks := text_device_blocks(td)
|
|
defer text_blocks_destroy(blocks)
|
|
testing.expect(t, len(blocks) > 0, "what was collected before the budget ran out is still text")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End to end, through the real interpreter
|
|
//
|
|
// Everything above hands the device Text runs built by hand. These run actual
|
|
// content streams through content.odin, which is where the two files' readings
|
|
// of `Text_Item.trm` have to agree: the interpreter folds the font matrix into
|
|
// it and this device divides that back out, and if either changed its mind
|
|
// every threshold here would be a thousand times off.
|
|
//
|
|
// The fixture font is half an em wide for every code -- /BaseFont is not one
|
|
// of the standard 14, so no AFM metric can get in ahead of /MissingWidth --
|
|
// which makes every expected position arithmetic at 10 point.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
@(private = "file")
|
|
TDT_FONT :: "<< /Type /Font /Subtype /Type1 /BaseFont /Halfwidth " +
|
|
"/FontDescriptor << /Flags 32 /MissingWidth 500 >> >>"
|
|
|
|
@(private = "file")
|
|
TDT_TYPE3 :: "<< /Type /Font /Subtype /Type3 /FontMatrix [0.01 0 0 0.01 0 0] " +
|
|
"/FontBBox [0 0 100 100] /FirstChar 65 /LastChar 66 /Widths [100 100] " +
|
|
"/Encoding << /Type /Encoding /Differences [65 /A 66 /B] >> >>"
|
|
|
|
// A /ToUnicode that maps one code to a ligature's two letters and another to
|
|
// a single character, so that the multi-character path and the ordinary one
|
|
// are both live in the same font.
|
|
@(private = "file")
|
|
TDT_TOUNICODE :: `begincmap
|
|
1 begincodespacerange <00> <ff> endcodespacerange
|
|
2 beginbfchar
|
|
<41> <00660069>
|
|
<42> <0058>
|
|
endbfchar
|
|
endcmap`
|
|
|
|
@(private = "file")
|
|
tdt_resources :: proc(doc: ^Document) -> ^Dict {
|
|
res := new_dict(doc.allocator)
|
|
fonts := new_dict(doc.allocator)
|
|
dict_set(fonts, "F1", cf_parse(doc, TDT_FONT))
|
|
|
|
if lig, ok := to_dict(cf_parse(doc, TDT_FONT)); ok {
|
|
dict_set(lig, "ToUnicode", cf_stream(doc, "<< >>", transmute([]byte)string(TDT_TOUNICODE)))
|
|
dict_set(fonts, "FL", lig)
|
|
}
|
|
|
|
t3 := cf_parse(doc, TDT_TYPE3)
|
|
if t3d, ok := to_dict(t3); ok {
|
|
procs := new_dict(doc.allocator)
|
|
// A whole-em square, declared with d0 the way a Type 3 glyph must.
|
|
dict_set(procs, "A", cf_stream(doc, "<< >>",
|
|
transmute([]byte)string("100 0 d0 0 0 100 100 re f")))
|
|
dict_set(procs, "B", cf_stream(doc, "<< >>",
|
|
transmute([]byte)string("100 0 d0")))
|
|
dict_set(t3d, "CharProcs", procs)
|
|
}
|
|
dict_set(fonts, "T3", t3)
|
|
dict_set(res, "Font", fonts)
|
|
return res
|
|
}
|
|
|
|
// tdt_extract runs a content stream under the page flip a real render installs.
|
|
@(private = "file")
|
|
tdt_extract :: proc(t: ^testing.T, doc: ^Document, src: string) -> string {
|
|
td, err := text_device_make()
|
|
if err != .None || td == nil {
|
|
testing.expect(t, false, "text device")
|
|
return ""
|
|
}
|
|
defer text_device_destroy(td)
|
|
flip := Matrix{1, 0, 0, -1, 0, 792}
|
|
rerr := run_content(doc, text_device_as_device(td), transmute([]byte)src,
|
|
tdt_resources(doc), flip)
|
|
testing.expect_value(t, rerr, Error.None)
|
|
return text_device_result(td)
|
|
}
|
|
|
|
@(test)
|
|
test_text_device_end_to_end_lines :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
s := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td (Hello) Tj 0 -12 Td (World) Tj ET")
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "Hello\nWorld")
|
|
|
|
// Four lines of leading is a paragraph break, not a line break.
|
|
p := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td (one) Tj 0 -40 Td (two) Tj ET")
|
|
defer delete(p)
|
|
testing.expect_value(t, p, "one\n\ntwo")
|
|
}
|
|
|
|
// TJ's numbers are thousandths of an em, subtracted from the pen. A -250 is a
|
|
// word space nobody wrote; a -80 is kerning.
|
|
@(test)
|
|
test_text_device_end_to_end_tj_spacing :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
s := tdt_extract(t, doc, "BT /F1 10 Tf 100 700 Td [(the) -250 (quick) -80 (brown)] TJ ET")
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "the quickbrown")
|
|
}
|
|
|
|
// Render mode 3 is the OCR layer under a scan.
|
|
@(test)
|
|
test_text_device_end_to_end_invisible :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
s := tdt_extract(t, doc, "BT /F1 10 Tf 3 Tr 100 700 Td (scanned) Tj ET")
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "scanned")
|
|
}
|
|
|
|
// Modes 2 and 4 to 6 reach the device twice: once to be painted and once as a
|
|
// clip. The page still says it once.
|
|
@(test)
|
|
test_text_device_end_to_end_clipped_text_is_not_doubled :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
for mode in ([]string{"2", "4", "5", "6", "7"}) {
|
|
src := strings.concatenate({"BT /F1 10 Tf ", mode, " Tr 100 700 Td (clip) Tj ET"})
|
|
defer delete(src)
|
|
s := tdt_extract(t, doc, src)
|
|
defer delete(s)
|
|
testing.expectf(t, s == "clip", "render mode %s extracted %q", mode, s)
|
|
}
|
|
}
|
|
|
|
// Tc widens every gap in the run; Tw widens the one after a space glyph. The
|
|
// first must not become spaces and the second must not become two.
|
|
@(test)
|
|
test_text_device_end_to_end_char_and_word_spacing :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
// 3 points of tracking at 10 point: 0.3 em, over the space threshold.
|
|
a := tdt_extract(t, doc, "BT /F1 10 Tf 3 Tc 100 700 Td (SPACED) Tj ET")
|
|
defer delete(a)
|
|
testing.expect_value(t, a, "SPACED")
|
|
|
|
// 5 points of word spacing, on top of the space glyph's own 5.
|
|
b := tdt_extract(t, doc, "BT /F1 10 Tf 5 Tw 100 700 Td (a b) Tj ET")
|
|
defer delete(b)
|
|
testing.expect_value(t, b, "a b")
|
|
}
|
|
|
|
// A Type 3 glyph is a content stream, so it never reaches a text hook as an
|
|
// outline -- the interpreter hands the run over separately, which is the only
|
|
// reason a Type 3 page extracts at all. Its widths are in the font's own
|
|
// glyph space and its /FontMatrix is the only thing that says how wide an em
|
|
// is: read as thousandths, these two glyphs would be a tenth of the size and
|
|
// the line would come out as "A B".
|
|
@(test)
|
|
test_text_device_end_to_end_type3 :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
s := tdt_extract(t, doc, "BT /T3 10 Tf 100 700 Td (AB) Tj ET")
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "AB")
|
|
}
|
|
|
|
// A page rotated 90 degrees puts the rotation in the CTM, and every glyph
|
|
// matrix carries it. Reading order comes out of the matrices, so the text is
|
|
// the same text.
|
|
@(test)
|
|
test_text_device_end_to_end_rotated_page :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
td, _ := text_device_make()
|
|
defer text_device_destroy(td)
|
|
// The base matrix a viewer builds for /Rotate 90: flip y, then turn.
|
|
ctm := matrix_mul(Matrix{1, 0, 0, -1, 0, 792}, rotate_matrix(90))
|
|
err := run_content(doc, text_device_as_device(td),
|
|
transmute([]byte)string("BT /F1 10 Tf 100 700 Td (Hello) Tj 0 -12 Td (World) Tj ET"),
|
|
tdt_resources(doc), ctm)
|
|
testing.expect_value(t, err, Error.None)
|
|
|
|
s := text_device_result(td)
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "Hello\nWorld")
|
|
}
|
|
|
|
// A ligature glyph stands for several characters, and /ToUnicode is where the
|
|
// producer says so. Reporting only the first of them is how "Specification"
|
|
// extracts as "Specifcation" -- which is what the corpus does when this is
|
|
// missing.
|
|
@(test)
|
|
test_text_device_end_to_end_ligature :: proc(t: ^testing.T) {
|
|
doc := cf_open(t)
|
|
defer cf_close(t, doc)
|
|
|
|
s := tdt_extract(t, doc, "BT /FL 10 Tf 100 700 Td (ABA) Tj ET")
|
|
defer delete(s)
|
|
testing.expect_value(t, s, "fiXfi")
|
|
}
|