2018-03-22 14:03:47 +00:00
|
|
|
|
/*
|
|
|
|
|
* This file is subject to the terms and conditions defined in
|
|
|
|
|
* file 'LICENSE.md', which is part of this source code package.
|
|
|
|
|
*/
|
|
|
|
|
|
2018-03-22 13:01:04 +00:00
|
|
|
|
package extractor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
2018-07-13 17:40:27 +10:00
|
|
|
|
"fmt"
|
2018-10-09 11:49:59 +11:00
|
|
|
|
"math"
|
2018-10-30 21:55:30 +11:00
|
|
|
|
"path/filepath"
|
|
|
|
|
"runtime"
|
2018-08-22 12:29:34 +10:00
|
|
|
|
"sort"
|
2018-07-13 17:40:27 +10:00
|
|
|
|
"strings"
|
2018-11-28 18:06:03 +11:00
|
|
|
|
"unicode"
|
2018-03-22 13:01:04 +00:00
|
|
|
|
|
|
|
|
|
"github.com/unidoc/unidoc/common"
|
|
|
|
|
"github.com/unidoc/unidoc/pdf/contentstream"
|
2018-07-15 16:28:56 +10:00
|
|
|
|
"github.com/unidoc/unidoc/pdf/core"
|
2018-11-30 16:53:48 +00:00
|
|
|
|
"github.com/unidoc/unidoc/pdf/internal/transform"
|
2018-03-22 13:01:04 +00:00
|
|
|
|
"github.com/unidoc/unidoc/pdf/model"
|
2018-11-28 18:06:03 +11:00
|
|
|
|
"golang.org/x/text/unicode/norm"
|
2018-03-22 13:01:04 +00:00
|
|
|
|
)
|
|
|
|
|
|
2018-06-27 12:25:59 +10:00
|
|
|
|
// ExtractText processes and extracts all text data in content streams and returns as a string.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// It takes into account character encodings in the PDF file, which are decoded by
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// CharcodeBytesToUnicode.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// Characters that can't be decoded are replaced with MissingCodeRune ('\ufffd' = <20>).
|
2018-03-22 13:01:04 +00:00
|
|
|
|
func (e *Extractor) ExtractText() (string, error) {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
text, _, _, err := e.ExtractTextWithStats()
|
2018-07-13 17:40:27 +10:00
|
|
|
|
return text, err
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-30 16:53:48 +00:00
|
|
|
|
// ExtractTextWithStats works like ExtractText but returns the number of characters in the output (`numChars`) and the
|
|
|
|
|
// the number of characters that were not decoded (`numMisses`).
|
|
|
|
|
func (e *Extractor) ExtractTextWithStats() (extracted string, numChars int, numMisses int, err error) {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
textList, numChars, numMisses, err := e.ExtractXYText()
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if err != nil {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
return "", numChars, numMisses, err
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-13 17:40:27 +10:00
|
|
|
|
return textList.ToText(), numChars, numMisses, nil
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ExtractXYText returns the text contents of `e` as a TextList.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (e *Extractor) ExtractXYText() (*TextList, int, int, error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
textList := &TextList{}
|
|
|
|
|
state := newTextState()
|
2018-07-13 17:40:27 +10:00
|
|
|
|
fontStack := fontStacker{}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
var to *textObject
|
2018-03-22 13:01:04 +00:00
|
|
|
|
|
|
|
|
|
cstreamParser := contentstream.NewContentStreamParser(e.contents)
|
|
|
|
|
operations, err := cstreamParser.Parse()
|
|
|
|
|
if err != nil {
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Debug("ERROR: ExtractXYText parse failed. err=%v", err)
|
2018-07-13 17:40:27 +10:00
|
|
|
|
return textList, state.numChars, state.numMisses, err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
processor := contentstream.NewContentStreamProcessor(*operations)
|
|
|
|
|
|
|
|
|
|
processor.AddHandler(contentstream.HandlerConditionEnumAllOperands, "",
|
2018-06-27 16:31:28 +10:00
|
|
|
|
func(op *contentstream.ContentStreamOperation, gs contentstream.GraphicsState,
|
|
|
|
|
resources *model.PdfPageResources) error {
|
|
|
|
|
|
2018-03-22 13:01:04 +00:00
|
|
|
|
operand := op.Operand
|
2018-06-27 16:31:28 +10:00
|
|
|
|
|
2018-03-22 13:01:04 +00:00
|
|
|
|
switch operand {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
case "q":
|
|
|
|
|
if !fontStack.empty() {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
common.Log.Trace("Save font state: %s\n%s",
|
2018-07-13 17:40:27 +10:00
|
|
|
|
fontStack.peek(), fontStack.String())
|
|
|
|
|
fontStack.push(fontStack.peek())
|
|
|
|
|
}
|
|
|
|
|
if state.Tf != nil {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
common.Log.Trace("Save font state: %s\n->%s\n%s",
|
2018-07-13 17:40:27 +10:00
|
|
|
|
fontStack.peek(), state.Tf, fontStack.String())
|
|
|
|
|
fontStack.push(state.Tf)
|
|
|
|
|
}
|
|
|
|
|
case "Q":
|
|
|
|
|
if !fontStack.empty() {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
common.Log.Trace("Restore font state: %s\n->%s\n%s",
|
2018-07-13 17:40:27 +10:00
|
|
|
|
fontStack.peek(), fontStack.get(-2), fontStack.String())
|
|
|
|
|
fontStack.pop()
|
|
|
|
|
}
|
|
|
|
|
if len(fontStack) >= 2 {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
common.Log.Trace("Restore font state: %s\n->%s\n%s",
|
2018-07-13 17:40:27 +10:00
|
|
|
|
state.Tf, fontStack.peek(), fontStack.String())
|
|
|
|
|
state.Tf = fontStack.pop()
|
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
case "BT": // Begin text
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// Begin a text object, initializing the text matrix, Tm, and the text line matrix,
|
|
|
|
|
// Tlm, to the identity matrix. Text objects shall not be nested; a second BT shall
|
|
|
|
|
// not appear before an ET.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if to != nil {
|
|
|
|
|
common.Log.Debug("BT called while in a text object")
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-07-13 17:40:27 +10:00
|
|
|
|
to = newTextObject(e, gs, &state, &fontStack)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
case "ET": // End Text
|
|
|
|
|
*textList = append(*textList, to.Texts...)
|
|
|
|
|
to = nil
|
|
|
|
|
case "T*": // Move to start of next text line
|
|
|
|
|
to.nextLine()
|
|
|
|
|
case "Td": // Move text location
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 2, true); !ok {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
x, y, err := toFloatXY(op.Params)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.moveText(x, y)
|
2018-11-26 08:09:52 +11:00
|
|
|
|
case "TD": // Move text location and set leading.
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 2, true); !ok {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
x, y, err := toFloatXY(op.Params)
|
|
|
|
|
if err != nil {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
to.moveTextSetLeading(x, y)
|
|
|
|
|
case "Tj": // Show text
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tj op=%s err=%v", op, err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-07-21 21:20:39 +10:00
|
|
|
|
charcodes, ok := core.GetStringBytes(op.Params[0])
|
|
|
|
|
if !ok {
|
|
|
|
|
common.Log.Debug("ERROR: Tj op=%s GetStringBytes failed", op)
|
|
|
|
|
return core.ErrTypeError
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return to.showText(charcodes)
|
|
|
|
|
case "TJ": // Show text with adjustable spacing
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: TJ err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-07-25 13:19:09 +10:00
|
|
|
|
args, ok := core.GetArray(op.Params[0])
|
2018-07-21 21:20:39 +10:00
|
|
|
|
if !ok {
|
|
|
|
|
common.Log.Debug("ERROR: Tj op=%s GetArrayVal failed", op)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return to.showTextAdjusted(args)
|
|
|
|
|
case "'": // Move to next line and show text
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: ' err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-07-21 21:20:39 +10:00
|
|
|
|
charcodes, ok := core.GetStringBytes(op.Params[0])
|
2018-03-22 13:01:04 +00:00
|
|
|
|
if !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: ' op=%s GetStringBytes failed", op)
|
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
to.nextLine()
|
|
|
|
|
return to.showText(charcodes)
|
|
|
|
|
case `"`: // Set word and character spacing, move to next line, and show text
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: \" err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
x, y, err := toFloatXY(op.Params[:2])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
charcodes, ok := core.GetStringBytes(op.Params[2])
|
2018-03-22 13:01:04 +00:00
|
|
|
|
if !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: \" op=%s GetStringBytes failed", op)
|
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.setCharSpacing(x)
|
|
|
|
|
to.setWordSpacing(y)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
to.nextLine()
|
|
|
|
|
return to.showText(charcodes)
|
|
|
|
|
case "TL": // Set text leading
|
2018-07-15 16:45:47 +10:00
|
|
|
|
y, err := floatParam(op)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if err != nil {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: TL err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.setTextLeading(y)
|
|
|
|
|
case "Tc": // Set character spacing
|
2018-07-15 16:45:47 +10:00
|
|
|
|
y, err := floatParam(op)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if err != nil {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tc err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.setCharSpacing(y)
|
|
|
|
|
case "Tf": // Set font
|
2018-11-21 13:14:11 +11:00
|
|
|
|
if to == nil {
|
|
|
|
|
// This is needed for ~/testdata/26-Hazard-Thermal-environment.pdf
|
|
|
|
|
to = newTextObject(e, gs, &state, &fontStack)
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 2, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tf err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-07-21 21:20:39 +10:00
|
|
|
|
name, ok := core.GetNameVal(op.Params[0])
|
2018-03-22 13:01:04 +00:00
|
|
|
|
if !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tf op=%s GetNameVal failed", op)
|
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
size, err := core.GetNumberAsFloat(op.Params[1])
|
2018-07-21 21:20:39 +10:00
|
|
|
|
if !ok {
|
|
|
|
|
common.Log.Debug("ERROR: Tf op=%s GetFloatVal failed. err=%v", op, err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
err = to.setFont(name, size)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
case "Tm": // Set text matrix
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 6, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tm err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
floats, err := core.GetNumbersAsFloat(op.Params)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if err != nil {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.setTextMatrix(floats)
|
|
|
|
|
case "Tr": // Set text rendering mode
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tr err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-07-21 21:20:39 +10:00
|
|
|
|
mode, ok := core.GetIntVal(op.Params[0])
|
2018-03-22 13:01:04 +00:00
|
|
|
|
if !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Tr op=%s GetIntVal failed", op)
|
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
to.setTextRenderMode(mode)
|
|
|
|
|
case "Ts": // Set text rise
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("ERROR: Ts err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
y, err := core.GetNumberAsFloat(op.Params[0])
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if err != nil {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.setTextRise(y)
|
|
|
|
|
case "Tw": // Set word spacing
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
y, err := core.GetNumberAsFloat(op.Params[0])
|
2018-03-22 13:01:04 +00:00
|
|
|
|
if err != nil {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
2018-07-21 21:20:39 +10:00
|
|
|
|
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
to.setWordSpacing(y)
|
|
|
|
|
case "Tz": // Set horizontal scaling
|
2018-07-15 16:28:56 +10:00
|
|
|
|
if ok, err := to.checkOp(op, 1, true); !ok {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
y, err := core.GetNumberAsFloat(op.Params[0])
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if err != nil {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
common.Log.Debug("ERROR: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
to.setHorizScaling(y)
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
err = processor.Process(e.resources)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if err != nil {
|
2018-11-18 17:21:30 +11:00
|
|
|
|
common.Log.Debug("ERROR: Processing: err=%v", err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-13 17:40:27 +10:00
|
|
|
|
return textList, state.numChars, state.numMisses, err
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
// Text operators
|
|
|
|
|
//
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// moveText "Td" Moves start of text by `tx`,`ty`.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Move to the start of the next line, offset from the start of the current line by (tx, ty).
|
|
|
|
|
// tx and ty are in unscaled text space units.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) moveText(tx, ty float64) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.moveTo(tx, ty)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// moveTextSetLeading "TD" Move text location and set leading.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Move to the start of the next line, offset from the start of the current line by (tx, ty). As a
|
|
|
|
|
// side effect, this operator shall set the leading parameter in the text state. This operator shall
|
|
|
|
|
// have the same effect as this code:
|
|
|
|
|
// −ty TL
|
|
|
|
|
// tx ty Td
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) moveTextSetLeading(tx, ty float64) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.State.Tl = -ty
|
|
|
|
|
to.moveTo(tx, ty)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// nextLine "T*"" Moves start of text `Line` to next text line
|
|
|
|
|
// Move to the start of the next line. This operator has the same effect as the code
|
|
|
|
|
// 0 -Tl Td
|
|
|
|
|
// where Tl denotes the current leading parameter in the text state. The negative of Tl is used
|
|
|
|
|
// here because Tl is the text leading expressed as a positive number. Going to the next line
|
|
|
|
|
// entails decreasing the y coordinate. (page 250)
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) nextLine() {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.moveTo(0, -to.State.Tl)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 21:19:02 +11:00
|
|
|
|
// setTextMatrix "Tm".
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Set the text matrix, Tm, and the text line matrix, Tlm to the Matrix specified by the 6 numbers
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// in `f` (page 250).
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setTextMatrix(f []float64) {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
if len(f) != 6 {
|
|
|
|
|
common.Log.Debug("ERROR: len(f) != 6 (%d)", len(f))
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
a, b, c, d, tx, ty := f[0], f[1], f[2], f[3], f[4], f[5]
|
2018-11-30 16:53:48 +00:00
|
|
|
|
to.Tm = transform.NewMatrix(a, b, c, d, tx, ty)
|
2018-11-27 13:37:12 +11:00
|
|
|
|
to.Tlm = to.Tm
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// showText "Tj". Show a text string.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) showText(charcodes []byte) error {
|
2018-07-02 16:46:43 +10:00
|
|
|
|
return to.renderText(charcodes)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// showTextAdjusted "TJ". Show text with adjustable spacing.
|
2018-07-25 13:19:09 +10:00
|
|
|
|
func (to *textObject) showTextAdjusted(args *core.PdfObjectArray) error {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
vertical := false
|
2018-07-25 13:19:09 +10:00
|
|
|
|
for _, o := range args.Elements() {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
switch o.(type) {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
case *core.PdfObjectFloat, *core.PdfObjectInteger:
|
2018-08-22 12:29:34 +10:00
|
|
|
|
x, err := core.GetNumberAsFloat(o)
|
|
|
|
|
if err != nil {
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Debug("ERROR: showTextAdjusted. Bad numerical arg. o=%s args=%+v", o, args)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
return err
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
dx, dy := -x*0.001*to.State.Tfs, 0.0
|
|
|
|
|
if vertical {
|
|
|
|
|
dy, dx = dx, dy
|
|
|
|
|
}
|
2018-11-30 16:53:48 +00:00
|
|
|
|
td := translationMatrix(transform.Point{X: dx, Y: dy})
|
2018-11-19 14:19:50 +11:00
|
|
|
|
to.Tm = td.Mult(to.Tm)
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Trace("showTextAdjusted: dx,dy=%3f,%.3f Tm=%s", dx, dy, to.Tm)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
case *core.PdfObjectString:
|
2018-07-21 21:20:39 +10:00
|
|
|
|
charcodes, ok := core.GetStringBytes(o)
|
|
|
|
|
if !ok {
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Trace("showTextAdjusted: Bad string arg. o=%s args=%+v", o, args)
|
2018-07-21 21:20:39 +10:00
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.renderText(charcodes)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
default:
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Debug("ERROR: showTextAdjusted. Unexpected type (%T) args=%+v", o, args)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setTextLeading "TL". Set text leading.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setTextLeading(y float64) {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
if to == nil || to.State == nil {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
to.State.Tl = y
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setCharSpacing "Tc". Set character spacing.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setCharSpacing(x float64) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
to.State.Tc = x
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setFont "Tf". Set font.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setFont(name string, size float64) error {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
font, err := to.getFont(name)
|
2018-07-03 14:26:42 +10:00
|
|
|
|
if err == nil {
|
|
|
|
|
to.State.Tf = font
|
2018-07-13 17:40:27 +10:00
|
|
|
|
if len(*to.fontStack) == 0 {
|
|
|
|
|
to.fontStack.push(font)
|
|
|
|
|
} else {
|
|
|
|
|
(*to.fontStack)[len(*to.fontStack)-1] = font
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
} else if err == model.ErrFontNotSupported {
|
2018-07-24 21:32:02 +10:00
|
|
|
|
// XXX: Do we need to handle this case in a special way?
|
2018-07-04 18:00:37 +10:00
|
|
|
|
return err
|
2018-07-03 14:26:42 +10:00
|
|
|
|
} else {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.State.Tfs = size
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setTextRenderMode "Tr". Set text rendering mode.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setTextRenderMode(mode int) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
to.State.Tmode = RenderMode(mode)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setTextRise "Ts". Set text rise.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setTextRise(y float64) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
to.State.Trise = y
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setWordSpacing "Tw". Set word spacing.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setWordSpacing(y float64) {
|
2018-11-22 22:01:04 +11:00
|
|
|
|
if to == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-11-18 17:21:30 +11:00
|
|
|
|
to.State.Tw = y
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// setHorizScaling "Tz". Set horizontal scaling.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) setHorizScaling(y float64) {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
to.State.Th = y
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 23:25:17 +00:00
|
|
|
|
// floatParam returns the single float parameter of operator `op`, or an error if it doesn't have
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// a single float parameter or we aren't in a text stream.
|
2018-07-15 16:45:47 +10:00
|
|
|
|
func floatParam(op *contentstream.ContentStreamOperation) (float64, error) {
|
|
|
|
|
if len(op.Params) != 1 {
|
2018-11-21 13:14:11 +11:00
|
|
|
|
err := errors.New("incorrect parameter count")
|
2018-07-15 16:45:47 +10:00
|
|
|
|
common.Log.Debug("ERROR: %#q should have %d input params, got %d %+v",
|
|
|
|
|
op.Operand, 1, len(op.Params), op.Params)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return 0.0, err
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return core.GetNumberAsFloat(op.Params[0])
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// checkOp returns true if we are in a text stream and `op` has `numParams` params.
|
|
|
|
|
// If `hard` is true and the number of params don't match, an error is returned.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) checkOp(op *contentstream.ContentStreamOperation, numParams int,
|
2018-07-15 16:28:56 +10:00
|
|
|
|
hard bool) (ok bool, err error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if to == nil {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
var params []core.PdfObject
|
2018-11-21 13:14:11 +11:00
|
|
|
|
if numParams > 0 {
|
|
|
|
|
params = op.Params
|
|
|
|
|
if len(params) > numParams {
|
|
|
|
|
params = params[:numParams]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
common.Log.Debug("%#q operand outside text. params=%+v", op.Operand, params)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
if numParams >= 0 {
|
|
|
|
|
if len(op.Params) != numParams {
|
|
|
|
|
if hard {
|
2018-11-21 13:14:11 +11:00
|
|
|
|
err = errors.New("incorrect parameter count")
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
common.Log.Debug("ERROR: %#q should have %d input params, got %d %+v",
|
2018-06-27 16:31:28 +10:00
|
|
|
|
op.Operand, numParams, len(op.Params), op.Params)
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return false, err
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return true, nil
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// fontStacker is the PDF font stack implementation.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
type fontStacker []*model.PdfFont
|
|
|
|
|
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// String returns a string describing the current state of the font stack.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) String() string {
|
|
|
|
|
parts := []string{"---- font stack"}
|
|
|
|
|
for i, font := range *fontStack {
|
|
|
|
|
s := "<nil>"
|
|
|
|
|
if font != nil {
|
|
|
|
|
s = font.String()
|
|
|
|
|
}
|
|
|
|
|
parts = append(parts, fmt.Sprintf("\t%2d: %s", i, s))
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, "\n")
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
|
|
|
|
// push pushes `font` onto the font stack.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) push(font *model.PdfFont) {
|
|
|
|
|
*fontStack = append(*fontStack, font)
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// pop pops and returns the element on the top of the font stack if there is one or nil if there isn't.
|
2018-07-15 16:28:56 +10:00
|
|
|
|
func (fontStack *fontStacker) pop() *model.PdfFont {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
if fontStack.empty() {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return nil
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
font := (*fontStack)[len(*fontStack)-1]
|
2018-07-13 17:40:27 +10:00
|
|
|
|
*fontStack = (*fontStack)[:len(*fontStack)-1]
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return font
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// peek returns the element on the top of the font stack if there is one or nil if there isn't.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (fontStack *fontStacker) peek() *model.PdfFont {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
if fontStack.empty() {
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return nil
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return (*fontStack)[len(*fontStack)-1]
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// get returns the `idx`'th element of the font stack if there is one or nil if there isn't.
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// idx = 0: bottom of font stack
|
|
|
|
|
// idx = len(fontstack) - 1: top of font stack
|
|
|
|
|
// idx = -n is same as dx = len(fontstack) - n, so fontstack.get(-1) is same as fontstack.peek()
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (fontStack *fontStacker) get(idx int) *model.PdfFont {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
if idx < 0 {
|
|
|
|
|
idx += fontStack.size()
|
|
|
|
|
}
|
|
|
|
|
if idx < 0 || idx > fontStack.size()-1 {
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return nil
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return (*fontStack)[idx]
|
2018-07-13 17:40:27 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
|
|
|
|
// empty returns true if the font stack is empty.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) empty() bool {
|
|
|
|
|
return len(*fontStack) == 0
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
|
|
|
|
// size returns the number of elements in the font stack.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) size() int {
|
|
|
|
|
return len(*fontStack)
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// 9.3 Text State Parameters and Operators (page 243)
|
|
|
|
|
// Some of these parameters are expressed in unscaled text space units. This means that they shall
|
|
|
|
|
// be specified in a coordinate system that shall be defined by the text matrix, Tm but shall not be
|
|
|
|
|
// scaled by the font size parameter, Tfs.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
|
|
|
|
|
// textState represents the text state.
|
|
|
|
|
type textState struct {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
Tc float64 // Character spacing. Unscaled text space units.
|
|
|
|
|
Tw float64 // Word spacing. Unscaled text space units.
|
2018-09-20 11:49:44 +10:00
|
|
|
|
Th float64 // Horizontal scaling.
|
|
|
|
|
Tl float64 // Leading. Unscaled text space units. Used by TD,T*,'," see Table 108.
|
|
|
|
|
Tfs float64 // Text font size.
|
|
|
|
|
Tmode RenderMode // Text rendering mode.
|
|
|
|
|
Trise float64 // Text rise. Unscaled text space units. Set by Ts.
|
|
|
|
|
Tf *model.PdfFont // Text font.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
// For debugging
|
|
|
|
|
numChars int
|
|
|
|
|
numMisses int
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 9.4.1 General (page 248)
|
|
|
|
|
// A PDF text object consists of operators that may show text strings, move the text position, and
|
|
|
|
|
// set text state and certain other parameters. In addition, two parameters may be specified only
|
|
|
|
|
// within a text object and shall not persist from one text object to the next:
|
2018-06-28 11:11:43 +10:00
|
|
|
|
// • Tm, the text matrix
|
|
|
|
|
// • Tlm, the text line matrix
|
2018-06-27 16:31:28 +10:00
|
|
|
|
//
|
|
|
|
|
// Text space is converted to device space by this transform (page 252)
|
2018-09-20 11:49:44 +10:00
|
|
|
|
// Trm is the text rendering matrix
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// | Tfs x Th 0 0 |
|
|
|
|
|
// Trm = | 0 Tfs 0 | × Tm × CTM
|
|
|
|
|
// | 0 Trise 1 |
|
2018-09-20 11:49:44 +10:00
|
|
|
|
// This corresponds to the following code in renderText()
|
|
|
|
|
// trm := stateMatrix.Mult(to.Tm).Mult(to.gs.CTM))
|
2018-07-25 12:00:49 +10:00
|
|
|
|
|
|
|
|
|
// textObject represents a PDF text object.
|
|
|
|
|
type textObject struct {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
e *Extractor
|
|
|
|
|
gs contentstream.GraphicsState
|
|
|
|
|
fontStack *fontStacker
|
2018-07-25 12:00:49 +10:00
|
|
|
|
State *textState
|
2018-11-30 16:53:48 +00:00
|
|
|
|
Tm transform.Matrix // Text matrix. For the character pointer.
|
|
|
|
|
Tlm transform.Matrix // Text line matrix. For the start of line pointer.
|
|
|
|
|
Texts []XYText // Text gets written here.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// newTextState returns a default textState.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func newTextState() textState {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
return textState{
|
|
|
|
|
Th: 100,
|
|
|
|
|
Tmode: RenderModeFill,
|
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// newTextObject returns a default textObject.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func newTextObject(e *Extractor, gs contentstream.GraphicsState, state *textState,
|
|
|
|
|
fontStack *fontStacker) *textObject {
|
|
|
|
|
return &textObject{
|
2018-07-13 17:40:27 +10:00
|
|
|
|
e: e,
|
|
|
|
|
gs: gs,
|
|
|
|
|
fontStack: fontStack,
|
|
|
|
|
State: state,
|
2018-11-30 16:53:48 +00:00
|
|
|
|
Tm: transform.IdentityMatrix(),
|
|
|
|
|
Tlm: transform.IdentityMatrix(),
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 23:25:17 +00:00
|
|
|
|
// renderText processes and renders byte array `data` for extraction purposes.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) renderText(data []byte) error {
|
2018-09-20 11:49:44 +10:00
|
|
|
|
font := to.getCurrentFont()
|
2018-09-18 12:18:04 +10:00
|
|
|
|
|
2018-10-30 21:55:30 +11:00
|
|
|
|
charcodes := font.BytesToCharcodes(data)
|
|
|
|
|
|
|
|
|
|
runes, numChars, numMisses := font.CharcodesToUnicode(charcodes)
|
2018-11-28 18:06:03 +11:00
|
|
|
|
if numMisses > 0 {
|
|
|
|
|
common.Log.Debug("renderText: numChars=%d numMisses=%d", numChars, numMisses)
|
|
|
|
|
}
|
2018-10-30 21:55:30 +11:00
|
|
|
|
|
2018-07-25 12:00:49 +10:00
|
|
|
|
to.State.numChars += numChars
|
|
|
|
|
to.State.numMisses += numMisses
|
|
|
|
|
|
2018-09-20 11:49:44 +10:00
|
|
|
|
state := to.State
|
|
|
|
|
tfs := state.Tfs
|
|
|
|
|
th := state.Th / 100.0
|
2018-10-09 11:49:59 +11:00
|
|
|
|
spaceMetrics, err := font.GetRuneCharMetrics(' ')
|
|
|
|
|
if err != nil {
|
|
|
|
|
spaceMetrics, _ = model.DefaultFont().GetRuneCharMetrics(' ')
|
|
|
|
|
}
|
|
|
|
|
spaceWidth := spaceMetrics.Wx * glyphTextRatio
|
2018-11-12 11:04:09 +11:00
|
|
|
|
common.Log.Trace("spaceWidth=%.2f text=%q font=%s fontSize=%.1f", spaceWidth, runes, font, tfs)
|
2018-09-20 11:49:44 +10:00
|
|
|
|
|
2018-11-30 16:53:48 +00:00
|
|
|
|
stateMatrix := transform.NewMatrix(
|
2018-09-20 11:49:44 +10:00
|
|
|
|
tfs*th, 0,
|
|
|
|
|
0, tfs,
|
|
|
|
|
0, state.Trise)
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Trace("renderText: %d codes=%+v runes=%q", len(charcodes), charcodes, runes)
|
2018-11-18 17:21:30 +11:00
|
|
|
|
|
2018-10-30 21:55:30 +11:00
|
|
|
|
for i, r := range runes {
|
2018-11-28 18:06:03 +11:00
|
|
|
|
// XXX(peterwilliams97) Need to find and fix cases where this happens.
|
|
|
|
|
if r == "\x00" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-30 21:55:30 +11:00
|
|
|
|
code := charcodes[i]
|
2018-09-20 11:49:44 +10:00
|
|
|
|
// The location of the text on the page in device coordinates is given by trm, the text
|
|
|
|
|
// rendering matrix.
|
|
|
|
|
trm := stateMatrix.Mult(to.Tm).Mult(to.gs.CTM)
|
|
|
|
|
|
|
|
|
|
// calculate the text location displacement due to writing `r`. We will use this to update
|
|
|
|
|
// to.Tm
|
|
|
|
|
|
|
|
|
|
// w is the unscaled movement at the end of a word.
|
|
|
|
|
w := 0.0
|
2018-10-30 21:55:30 +11:00
|
|
|
|
if r == " " {
|
2018-09-20 11:49:44 +10:00
|
|
|
|
w = state.Tw
|
|
|
|
|
}
|
2018-10-09 11:49:59 +11:00
|
|
|
|
|
2018-10-30 21:55:30 +11:00
|
|
|
|
m, ok := font.GetCharMetrics(code)
|
|
|
|
|
if !ok {
|
2018-11-08 15:20:12 +11:00
|
|
|
|
common.Log.Debug("ERROR: No metric for code=%d r=0x%04x=%+q %s", code, r, r, font)
|
2018-10-30 21:55:30 +11:00
|
|
|
|
return errors.New("no char metrics")
|
2018-09-20 11:49:44 +10:00
|
|
|
|
}
|
2018-10-30 21:55:30 +11:00
|
|
|
|
|
2018-10-09 11:49:59 +11:00
|
|
|
|
// c is the character size in unscaled text units.
|
2018-11-30 16:53:48 +00:00
|
|
|
|
c := transform.Point{X: m.Wx * glyphTextRatio, Y: m.Wy * glyphTextRatio}
|
2018-10-30 21:55:30 +11:00
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// t0 is the end of this character.
|
2018-10-09 11:49:59 +11:00
|
|
|
|
// t is the displacement of the text cursor when the character is rendered.
|
2018-11-30 16:53:48 +00:00
|
|
|
|
t0 := transform.Point{X: (c.X*tfs + w) * th}
|
|
|
|
|
t := transform.Point{X: (c.X*tfs + state.Tc + w) * th}
|
2018-09-20 11:49:44 +10:00
|
|
|
|
|
2018-11-19 14:19:50 +11:00
|
|
|
|
// td, td0 are t, t0 in matrix form.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// td0 is where this character ends. td is where the next character starts.
|
2018-11-18 17:21:30 +11:00
|
|
|
|
td0 := translationMatrix(t0)
|
2018-10-09 11:49:59 +11:00
|
|
|
|
td := translationMatrix(t)
|
|
|
|
|
|
2018-11-26 17:17:17 +11:00
|
|
|
|
common.Log.Trace("\"%s\" stateMatrix=%s CTM=%s Tm=%s", r, stateMatrix, to.gs.CTM, to.Tm)
|
|
|
|
|
common.Log.Trace("tfs=%.3f th=%.3f Tc=%.3f w=%.3f (Tw=%.3f)", tfs, th, state.Tc, w, state.Tw)
|
|
|
|
|
common.Log.Trace("m=%s c=%+v t0=%+v td0=%s trm0=%s", m, c, t0, td0, td0.Mult(to.Tm).Mult(to.gs.CTM))
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
xyt := to.newXYText(
|
2018-11-18 17:21:30 +11:00
|
|
|
|
string(r),
|
2018-11-26 17:17:17 +11:00
|
|
|
|
trm,
|
2018-11-18 17:21:30 +11:00
|
|
|
|
translation(td0.Mult(to.Tm).Mult(to.gs.CTM)),
|
2018-11-28 18:06:03 +11:00
|
|
|
|
1.0*trm.ScalingFactorY(),
|
2018-11-18 17:21:30 +11:00
|
|
|
|
spaceWidth*trm.ScalingFactorX())
|
2018-11-27 13:37:12 +11:00
|
|
|
|
common.Log.Trace("i=%d code=%d xyt=%s trm=%s", i, code, xyt, trm)
|
2018-10-09 11:49:59 +11:00
|
|
|
|
to.Texts = append(to.Texts, xyt)
|
|
|
|
|
|
|
|
|
|
// update the text matrix by the displacement of the text location.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
to.Tm = td.Mult(to.Tm)
|
2018-11-26 08:09:52 +11:00
|
|
|
|
common.Log.Trace("to.Tm=%s", to.Tm)
|
2018-09-20 11:49:44 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return nil
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-20 11:49:44 +10:00
|
|
|
|
// glyphTextRatio converts Glyph metrics units to unscaled text space units.
|
|
|
|
|
const glyphTextRatio = 1.0 / 1000.0
|
|
|
|
|
|
|
|
|
|
// translation returns the translation part of `m`.
|
2018-11-30 16:53:48 +00:00
|
|
|
|
func translation(m transform.Matrix) transform.Point {
|
2018-09-20 11:49:44 +10:00
|
|
|
|
tx, ty := m.Translation()
|
2018-11-30 16:53:48 +00:00
|
|
|
|
return transform.Point{tx, ty}
|
2018-09-20 11:49:44 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// translationMatrix returns a matrix that translates by `p`.
|
2018-11-30 16:53:48 +00:00
|
|
|
|
func translationMatrix(p transform.Point) transform.Matrix {
|
|
|
|
|
return transform.TranslationMatrix(p.X, p.Y)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// moveTo moves the start of line pointer by `tx`,`ty` and sets the text pointer to the
|
|
|
|
|
// start of line pointer.
|
|
|
|
|
// Move to the start of the next line, offset from the start of the current line by (tx, ty).
|
|
|
|
|
// `tx` and `ty` are in unscaled text space units.
|
|
|
|
|
func (to *textObject) moveTo(tx, ty float64) {
|
2018-11-30 16:53:48 +00:00
|
|
|
|
to.Tlm = transform.NewMatrix(1, 0, 0, 1, tx, ty).Mult(to.Tlm)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
to.Tm = to.Tlm
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-03 16:38:58 +10:00
|
|
|
|
// XYText represents text drawn on a page and its position in device coordinates.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// All dimensions are in device coordinates.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
type XYText struct {
|
2018-11-30 16:53:48 +00:00
|
|
|
|
Text string // The text.
|
|
|
|
|
Orient int // The text orientation.
|
|
|
|
|
OrientedStart transform.Point // Left of text in orientation where text is horizontal.
|
|
|
|
|
OrientedEnd transform.Point // Right of text in orientation where text is horizontal.
|
|
|
|
|
Height float64 // Text height.
|
|
|
|
|
SpaceWidth float64 // Best guess at the width of a space in the font the text was rendered with.
|
|
|
|
|
count int64 // To help with reading debug logs.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newXYText returns an XYText for text `text` rendered with text rendering matrix `trm` and end
|
|
|
|
|
// of character device coordinates `end`. `spaceWidth` is our best guess at the width of a space in
|
|
|
|
|
// the font the text is rendered in device coordinates.
|
2018-11-30 16:53:48 +00:00
|
|
|
|
func (to *textObject) newXYText(text string, trm transform.Matrix, end transform.Point,
|
2018-11-28 18:06:03 +11:00
|
|
|
|
height, spaceWidth float64) XYText {
|
2018-11-27 13:37:12 +11:00
|
|
|
|
to.e.textCount++
|
2018-11-26 17:17:17 +11:00
|
|
|
|
theta := trm.Angle()
|
2018-11-28 18:06:03 +11:00
|
|
|
|
if theta%180 == 0 {
|
|
|
|
|
height = trm.ScalingFactorY()
|
|
|
|
|
} else {
|
|
|
|
|
height = trm.ScalingFactorX()
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-18 17:21:30 +11:00
|
|
|
|
return XYText{
|
2018-11-26 17:17:17 +11:00
|
|
|
|
Text: text,
|
2018-11-27 13:37:12 +11:00
|
|
|
|
Orient: theta,
|
2018-11-26 17:17:17 +11:00
|
|
|
|
OrientedStart: translation(trm).Rotate(theta),
|
|
|
|
|
OrientedEnd: end.Rotate(theta),
|
2018-11-28 18:06:03 +11:00
|
|
|
|
Height: height,
|
2018-11-26 17:17:17 +11:00
|
|
|
|
SpaceWidth: spaceWidth,
|
2018-11-27 13:37:12 +11:00
|
|
|
|
count: to.e.textCount,
|
2018-11-18 17:21:30 +11:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// String returns a string describing `t`.
|
2018-09-20 11:49:44 +10:00
|
|
|
|
func (t XYText) String() string {
|
2018-11-28 18:06:03 +11:00
|
|
|
|
return fmt.Sprintf("XYText{@%03d [%.3f,%.3f] %.1f %d° %q}",
|
2018-11-27 13:37:12 +11:00
|
|
|
|
t.count, t.OrientedStart.X, t.OrientedStart.Y, t.Width(), t.Orient, truncate(t.Text, 100))
|
2018-10-09 11:49:59 +11:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 17:17:17 +11:00
|
|
|
|
// Width returns the width of `t`.Text in the text direction.
|
2018-10-09 11:49:59 +11:00
|
|
|
|
func (t XYText) Width() float64 {
|
2018-11-26 17:17:17 +11:00
|
|
|
|
return math.Abs(t.OrientedStart.X - t.OrientedEnd.X)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-03 16:38:58 +10:00
|
|
|
|
// TextList is a list of texts and their positions on a PDF page.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
type TextList []XYText
|
|
|
|
|
|
2018-09-03 16:38:58 +10:00
|
|
|
|
// Length returns the number of elements in `tl`.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
func (tl TextList) Length() int {
|
|
|
|
|
return len(tl)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 18:06:03 +11:00
|
|
|
|
// height returns the max height of the elements in `tl`.
|
|
|
|
|
func (tl TextList) height() float64 {
|
|
|
|
|
fontHeight := 0.0
|
|
|
|
|
for _, t := range tl {
|
|
|
|
|
if t.Height > fontHeight {
|
|
|
|
|
fontHeight = t.Height
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return fontHeight
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
// ToText returns the contents of `tl` as a single string.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
func (tl TextList) ToText() string {
|
2018-10-30 21:55:30 +11:00
|
|
|
|
tl.printTexts("ToText: before sorting")
|
2018-11-28 18:06:03 +11:00
|
|
|
|
|
2018-08-22 12:29:34 +10:00
|
|
|
|
tl.SortPosition()
|
|
|
|
|
|
|
|
|
|
lines := tl.toLines()
|
2018-11-28 23:25:17 +00:00
|
|
|
|
texts := make([]string, 0, len(lines))
|
2018-08-22 12:29:34 +10:00
|
|
|
|
for _, l := range lines {
|
|
|
|
|
texts = append(texts, l.Text)
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(texts, "\n")
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 21:19:02 +11:00
|
|
|
|
// SortPosition sorts a text list by its elements' position on a page.
|
|
|
|
|
// Sorting is by orientation then top to bottom, left to right when page is orientated so that text
|
|
|
|
|
// is horizontal.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func (tl *TextList) SortPosition() {
|
2018-11-28 18:06:03 +11:00
|
|
|
|
fontHeight := tl.height()
|
|
|
|
|
// We sort with a y tolerance to allow for subscripts, diacritics etc.
|
|
|
|
|
tol := min(fontHeight*0.2, 5.0)
|
|
|
|
|
common.Log.Trace("SortPosition: fontHeight=%.1f tol=%.1f", fontHeight, tol)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
sort.SliceStable(*tl, func(i, j int) bool {
|
|
|
|
|
ti, tj := (*tl)[i], (*tl)[j]
|
2018-11-10 21:19:02 +11:00
|
|
|
|
if ti.Orient != tj.Orient {
|
2018-11-26 17:17:17 +11:00
|
|
|
|
return ti.Orient < tj.Orient
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
2018-11-28 18:06:03 +11:00
|
|
|
|
if math.Abs(ti.OrientedStart.Y-tj.OrientedStart.Y) > tol {
|
2018-11-26 17:17:17 +11:00
|
|
|
|
return ti.OrientedStart.Y > tj.OrientedStart.Y
|
2018-11-10 21:19:02 +11:00
|
|
|
|
}
|
2018-11-26 17:17:17 +11:00
|
|
|
|
return ti.OrientedStart.X < tj.OrientedStart.X
|
2018-08-22 12:29:34 +10:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Line represents a line of text on a page.
|
|
|
|
|
type Line struct {
|
2018-10-09 13:47:43 +11:00
|
|
|
|
Y float64 // y position of line.
|
|
|
|
|
Dx []float64 // x distance between successive words in line.
|
|
|
|
|
Text string // text in the line.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
Words []string // words in the line.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 17:17:17 +11:00
|
|
|
|
// toLines returns the text and positions in `tl` as a slice of Line.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// NOTE: Caller must sort the text list top-to-bottom, left-to-write (for orientation adjusted so
|
2018-11-10 21:19:02 +11:00
|
|
|
|
// that text is horizontal) before calling this function.
|
2018-11-26 17:17:17 +11:00
|
|
|
|
func (tl TextList) toLines() []Line {
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// We divide `tl` into slices which contain texts with the same orientation, extract the lines
|
|
|
|
|
// for each orientation then return the concatention of these lines sorted by orientation.
|
2018-11-28 23:25:17 +00:00
|
|
|
|
tlOrient := make(map[int]TextList, len(tl))
|
2018-11-26 17:17:17 +11:00
|
|
|
|
for _, t := range tl {
|
|
|
|
|
tlOrient[t.Orient] = append(tlOrient[t.Orient], t)
|
2018-11-10 21:19:02 +11:00
|
|
|
|
}
|
2018-11-28 23:33:31 +00:00
|
|
|
|
var lines []Line
|
2018-11-27 13:37:12 +11:00
|
|
|
|
for _, o := range orientKeys(tlOrient) {
|
2018-11-26 17:17:17 +11:00
|
|
|
|
lines = append(lines, tlOrient[o].toLinesOrient()...)
|
|
|
|
|
}
|
|
|
|
|
return lines
|
2018-11-10 21:19:02 +11:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-19 16:50:28 +11:00
|
|
|
|
// toLinesOrient returns the text and positions in `tl` as a slice of Line.
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// NOTE: This function only works on text lists where all text is the same orientation so it should
|
|
|
|
|
// only be called from toLines.
|
|
|
|
|
// Caller must sort the text list top-to-bottom, left-to-write (for orientation adjusted so
|
|
|
|
|
// that text is horizontal) before calling this function.
|
2018-11-26 17:17:17 +11:00
|
|
|
|
func (tl TextList) toLinesOrient() []Line {
|
2018-10-30 21:55:30 +11:00
|
|
|
|
tl.printTexts("toLines: before")
|
2018-11-26 17:17:17 +11:00
|
|
|
|
if len(tl) == 0 {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
return []Line{}
|
|
|
|
|
}
|
2018-11-28 23:25:17 +00:00
|
|
|
|
var lines []Line
|
|
|
|
|
var words []string
|
|
|
|
|
var x []float64
|
2018-11-26 17:17:17 +11:00
|
|
|
|
y := tl[0].OrientedStart.Y
|
2018-10-09 11:49:59 +11:00
|
|
|
|
|
|
|
|
|
scanning := false
|
|
|
|
|
|
2018-11-28 23:25:17 +00:00
|
|
|
|
averageCharWidth := exponAve{}
|
|
|
|
|
wordSpacing := exponAve{}
|
2018-11-27 13:37:12 +11:00
|
|
|
|
lastEndX := 0.0 // lastEndX is tl[i-1].OrientedEnd.X
|
2018-10-09 11:49:59 +11:00
|
|
|
|
|
2018-11-26 17:17:17 +11:00
|
|
|
|
for _, t := range tl {
|
|
|
|
|
if t.OrientedStart.Y < y {
|
2018-08-22 12:29:34 +10:00
|
|
|
|
if len(words) > 0 {
|
2018-10-09 13:47:43 +11:00
|
|
|
|
line := newLine(y, x, words)
|
|
|
|
|
if averageCharWidth.running {
|
2018-11-28 18:06:03 +11:00
|
|
|
|
line = combineDiacritics(line, averageCharWidth.ave)
|
2018-10-09 13:47:43 +11:00
|
|
|
|
line = removeDuplicates(line, averageCharWidth.ave)
|
|
|
|
|
}
|
|
|
|
|
lines = append(lines, line)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
2018-10-09 13:47:43 +11:00
|
|
|
|
words = []string{}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
x = []float64{}
|
2018-11-26 17:17:17 +11:00
|
|
|
|
y = t.OrientedStart.Y
|
2018-10-09 11:49:59 +11:00
|
|
|
|
scanning = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect text movements that represent spaces on the printed page.
|
|
|
|
|
// We use a heuristic from PdfBox: If the next character starts to the right of where a
|
|
|
|
|
// character after a space at "normal spacing" would start, then there is a space before it.
|
|
|
|
|
// The tricky thing to guess here is the width of a space at normal spacing.
|
|
|
|
|
// We follow PdfBox and use min(deltaSpace, deltaCharWidth).
|
|
|
|
|
deltaSpace := 0.0
|
|
|
|
|
if t.SpaceWidth == 0 {
|
|
|
|
|
deltaSpace = math.MaxFloat64
|
|
|
|
|
} else {
|
|
|
|
|
wordSpacing.update(t.SpaceWidth)
|
|
|
|
|
deltaSpace = wordSpacing.ave * 0.5
|
|
|
|
|
}
|
2018-10-09 19:05:38 +11:00
|
|
|
|
averageCharWidth.update(t.Width())
|
2018-10-09 11:49:59 +11:00
|
|
|
|
deltaCharWidth := averageCharWidth.ave * 0.3
|
|
|
|
|
|
|
|
|
|
isSpace := false
|
2018-11-28 23:25:17 +00:00
|
|
|
|
nextWordX := lastEndX + minFloat(deltaSpace, deltaCharWidth)
|
2018-10-09 11:49:59 +11:00
|
|
|
|
if scanning && t.Text != " " {
|
2018-11-26 17:17:17 +11:00
|
|
|
|
isSpace = nextWordX < t.OrientedStart.X
|
2018-10-09 11:49:59 +11:00
|
|
|
|
}
|
2018-11-26 17:17:17 +11:00
|
|
|
|
common.Log.Trace("t=%s", t)
|
|
|
|
|
common.Log.Trace("width=%.2f delta=%.2f deltaSpace=%.2g deltaCharWidth=%.2g",
|
2018-11-28 23:25:17 +00:00
|
|
|
|
t.Width(), minFloat(deltaSpace, deltaCharWidth), deltaSpace, deltaCharWidth)
|
2018-11-26 17:17:17 +11:00
|
|
|
|
common.Log.Trace("%+q [%.1f, %.1f] lastEndX=%.2f nextWordX=%.2f (%.2f) isSpace=%t",
|
2018-11-28 23:25:17 +00:00
|
|
|
|
t.Text, t.OrientedStart.X, t.OrientedStart.Y, lastEndX, nextWordX,
|
2018-11-26 17:17:17 +11:00
|
|
|
|
nextWordX-t.OrientedStart.X, isSpace)
|
2018-11-18 17:21:30 +11:00
|
|
|
|
|
2018-10-09 11:49:59 +11:00
|
|
|
|
if isSpace {
|
|
|
|
|
words = append(words, " ")
|
2018-11-26 17:17:17 +11:00
|
|
|
|
x = append(x, (lastEndX+t.OrientedStart.X)*0.5)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
2018-10-09 11:49:59 +11:00
|
|
|
|
|
|
|
|
|
// Add the text to the line.
|
2018-11-26 17:17:17 +11:00
|
|
|
|
lastEndX = t.OrientedEnd.X
|
2018-08-22 12:29:34 +10:00
|
|
|
|
words = append(words, t.Text)
|
2018-11-26 17:17:17 +11:00
|
|
|
|
x = append(x, t.OrientedStart.X)
|
2018-10-09 11:49:59 +11:00
|
|
|
|
scanning = true
|
2018-11-26 17:17:17 +11:00
|
|
|
|
common.Log.Trace("lastEndX=%.2f", lastEndX)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
if len(words) > 0 {
|
2018-10-09 13:47:43 +11:00
|
|
|
|
line := newLine(y, x, words)
|
|
|
|
|
if averageCharWidth.running {
|
|
|
|
|
line = removeDuplicates(line, averageCharWidth.ave)
|
|
|
|
|
}
|
|
|
|
|
lines = append(lines, line)
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
return lines
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-27 13:37:12 +11:00
|
|
|
|
// orientKeys returns the keys of `tlOrient` as a sorted slice.
|
|
|
|
|
func orientKeys(tlOrient map[int]TextList) []int {
|
|
|
|
|
keys := []int{}
|
|
|
|
|
for k := range tlOrient {
|
|
|
|
|
keys = append(keys, k)
|
|
|
|
|
}
|
|
|
|
|
sort.Ints(keys)
|
|
|
|
|
return keys
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-26 08:09:52 +11:00
|
|
|
|
// min returns the lesser of `a` and `b`.
|
2018-10-09 11:49:59 +11:00
|
|
|
|
func min(a, b float64) float64 {
|
|
|
|
|
if a < b {
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 23:25:17 +00:00
|
|
|
|
// exponAve implements an exponential average.
|
|
|
|
|
type exponAve struct {
|
2018-10-09 11:49:59 +11:00
|
|
|
|
ave float64 // Current average value.
|
|
|
|
|
running bool // Has `ave` been set?
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// update updates the exponential average `exp.ave` and returns it
|
2018-11-28 23:25:17 +00:00
|
|
|
|
func (exp *exponAve) update(x float64) float64 {
|
2018-10-09 11:49:59 +11:00
|
|
|
|
if !exp.running {
|
|
|
|
|
exp.ave = x
|
2018-10-09 13:47:43 +11:00
|
|
|
|
exp.running = true
|
2018-10-09 11:49:59 +11:00
|
|
|
|
} else {
|
|
|
|
|
exp.ave = (exp.ave + x) * 0.5
|
|
|
|
|
}
|
|
|
|
|
return exp.ave
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 23:25:17 +00:00
|
|
|
|
const isDebug = false
|
|
|
|
|
|
|
|
|
|
// printTexts is a debugging function.
|
|
|
|
|
// TODO(peterwilliams97) Remove this.
|
2018-10-30 21:55:30 +11:00
|
|
|
|
func (tl *TextList) printTexts(message string) {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
if !isDebug {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-30 21:55:30 +11:00
|
|
|
|
_, file, line, ok := runtime.Caller(1)
|
|
|
|
|
if !ok {
|
|
|
|
|
file = "???"
|
|
|
|
|
line = 0
|
|
|
|
|
} else {
|
|
|
|
|
file = filepath.Base(file)
|
|
|
|
|
}
|
|
|
|
|
prefix := fmt.Sprintf("[%s:%d]", file, line)
|
|
|
|
|
|
2018-11-18 17:21:30 +11:00
|
|
|
|
common.Log.Debug("=====================================")
|
|
|
|
|
common.Log.Debug("printTexts %s %s", prefix, message)
|
|
|
|
|
common.Log.Debug("%d texts", len(*tl))
|
2018-10-30 21:55:30 +11:00
|
|
|
|
parts := []string{}
|
|
|
|
|
for i, t := range *tl {
|
2018-09-20 11:49:44 +10:00
|
|
|
|
fmt.Printf("%5d: %s\n", i, t.String())
|
2018-10-30 21:55:30 +11:00
|
|
|
|
parts = append(parts, t.Text)
|
2018-09-20 11:49:44 +10:00
|
|
|
|
}
|
2018-11-18 17:21:30 +11:00
|
|
|
|
common.Log.Debug("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
|
2018-10-30 21:55:30 +11:00
|
|
|
|
fmt.Printf("%s\n", strings.Join(parts, ""))
|
2018-11-18 17:21:30 +11:00
|
|
|
|
common.Log.Debug("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^")
|
2018-09-20 11:49:44 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-03 16:38:58 +10:00
|
|
|
|
// newLine returns the Line representation of strings `words` with y coordinate `y` and x
|
|
|
|
|
// coordinates `x`.
|
2018-08-22 12:29:34 +10:00
|
|
|
|
func newLine(y float64, x []float64, words []string) Line {
|
2018-11-28 23:25:17 +00:00
|
|
|
|
dx := make([]float64, 0, len(x))
|
2018-08-22 12:29:34 +10:00
|
|
|
|
for i := 1; i < len(x); i++ {
|
|
|
|
|
dx = append(dx, x[i]-x[i-1])
|
|
|
|
|
}
|
2018-10-09 13:47:43 +11:00
|
|
|
|
return Line{Y: y, Dx: dx, Text: strings.Join(words, ""), Words: words}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 21:19:02 +11:00
|
|
|
|
// removeDuplicates returns `line` with duplicate characters removed. `charWidth` is the average
|
|
|
|
|
// character width for the line.
|
2018-10-09 13:47:43 +11:00
|
|
|
|
func removeDuplicates(line Line, charWidth float64) Line {
|
|
|
|
|
if len(line.Dx) == 0 {
|
|
|
|
|
return line
|
|
|
|
|
}
|
2018-11-10 21:19:02 +11:00
|
|
|
|
|
2018-10-09 13:47:43 +11:00
|
|
|
|
tol := charWidth * 0.3
|
|
|
|
|
words := []string{line.Words[0]}
|
|
|
|
|
dxList := []float64{}
|
2018-10-09 19:05:38 +11:00
|
|
|
|
|
2018-10-09 13:47:43 +11:00
|
|
|
|
w0 := line.Words[0]
|
|
|
|
|
for i, dx := range line.Dx {
|
|
|
|
|
w := line.Words[i+1]
|
|
|
|
|
if w != w0 || dx > tol {
|
|
|
|
|
words = append(words, w)
|
|
|
|
|
dxList = append(dxList, dx)
|
|
|
|
|
}
|
|
|
|
|
w0 = w
|
|
|
|
|
}
|
|
|
|
|
return Line{Y: line.Y, Dx: dxList, Text: strings.Join(words, ""), Words: words}
|
2018-08-22 12:29:34 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-28 18:06:03 +11:00
|
|
|
|
// combineDiacritics returns `line` with diacritics close to characters combined with the characters.
|
|
|
|
|
// `charWidth` is the average character width for the line.
|
|
|
|
|
// We have to do this because PDF can render diacritics separately to the characters they attach to
|
|
|
|
|
// in extracted text.
|
|
|
|
|
func combineDiacritics(line Line, charWidth float64) Line {
|
|
|
|
|
if len(line.Dx) == 0 {
|
|
|
|
|
return line
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tol := charWidth * 0.2
|
|
|
|
|
common.Log.Trace("combineDiacritics: charWidth=%.2f tol=%.2f", charWidth, tol)
|
|
|
|
|
|
|
|
|
|
words := []string{}
|
|
|
|
|
dxList := []float64{}
|
|
|
|
|
w := line.Words[0]
|
|
|
|
|
w, c := countDiacritic(w)
|
|
|
|
|
delta := 0.0
|
|
|
|
|
dx0 := 0.0
|
|
|
|
|
parts := []string{w}
|
|
|
|
|
numChars := c
|
|
|
|
|
|
|
|
|
|
for i := 0; i < len(line.Dx); i++ {
|
|
|
|
|
w = line.Words[i+1]
|
|
|
|
|
w, c := countDiacritic(w)
|
|
|
|
|
dx := line.Dx[i]
|
|
|
|
|
if numChars+c <= 1 && delta+dx <= tol {
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
dx0 = dx
|
|
|
|
|
} else {
|
|
|
|
|
delta += dx
|
|
|
|
|
}
|
|
|
|
|
parts = append(parts, w)
|
|
|
|
|
numChars += c
|
|
|
|
|
} else {
|
|
|
|
|
if len(parts) > 0 {
|
|
|
|
|
if len(words) > 0 {
|
|
|
|
|
dxList = append(dxList, dx0)
|
|
|
|
|
}
|
|
|
|
|
words = append(words, combine(parts))
|
|
|
|
|
}
|
|
|
|
|
parts = []string{w}
|
|
|
|
|
numChars = c
|
|
|
|
|
dx0 = dx
|
|
|
|
|
delta = 0.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(parts) > 0 {
|
|
|
|
|
if len(words) > 0 {
|
|
|
|
|
dxList = append(dxList, dx0)
|
|
|
|
|
}
|
|
|
|
|
words = append(words, combine(parts))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(words) != len(dxList)+1 {
|
|
|
|
|
common.Log.Error("Inconsistent: \nwords=%d %q\ndxList=%d %.2f",
|
|
|
|
|
len(words), words, len(dxList), dxList)
|
|
|
|
|
return line
|
|
|
|
|
}
|
|
|
|
|
return Line{Y: line.Y, Dx: dxList, Text: strings.Join(words, ""), Words: words}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// combine combines any diacritics in `parts` with the single non-diacritic character in `parts`.
|
|
|
|
|
func combine(parts []string) string {
|
|
|
|
|
if len(parts) == 1 {
|
|
|
|
|
// Must be a non-diacritic.
|
|
|
|
|
return parts[0]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to put the diacritics before the non-diacritic for NFKC normalization to work.
|
|
|
|
|
diacritic := map[string]bool{}
|
|
|
|
|
for _, w := range parts {
|
|
|
|
|
r := []rune(w)[0]
|
|
|
|
|
diacritic[w] = unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Sk, r)
|
|
|
|
|
}
|
|
|
|
|
sort.SliceStable(parts, func(i, j int) bool { return !diacritic[parts[i]] && diacritic[parts[j]] })
|
|
|
|
|
|
|
|
|
|
// Construct the NFKC-normalized concatenation of the diacritics and the non-diacritic.
|
|
|
|
|
for i, w := range parts {
|
|
|
|
|
parts[i] = strings.TrimSpace(norm.NFKC.String(w))
|
|
|
|
|
}
|
|
|
|
|
return strings.Join(parts, "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// countDiacritic returns the combining diacritic version of `w` (usually itself) and the number of
|
|
|
|
|
// non-diacritics in `w` (0 or 1)
|
|
|
|
|
func countDiacritic(w string) (string, int) {
|
|
|
|
|
runes := []rune(w)
|
|
|
|
|
if len(runes) != 1 {
|
|
|
|
|
return w, 1
|
|
|
|
|
}
|
|
|
|
|
r := runes[0]
|
|
|
|
|
c := 1
|
|
|
|
|
if unicode.Is(unicode.Mn, r) || unicode.Is(unicode.Sk, r) {
|
|
|
|
|
c = 0
|
|
|
|
|
}
|
|
|
|
|
if w2, ok := diacritics[r]; ok {
|
|
|
|
|
c = 0
|
|
|
|
|
w = w2
|
|
|
|
|
}
|
|
|
|
|
return w, c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// diacritics is a map of diacritic characters that are not classified as unicode.Mn or unicode.Sk
|
|
|
|
|
// and the corresponding unicode.Mn or unicode.Sk characters. This map was copied from PdfBox.
|
|
|
|
|
var diacritics = map[rune]string{
|
|
|
|
|
0x0060: "\u0300",
|
|
|
|
|
0x02CB: "\u0300",
|
|
|
|
|
0x0027: "\u0301",
|
|
|
|
|
0x02B9: "\u0301",
|
|
|
|
|
0x02CA: "\u0301",
|
|
|
|
|
0x005e: "\u0302",
|
|
|
|
|
0x02C6: "\u0302",
|
|
|
|
|
0x007E: "\u0303",
|
|
|
|
|
0x02C9: "\u0304",
|
|
|
|
|
0x00B0: "\u030A",
|
|
|
|
|
0x02BA: "\u030B",
|
|
|
|
|
0x02C7: "\u030C",
|
|
|
|
|
0x02C8: "\u030D",
|
|
|
|
|
0x0022: "\u030E",
|
|
|
|
|
0x02BB: "\u0312",
|
|
|
|
|
0x02BC: "\u0313",
|
|
|
|
|
0x0486: "\u0313",
|
|
|
|
|
0x055A: "\u0313",
|
|
|
|
|
0x02BD: "\u0314",
|
|
|
|
|
0x0485: "\u0314",
|
|
|
|
|
0x0559: "\u0314",
|
|
|
|
|
0x02D4: "\u031D",
|
|
|
|
|
0x02D5: "\u031E",
|
|
|
|
|
0x02D6: "\u031F",
|
|
|
|
|
0x02D7: "\u0320",
|
|
|
|
|
0x02B2: "\u0321",
|
|
|
|
|
0x02CC: "\u0329",
|
|
|
|
|
0x02B7: "\u032B",
|
|
|
|
|
0x02CD: "\u0331",
|
|
|
|
|
0x005F: "\u0332",
|
|
|
|
|
0x204E: "\u0359",
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-20 11:49:44 +10:00
|
|
|
|
// getCurrentFont returns the font on top of the font stack, or DefaultFont if the font stack is
|
|
|
|
|
// empty.
|
|
|
|
|
func (to *textObject) getCurrentFont() *model.PdfFont {
|
|
|
|
|
if to.fontStack.empty() {
|
|
|
|
|
common.Log.Debug("ERROR: No font defined. Using default.")
|
|
|
|
|
return model.DefaultFont()
|
|
|
|
|
}
|
|
|
|
|
return to.fontStack.peek()
|
|
|
|
|
}
|
|
|
|
|
|
2018-06-28 11:11:43 +10:00
|
|
|
|
// getFont returns the font named `name` if it exists in the page's resources or an error if it
|
2018-09-17 12:12:06 +10:00
|
|
|
|
// doesn't. It caches the returned fonts.
|
2018-07-25 12:00:49 +10:00
|
|
|
|
func (to *textObject) getFont(name string) (*model.PdfFont, error) {
|
2018-09-22 09:28:18 +10:00
|
|
|
|
if to.e.fontCache != nil {
|
|
|
|
|
to.e.accessCount++
|
|
|
|
|
entry, ok := to.e.fontCache[name]
|
|
|
|
|
if ok {
|
|
|
|
|
entry.access = to.e.accessCount
|
|
|
|
|
return entry.font, nil
|
|
|
|
|
}
|
2018-09-17 12:12:06 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Font not in cache. Load it.
|
|
|
|
|
font, err := to.getFontDirect(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-22 09:28:18 +10:00
|
|
|
|
if to.e.fontCache != nil {
|
|
|
|
|
entry := fontEntry{font, to.e.accessCount}
|
|
|
|
|
|
|
|
|
|
// Eject a victim if the cache is full.
|
|
|
|
|
if len(to.e.fontCache) >= maxFontCache {
|
|
|
|
|
names := []string{}
|
|
|
|
|
for name := range to.e.fontCache {
|
|
|
|
|
names = append(names, name)
|
|
|
|
|
}
|
|
|
|
|
sort.Slice(names, func(i, j int) bool {
|
|
|
|
|
return to.e.fontCache[names[i]].access < to.e.fontCache[names[j]].access
|
|
|
|
|
})
|
|
|
|
|
delete(to.e.fontCache, names[0])
|
2018-09-17 12:12:06 +10:00
|
|
|
|
}
|
2018-09-22 09:28:18 +10:00
|
|
|
|
to.e.fontCache[name] = entry
|
2018-09-17 12:12:06 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return font, nil
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-21 16:43:10 +10:00
|
|
|
|
// fontEntry is a entry in the font cache.
|
2018-09-17 12:12:06 +10:00
|
|
|
|
type fontEntry struct {
|
|
|
|
|
font *model.PdfFont // The font being cached.
|
|
|
|
|
access int64 // Last access. Used to determine LRU cache victims.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// maxFontCache is the maximum number of PdfFont's in fontCache.
|
|
|
|
|
const maxFontCache = 10
|
|
|
|
|
|
|
|
|
|
// getFontDirect returns the font named `name` if it exists in the page's resources or an error if
|
2018-11-28 23:25:17 +00:00
|
|
|
|
// it doesn't. Accesses page resources directly (not cached).
|
2018-09-17 12:12:06 +10:00
|
|
|
|
func (to *textObject) getFontDirect(name string) (*model.PdfFont, error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
fontObj, err := to.getFontDict(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
font, err := model.NewPdfFontFromPdfObject(fontObj)
|
|
|
|
|
if err != nil {
|
2018-09-17 12:12:06 +10:00
|
|
|
|
common.Log.Debug("getFontDirect: NewPdfFontFromPdfObject failed. name=%#q err=%v", name, err)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
return font, err
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-17 12:12:06 +10:00
|
|
|
|
// getFontDict returns the font dict with key `name` if it exists in the page's Font resources or
|
2018-07-25 12:00:49 +10:00
|
|
|
|
// an error if it doesn't.
|
|
|
|
|
func (to *textObject) getFontDict(name string) (fontObj core.PdfObject, err error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
resources := to.e.resources
|
|
|
|
|
if resources == nil {
|
|
|
|
|
common.Log.Debug("getFontDict. No resources. name=%#q", name)
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return nil, nil
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
fontObj, found := resources.GetFontByName(core.PdfObjectName(name))
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if !found {
|
2018-07-25 12:00:49 +10:00
|
|
|
|
common.Log.Debug("ERROR: getFontDict: Font not found: name=%#q", name)
|
2018-11-21 13:14:11 +11:00
|
|
|
|
return nil, errors.New("font not in resources")
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-25 12:00:49 +10:00
|
|
|
|
return fontObj, nil
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|