From 104fd6da522c8828571580cb30f324c3cbe7283f Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Mon, 24 Dec 2018 17:54:48 -0500 Subject: Add matrix and annotation appearance stream handling Generate page content fragment for rendering appearance streams including all matrix calculation. --- libqpdf/QPDFAnnotationObjectHelper.cc | 192 ++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) (limited to 'libqpdf') diff --git a/libqpdf/QPDFAnnotationObjectHelper.cc b/libqpdf/QPDFAnnotationObjectHelper.cc index ee1d8180..a5b824f3 100644 --- a/libqpdf/QPDFAnnotationObjectHelper.cc +++ b/libqpdf/QPDFAnnotationObjectHelper.cc @@ -1,5 +1,10 @@ #include #include +#include +#include +#include +#include +#include QPDFAnnotationObjectHelper::Members::~Members() { @@ -73,3 +78,190 @@ QPDFAnnotationObjectHelper::getAppearanceStream( QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN null"); return QPDFObjectHandle::newNull(); } + +std::string +QPDFAnnotationObjectHelper::getAnnotationAppearanceMatrix(int rotate) +{ + // The appearance matrix is the transformation in effect when + // rendering an appearance stream's content. The appearance stream + // itself is a form XObject, which has a /BBox and an optional + // /Matrix. The /BBox describes the bounding box of the annotation + // in unrotated page coordinates. /Matrix may be applied to the + // bounding box to transform the bounding box. The effect of this + // is that the transformed box is still fit within the area the + // annotation designates in its /Rect field. + + // The algorithm for computing the appearance matrix described in + // section 12.5.5 of the ISO-32000 PDF spec. It is as follows: + + // 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 + // translating the lower left corner and then scaling so that + // the upper right corner also matches. + + // 4. Concatenate /Matrix to A to get matrix AA. This matrix + // translates from appearance stream coordinates to page + // coordinates. + + // If the annotation's /F flag has bit 4 set, we modify the matrix + // to also rotate the annotation in the opposite direction, and we + // adjust the destination rectangle by rotating it about the upper + // left hand corner so that the annotation will appear upright on + // the rotated page. + + // You can see that the above algorithm works by imagining the + // following: + + // * In the simple case of where /BBox = /Rect and /Matrix is the + // identity matrix, the transformed quadrilateral in step 1 will + // be the bounding box. Since the bounding box is upright, T + // will be the bounding box. Since /BBox = /Rect, matrix A is + // the identity matrix, and matrix AA in step 4 is also the + // identity matrix. + // + // * Imagine that the rectangle is different from the bounding + // box. In this case, matrix A just transforms the bounding box + // to the rectangle by scaling and translating, effectively + // squeezing or stretching it into /Rect. + // + // * Imagine that /Matrix rotates the annotation by 30 degrees. + // The transformed bounding box would stick out, and T would be + // too big. In this case, matrix A shrinks T down until it fits + // in /Rect. + + QPDFObjectHandle rect_obj = this->oh.getKey("/Rect"); + QPDFObjectHandle flags = this->oh.getKey("/F"); + QPDFObjectHandle as = getAppearanceStream("/N").getDict(); + QPDFObjectHandle bbox_obj = as.getKey("/BBox"); + QPDFObjectHandle matrix_obj = as.getKey("/Matrix"); + + 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(); + if (rotate && flags.isInteger() && (flags.getIntValue() & 16)) + { + // 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(); + std::vector bx(4); + std::vector by(4); + matrix.transform(bbox.llx, bbox.lly, bx.at(0), by.at(0)); + matrix.transform(bbox.llx, bbox.ury, bx.at(1), by.at(1)); + matrix.transform(bbox.urx, bbox.lly, bx.at(2), by.at(2)); + matrix.transform(bbox.urx, bbox.ury, bx.at(3), by.at(3)); + // Find the transformed appearance box + double t_llx = *std::min_element(bx.begin(), bx.end()); + double t_urx = *std::max_element(bx.begin(), bx.end()); + double t_lly = *std::min_element(by.begin(), by.end()); + double t_ury = *std::max_element(by.begin(), by.end()); + 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); + // Concatenate the user-specified matrix + AA.concat(matrix); + return AA.unparse(); +} + +std::string +QPDFAnnotationObjectHelper::getPageContentForAppearance(int rotate) +{ + QPDFObjectHandle as = getAppearanceStream("/N"); + if (! (as.isStream() && as.getDict().getKey("/BBox").isRectangle())) + { + return ""; + } + + QPDFObjectHandle::Rectangle rect = + as.getDict().getKey("/BBox").getArrayAsRectangle(); + std::string cm = getAnnotationAppearanceMatrix(rotate); + if (cm.empty()) + { + return ""; + } + std::string as_content = ( + "q\n" + + cm + " cm\n" + + QUtil::double_to_string(rect.llx, 5) + " " + + QUtil::double_to_string(rect.lly, 5) + " " + + QUtil::double_to_string(rect.urx - rect.llx, 5) + " " + + QUtil::double_to_string(rect.ury - rect.lly, 5) + " " + + "re W n\n"); + PointerHolder buf = as.getStreamData(qpdf_dl_all); + as_content += std::string( + reinterpret_cast(buf->getBuffer()), + buf->getSize()); + as_content += "\nQ\n"; + return as_content; +} -- cgit v1.2.3-54-g00ecf