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-06-28 11:11:43 +10:00
|
|
|
|
// The current version of this file is a halfway step from the old UniDoc text extractor to a
|
|
|
|
|
// full PDF font parser.
|
|
|
|
|
// We will soon implement all the functions marked as `Not implemented yet`.
|
|
|
|
|
|
2018-03-22 13:01:04 +00:00
|
|
|
|
package extractor
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"errors"
|
2018-07-13 17:40:27 +10:00
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
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-03-22 13:01:04 +00:00
|
|
|
|
"github.com/unidoc/unidoc/pdf/model"
|
2018-06-27 16:31:28 +10:00
|
|
|
|
"github.com/unidoc/unidoc/pdf/model/fonts"
|
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-07-15 16:28:56 +10:00
|
|
|
|
// It takes into account character encoding in the PDF file, which is decoded by
|
|
|
|
|
// CharcodeBytesToUnicode.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// The text is processed linearly e.g. in the order in which it appears. A best effort is done to
|
|
|
|
|
// add spaces and newlines.
|
2018-03-22 13:01:04 +00:00
|
|
|
|
func (e *Extractor) ExtractText() (string, error) {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
text, _, _, err := e.ExtractText2()
|
|
|
|
|
return text, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (e *Extractor) ExtractText2() (string, int, int, error) {
|
|
|
|
|
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-06-27 16:31:28 +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-06-27 16:31:28 +10:00
|
|
|
|
common.Log.Debug("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
|
|
|
|
}
|
|
|
|
|
|
2018-07-15 17:22:00 +10:00
|
|
|
|
// fmt.Println("========================= xxx =========================")
|
|
|
|
|
// fmt.Printf("%s\n", e.contents)
|
|
|
|
|
// fmt.Println("========================= ||| =========================")
|
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
|
|
|
|
// common.Log.Debug("++Operand: %s", op.String())
|
|
|
|
|
|
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-06-27 16:31:28 +10:00
|
|
|
|
to.renderRawText("\n")
|
|
|
|
|
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-21 21:20:39 +10:00
|
|
|
|
args, ok := core.GetArrayVal(op.Params[0])
|
|
|
|
|
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-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 "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-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-06-27 16:59:35 +10:00
|
|
|
|
floats, err := model.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 {
|
|
|
|
|
common.Log.Error("ERROR: Processing: err=%v", err)
|
|
|
|
|
}
|
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
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
// moveText "Td" Moves start of text by `tx`,`ty`
|
|
|
|
|
// 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) moveText(tx, ty float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// moveTextSetLeading "TD" Move text location and set leading
|
|
|
|
|
// 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
|
|
|
|
|
func (to *TextObject) moveTextSetLeading(tx, ty float64) {
|
|
|
|
|
// Not implemented yet
|
2018-06-27 16:46:33 +10:00
|
|
|
|
// The following is supposed to be equivalent to the existing Unidoc implementation.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if tx > 0 {
|
|
|
|
|
to.renderRawText(" ")
|
|
|
|
|
}
|
|
|
|
|
if ty < 0 {
|
|
|
|
|
// TODO: More flexible space characters?
|
|
|
|
|
to.renderRawText("\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
func (to *TextObject) nextLine() {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setTextMatrix "Tm"
|
|
|
|
|
// Set the text matrix, Tm, and the text line matrix, Tlm to the Matrix specified by the 6 numbers
|
|
|
|
|
// in `f` (page 250)
|
|
|
|
|
func (to *TextObject) setTextMatrix(f []float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
// The following is supposed to be equivalent to the existing Unidoc implementation.
|
|
|
|
|
tx, ty := f[4], f[5]
|
|
|
|
|
if to.yPos == -1 {
|
|
|
|
|
to.yPos = tx
|
|
|
|
|
} else if to.yPos > ty {
|
|
|
|
|
to.renderRawText("\n")
|
|
|
|
|
to.xPos, to.yPos = tx, ty
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if to.xPos == -1 {
|
|
|
|
|
to.xPos = tx
|
|
|
|
|
} else if to.xPos < ty {
|
|
|
|
|
to.renderRawText("\t")
|
|
|
|
|
to.xPos = tx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// showText "Tj" Show a text string
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// showTextAdjusted "TJ" Show text with adjustable spacing
|
2018-07-15 16:28:56 +10:00
|
|
|
|
func (to *TextObject) showTextAdjusted(args []core.PdfObject) error {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
for _, o := range args {
|
|
|
|
|
switch o.(type) {
|
2018-07-15 16:28:56 +10:00
|
|
|
|
case *core.PdfObjectFloat, *core.PdfObjectInteger:
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Not implemented yet
|
|
|
|
|
// The following is supposed to be equivalent to the existing Unidoc implementation.
|
2018-07-15 16:28:56 +10:00
|
|
|
|
v, _ := core.GetNumberAsFloat(o)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if v < -100 {
|
|
|
|
|
to.renderRawText("\n")
|
|
|
|
|
}
|
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 {
|
|
|
|
|
common.Log.Debug("ERROR: showTextAdjusted: GetStringBytes failed. args=%+v", args)
|
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
2018-07-21 21:20:39 +10:00
|
|
|
|
err := to.renderText(charcodes)
|
2018-07-02 16:46:43 +10:00
|
|
|
|
if err != nil {
|
2018-07-21 21:20:39 +10:00
|
|
|
|
common.Log.Debug("showTextAdjusted: renderText failed. args=%+v err=%v", args, err)
|
2018-07-02 16:46:43 +10:00
|
|
|
|
return err
|
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
default:
|
|
|
|
|
common.Log.Debug("showTextAdjusted. Unexpected type args=%+v", args)
|
2018-07-15 16:28:56 +10:00
|
|
|
|
return core.ErrTypeError
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setTextLeading "TL" Set text leading
|
|
|
|
|
func (to *TextObject) setTextLeading(y float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setCharSpacing "Tc" Set character spacing
|
|
|
|
|
func (to *TextObject) setCharSpacing(x float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setFont "Tf" Set font
|
|
|
|
|
func (to *TextObject) setFont(name string, size float64) error {
|
|
|
|
|
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 {
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
// to.State.Tfs = size
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setTextRenderMode "Tr" Set text rendering mode
|
|
|
|
|
func (to *TextObject) setTextRenderMode(mode int) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setTextRise "Ts" Set text rise
|
|
|
|
|
func (to *TextObject) setTextRise(y float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setWordSpacing "Tw" Set word spacing
|
|
|
|
|
func (to *TextObject) setWordSpacing(y float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setHorizScaling "Tz" Set horizontal scaling
|
|
|
|
|
func (to *TextObject) setHorizScaling(y float64) {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// floatParam returns the single float parameter of operatr `op`, or an error if it doesn't have
|
|
|
|
|
// 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 {
|
|
|
|
|
err := errors.New("Incorrect parameter count")
|
|
|
|
|
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.
|
|
|
|
|
func (to *TextObject) checkOp(op *contentstream.ContentStreamOperation, numParams int,
|
|
|
|
|
hard bool) (ok bool, err error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
if to == nil {
|
|
|
|
|
common.Log.Debug("%#q operand outside text", op.Operand)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if numParams >= 0 {
|
|
|
|
|
if len(op.Params) != numParams {
|
|
|
|
|
if hard {
|
|
|
|
|
err = errors.New("Incorrect parameter count")
|
|
|
|
|
}
|
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)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ok = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-15 16:28:56 +10:00
|
|
|
|
// fontStacker is the PDF font stack implementation.
|
|
|
|
|
// I think this is correct. It has worked on my tests so far.
|
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
|
|
|
|
|
|
|
|
|
// pop pops and returns the element on the top of the font stack if there is one, or nil if there isn't.
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
// peek returns the element on the top of the font stack if there is one, or nil if there isn't.
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) peek() (font *model.PdfFont) {
|
|
|
|
|
if fontStack.empty() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
font = (*fontStack)[len(*fontStack)-1]
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-07-15 16:28:56 +10:00
|
|
|
|
|
|
|
|
|
// get returns the `idx`'th element of the font stack if there is one, or nil if there isn't.
|
|
|
|
|
// 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-13 17:40:27 +10:00
|
|
|
|
func (fontStack *fontStacker) get(idx int) (font *model.PdfFont) {
|
|
|
|
|
if idx < 0 {
|
|
|
|
|
idx += fontStack.size()
|
|
|
|
|
}
|
|
|
|
|
if idx < 0 || idx > fontStack.size()-1 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
font = (*fontStack)[idx]
|
|
|
|
|
return
|
|
|
|
|
}
|
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.
|
|
|
|
|
type TextState struct {
|
|
|
|
|
// Tc float64 // Character spacing. Unscaled text space units.
|
|
|
|
|
// Tw float64 // Word spacing. Unscaled text space units.
|
|
|
|
|
// 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)
|
|
|
|
|
// | Tfs x Th 0 0 |
|
|
|
|
|
// Trm = | 0 Tfs 0 | × Tm × CTM
|
|
|
|
|
// | 0 Trise 1 |
|
|
|
|
|
//
|
|
|
|
|
type TextObject struct {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
e *Extractor
|
|
|
|
|
gs contentstream.GraphicsState
|
|
|
|
|
fontStack *fontStacker
|
|
|
|
|
State *TextState
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Tm contentstream.Matrix // Text matrix. For the character pointer.
|
|
|
|
|
// Tlm contentstream.Matrix // Text line matrix. For the start of line pointer.
|
|
|
|
|
Texts []XYText // Text gets written here.
|
|
|
|
|
|
|
|
|
|
// These fields are used to implement existing UniDoc behaviour.
|
|
|
|
|
xPos, yPos float64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newTextState returns a default TextState
|
|
|
|
|
func newTextState() TextState {
|
|
|
|
|
// Not implemented yet
|
|
|
|
|
return TextState{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newTextObject returns a default TextObject
|
2018-07-13 17:40:27 +10:00
|
|
|
|
func newTextObject(e *Extractor, gs contentstream.GraphicsState, state *TextState,
|
|
|
|
|
fontStack *fontStacker) *TextObject {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return &TextObject{
|
2018-07-13 17:40:27 +10:00
|
|
|
|
e: e,
|
|
|
|
|
gs: gs,
|
|
|
|
|
fontStack: fontStack,
|
|
|
|
|
State: state,
|
2018-06-27 16:31:28 +10:00
|
|
|
|
// Tm: contentstream.IdentityMatrix(),
|
|
|
|
|
// Tlm: contentstream.IdentityMatrix(),
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
2018-06-27 22:01:17 +10:00
|
|
|
|
// renderRawText writes `text` directly to the extracted text
|
2018-06-27 16:31:28 +10:00
|
|
|
|
func (to *TextObject) renderRawText(text string) {
|
|
|
|
|
to.Texts = append(to.Texts, XYText{text})
|
|
|
|
|
}
|
2018-03-22 13:01:04 +00:00
|
|
|
|
|
2018-06-27 22:01:17 +10:00
|
|
|
|
// renderText emits byte array `data` to the calling program
|
2018-07-02 16:46:43 +10:00
|
|
|
|
func (to *TextObject) renderText(data []byte) (err error) {
|
2018-06-27 16:31:28 +10:00
|
|
|
|
text := ""
|
2018-07-13 17:40:27 +10:00
|
|
|
|
if len(*to.fontStack) == 0 {
|
2018-07-02 16:46:43 +10:00
|
|
|
|
common.Log.Debug("ERROR: No font defined. data=%#q", string(data))
|
2018-06-27 22:01:17 +10:00
|
|
|
|
text = string(data)
|
2018-07-13 17:40:27 +10:00
|
|
|
|
err = model.ErrNoFont
|
2018-06-27 16:31:28 +10:00
|
|
|
|
} else {
|
2018-07-13 17:40:27 +10:00
|
|
|
|
font := to.fontStack.peek()
|
|
|
|
|
var numChars, numMisses int
|
|
|
|
|
text, numChars, numMisses = font.CharcodeBytesToUnicode(data)
|
|
|
|
|
to.State.numChars += numChars
|
|
|
|
|
to.State.numMisses += numMisses
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
to.Texts = append(to.Texts, XYText{text})
|
2018-07-02 16:46:43 +10:00
|
|
|
|
return
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XYText represents text and its position in device coordinates
|
|
|
|
|
type XYText struct {
|
|
|
|
|
Text string
|
|
|
|
|
// Position and rendering fields. Not implemented yet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// String returns a string describing `t`
|
|
|
|
|
func (t *XYText) String() string {
|
|
|
|
|
return truncate(t.Text, 100)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TextList is a list of texts and their position on a pdf page
|
|
|
|
|
type TextList []XYText
|
|
|
|
|
|
|
|
|
|
func (tl *TextList) Length() int {
|
|
|
|
|
return len(*tl)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ToText returns the contents of `tl` as a single string
|
|
|
|
|
func (tl *TextList) ToText() string {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
for _, t := range *tl {
|
|
|
|
|
buf.WriteString(t.Text)
|
|
|
|
|
}
|
2018-03-22 13:17:09 +00:00
|
|
|
|
procBuf(&buf)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
return buf.String()
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
// doesn't
|
2018-06-27 16:31:28 +10:00
|
|
|
|
func (to *TextObject) getFont(name string) (*model.PdfFont, error) {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
|
|
|
|
|
// This is a hack for testing.
|
2018-07-15 17:22:00 +10:00
|
|
|
|
if name == "UniDocCourier" {
|
2018-07-07 09:45:55 +10:00
|
|
|
|
return model.NewStandard14Font("Courier")
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
common.Log.Debug("getFont: NewPdfFontFromPdfObject failed. name=%#q err=%v", name, err)
|
|
|
|
|
}
|
|
|
|
|
return font, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getFontDict returns the font object called `name` if it exists in the page's Font resources or
|
|
|
|
|
// an error if it doesn't
|
2018-06-27 16:46:33 +10:00
|
|
|
|
// XXX: TODO: Can we cache font values?
|
2018-07-15 16:28:56 +10:00
|
|
|
|
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)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
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 {
|
|
|
|
|
err = errors.New("Font not in resources")
|
|
|
|
|
common.Log.Debug("ERROR: getFontDict: Font not found: name=%#q err=%v", name, err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2018-03-22 13:17:09 +00:00
|
|
|
|
|
2018-07-15 17:22:00 +10:00
|
|
|
|
// getCharMetrics returns the character metrics for the code points in `text1` for font `font`.
|
2018-06-27 16:31:28 +10:00
|
|
|
|
func getCharMetrics(font *model.PdfFont, text string) (metrics []fonts.CharMetrics, err error) {
|
|
|
|
|
encoder := font.Encoder()
|
|
|
|
|
if encoder == nil {
|
|
|
|
|
err = errors.New("No font encoder")
|
|
|
|
|
}
|
|
|
|
|
for _, r := range text {
|
|
|
|
|
glyph, found := encoder.RuneToGlyph(r)
|
|
|
|
|
if !found {
|
|
|
|
|
common.Log.Debug("Error! Glyph not found for rune=%s", r)
|
|
|
|
|
glyph = "space"
|
|
|
|
|
}
|
|
|
|
|
m, ok := font.GetGlyphCharMetrics(glyph)
|
|
|
|
|
if !ok {
|
2018-06-28 11:11:43 +10:00
|
|
|
|
common.Log.Debug("ERROR: Metrics not found for rune=%+v glyph=%#q", r, glyph)
|
2018-06-27 16:31:28 +10:00
|
|
|
|
}
|
|
|
|
|
metrics = append(metrics, m)
|
|
|
|
|
}
|
|
|
|
|
return
|
2018-03-22 13:01:04 +00:00
|
|
|
|
}
|