mirror of
https://github.com/qpdf/qpdf.git
synced 2024-11-11 15:40:58 +00:00
284 lines
9.6 KiB
C++
284 lines
9.6 KiB
C++
#include <qpdf/QPDFAnnotationObjectHelper.hh>
|
|
#include <qpdf/QTC.hh>
|
|
#include <qpdf/QPDFMatrix.hh>
|
|
#include <qpdf/QUtil.hh>
|
|
#include <qpdf/QPDF.hh>
|
|
#include <qpdf/QPDFNameTreeObjectHelper.hh>
|
|
|
|
QPDFAnnotationObjectHelper::Members::~Members()
|
|
{
|
|
}
|
|
|
|
QPDFAnnotationObjectHelper::Members::Members()
|
|
{
|
|
}
|
|
|
|
QPDFAnnotationObjectHelper::QPDFAnnotationObjectHelper(QPDFObjectHandle oh) :
|
|
QPDFObjectHelper(oh)
|
|
{
|
|
}
|
|
|
|
std::string
|
|
QPDFAnnotationObjectHelper::getSubtype()
|
|
{
|
|
return this->oh.getKey("/Subtype").getName();
|
|
}
|
|
|
|
QPDFObjectHandle::Rectangle
|
|
QPDFAnnotationObjectHelper::getRect()
|
|
{
|
|
return this->oh.getKey("/Rect").getArrayAsRectangle();
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFAnnotationObjectHelper::getAppearanceDictionary()
|
|
{
|
|
return this->oh.getKey("/AP");
|
|
}
|
|
|
|
std::string
|
|
QPDFAnnotationObjectHelper::getAppearanceState()
|
|
{
|
|
if (this->oh.getKey("/AS").isName())
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS present");
|
|
return this->oh.getKey("/AS").getName();
|
|
}
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS absent");
|
|
return "";
|
|
}
|
|
|
|
int
|
|
QPDFAnnotationObjectHelper::getFlags()
|
|
{
|
|
QPDFObjectHandle flags_obj = this->oh.getKey("/F");
|
|
return flags_obj.isInteger() ? flags_obj.getIntValueAsInt() : 0;
|
|
}
|
|
|
|
QPDFObjectHandle
|
|
QPDFAnnotationObjectHelper::getAppearanceStream(
|
|
std::string const& which,
|
|
std::string const& state)
|
|
{
|
|
QPDFObjectHandle ap = getAppearanceDictionary();
|
|
std::string desired_state = state.empty() ? getAppearanceState() : state;
|
|
if (ap.isDictionary())
|
|
{
|
|
QPDFObjectHandle ap_sub = ap.getKey(which);
|
|
if (ap_sub.isStream() && desired_state.empty())
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP stream");
|
|
return ap_sub;
|
|
}
|
|
if (ap_sub.isDictionary() && (! desired_state.empty()))
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP dictionary");
|
|
QPDFObjectHandle ap_sub_val = ap_sub.getKey(desired_state);
|
|
if (ap_sub_val.isStream())
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP sub stream");
|
|
return ap_sub_val;
|
|
}
|
|
}
|
|
}
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP null");
|
|
return QPDFObjectHandle::newNull();
|
|
}
|
|
|
|
std::string
|
|
QPDFAnnotationObjectHelper::getPageContentForAppearance(
|
|
std::string const& name, int rotate,
|
|
int required_flags, int forbidden_flags)
|
|
{
|
|
if (! getAppearanceStream("/N").isStream())
|
|
{
|
|
return "";
|
|
}
|
|
|
|
// The appearance matrix computed by this method is the
|
|
// transformation matrix that needs to be in effect when drawing
|
|
// this annotation's appearance stream on the page. The algorithm
|
|
// for computing the appearance matrix described in section 12.5.5
|
|
// of the ISO-32000 PDF spec is similar but not identical to what
|
|
// we are doing here.
|
|
|
|
// When rendering an appearance stream associated with an
|
|
// annotation, there are four relevant components:
|
|
//
|
|
// * The appearance stream's bounding box (/BBox)
|
|
// * The appearance stream's matrix (/Matrix)
|
|
// * The annotation's rectangle (/Rect)
|
|
// * In the case of form fields with the NoRotate flag, the
|
|
// page's rotation
|
|
|
|
// When rendering a form xobject in isolation, just drawn with a
|
|
// /Do operator, there is no form field, so page rotation is not
|
|
// relevant, and there is no annotation, so /Rect is not relevant,
|
|
// so only /BBox and /Matrix are relevant. The effect of these are
|
|
// as follows:
|
|
|
|
// * /BBox is treated as a clipping region
|
|
// * /Matrix is applied as a transformation prior to rendering the
|
|
// appearance stream.
|
|
|
|
// There is no relationship between /BBox and /Matrix in this
|
|
// case.
|
|
|
|
// When rendering a form xobject in the context of an annotation,
|
|
// things are a little different. In particular, a matrix is
|
|
// established such that /BBox, when transformed by /Matrix, would
|
|
// fit completely inside of /Rect. /BBox is no longer a clipping
|
|
// region. To illustrate the difference, consider a /Matrix of
|
|
// [2 0 0 2 0 0], which is scaling by a factor of two along both
|
|
// axes. If the appearance stream drew a rectangle equal to /BBox,
|
|
// in the case of the form xobject in isolation, this matrix would
|
|
// cause only the lower-left quadrant of the rectangle to be
|
|
// visible since the scaling would cause the rest of it to fall
|
|
// outside of the clipping region. In the case of the form xobject
|
|
// displayed in the context of an annotation, such a matrix would
|
|
// have no effect at all because it would be applied to the
|
|
// bounding box first, and then when the resulting enclosing
|
|
// quadrilateral was transformed to fit into /Rect, the effect of
|
|
// the scaling would be undone.
|
|
|
|
// Our job is to create a transformation matrix that compensates
|
|
// for these differences so that the appearance stream of an
|
|
// annotation can be drawn as a regular form xobject.
|
|
|
|
// To do this, we perform the following steps, which overlap
|
|
// significantly with the algorithm in 12.5.5:
|
|
|
|
// 1. Transform the four corners of /BBox by applying /Matrix to
|
|
// them, creating an arbitrarily transformed quadrilateral.
|
|
|
|
// 2. Find the minimum upright rectangle that encompasses the
|
|
// resulting quadrilateral. This is the "transformed appearance
|
|
// box", T.
|
|
|
|
// 3. Compute matrix A that maps the lower left and upper right
|
|
// corners of T to the annotation's /Rect. This can be done by
|
|
// scaling so that the sizes match and translating so that the
|
|
// scaled T exactly overlaps /Rect.
|
|
|
|
// If the annotation's /F flag has bit 4 set, this means that
|
|
// annotation is to be rotated about its upper left corner to
|
|
// counteract any rotation of the page so it remains upright. To
|
|
// achieve this effect, we do the following extra steps:
|
|
|
|
// 1. Perform the rotation on /BBox box prior to transforming it
|
|
// with /Matrix (by replacing matrix with concatenation of
|
|
// matrix onto the rotation)
|
|
|
|
// 2. Rotate the destination rectangle by the specified amount
|
|
|
|
// 3. Apply the rotation to A as computed above to get the final
|
|
// appearance matrix.
|
|
|
|
QPDFObjectHandle rect_obj = this->oh.getKey("/Rect");
|
|
QPDFObjectHandle as = getAppearanceStream("/N").getDict();
|
|
QPDFObjectHandle bbox_obj = as.getKey("/BBox");
|
|
QPDFObjectHandle matrix_obj = as.getKey("/Matrix");
|
|
|
|
int flags = getFlags();
|
|
if (flags & forbidden_flags)
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper forbidden flags");
|
|
return "";
|
|
}
|
|
if ((flags & required_flags) != required_flags)
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper missing required flags");
|
|
return "";
|
|
}
|
|
|
|
if (! (bbox_obj.isRectangle() && rect_obj.isRectangle()))
|
|
{
|
|
return "";
|
|
}
|
|
QPDFMatrix matrix;
|
|
if (matrix_obj.isMatrix())
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper explicit matrix");
|
|
matrix = QPDFMatrix(matrix_obj.getArrayAsMatrix());
|
|
}
|
|
else
|
|
{
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper default matrix");
|
|
}
|
|
QPDFObjectHandle::Rectangle rect = rect_obj.getArrayAsRectangle();
|
|
bool do_rotate = (rotate && (flags & an_no_rotate));
|
|
if (do_rotate)
|
|
{
|
|
// If the the annotation flags include the NoRotate bit and
|
|
// the page is rotated, we have to rotate the annotation about
|
|
// its upper left corner by the same amount in the opposite
|
|
// direction so that it will remain upright in absolute
|
|
// coordinates. Since the semantics of /Rotate for a page are
|
|
// to rotate the page, while the effect of rotating using a
|
|
// transformation matrix is to rotate the coordinate system,
|
|
// the opposite directionality is explicit in the code.
|
|
QPDFMatrix mr;
|
|
mr.rotatex90(rotate);
|
|
mr.concat(matrix);
|
|
matrix = mr;
|
|
double rect_w = rect.urx - rect.llx;
|
|
double rect_h = rect.ury - rect.lly;
|
|
switch (rotate)
|
|
{
|
|
case 90:
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 90");
|
|
rect = QPDFObjectHandle::Rectangle(
|
|
rect.llx,
|
|
rect.ury,
|
|
rect.llx + rect_h,
|
|
rect.ury + rect_w);
|
|
break;
|
|
case 180:
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 180");
|
|
rect = QPDFObjectHandle::Rectangle(
|
|
rect.llx - rect_w,
|
|
rect.ury,
|
|
rect.llx,
|
|
rect.ury + rect_h);
|
|
break;
|
|
case 270:
|
|
QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 270");
|
|
rect = QPDFObjectHandle::Rectangle(
|
|
rect.llx - rect_h,
|
|
rect.ury - rect_w,
|
|
rect.llx,
|
|
rect.ury);
|
|
break;
|
|
default:
|
|
// ignore
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Transform bounding box by matrix to get T
|
|
QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
|
|
QPDFObjectHandle::Rectangle T = matrix.transformRectangle(bbox);
|
|
if ((T.urx == T.llx) || (T.ury == T.lly))
|
|
{
|
|
// avoid division by zero
|
|
return "";
|
|
}
|
|
// Compute a matrix to transform the appearance box to the rectangle
|
|
QPDFMatrix AA;
|
|
AA.translate(rect.llx, rect.lly);
|
|
AA.scale((rect.urx - rect.llx) / (T.urx - T.llx),
|
|
(rect.ury - rect.lly) / (T.ury - T.lly));
|
|
AA.translate(-T.llx, -T.lly);
|
|
if (do_rotate)
|
|
{
|
|
AA.rotatex90(rotate);
|
|
}
|
|
|
|
as.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form"));
|
|
return (
|
|
"q\n" +
|
|
AA.unparse() + " cm\n" +
|
|
name + " Do\n" +
|
|
"Q\n");
|
|
}
|