mirror of https://gitee.com/godoos/godoos.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1047 lines
23 KiB
1047 lines
23 KiB
package pdf
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// A Page represent a single page in a PDF file.
|
|
// The methods interpret a Page dictionary stored in V.
|
|
type Page struct {
|
|
V Value
|
|
}
|
|
|
|
// Page returns the page for the given page number.
|
|
// Page numbers are indexed starting at 1, not 0.
|
|
// If the page is not found, Page returns a Page with p.V.IsNull().
|
|
func (r *Reader) Page(num int) Page {
|
|
num-- // now 0-indexed
|
|
page := r.Trailer().Key("Root").Key("Pages")
|
|
Search:
|
|
for page.Key("Type").Name() == "Pages" {
|
|
count := int(page.Key("Count").Int64())
|
|
if count < num {
|
|
return Page{}
|
|
}
|
|
kids := page.Key("Kids")
|
|
for i := 0; i < kids.Len(); i++ {
|
|
kid := kids.Index(i)
|
|
if kid.Key("Type").Name() == "Pages" {
|
|
c := int(kid.Key("Count").Int64())
|
|
if num < c {
|
|
page = kid
|
|
continue Search
|
|
}
|
|
num -= c
|
|
continue
|
|
}
|
|
if kid.Key("Type").Name() == "Page" {
|
|
if num == 0 {
|
|
return Page{kid}
|
|
}
|
|
num--
|
|
}
|
|
}
|
|
break
|
|
}
|
|
return Page{}
|
|
}
|
|
|
|
// NumPage returns the number of pages in the PDF file.
|
|
func (r *Reader) NumPage() int {
|
|
return int(r.Trailer().Key("Root").Key("Pages").Key("Count").Int64())
|
|
}
|
|
|
|
// GetPlainText returns all the text in the PDF file
|
|
func (r *Reader) GetPlainText() (reader io.Reader, err error) {
|
|
pages := r.NumPage()
|
|
var buf bytes.Buffer
|
|
fonts := make(map[string]*Font)
|
|
for i := 1; i <= pages; i++ {
|
|
p := r.Page(i)
|
|
for _, name := range p.Fonts() { // cache fonts so we don't continually parse charmap
|
|
if _, ok := fonts[name]; !ok {
|
|
f := p.Font(name)
|
|
fonts[name] = &f
|
|
}
|
|
}
|
|
text, _ := p.GetPlainText(fonts)
|
|
// if err != nil {
|
|
// return &bytes.Buffer{}, err
|
|
// }
|
|
buf.WriteString(text)
|
|
}
|
|
return &buf, nil
|
|
}
|
|
|
|
func (p Page) findInherited(key string) Value {
|
|
for v := p.V; !v.IsNull(); v = v.Key("Parent") {
|
|
if r := v.Key(key); !r.IsNull() {
|
|
return r
|
|
}
|
|
}
|
|
return Value{}
|
|
}
|
|
|
|
/*
|
|
func (p Page) MediaBox() Value {
|
|
return p.findInherited("MediaBox")
|
|
}
|
|
|
|
func (p Page) CropBox() Value {
|
|
return p.findInherited("CropBox")
|
|
}
|
|
*/
|
|
|
|
// Resources returns the resources dictionary associated with the page.
|
|
func (p Page) Resources() Value {
|
|
return p.findInherited("Resources")
|
|
}
|
|
|
|
// Fonts returns a list of the fonts associated with the page.
|
|
func (p Page) Fonts() []string {
|
|
return p.Resources().Key("Font").Keys()
|
|
}
|
|
|
|
// Font returns the font with the given name associated with the page.
|
|
func (p Page) Font(name string) Font {
|
|
return Font{p.Resources().Key("Font").Key(name), nil}
|
|
}
|
|
|
|
// A Font represent a font in a PDF file.
|
|
// The methods interpret a Font dictionary stored in V.
|
|
type Font struct {
|
|
V Value
|
|
enc TextEncoding
|
|
}
|
|
|
|
// BaseFont returns the font's name (BaseFont property).
|
|
func (f Font) BaseFont() string {
|
|
return f.V.Key("BaseFont").Name()
|
|
}
|
|
|
|
// FirstChar returns the code point of the first character in the font.
|
|
func (f Font) FirstChar() int {
|
|
return int(f.V.Key("FirstChar").Int64())
|
|
}
|
|
|
|
// LastChar returns the code point of the last character in the font.
|
|
func (f Font) LastChar() int {
|
|
return int(f.V.Key("LastChar").Int64())
|
|
}
|
|
|
|
// Widths returns the widths of the glyphs in the font.
|
|
// In a well-formed PDF, len(f.Widths()) == f.LastChar()+1 - f.FirstChar().
|
|
func (f Font) Widths() []float64 {
|
|
x := f.V.Key("Widths")
|
|
var out []float64
|
|
for i := 0; i < x.Len(); i++ {
|
|
out = append(out, x.Index(i).Float64())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Width returns the width of the given code point.
|
|
func (f Font) Width(code int) float64 {
|
|
first := f.FirstChar()
|
|
last := f.LastChar()
|
|
if code < first || last < code {
|
|
return 0
|
|
}
|
|
return f.V.Key("Widths").Index(code - first).Float64()
|
|
}
|
|
|
|
// Encoder returns the encoding between font code point sequences and UTF-8.
|
|
func (f Font) Encoder() TextEncoding {
|
|
if f.enc == nil { // caching the Encoder so we don't have to continually parse charmap
|
|
f.enc = f.getEncoder()
|
|
}
|
|
return f.enc
|
|
}
|
|
|
|
func (f Font) getEncoder() TextEncoding {
|
|
enc := f.V.Key("Encoding")
|
|
switch enc.Kind() {
|
|
case Name:
|
|
switch enc.Name() {
|
|
case "WinAnsiEncoding":
|
|
return &byteEncoder{&winAnsiEncoding}
|
|
case "MacRomanEncoding":
|
|
return &byteEncoder{&macRomanEncoding}
|
|
case "Identity-H":
|
|
return f.charmapEncoding()
|
|
default:
|
|
if DebugOn {
|
|
println("unknown encoding", enc.Name())
|
|
}
|
|
return &nopEncoder{}
|
|
}
|
|
case Dict:
|
|
return &dictEncoder{enc.Key("Differences")}
|
|
case Null:
|
|
return f.charmapEncoding()
|
|
default:
|
|
if DebugOn {
|
|
println("unexpected encoding", enc.String())
|
|
}
|
|
return &nopEncoder{}
|
|
}
|
|
}
|
|
|
|
func (f *Font) charmapEncoding() TextEncoding {
|
|
toUnicode := f.V.Key("ToUnicode")
|
|
if toUnicode.Kind() == Stream {
|
|
m := readCmap(toUnicode)
|
|
if m == nil {
|
|
return &nopEncoder{}
|
|
}
|
|
return m
|
|
}
|
|
|
|
return &byteEncoder{&pdfDocEncoding}
|
|
}
|
|
|
|
type dictEncoder struct {
|
|
v Value
|
|
}
|
|
|
|
func (e *dictEncoder) Decode(raw string) (text string) {
|
|
r := make([]rune, 0, len(raw))
|
|
for i := 0; i < len(raw); i++ {
|
|
ch := rune(raw[i])
|
|
n := -1
|
|
for j := 0; j < e.v.Len(); j++ {
|
|
x := e.v.Index(j)
|
|
if x.Kind() == Integer {
|
|
n = int(x.Int64())
|
|
continue
|
|
}
|
|
if x.Kind() == Name {
|
|
if int(raw[i]) == n {
|
|
r := nameToRune[x.Name()]
|
|
if r != 0 {
|
|
ch = r
|
|
break
|
|
}
|
|
}
|
|
n++
|
|
}
|
|
}
|
|
r = append(r, ch)
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
// A TextEncoding represents a mapping between
|
|
// font code points and UTF-8 text.
|
|
type TextEncoding interface {
|
|
// Decode returns the UTF-8 text corresponding to
|
|
// the sequence of code points in raw.
|
|
Decode(raw string) (text string)
|
|
}
|
|
|
|
type nopEncoder struct {
|
|
}
|
|
|
|
func (e *nopEncoder) Decode(raw string) (text string) {
|
|
return raw
|
|
}
|
|
|
|
type byteEncoder struct {
|
|
table *[256]rune
|
|
}
|
|
|
|
func (e *byteEncoder) Decode(raw string) (text string) {
|
|
r := make([]rune, 0, len(raw))
|
|
for i := 0; i < len(raw); i++ {
|
|
r = append(r, e.table[raw[i]])
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
type byteRange struct {
|
|
low string
|
|
high string
|
|
}
|
|
|
|
type bfchar struct {
|
|
orig string
|
|
repl string
|
|
}
|
|
|
|
type bfrange struct {
|
|
lo string
|
|
hi string
|
|
dst Value
|
|
}
|
|
|
|
type cmap struct {
|
|
space [4][]byteRange // codespace range
|
|
bfrange []bfrange
|
|
bfchar []bfchar
|
|
}
|
|
|
|
func (m *cmap) Decode(raw string) (text string) {
|
|
var r []rune
|
|
Parse:
|
|
for len(raw) > 0 {
|
|
for n := 1; n <= 4 && n <= len(raw); n++ { // number of digits in character replacement (1-4 possible)
|
|
for _, space := range m.space[n-1] { // find matching codespace Ranges for number of digits
|
|
if space.low <= raw[:n] && raw[:n] <= space.high { // see if value is in range
|
|
text := raw[:n]
|
|
raw = raw[n:]
|
|
for _, bfchar := range m.bfchar { // check for matching bfchar
|
|
if len(bfchar.orig) == n && bfchar.orig == text {
|
|
r = append(r, []rune(utf16Decode(bfchar.repl))...)
|
|
continue Parse
|
|
}
|
|
}
|
|
for _, bfrange := range m.bfrange { // check for matching bfrange
|
|
if len(bfrange.lo) == n && bfrange.lo <= text && text <= bfrange.hi {
|
|
if bfrange.dst.Kind() == String {
|
|
s := bfrange.dst.RawString()
|
|
if bfrange.lo != text { // value isn't at the beginning of the range so scale result
|
|
b := []byte(s)
|
|
b[len(b)-1] += text[len(text)-1] - bfrange.lo[len(bfrange.lo)-1] // increment last byte by difference
|
|
s = string(b)
|
|
}
|
|
r = append(r, []rune(utf16Decode(s))...)
|
|
continue Parse
|
|
}
|
|
if bfrange.dst.Kind() == Array {
|
|
n := text[len(text)-1] - bfrange.lo[len(bfrange.lo)-1]
|
|
v := bfrange.dst.Index(int(n))
|
|
if v.Kind() == String {
|
|
s := v.RawString()
|
|
r = append(r, []rune(utf16Decode(s))...)
|
|
continue Parse
|
|
}
|
|
if DebugOn {
|
|
fmt.Printf("array %v\n", bfrange.dst)
|
|
}
|
|
} else {
|
|
if DebugOn {
|
|
fmt.Printf("unknown dst %v\n", bfrange.dst)
|
|
}
|
|
}
|
|
r = append(r, noRune)
|
|
continue Parse
|
|
}
|
|
}
|
|
r = append(r, noRune)
|
|
continue Parse
|
|
}
|
|
}
|
|
}
|
|
if DebugOn {
|
|
println("no code space found")
|
|
}
|
|
r = append(r, noRune)
|
|
raw = raw[1:]
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
func readCmap(toUnicode Value) *cmap {
|
|
n := -1
|
|
var m cmap
|
|
ok := true
|
|
Interpret(toUnicode, func(stk *Stack, op string) {
|
|
if !ok {
|
|
return
|
|
}
|
|
switch op {
|
|
case "findresource":
|
|
stk.Pop() // category
|
|
stk.Pop() // key
|
|
stk.Push(newDict())
|
|
case "begincmap":
|
|
stk.Push(newDict())
|
|
case "endcmap":
|
|
stk.Pop()
|
|
case "begincodespacerange":
|
|
n = int(stk.Pop().Int64())
|
|
case "endcodespacerange":
|
|
if n < 0 {
|
|
if DebugOn {
|
|
println("missing begincodespacerange")
|
|
}
|
|
ok = false
|
|
return
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
hi, lo := stk.Pop().RawString(), stk.Pop().RawString()
|
|
if len(lo) == 0 || len(lo) != len(hi) {
|
|
if DebugOn {
|
|
println("bad codespace range")
|
|
}
|
|
ok = false
|
|
return
|
|
}
|
|
m.space[len(lo)-1] = append(m.space[len(lo)-1], byteRange{lo, hi})
|
|
}
|
|
n = -1
|
|
case "beginbfchar":
|
|
n = int(stk.Pop().Int64())
|
|
case "endbfchar":
|
|
if n < 0 {
|
|
panic("missing beginbfchar")
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
repl, orig := stk.Pop().RawString(), stk.Pop().RawString()
|
|
m.bfchar = append(m.bfchar, bfchar{orig, repl})
|
|
}
|
|
case "beginbfrange":
|
|
n = int(stk.Pop().Int64())
|
|
case "endbfrange":
|
|
if n < 0 {
|
|
panic("missing beginbfrange")
|
|
}
|
|
for i := 0; i < n; i++ {
|
|
dst, srcHi, srcLo := stk.Pop(), stk.Pop().RawString(), stk.Pop().RawString()
|
|
m.bfrange = append(m.bfrange, bfrange{srcLo, srcHi, dst})
|
|
}
|
|
case "defineresource":
|
|
stk.Pop().Name() // category
|
|
value := stk.Pop()
|
|
stk.Pop().Name() // key
|
|
stk.Push(value)
|
|
default:
|
|
if DebugOn {
|
|
println("interp\t", op)
|
|
}
|
|
}
|
|
})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &m
|
|
}
|
|
|
|
type matrix [3][3]float64
|
|
|
|
var ident = matrix{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}
|
|
|
|
func (x matrix) mul(y matrix) matrix {
|
|
var z matrix
|
|
for i := 0; i < 3; i++ {
|
|
for j := 0; j < 3; j++ {
|
|
for k := 0; k < 3; k++ {
|
|
z[i][j] += x[i][k] * y[k][j]
|
|
}
|
|
}
|
|
}
|
|
return z
|
|
}
|
|
|
|
// A Text represents a single piece of text drawn on a page.
|
|
type Text struct {
|
|
Font string // the font used
|
|
FontSize float64 // the font size, in points (1/72 of an inch)
|
|
X float64 // the X coordinate, in points, increasing left to right
|
|
Y float64 // the Y coordinate, in points, increasing bottom to top
|
|
W float64 // the width of the text, in points
|
|
S string // the actual UTF-8 text
|
|
}
|
|
|
|
// A Rect represents a rectangle.
|
|
type Rect struct {
|
|
Min, Max Point
|
|
}
|
|
|
|
// A Point represents an X, Y pair.
|
|
type Point struct {
|
|
X float64
|
|
Y float64
|
|
}
|
|
|
|
// Content describes the basic content on a page: the text and any drawn rectangles.
|
|
type Content struct {
|
|
Text []Text
|
|
Rect []Rect
|
|
}
|
|
|
|
type gstate struct {
|
|
Tc float64
|
|
Tw float64
|
|
Th float64
|
|
Tl float64
|
|
Tf Font
|
|
Tfs float64
|
|
Tmode int
|
|
Trise float64
|
|
Tm matrix
|
|
Tlm matrix
|
|
Trm matrix
|
|
CTM matrix
|
|
}
|
|
|
|
// GetPlainText returns the page's all text without format.
|
|
// fonts can be passed in (to improve parsing performance) or left nil
|
|
func (p Page) GetPlainText(fonts map[string]*Font) (result string, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
result = ""
|
|
err = errors.New(fmt.Sprint(r))
|
|
}
|
|
}()
|
|
|
|
strm := p.V.Key("Contents")
|
|
var enc TextEncoding = &nopEncoder{}
|
|
|
|
if fonts == nil {
|
|
fonts = make(map[string]*Font)
|
|
for _, font := range p.Fonts() {
|
|
f := p.Font(font)
|
|
fonts[font] = &f
|
|
}
|
|
}
|
|
|
|
var textBuilder bytes.Buffer
|
|
showText := func(s string) {
|
|
for _, ch := range enc.Decode(s) {
|
|
_, err := textBuilder.WriteRune(ch)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
Interpret(strm, func(stk *Stack, op string) {
|
|
n := stk.Len()
|
|
args := make([]Value, n)
|
|
for i := n - 1; i >= 0; i-- {
|
|
args[i] = stk.Pop()
|
|
}
|
|
|
|
switch op {
|
|
default:
|
|
return
|
|
case "T*": // move to start of next line
|
|
showText("\n")
|
|
case "Tf": // set text font and size
|
|
if len(args) != 2 {
|
|
panic("bad TL")
|
|
}
|
|
if font, ok := fonts[args[0].Name()]; ok {
|
|
enc = font.Encoder()
|
|
} else {
|
|
enc = &nopEncoder{}
|
|
}
|
|
case "\"": // set spacing, move to next line, and show text
|
|
if len(args) != 3 {
|
|
panic("bad \" operator")
|
|
}
|
|
fallthrough
|
|
case "'": // move to next line and show text
|
|
if len(args) != 1 {
|
|
panic("bad ' operator")
|
|
}
|
|
fallthrough
|
|
case "Tj": // show text
|
|
if len(args) != 1 {
|
|
panic("bad Tj operator")
|
|
}
|
|
showText(args[0].RawString())
|
|
case "TJ": // show text, allowing individual glyph positioning
|
|
v := args[0]
|
|
for i := 0; i < v.Len(); i++ {
|
|
x := v.Index(i)
|
|
if x.Kind() == String {
|
|
showText(x.RawString())
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return textBuilder.String(), nil
|
|
}
|
|
|
|
// Column represents the contents of a column
|
|
type Column struct {
|
|
Position int64
|
|
Content TextVertical
|
|
}
|
|
|
|
// Columns is a list of column
|
|
type Columns []*Column
|
|
|
|
// GetTextByColumn returns the page's all text grouped by column
|
|
func (p Page) GetTextByColumn() (Columns, error) {
|
|
result := Columns{}
|
|
var err error
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
result = Columns{}
|
|
err = errors.New(fmt.Sprint(r))
|
|
}
|
|
}()
|
|
|
|
showText := func(enc TextEncoding, currentX, currentY float64, s string) {
|
|
var textBuilder bytes.Buffer
|
|
|
|
for _, ch := range enc.Decode(s) {
|
|
_, err := textBuilder.WriteRune(ch)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
text := Text{
|
|
S: textBuilder.String(),
|
|
X: currentX,
|
|
Y: currentY,
|
|
}
|
|
|
|
var currentColumn *Column
|
|
columnFound := false
|
|
for _, column := range result {
|
|
if int64(currentX) == column.Position {
|
|
currentColumn = column
|
|
columnFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !columnFound {
|
|
currentColumn = &Column{
|
|
Position: int64(currentX),
|
|
Content: TextVertical{},
|
|
}
|
|
result = append(result, currentColumn)
|
|
}
|
|
|
|
currentColumn.Content = append(currentColumn.Content, text)
|
|
}
|
|
|
|
p.walkTextBlocks(showText)
|
|
|
|
for _, column := range result {
|
|
sort.Sort(column.Content)
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].Position < result[j].Position
|
|
})
|
|
|
|
return result, err
|
|
}
|
|
|
|
// Row represents the contents of a row
|
|
type Row struct {
|
|
Position int64
|
|
Content TextHorizontal
|
|
}
|
|
|
|
// Rows is a list of rows
|
|
type Rows []*Row
|
|
|
|
// GetTextByRow returns the page's all text grouped by rows
|
|
func (p Page) GetTextByRow() (Rows, error) {
|
|
result := Rows{}
|
|
var err error
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
result = Rows{}
|
|
err = errors.New(fmt.Sprint(r))
|
|
}
|
|
}()
|
|
|
|
showText := func(enc TextEncoding, currentX, currentY float64, s string) {
|
|
var textBuilder bytes.Buffer
|
|
for _, ch := range enc.Decode(s) {
|
|
_, err := textBuilder.WriteRune(ch)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
// if DebugOn {
|
|
// fmt.Println(textBuilder.String())
|
|
// }
|
|
|
|
text := Text{
|
|
S: textBuilder.String(),
|
|
X: currentX,
|
|
Y: currentY,
|
|
}
|
|
|
|
var currentRow *Row
|
|
rowFound := false
|
|
for _, row := range result {
|
|
if int64(currentY) == row.Position {
|
|
currentRow = row
|
|
rowFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !rowFound {
|
|
currentRow = &Row{
|
|
Position: int64(currentY),
|
|
Content: TextHorizontal{},
|
|
}
|
|
result = append(result, currentRow)
|
|
}
|
|
|
|
currentRow.Content = append(currentRow.Content, text)
|
|
}
|
|
|
|
p.walkTextBlocks(showText)
|
|
|
|
for _, row := range result {
|
|
sort.Sort(row.Content)
|
|
}
|
|
|
|
sort.Slice(result, func(i, j int) bool {
|
|
return result[i].Position > result[j].Position
|
|
})
|
|
|
|
return result, err
|
|
}
|
|
|
|
func (p Page) walkTextBlocks(walker func(enc TextEncoding, x, y float64, s string)) {
|
|
strm := p.V.Key("Contents")
|
|
|
|
fonts := make(map[string]*Font)
|
|
for _, font := range p.Fonts() {
|
|
f := p.Font(font)
|
|
fonts[font] = &f
|
|
}
|
|
|
|
var enc TextEncoding = &nopEncoder{}
|
|
var currentX, currentY float64
|
|
Interpret(strm, func(stk *Stack, op string) {
|
|
n := stk.Len()
|
|
args := make([]Value, n)
|
|
for i := n - 1; i >= 0; i-- {
|
|
args[i] = stk.Pop()
|
|
}
|
|
|
|
// if DebugOn {
|
|
// fmt.Println(op, "->", args)
|
|
// }
|
|
|
|
switch op {
|
|
default:
|
|
return
|
|
case "T*": // move to start of next line
|
|
case "Tf": // set text font and size
|
|
if len(args) != 2 {
|
|
panic("bad TL")
|
|
}
|
|
|
|
if font, ok := fonts[args[0].Name()]; ok {
|
|
enc = font.Encoder()
|
|
} else {
|
|
enc = &nopEncoder{}
|
|
}
|
|
case "\"": // set spacing, move to next line, and show text
|
|
if len(args) != 3 {
|
|
panic("bad \" operator")
|
|
}
|
|
fallthrough
|
|
case "'": // move to next line and show text
|
|
if len(args) != 1 {
|
|
panic("bad ' operator")
|
|
}
|
|
fallthrough
|
|
case "Tj": // show text
|
|
if len(args) != 1 {
|
|
panic("bad Tj operator")
|
|
}
|
|
|
|
walker(enc, currentX, currentY, args[0].RawString())
|
|
case "TJ": // show text, allowing individual glyph positioning
|
|
v := args[0]
|
|
for i := 0; i < v.Len(); i++ {
|
|
x := v.Index(i)
|
|
if x.Kind() == String {
|
|
walker(enc, currentX, currentY, x.RawString())
|
|
}
|
|
}
|
|
case "Td":
|
|
walker(enc, currentX, currentY, "")
|
|
case "Tm":
|
|
currentX = args[4].Float64()
|
|
currentY = args[5].Float64()
|
|
}
|
|
})
|
|
}
|
|
|
|
// Content returns the page's content.
|
|
func (p Page) Content() Content {
|
|
strm := p.V.Key("Contents")
|
|
var enc TextEncoding = &nopEncoder{}
|
|
|
|
var g = gstate{
|
|
Th: 1,
|
|
CTM: ident,
|
|
}
|
|
|
|
var text []Text
|
|
showText := func(s string) {
|
|
n := 0
|
|
decoded := enc.Decode(s)
|
|
for _, ch := range decoded {
|
|
var w0 float64
|
|
if n < len(s) {
|
|
w0 = g.Tf.Width(int(s[n]))
|
|
}
|
|
n++
|
|
|
|
f := g.Tf.BaseFont()
|
|
if i := strings.Index(f, "+"); i >= 0 {
|
|
f = f[i+1:]
|
|
}
|
|
|
|
Trm := matrix{{g.Tfs * g.Th, 0, 0}, {0, g.Tfs, 0}, {0, g.Trise, 1}}.mul(g.Tm).mul(g.CTM)
|
|
text = append(text, Text{f, Trm[0][0], Trm[2][0], Trm[2][1], w0 / 1000 * Trm[0][0], string(ch)})
|
|
|
|
tx := w0/1000*g.Tfs + g.Tc
|
|
tx *= g.Th
|
|
g.Tm = matrix{{1, 0, 0}, {0, 1, 0}, {tx, 0, 1}}.mul(g.Tm)
|
|
}
|
|
}
|
|
|
|
var rect []Rect
|
|
var gstack []gstate
|
|
Interpret(strm, func(stk *Stack, op string) {
|
|
n := stk.Len()
|
|
args := make([]Value, n)
|
|
for i := n - 1; i >= 0; i-- {
|
|
args[i] = stk.Pop()
|
|
}
|
|
switch op {
|
|
default:
|
|
// if DebugOn {
|
|
// fmt.Println(op, args)
|
|
// }
|
|
return
|
|
|
|
case "cm": // update g.CTM
|
|
if len(args) != 6 {
|
|
panic("bad g.Tm")
|
|
}
|
|
var m matrix
|
|
for i := 0; i < 6; i++ {
|
|
m[i/2][i%2] = args[i].Float64()
|
|
}
|
|
m[2][2] = 1
|
|
g.CTM = m.mul(g.CTM)
|
|
|
|
case "gs": // set parameters from graphics state resource
|
|
//gs := p.Resources().Key("ExtGState").Key(args[0].Name())
|
|
//font := gs.Key("Font")
|
|
//if font.Kind() == Array && font.Len() == 2 {
|
|
// if DebugOn {
|
|
// fmt.Println("FONT", font)
|
|
// }
|
|
//}
|
|
|
|
case "f": // fill
|
|
case "g": // setgray
|
|
case "l": // lineto
|
|
case "m": // moveto
|
|
|
|
case "cs": // set colorspace non-stroking
|
|
case "scn": // set color non-stroking
|
|
|
|
case "re": // append rectangle to path
|
|
if len(args) != 4 {
|
|
panic("bad re")
|
|
}
|
|
x, y, w, h := args[0].Float64(), args[1].Float64(), args[2].Float64(), args[3].Float64()
|
|
rect = append(rect, Rect{Point{x, y}, Point{x + w, y + h}})
|
|
|
|
case "q": // save graphics state
|
|
gstack = append(gstack, g)
|
|
|
|
case "Q": // restore graphics state
|
|
n := len(gstack) - 1
|
|
g = gstack[n]
|
|
gstack = gstack[:n]
|
|
|
|
case "BT": // begin text (reset text matrix and line matrix)
|
|
g.Tm = ident
|
|
g.Tlm = g.Tm
|
|
|
|
case "ET": // end text
|
|
|
|
case "T*": // move to start of next line
|
|
x := matrix{{1, 0, 0}, {0, 1, 0}, {0, -g.Tl, 1}}
|
|
g.Tlm = x.mul(g.Tlm)
|
|
g.Tm = g.Tlm
|
|
|
|
case "Tc": // set character spacing
|
|
if len(args) != 1 {
|
|
panic("bad g.Tc")
|
|
}
|
|
g.Tc = args[0].Float64()
|
|
|
|
case "TD": // move text position and set leading
|
|
if len(args) != 2 {
|
|
panic("bad Td")
|
|
}
|
|
g.Tl = -args[1].Float64()
|
|
fallthrough
|
|
case "Td": // move text position
|
|
if len(args) != 2 {
|
|
panic("bad Td")
|
|
}
|
|
tx := args[0].Float64()
|
|
ty := args[1].Float64()
|
|
x := matrix{{1, 0, 0}, {0, 1, 0}, {tx, ty, 1}}
|
|
g.Tlm = x.mul(g.Tlm)
|
|
g.Tm = g.Tlm
|
|
|
|
case "Tf": // set text font and size
|
|
if len(args) != 2 {
|
|
panic("bad TL")
|
|
}
|
|
f := args[0].Name()
|
|
g.Tf = p.Font(f)
|
|
enc = g.Tf.Encoder()
|
|
if enc == nil {
|
|
if DebugOn {
|
|
println("no cmap for", f)
|
|
}
|
|
enc = &nopEncoder{}
|
|
}
|
|
g.Tfs = args[1].Float64()
|
|
|
|
case "\"": // set spacing, move to next line, and show text
|
|
if len(args) != 3 {
|
|
panic("bad \" operator")
|
|
}
|
|
g.Tw = args[0].Float64()
|
|
g.Tc = args[1].Float64()
|
|
args = args[2:]
|
|
fallthrough
|
|
case "'": // move to next line and show text
|
|
if len(args) != 1 {
|
|
panic("bad ' operator")
|
|
}
|
|
x := matrix{{1, 0, 0}, {0, 1, 0}, {0, -g.Tl, 1}}
|
|
g.Tlm = x.mul(g.Tlm)
|
|
g.Tm = g.Tlm
|
|
fallthrough
|
|
case "Tj": // show text
|
|
if len(args) != 1 {
|
|
panic("bad Tj operator")
|
|
}
|
|
showText(args[0].RawString())
|
|
|
|
case "TJ": // show text, allowing individual glyph positioning
|
|
v := args[0]
|
|
for i := 0; i < v.Len(); i++ {
|
|
x := v.Index(i)
|
|
if x.Kind() == String {
|
|
showText(x.RawString())
|
|
} else {
|
|
tx := -x.Float64() / 1000 * g.Tfs * g.Th
|
|
g.Tm = matrix{{1, 0, 0}, {0, 1, 0}, {tx, 0, 1}}.mul(g.Tm)
|
|
}
|
|
}
|
|
showText("\n")
|
|
|
|
case "TL": // set text leading
|
|
if len(args) != 1 {
|
|
panic("bad TL")
|
|
}
|
|
g.Tl = args[0].Float64()
|
|
|
|
case "Tm": // set text matrix and line matrix
|
|
if len(args) != 6 {
|
|
panic("bad g.Tm")
|
|
}
|
|
var m matrix
|
|
for i := 0; i < 6; i++ {
|
|
m[i/2][i%2] = args[i].Float64()
|
|
}
|
|
m[2][2] = 1
|
|
g.Tm = m
|
|
g.Tlm = m
|
|
|
|
case "Tr": // set text rendering mode
|
|
if len(args) != 1 {
|
|
panic("bad Tr")
|
|
}
|
|
g.Tmode = int(args[0].Int64())
|
|
|
|
case "Ts": // set text rise
|
|
if len(args) != 1 {
|
|
panic("bad Ts")
|
|
}
|
|
g.Trise = args[0].Float64()
|
|
|
|
case "Tw": // set word spacing
|
|
if len(args) != 1 {
|
|
panic("bad g.Tw")
|
|
}
|
|
g.Tw = args[0].Float64()
|
|
|
|
case "Tz": // set horizontal text scaling
|
|
if len(args) != 1 {
|
|
panic("bad Tz")
|
|
}
|
|
g.Th = args[0].Float64() / 100
|
|
}
|
|
})
|
|
return Content{text, rect}
|
|
}
|
|
|
|
// TextVertical implements sort.Interface for sorting
|
|
// a slice of Text values in vertical order, top to bottom,
|
|
// and then left to right within a line.
|
|
type TextVertical []Text
|
|
|
|
func (x TextVertical) Len() int { return len(x) }
|
|
func (x TextVertical) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
|
func (x TextVertical) Less(i, j int) bool {
|
|
if x[i].Y != x[j].Y {
|
|
return x[i].Y > x[j].Y
|
|
}
|
|
return x[i].X < x[j].X
|
|
}
|
|
|
|
// TextHorizontal implements sort.Interface for sorting
|
|
// a slice of Text values in horizontal order, left to right,
|
|
// and then top to bottom within a column.
|
|
type TextHorizontal []Text
|
|
|
|
func (x TextHorizontal) Len() int { return len(x) }
|
|
func (x TextHorizontal) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
|
func (x TextHorizontal) Less(i, j int) bool {
|
|
if x[i].X != x[j].X {
|
|
return x[i].X < x[j].X
|
|
}
|
|
return x[i].Y > x[j].Y
|
|
}
|
|
|
|
// An Outline is a tree describing the outline (also known as the table of contents)
|
|
// of a document.
|
|
type Outline struct {
|
|
Title string // title for this element
|
|
Child []Outline // child elements
|
|
}
|
|
|
|
// Outline returns the document outline.
|
|
// The Outline returned is the root of the outline tree and typically has no Title itself.
|
|
// That is, the children of the returned root are the top-level entries in the outline.
|
|
func (r *Reader) Outline() Outline {
|
|
return buildOutline(r.Trailer().Key("Root").Key("Outlines"))
|
|
}
|
|
|
|
func buildOutline(entry Value) Outline {
|
|
var x Outline
|
|
x.Title = entry.Key("Title").Text()
|
|
for child := entry.Key("First"); child.Kind() == Dict; child = child.Key("Next") {
|
|
x.Child = append(x.Child, buildOutline(child))
|
|
}
|
|
return x
|
|
}
|
|
|