blob: 894f60f32e1357cc6712127ad4cb32f9a7c11d11 [file] [log] [blame]
//========================================================================
//
// Annot.cc
//
// Copyright 2000-2003 Glyph & Cog, LLC
//
//========================================================================
#include <config.h>
#ifdef USE_GCC_PRAGMAS
#pragma implementation
#endif
#include <stdlib.h>
#include <math.h>
#include "goo/gmem.h"
#include "GooList.h"
#include "Error.h"
#include "Object.h"
#include "Catalog.h"
#include "Gfx.h"
#include "Lexer.h"
#include "Annot.h"
#include "GfxFont.h"
#include "CharCodeToUnicode.h"
#include "Form.h"
#include "Error.h"
#define annotFlagHidden 0x0002
#define annotFlagPrint 0x0004
#define annotFlagNoView 0x0020
#define fieldFlagReadOnly 0x00000001
#define fieldFlagRequired 0x00000002
#define fieldFlagNoExport 0x00000004
#define fieldFlagMultiline 0x00001000
#define fieldFlagPassword 0x00002000
#define fieldFlagNoToggleToOff 0x00004000
#define fieldFlagRadio 0x00008000
#define fieldFlagPushbutton 0x00010000
#define fieldFlagCombo 0x00020000
#define fieldFlagEdit 0x00040000
#define fieldFlagSort 0x00080000
#define fieldFlagFileSelect 0x00100000
#define fieldFlagMultiSelect 0x00200000
#define fieldFlagDoNotSpellCheck 0x00400000
#define fieldFlagDoNotScroll 0x00800000
#define fieldFlagComb 0x01000000
#define fieldFlagRichText 0x02000000
#define fieldFlagRadiosInUnison 0x02000000
#define fieldFlagCommitOnSelChange 0x04000000
#define fieldQuadLeft 0
#define fieldQuadCenter 1
#define fieldQuadRight 2
// distance of Bezier control point from center for circle approximation
// = (4 * (sqrt(2) - 1) / 3) * r
#define bezierCircle 0.55228475
//------------------------------------------------------------------------
// AnnotBorderStyle
//------------------------------------------------------------------------
AnnotBorderStyle::AnnotBorderStyle(AnnotBorderType typeA, double widthA,
double *dashA, int dashLengthA,
double rA, double gA, double bA) {
type = typeA;
width = widthA;
dash = dashA;
dashLength = dashLengthA;
r = rA;
g = gA;
b = bA;
}
AnnotBorderStyle::~AnnotBorderStyle() {
if (dash) {
gfree(dash);
}
}
//------------------------------------------------------------------------
// Annot
//------------------------------------------------------------------------
Annot::Annot(XRef *xrefA, Dict *acroForm, Dict *dict, const Ref& aref, Catalog* catalog)
{
hasRef = true;
ref = aref;
initialize (xrefA, acroForm, dict, catalog);
}
Annot::Annot(XRef *xrefA, Dict *acroForm, Dict *dict, Catalog* catalog) {
hasRef = false;
initialize (xrefA, acroForm, dict, catalog);
}
void Annot::initialize(XRef *xrefA, Dict *acroForm, Dict *dict, Catalog *catalog) {
Object apObj, asObj, obj1, obj2, obj3;
AnnotBorderType borderType;
double borderWidth;
double *borderDash;
int borderDashLength;
double borderR, borderG, borderB;
double t;
ok = gTrue;
xref = xrefA;
appearBuf = NULL;
fontSize = 0;
type = NULL;
widget = NULL;
borderStyle = NULL;
//----- get the FormWidget
if (hasRef) {
Form *form = catalog->getForm ();
if (form)
widget = form->findWidgetByRef (ref);
}
//----- parse the type
if (dict->lookup("Subtype", &obj1)->isName()) {
type = new GooString(obj1.getName());
}
obj1.free();
//----- parse the rectangle
if (dict->lookup("Rect", &obj1)->isArray() &&
obj1.arrayGetLength() == 4) {
readArrayNum(&obj1, 0, &xMin);
readArrayNum(&obj1, 1, &yMin);
readArrayNum(&obj1, 2, &xMax);
readArrayNum(&obj1, 3, &yMax);
if (ok) {
if (xMin > xMax) {
t = xMin; xMin = xMax; xMax = t;
}
if (yMin > yMax) {
t = yMin; yMin = yMax; yMax = t;
}
} else {
xMin = yMin = 0;
xMax = yMax = 1;
error(-1, "Bad bounding box for annotation");
ok = gFalse;
}
} else {
xMin = yMin = 0;
xMax = yMax = 1;
error(-1, "Bad bounding box for annotation");
ok = gFalse;
}
obj1.free();
//----- get the flags
if (dict->lookup("F", &obj1)->isInt()) {
flags = obj1.getInt();
} else {
flags = 0;
}
obj1.free ();
//check for hidden annot
if (flags & 0x2)
hidden = true;
else
hidden = false;
// check if field apperances need to be regenerated
// Only text or choice fields needs to have appearance regenerated
// see section 8.6.2 "Variable Text" of PDFReference
regen = gFalse;
Form::fieldLookup(dict, "FT", &obj3);
if (obj3.isName("Tx") || obj3.isName("Ch")) {
if (acroForm) {
acroForm->lookup("NeedAppearances", &obj1);
if (obj1.isBool() && obj1.getBool()) {
regen = gTrue;
}
obj1.free();
}
}
if (dict->lookup("AP", &apObj)->isDict()) {
if (dict->lookup("AS", &asObj)->isName()) {
if (apObj.dictLookup("N", &obj1)->isDict()) {
if (obj1.dictLookupNF(asObj.getName(), &obj2)->isRef()) {
obj2.copy(&appearance);
ok = gTrue;
} else {
obj2.free();
if (obj1.dictLookupNF("Off", &obj2)->isRef()) {
obj2.copy(&appearance);
ok = gTrue;
} else
regen = gTrue;
}
obj2.free();
}
obj1.free();
} else {
if (apObj.dictLookupNF("N", &obj1)->isRef()) {
obj1.copy(&appearance);
ok = gTrue;
} else
regen = gTrue;
obj1.free();
}
asObj.free();
} else {
// If field doesn't have an AP we'll have to generate it
regen = gTrue;
}
apObj.free();
obj3.free();
//----- parse the border style
borderType = annotBorderSolid;
borderWidth = 1;
borderDash = NULL;
borderDashLength = 0;
borderR = 0;
borderG = 0;
borderB = 1;
if (dict->lookup("BS", &obj1)->isDict()) {
if (obj1.dictLookup("S", &obj2)->isName()) {
if (obj2.isName("S")) {
borderType = annotBorderSolid;
} else if (obj2.isName("D")) {
borderType = annotBorderDashed;
} else if (obj2.isName("B")) {
borderType = annotBorderBeveled;
} else if (obj2.isName("I")) {
borderType = annotBorderInset;
} else if (obj2.isName("U")) {
borderType = annotBorderUnderlined;
}
}
obj2.free();
if (obj1.dictLookup("W", &obj2)->isNum()) {
borderWidth = obj2.getNum();
}
obj2.free();
if (obj1.dictLookup("D", &obj2)->isArray()) {
borderDashLength = obj2.arrayGetLength();
borderDash = (double *)gmallocn(borderDashLength, sizeof(double));
for (int i = 0; i < borderDashLength; ++i) {
if (obj2.arrayGet(i, &obj3)->isNum()) {
borderDash[i] = obj3.getNum();
} else {
borderDash[i] = 1;
}
obj3.free();
}
}
obj2.free();
} else {
obj1.free();
if (dict->lookup("Border", &obj1)->isArray()) {
if (obj1.arrayGetLength() >= 3) {
if (obj1.arrayGet(2, &obj2)->isNum()) {
borderWidth = obj2.getNum();
}
obj2.free();
if (obj1.arrayGetLength() >= 4) {
if (obj1.arrayGet(3, &obj2)->isArray()) {
borderType = annotBorderDashed;
borderDashLength = obj2.arrayGetLength();
borderDash = (double *)gmallocn(borderDashLength, sizeof(double));
for (int i = 0; i < borderDashLength; ++i) {
if (obj2.arrayGet(i, &obj3)->isNum()) {
borderDash[i] = obj3.getNum();
} else {
borderDash[i] = 1;
}
obj3.free();
}
} else {
// Adobe draws no border at all if the last element is of
// the wrong type.
borderWidth = 0;
}
}
}
}
obj1.free();
}
if (dict->lookup("C", &obj1)->isArray() && obj1.arrayGetLength() == 3) {
if (obj1.arrayGet(0, &obj2)->isNum()) {
borderR = obj2.getNum();
}
obj1.free();
if (obj1.arrayGet(1, &obj2)->isNum()) {
borderG = obj2.getNum();
}
obj1.free();
if (obj1.arrayGet(2, &obj2)->isNum()) {
borderB = obj2.getNum();
}
obj1.free();
}
obj1.free();
borderStyle = new AnnotBorderStyle(borderType, borderWidth,
borderDash, borderDashLength,
borderR, borderG, borderB);
}
void Annot::readArrayNum(Object *pdfArray, int key, double *value) {
Object valueObject;
pdfArray->arrayGet(key, &valueObject);
if (valueObject.isNum()) {
*value = valueObject.getNum();
} else {
*value = 0;
ok = gFalse;
}
valueObject.free();
}
Annot::~Annot() {
if (type) {
delete type;
}
appearance.free();
if (appearBuf) {
delete appearBuf;
}
if (borderStyle) {
delete borderStyle;
}
}
void Annot::generateFieldAppearance(Dict *field, Dict *annot, Dict *acroForm) {
Object mkObj, ftObj, appearDict, drObj, obj1, obj2, obj3;
Dict *mkDict;
MemStream *appearStream;
GfxFontDict *fontDict;
GBool hasCaption;
double w, dx, dy, r;
double *dash;
GooString *caption, *da;
GooString **text;
GBool *selection;
int dashLength, ff, quadding, comb, nOptions, topIdx, i, j;
GBool modified;
// must be a Widget annotation
if (type->cmp("Widget")) {
return;
}
// do not regenerate appearence if widget has not changed
if (widget && widget->isModified ()) {
modified = gTrue;
} else {
modified = gFalse;
}
// only regenerate when it doesn't have an AP or
// it already has an AP but widget has been modified
if (!regen && !modified) {
return;
}
appearBuf = new GooString ();
// get the appearance characteristics (MK) dictionary
if (annot->lookup("MK", &mkObj)->isDict()) {
mkDict = mkObj.getDict();
} else {
mkDict = NULL;
}
// draw the background
if (mkDict) {
if (mkDict->lookup("BG", &obj1)->isArray() &&
obj1.arrayGetLength() > 0) {
setColor(obj1.getArray(), gTrue, 0);
appearBuf->appendf("0 0 {0:.2f} {1:.2f} re f\n",
xMax - xMin, yMax - yMin);
}
obj1.free();
}
// get the field type
Form::fieldLookup(field, "FT", &ftObj);
// get the field flags (Ff) value
if (Form::fieldLookup(field, "Ff", &obj1)->isInt()) {
ff = obj1.getInt();
} else {
ff = 0;
}
obj1.free();
// draw the border
if (mkDict) {
w = borderStyle->getWidth();
if (w > 0) {
mkDict->lookup("BC", &obj1);
if (!(obj1.isArray() && obj1.arrayGetLength() > 0)) {
mkDict->lookup("BG", &obj1);
}
if (obj1.isArray() && obj1.arrayGetLength() > 0) {
dx = xMax - xMin;
dy = yMax - yMin;
// radio buttons with no caption have a round border
hasCaption = mkDict->lookup("CA", &obj2)->isString();
obj2.free();
if (ftObj.isName("Btn") && (ff & fieldFlagRadio) && !hasCaption) {
r = 0.5 * (dx < dy ? dx : dy);
switch (borderStyle->getType()) {
case annotBorderDashed:
appearBuf->append("[");
borderStyle->getDash(&dash, &dashLength);
for (i = 0; i < dashLength; ++i) {
appearBuf->appendf(" {0:.2f}", dash[i]);
}
appearBuf->append("] 0 d\n");
// fall through to the solid case
case annotBorderSolid:
case annotBorderUnderlined:
appearBuf->appendf("{0:.2f} w\n", w);
setColor(obj1.getArray(), gFalse, 0);
drawCircle(0.5 * dx, 0.5 * dy, r - 0.5 * w, gFalse);
break;
case annotBorderBeveled:
case annotBorderInset:
appearBuf->appendf("{0:.2f} w\n", 0.5 * w);
setColor(obj1.getArray(), gFalse, 0);
drawCircle(0.5 * dx, 0.5 * dy, r - 0.25 * w, gFalse);
setColor(obj1.getArray(), gFalse,
borderStyle->getType() == annotBorderBeveled ? 1 : -1);
drawCircleTopLeft(0.5 * dx, 0.5 * dy, r - 0.75 * w);
setColor(obj1.getArray(), gFalse,
borderStyle->getType() == annotBorderBeveled ? -1 : 1);
drawCircleBottomRight(0.5 * dx, 0.5 * dy, r - 0.75 * w);
break;
}
} else {
switch (borderStyle->getType()) {
case annotBorderDashed:
appearBuf->append("[");
borderStyle->getDash(&dash, &dashLength);
for (i = 0; i < dashLength; ++i) {
appearBuf->appendf(" {0:.2f}", dash[i]);
}
appearBuf->append("] 0 d\n");
// fall through to the solid case
case annotBorderSolid:
appearBuf->appendf("{0:.2f} w\n", w);
setColor(obj1.getArray(), gFalse, 0);
appearBuf->appendf("{0:.2f} {0:.2f} {1:.2f} {2:.2f} re s\n",
0.5 * w, dx - w, dy - w);
break;
case annotBorderBeveled:
case annotBorderInset:
setColor(obj1.getArray(), gTrue,
borderStyle->getType() == annotBorderBeveled ? 1 : -1);
appearBuf->append("0 0 m\n");
appearBuf->appendf("0 {0:.2f} l\n", dy);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", dx, dy);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", dx - w, dy - w);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", w, dy - w);
appearBuf->appendf("{0:.2f} {0:.2f} l\n", w);
appearBuf->append("f\n");
setColor(obj1.getArray(), gTrue,
borderStyle->getType() == annotBorderBeveled ? -1 : 1);
appearBuf->append("0 0 m\n");
appearBuf->appendf("{0:.2f} 0 l\n", dx);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", dx, dy);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", dx - w, dy - w);
appearBuf->appendf("{0:.2f} {1:.2f} l\n", dx - w, w);
appearBuf->appendf("{0:.2f} {0:.2f} l\n", w);
appearBuf->append("f\n");
break;
case annotBorderUnderlined:
appearBuf->appendf("{0:.2f} w\n", w);
setColor(obj1.getArray(), gFalse, 0);
appearBuf->appendf("0 0 m {0:.2f} 0 l s\n", dx);
break;
}
// clip to the inside of the border
appearBuf->appendf("{0:.2f} {0:.2f} {1:.2f} {2:.2f} re W n\n",
w, dx - 2 * w, dy - 2 * w);
}
}
obj1.free();
}
}
// get the resource dictionary
acroForm->lookup("DR", &drObj);
// build the font dictionary
if (drObj.isDict() && drObj.dictLookup("Font", &obj1)->isDict()) {
fontDict = new GfxFontDict(xref, NULL, obj1.getDict());
} else {
fontDict = NULL;
}
obj1.free();
// get the default appearance string
if (Form::fieldLookup(field, "DA", &obj1)->isNull()) {
obj1.free();
acroForm->lookup("DA", &obj1);
}
if (obj1.isString()) {
da = obj1.getString()->copy();
//TODO: look for a font size / name HERE
// => create a function
} else {
da = NULL;
}
obj1.free();
// draw the field contents
if (ftObj.isName("Btn")) {
caption = NULL;
if (mkDict) {
if (mkDict->lookup("CA", &obj1)->isString()) {
caption = obj1.getString()->copy();
}
obj1.free();
}
// radio button
if (ff & fieldFlagRadio) {
//~ Acrobat doesn't draw a caption if there is no AP dict (?)
if (Form::fieldLookup(field, "V", &obj1)->isName()) {
if (annot->lookup("AS", &obj2)->isName(obj1.getName()) &&
strcmp (obj1.getName(), "Off") != 0) {
if (caption) {
drawText(caption, da, fontDict, gFalse, 0, fieldQuadCenter,
gFalse, gTrue);
} else {
if (mkDict) {
if (mkDict->lookup("BC", &obj3)->isArray() &&
obj3.arrayGetLength() > 0) {
dx = xMax - xMin;
dy = yMax - yMin;
setColor(obj3.getArray(), gTrue, 0);
drawCircle(0.5 * dx, 0.5 * dy, 0.2 * (dx < dy ? dx : dy),
gTrue);
}
obj3.free();
}
}
}
obj2.free();
}
obj1.free();
// pushbutton
} else if (ff & fieldFlagPushbutton) {
if (caption) {
drawText(caption, da, fontDict, gFalse, 0, fieldQuadCenter,
gFalse, gFalse);
}
// checkbox
} else {
// According to the PDF spec the off state must be named "Off",
// and the on state can be named anything, but Acrobat apparently
// looks for "Yes" and treats anything else as off.
if (Form::fieldLookup(field, "V", &obj1)->isName("Yes")) {
if (!caption) {
caption = new GooString("3"); // ZapfDingbats checkmark
}
drawText(caption, da, fontDict, gFalse, 0, fieldQuadCenter,
gFalse, gTrue);
}
obj1.free();
}
if (caption) {
delete caption;
}
} else if (ftObj.isName("Tx")) {
//~ value strings can be Unicode
if (Form::fieldLookup(field, "V", &obj1)->isString()) {
if (Form::fieldLookup(field, "Q", &obj2)->isInt()) {
quadding = obj2.getInt();
} else {
quadding = fieldQuadLeft;
}
obj2.free();
comb = 0;
if (ff & fieldFlagComb) {
if (Form::fieldLookup(field, "MaxLen", &obj2)->isInt()) {
comb = obj2.getInt();
}
obj2.free();
}
drawText(obj1.getString(), da, fontDict,
ff & fieldFlagMultiline, comb, quadding, gTrue, gFalse, ff & fieldFlagPassword);
}
obj1.free();
} else if (ftObj.isName("Ch")) {
//~ value/option strings can be Unicode
if (Form::fieldLookup(field, "Q", &obj1)->isInt()) {
quadding = obj1.getInt();
} else {
quadding = fieldQuadLeft;
}
obj1.free();
// combo box
if (ff & fieldFlagCombo) {
if (Form::fieldLookup(field, "V", &obj1)->isString()) {
drawText(obj1.getString(), da, fontDict,
gFalse, 0, quadding, gTrue, gFalse);
//~ Acrobat draws a popup icon on the right side
}
obj1.free();
// list box
} else {
if (field->lookup("Opt", &obj1)->isArray()) {
nOptions = obj1.arrayGetLength();
// get the option text
text = (GooString **)gmallocn(nOptions, sizeof(GooString *));
for (i = 0; i < nOptions; ++i) {
text[i] = NULL;
obj1.arrayGet(i, &obj2);
if (obj2.isString()) {
text[i] = obj2.getString()->copy();
} else if (obj2.isArray() && obj2.arrayGetLength() == 2) {
if (obj2.arrayGet(1, &obj3)->isString()) {
text[i] = obj3.getString()->copy();
}
obj3.free();
}
obj2.free();
if (!text[i]) {
text[i] = new GooString();
}
}
// get the selected option(s)
selection = (GBool *)gmallocn(nOptions, sizeof(GBool));
//~ need to use the I field in addition to the V field
Form::fieldLookup(field, "V", &obj2);
for (i = 0; i < nOptions; ++i) {
selection[i] = gFalse;
if (obj2.isString()) {
if (!obj2.getString()->cmp(text[i])) {
selection[i] = gTrue;
}
} else if (obj2.isArray()) {
for (j = 0; j < obj2.arrayGetLength(); ++j) {
if (obj2.arrayGet(j, &obj3)->isString() &&
!obj3.getString()->cmp(text[i])) {
selection[i] = gTrue;
}
obj3.free();
}
}
}
obj2.free();
// get the top index
if (field->lookup("TI", &obj2)->isInt()) {
topIdx = obj2.getInt();
} else {
topIdx = 0;
}
obj2.free();
// draw the text
drawListBox(text, selection, nOptions, topIdx, da, fontDict, quadding);
for (i = 0; i < nOptions; ++i) {
delete text[i];
}
gfree(text);
gfree(selection);
}
obj1.free();
}
} else if (ftObj.isName("Sig")) {
//~unimp
} else {
error(-1, "Unknown field type");
}
if (da) {
delete da;
}
// build the appearance stream dictionary
appearDict.initDict(xref);
appearDict.dictAdd(copyString("Length"),
obj1.initInt(appearBuf->getLength()));
appearDict.dictAdd(copyString("Subtype"), obj1.initName("Form"));
obj1.initArray(xref);
obj1.arrayAdd(obj2.initReal(0));
obj1.arrayAdd(obj2.initReal(0));
obj1.arrayAdd(obj2.initReal(xMax - xMin));
obj1.arrayAdd(obj2.initReal(yMax - yMin));
appearDict.dictAdd(copyString("BBox"), &obj1);
// set the resource dictionary
if (drObj.isDict()) {
appearDict.dictAdd(copyString("Resources"), drObj.copy(&obj1));
}
drObj.free();
// build the appearance stream
appearStream = new MemStream(appearBuf->getCString(), 0,
appearBuf->getLength(), &appearDict);
appearance.free();
appearance.initStream(appearStream);
if (fontDict) {
delete fontDict;
}
ftObj.free();
mkObj.free();
}
// Set the current fill or stroke color, based on <a> (which should
// have 1, 3, or 4 elements). If <adjust> is +1, color is brightened;
// if <adjust> is -1, color is darkened; otherwise color is not
// modified.
void Annot::setColor(Array *a, GBool fill, int adjust) {
Object obj1;
double color[4];
int nComps, i;
nComps = a->getLength();
if (nComps > 4) {
nComps = 4;
}
for (i = 0; i < nComps && i < 4; ++i) {
if (a->get(i, &obj1)->isNum()) {
color[i] = obj1.getNum();
} else {
color[i] = 0;
}
obj1.free();
}
if (nComps == 4) {
adjust = -adjust;
}
if (adjust > 0) {
for (i = 0; i < nComps; ++i) {
color[i] = 0.5 * color[i] + 0.5;
}
} else if (adjust < 0) {
for (i = 0; i < nComps; ++i) {
color[i] = 0.5 * color[i];
}
}
if (nComps == 4) {
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:c}\n",
color[0], color[1], color[2], color[3],
fill ? 'k' : 'K');
} else if (nComps == 3) {
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:s}\n",
color[0], color[1], color[2],
fill ? "rg" : "RG");
} else {
appearBuf->appendf("{0:.2f} {1:c}\n",
color[0],
fill ? 'g' : 'G');
}
}
void Annot::writeTextString (GooString *text, GooString *appearBuf, int *i, int j,
CharCodeToUnicode *ccToUnicode, GBool password)
{
CharCode c;
int charSize;
if (*i == 0 && text->hasUnicodeMarker()) {
//we need to have an even number of chars
if (text->getLength () % 2 != 0) {
error(-1, "Annot::writeTextString, bad unicode string");
return;
}
//skip unicode marker an go one char forward because we read character by pairs
(*i) += 3;
charSize = 2;
} else
charSize = 1;
for (; (*i) < j; (*i)+=charSize) {
// Render '*' instead of characters for password
if (password)
appearBuf->append('*');
else {
c = text->getChar(*i);
if (ccToUnicode && text->hasUnicodeMarker()) {
char ctmp[2];
ctmp[0] = text->getChar((*i)-1);
ctmp[1] = text->getChar((*i));
ccToUnicode->mapToCharCode((Unicode*)ctmp, &c, 2);
if (c == '(' || c == ')' || c == '\\')
appearBuf->append('\\');
appearBuf->append(c);
} else {
c &= 0xff;
if (c == '(' || c == ')' || c == '\\') {
appearBuf->append('\\');
appearBuf->append(c);
} else if (c < 0x20 || c >= 0x80) {
appearBuf->appendf("\\{0:03o}", c);
} else {
appearBuf->append(c);
}
}
}
}
}
// Draw the variable text or caption for a field.
void Annot::drawText(GooString *text, GooString *da, GfxFontDict *fontDict,
GBool multiline, int comb, int quadding,
GBool txField, GBool forceZapfDingbats,
GBool password) {
GooList *daToks;
GooString *tok;
GfxFont *font;
double fontSize, fontSize2, border, x, xPrev, y, w, w2, wMax;
int tfPos, tmPos, i, j, k, c;
//~ if there is no MK entry, this should use the existing content stream,
//~ and only replace the marked content portion of it
//~ (this is only relevant for Tx fields)
// parse the default appearance string
tfPos = tmPos = -1;
if (da) {
daToks = new GooList();
i = 0;
while (i < da->getLength()) {
while (i < da->getLength() && Lexer::isSpace(da->getChar(i))) {
++i;
}
if (i < da->getLength()) {
for (j = i + 1;
j < da->getLength() && !Lexer::isSpace(da->getChar(j));
++j) ;
daToks->append(new GooString(da, i, j - i));
i = j;
}
}
for (i = 2; i < daToks->getLength(); ++i) {
if (i >= 2 && !((GooString *)daToks->get(i))->cmp("Tf")) {
tfPos = i - 2;
} else if (i >= 6 && !((GooString *)daToks->get(i))->cmp("Tm")) {
tmPos = i - 6;
}
}
} else {
daToks = NULL;
}
// force ZapfDingbats
//~ this should create the font if needed (?)
if (forceZapfDingbats) {
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos);
if (tok->cmp("/ZaDb")) {
tok->clear();
tok->append("/ZaDb");
}
}
}
// get the font and font size
font = NULL;
fontSize = 0;
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos);
if (tok->getLength() >= 1 && tok->getChar(0) == '/') {
if (!fontDict || !(font = fontDict->lookup(tok->getCString() + 1))) {
error(-1, "Unknown font in field's DA string");
}
} else {
error(-1, "Invalid font name in 'Tf' operator in field's DA string");
}
tok = (GooString *)daToks->get(tfPos + 1);
fontSize = atof(tok->getCString());
} else {
error(-1, "Missing 'Tf' operator in field's DA string");
}
if (!font) {
return;
}
// get the border width
border = borderStyle->getWidth();
// setup
if (txField) {
appearBuf->append("/Tx BMC\n");
}
appearBuf->append("q\n");
appearBuf->append("BT\n");
// multi-line text
if (multiline) {
// note: the comb flag is ignored in multiline mode
wMax = xMax - xMin - 2 * border - 4;
// compute font autosize
if (fontSize == 0) {
for (fontSize = 20; fontSize > 1; --fontSize) {
y = yMax - yMin;
w2 = 0;
i = 0;
while (i < text->getLength()) {
getNextLine(text, i, font, fontSize, wMax, &j, &w, &k);
if (w > w2) {
w2 = w;
}
i = k;
y -= fontSize;
}
// approximate the descender for the last line
if (y >= 0.33 * fontSize) {
break;
}
}
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos + 1);
tok->clear();
tok->appendf("{0:.2f}", fontSize);
}
}
// starting y coordinate
// (note: each line of text starts with a Td operator that moves
// down a line)
y = yMax - yMin;
// set the font matrix
if (tmPos >= 0) {
tok = (GooString *)daToks->get(tmPos + 4);
tok->clear();
tok->append('0');
tok = (GooString *)daToks->get(tmPos + 5);
tok->clear();
tok->appendf("{0:.2f}", y);
}
// write the DA string
if (daToks) {
for (i = 0; i < daToks->getLength(); ++i) {
appearBuf->append((GooString *)daToks->get(i))->append(' ');
}
}
// write the font matrix (if not part of the DA string)
if (tmPos < 0) {
appearBuf->appendf("1 0 0 1 0 {0:.2f} Tm\n", y);
}
// write a series of lines of text
i = 0;
xPrev = 0;
while (i < text->getLength()) {
getNextLine(text, i, font, fontSize, wMax, &j, &w, &k);
// compute text start position
switch (quadding) {
case fieldQuadLeft:
default:
x = border + 2;
break;
case fieldQuadCenter:
x = (xMax - xMin - w) / 2;
break;
case fieldQuadRight:
x = xMax - xMin - border - 2 - w;
break;
}
// draw the line
appearBuf->appendf("{0:.2f} {1:.2f} Td\n", x - xPrev, -fontSize);
appearBuf->append('(');
writeTextString (text, appearBuf, &i, j, font->getToUnicode(), password);
appearBuf->append(") Tj\n");
// next line
i = k;
xPrev = x;
}
// single-line text
} else {
//~ replace newlines with spaces? - what does Acrobat do?
// comb formatting
if (comb > 0) {
// compute comb spacing
w = (xMax - xMin - 2 * border) / comb;
// compute font autosize
if (fontSize == 0) {
fontSize = yMax - yMin - 2 * border;
if (w < fontSize) {
fontSize = w;
}
fontSize = floor(fontSize);
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos + 1);
tok->clear();
tok->appendf("{0:.2f}", fontSize);
}
}
// compute text start position
switch (quadding) {
case fieldQuadLeft:
default:
x = border + 2;
break;
case fieldQuadCenter:
x = border + 2 + 0.5 * (comb - text->getLength()) * w;
break;
case fieldQuadRight:
x = border + 2 + (comb - text->getLength()) * w;
break;
}
y = 0.5 * (yMax - yMin) - 0.4 * fontSize;
// set the font matrix
if (tmPos >= 0) {
tok = (GooString *)daToks->get(tmPos + 4);
tok->clear();
tok->appendf("{0:.2f}", x);
tok = (GooString *)daToks->get(tmPos + 5);
tok->clear();
tok->appendf("{0:.2f}", y);
}
// write the DA string
if (daToks) {
for (i = 0; i < daToks->getLength(); ++i) {
appearBuf->append((GooString *)daToks->get(i))->append(' ');
}
}
// write the font matrix (if not part of the DA string)
if (tmPos < 0) {
appearBuf->appendf("1 0 0 1 {0:.2f} {1:.2f} Tm\n", x, y);
}
// write the text string
//~ this should center (instead of left-justify) each character within
//~ its comb cell
for (i = 0; i < text->getLength(); ++i) {
if (i > 0) {
appearBuf->appendf("{0:.2f} 0 Td\n", w);
}
appearBuf->append('(');
//~ it would be better to call it only once for the whole string instead of once for
//each character => but we need to handle centering in writeTextString
writeTextString (text, appearBuf, &i, i+1, font->getToUnicode(), password);
appearBuf->append(") Tj\n");
}
// regular (non-comb) formatting
} else {
// compute string width
if (font && !font->isCIDFont()) {
w = 0;
for (i = 0; i < text->getLength(); ++i) {
w += ((Gfx8BitFont *)font)->getWidth(text->getChar(i));
}
} else {
// otherwise, make a crude estimate
w = text->getLength() * 0.5;
}
// compute font autosize
if (fontSize == 0) {
fontSize = yMax - yMin - 2 * border;
fontSize2 = (xMax - xMin - 4 - 2 * border) / w;
if (fontSize2 < fontSize) {
fontSize = fontSize2;
}
fontSize = floor(fontSize);
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos + 1);
tok->clear();
tok->appendf("{0:.2f}", fontSize);
}
}
// compute text start position
w *= fontSize;
switch (quadding) {
case fieldQuadLeft:
default:
x = border + 2;
break;
case fieldQuadCenter:
x = (xMax - xMin - w) / 2;
break;
case fieldQuadRight:
x = xMax - xMin - border - 2 - w;
break;
}
y = 0.5 * (yMax - yMin) - 0.4 * fontSize;
// set the font matrix
if (tmPos >= 0) {
tok = (GooString *)daToks->get(tmPos + 4);
tok->clear();
tok->appendf("{0:.2f}", x);
tok = (GooString *)daToks->get(tmPos + 5);
tok->clear();
tok->appendf("{0:.2f}", y);
}
// write the DA string
if (daToks) {
for (i = 0; i < daToks->getLength(); ++i) {
appearBuf->append((GooString *)daToks->get(i))->append(' ');
}
}
// write the font matrix (if not part of the DA string)
if (tmPos < 0) {
appearBuf->appendf("1 0 0 1 {0:.2f} {1:.2f} Tm\n", x, y);
}
// write the text string
appearBuf->append('(');
i=0;
writeTextString (text, appearBuf, &i, text->getLength(), font->getToUnicode(), password);
appearBuf->append(") Tj\n");
}
}
// cleanup
appearBuf->append("ET\n");
appearBuf->append("Q\n");
if (txField) {
appearBuf->append("EMC\n");
}
if (daToks) {
deleteGooList(daToks, GooString);
}
}
// Draw the variable text or caption for a field.
void Annot::drawListBox(GooString **text, GBool *selection,
int nOptions, int topIdx,
GooString *da, GfxFontDict *fontDict, GBool quadding) {
GooList *daToks;
GooString *tok;
GfxFont *font;
double fontSize, fontSize2, border, x, y, w, wMax;
int tfPos, tmPos, i, j, c;
//~ if there is no MK entry, this should use the existing content stream,
//~ and only replace the marked content portion of it
//~ (this is only relevant for Tx fields)
// parse the default appearance string
tfPos = tmPos = -1;
if (da) {
daToks = new GooList();
i = 0;
while (i < da->getLength()) {
while (i < da->getLength() && Lexer::isSpace(da->getChar(i))) {
++i;
}
if (i < da->getLength()) {
for (j = i + 1;
j < da->getLength() && !Lexer::isSpace(da->getChar(j));
++j) ;
daToks->append(new GooString(da, i, j - i));
i = j;
}
}
for (i = 2; i < daToks->getLength(); ++i) {
if (i >= 2 && !((GooString *)daToks->get(i))->cmp("Tf")) {
tfPos = i - 2;
} else if (i >= 6 && !((GooString *)daToks->get(i))->cmp("Tm")) {
tmPos = i - 6;
}
}
} else {
daToks = NULL;
}
// get the font and font size
font = NULL;
fontSize = 0;
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos);
if (tok->getLength() >= 1 && tok->getChar(0) == '/') {
if (!fontDict || !(font = fontDict->lookup(tok->getCString() + 1))) {
error(-1, "Unknown font in field's DA string");
}
} else {
error(-1, "Invalid font name in 'Tf' operator in field's DA string");
}
tok = (GooString *)daToks->get(tfPos + 1);
fontSize = atof(tok->getCString());
} else {
error(-1, "Missing 'Tf' operator in field's DA string");
}
if (!font) {
return;
}
// get the border width
border = borderStyle->getWidth();
// compute font autosize
if (fontSize == 0) {
wMax = 0;
for (i = 0; i < nOptions; ++i) {
if (font && !font->isCIDFont()) {
w = 0;
for (j = 0; j < text[i]->getLength(); ++j) {
w += ((Gfx8BitFont *)font)->getWidth(text[i]->getChar(j));
}
} else {
// otherwise, make a crude estimate
w = text[i]->getLength() * 0.5;
}
if (w > wMax) {
wMax = w;
}
}
fontSize = yMax - yMin - 2 * border;
fontSize2 = (xMax - xMin - 4 - 2 * border) / wMax;
if (fontSize2 < fontSize) {
fontSize = fontSize2;
}
fontSize = floor(fontSize);
if (tfPos >= 0) {
tok = (GooString *)daToks->get(tfPos + 1);
tok->clear();
tok->appendf("{0:.2f}", fontSize);
}
}
// draw the text
y = yMax - yMin - 1.1 * fontSize;
for (i = topIdx; i < nOptions; ++i) {
// setup
appearBuf->append("q\n");
// draw the background if selected
if (selection[i]) {
appearBuf->append("0 g f\n");
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} re f\n",
border,
y - 0.2 * fontSize,
xMax - xMin - 2 * border,
1.1 * fontSize);
}
// setup
appearBuf->append("BT\n");
// compute string width
if (font && !font->isCIDFont()) {
w = 0;
for (j = 0; j < text[i]->getLength(); ++j) {
w += ((Gfx8BitFont *)font)->getWidth(text[i]->getChar(j));
}
} else {
// otherwise, make a crude estimate
w = text[i]->getLength() * 0.5;
}
// compute text start position
w *= fontSize;
switch (quadding) {
case fieldQuadLeft:
default:
x = border + 2;
break;
case fieldQuadCenter:
x = (xMax - xMin - w) / 2;
break;
case fieldQuadRight:
x = xMax - xMin - border - 2 - w;
break;
}
// set the font matrix
if (tmPos >= 0) {
tok = (GooString *)daToks->get(tmPos + 4);
tok->clear();
tok->appendf("{0:.2f}", x);
tok = (GooString *)daToks->get(tmPos + 5);
tok->clear();
tok->appendf("{0:.2f}", y);
}
// write the DA string
if (daToks) {
for (j = 0; j < daToks->getLength(); ++j) {
appearBuf->append((GooString *)daToks->get(j))->append(' ');
}
}
// write the font matrix (if not part of the DA string)
if (tmPos < 0) {
appearBuf->appendf("1 0 0 1 {0:.2f} {1:.2f} Tm\n", x, y);
}
// change the text color if selected
if (selection[i]) {
appearBuf->append("1 g\n");
}
// write the text string
appearBuf->append('(');
j = 0;
writeTextString (text[i], appearBuf, &j, text[i]->getLength(), font->getToUnicode(), false);
appearBuf->append(") Tj\n");
// cleanup
appearBuf->append("ET\n");
appearBuf->append("Q\n");
// next line
y -= 1.1 * fontSize;
}
if (daToks) {
deleteGooList(daToks, GooString);
}
}
// Figure out how much text will fit on the next line. Returns:
// *end = one past the last character to be included
// *width = width of the characters start .. end-1
// *next = index of first character on the following line
void Annot::getNextLine(GooString *text, int start,
GfxFont *font, double fontSize, double wMax,
int *end, double *width, int *next) {
double w, dw;
int j, k, c;
// figure out how much text will fit on the line
//~ what does Adobe do with tabs?
w = 0;
for (j = start; j < text->getLength() && w <= wMax; ++j) {
c = text->getChar(j) & 0xff;
if (c == 0x0a || c == 0x0d) {
break;
}
if (font && !font->isCIDFont()) {
dw = ((Gfx8BitFont *)font)->getWidth(c) * fontSize;
} else {
// otherwise, make a crude estimate
dw = 0.5 * fontSize;
}
w += dw;
}
if (w > wMax) {
for (k = j; k > start && text->getChar(k-1) != ' '; --k) ;
for (; k > start && text->getChar(k-1) == ' '; --k) ;
if (k > start) {
j = k;
}
if (j == start) {
// handle the pathological case where the first character is
// too wide to fit on the line all by itself
j = start + 1;
}
}
*end = j;
// compute the width
w = 0;
for (k = start; k < j; ++k) {
if (font && !font->isCIDFont()) {
dw = ((Gfx8BitFont *)font)->getWidth(text->getChar(k)) * fontSize;
} else {
// otherwise, make a crude estimate
dw = 0.5 * fontSize;
}
w += dw;
}
*width = w;
// next line
while (j < text->getLength() && text->getChar(j) == ' ') {
++j;
}
if (j < text->getLength() && text->getChar(j) == 0x0d) {
++j;
}
if (j < text->getLength() && text->getChar(j) == 0x0a) {
++j;
}
*next = j;
}
// Draw an (approximate) circle of radius <r> centered at (<cx>, <cy>).
// If <fill> is true, the circle is filled; otherwise it is stroked.
void Annot::drawCircle(double cx, double cy, double r, GBool fill) {
appearBuf->appendf("{0:.2f} {1:.2f} m\n",
cx + r, cy);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx + r, cy + bezierCircle * r,
cx + bezierCircle * r, cy + r,
cx, cy + r);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx - bezierCircle * r, cy + r,
cx - r, cy + bezierCircle * r,
cx - r, cy);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx - r, cy - bezierCircle * r,
cx - bezierCircle * r, cy - r,
cx, cy - r);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx + bezierCircle * r, cy - r,
cx + r, cy - bezierCircle * r,
cx + r, cy);
appearBuf->append(fill ? "f\n" : "s\n");
}
// Draw the top-left half of an (approximate) circle of radius <r>
// centered at (<cx>, <cy>).
void Annot::drawCircleTopLeft(double cx, double cy, double r) {
double r2;
r2 = r / sqrt(2.0);
appearBuf->appendf("{0:.2f} {1:.2f} m\n",
cx + r2, cy + r2);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx + (1 - bezierCircle) * r2,
cy + (1 + bezierCircle) * r2,
cx - (1 - bezierCircle) * r2,
cy + (1 + bezierCircle) * r2,
cx - r2,
cy + r2);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx - (1 + bezierCircle) * r2,
cy + (1 - bezierCircle) * r2,
cx - (1 + bezierCircle) * r2,
cy - (1 - bezierCircle) * r2,
cx - r2,
cy - r2);
appearBuf->append("S\n");
}
// Draw the bottom-right half of an (approximate) circle of radius <r>
// centered at (<cx>, <cy>).
void Annot::drawCircleBottomRight(double cx, double cy, double r) {
double r2;
r2 = r / sqrt(2.0);
appearBuf->appendf("{0:.2f} {1:.2f} m\n",
cx - r2, cy - r2);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx - (1 - bezierCircle) * r2,
cy - (1 + bezierCircle) * r2,
cx + (1 - bezierCircle) * r2,
cy - (1 + bezierCircle) * r2,
cx + r2,
cy - r2);
appearBuf->appendf("{0:.2f} {1:.2f} {2:.2f} {3:.2f} {4:.2f} {5:.2f} c\n",
cx + (1 + bezierCircle) * r2,
cy - (1 - bezierCircle) * r2,
cx + (1 + bezierCircle) * r2,
cy + (1 - bezierCircle) * r2,
cx + r2,
cy + r2);
appearBuf->append("S\n");
}
void Annot::draw(Gfx *gfx, GBool printing) {
Object obj;
GBool isLink;
// check the flags
if ((flags & annotFlagHidden) ||
(printing && !(flags & annotFlagPrint)) ||
(!printing && (flags & annotFlagNoView))) {
return;
}
// draw the appearance stream
isLink = type && !type->cmp("Link");
appearance.fetch(xref, &obj);
gfx->drawAnnot(&obj, isLink ? borderStyle : (AnnotBorderStyle *)NULL,
xMin, yMin, xMax, yMax);
obj.free();
}
//------------------------------------------------------------------------
// Annots
//------------------------------------------------------------------------
Annots::Annots(XRef *xref, Catalog *catalog, Object *annotsObj) {
Dict *acroForm;
Annot *annot;
Object obj1;
int size;
int i;
annots = NULL;
size = 0;
nAnnots = 0;
acroForm = catalog->getAcroForm()->isDict() ?
catalog->getAcroForm()->getDict() : NULL;
if (annotsObj->isArray()) {
for (i = 0; i < annotsObj->arrayGetLength(); ++i) {
//get the Ref to this annot and pass it to Annot constructor
//this way, it'll be possible for the annot to retrieve the corresponding
//form widget
Object obj2;
Ref* pref;
if (annotsObj->arrayGet(i, &obj1)->isDict()) {
if (annotsObj->arrayGetNF(i, &obj2)->isRef())
annot = new Annot(xref, acroForm, obj1.getDict(), obj2.getRef(), catalog);
else
annot = new Annot(xref, acroForm, obj1.getDict(), catalog);
if (annot->isOk()) {
if (nAnnots >= size) {
size += 16;
annots = (Annot **)greallocn(annots, size, sizeof(Annot *));
}
annots[nAnnots++] = annot;
} else {
delete annot;
}
}
obj2.free();
obj1.free();
}
}
}
void Annots::generateAppearances(Dict *acroForm) {
Object obj1, obj2;
Ref ref;
int i;
if (acroForm->lookup("Fields", &obj1)->isArray()) {
for (i = 0; i < obj1.arrayGetLength(); ++i) {
if (obj1.arrayGetNF(i, &obj2)->isRef()) {
ref = obj2.getRef();
obj2.free();
obj1.arrayGet(i, &obj2);
} else {
ref.num = ref.gen = -1;
}
if (obj2.isDict()) {
scanFieldAppearances(obj2.getDict(), &ref, NULL, acroForm);
}
obj2.free();
}
}
obj1.free();
}
void Annots::scanFieldAppearances(Dict *node, Ref *ref, Dict *parent,
Dict *acroForm) {
Annot *annot;
Object obj1, obj2;
Ref ref2;
int i;
// non-terminal node: scan the children
if (node->lookup("Kids", &obj1)->isArray()) {
for (i = 0; i < obj1.arrayGetLength(); ++i) {
if (obj1.arrayGetNF(i, &obj2)->isRef()) {
ref2 = obj2.getRef();
obj2.free();
obj1.arrayGet(i, &obj2);
} else {
ref2.num = ref2.gen = -1;
}
if (obj2.isDict()) {
scanFieldAppearances(obj2.getDict(), &ref2, node, acroForm);
}
obj2.free();
}
obj1.free();
return;
}
obj1.free();
// terminal node: this is either a combined annot/field dict, or an
// annot dict whose parent is a field
if ((annot = findAnnot(ref))) {
node->lookupNF("Parent", &obj1);
if (!parent || !obj1.isNull()) {
annot->generateFieldAppearance(node, node, acroForm);
} else {
annot->generateFieldAppearance(parent, node, acroForm);
}
obj1.free();
}
}
Annot *Annots::findAnnot(Ref *ref) {
int i;
for (i = 0; i < nAnnots; ++i) {
if (annots[i]->match(ref)) {
return annots[i];
}
}
return NULL;
}
Annots::~Annots() {
int i;
for (i = 0; i < nAnnots; ++i) {
delete annots[i];
}
gfree(annots);
}