Commit 104fd6da522c8828571580cb30f324c3cbe7283f

Authored by Jay Berkenbilt
1 parent 5059ec0d

Add matrix and annotation appearance stream handling

Generate page content fragment for rendering appearance streams
including all matrix calculation.
include/qpdf/QPDFAnnotationObjectHelper.hh
... ... @@ -72,6 +72,24 @@ class QPDFAnnotationObjectHelper: public QPDFObjectHelper
72 72 QPDFObjectHandle getAppearanceStream(std::string const& which,
73 73 std::string const& state = "");
74 74  
  75 + // Return a matrix that transforms from the annotation's
  76 + // appearance stream's coordinates to the page's coordinates. This
  77 + // method also honors the annotation's NoRotate flag if set. The
  78 + // matrix is returned as a string representing the six floating
  79 + // point numbers to be passed to a cm operator. Returns the empty
  80 + // string if it is unable to compute the matrix for any reason.
  81 + // The value "rotate" should be set to the page's /Rotate value or
  82 + // 0 if none.
  83 + QPDF_DLL
  84 + std::string getAnnotationAppearanceMatrix(int rotate);
  85 +
  86 + // Generate text suitable for addition to the containing page's
  87 + // content stream that replaces this annotation's appearance
  88 + // stream. The value "rotate" should be set to the page's /Rotate
  89 + // value or 0 if none.
  90 + QPDF_DLL
  91 + std::string getPageContentForAppearance(int rotate);
  92 +
75 93 private:
76 94 class Members
77 95 {
... ...
libqpdf/QPDFAnnotationObjectHelper.cc
1 1 #include <qpdf/QPDFAnnotationObjectHelper.hh>
2 2 #include <qpdf/QTC.hh>
  3 +#include <qpdf/QPDFMatrix.hh>
  4 +#include <qpdf/QUtil.hh>
  5 +#include <qpdf/QPDF.hh>
  6 +#include <qpdf/QPDFNameTreeObjectHelper.hh>
  7 +#include <algorithm>
3 8  
4 9 QPDFAnnotationObjectHelper::Members::~Members()
5 10 {
... ... @@ -73,3 +78,190 @@ QPDFAnnotationObjectHelper::getAppearanceStream(
73 78 QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN null");
74 79 return QPDFObjectHandle::newNull();
75 80 }
  81 +
  82 +std::string
  83 +QPDFAnnotationObjectHelper::getAnnotationAppearanceMatrix(int rotate)
  84 +{
  85 + // The appearance matrix is the transformation in effect when
  86 + // rendering an appearance stream's content. The appearance stream
  87 + // itself is a form XObject, which has a /BBox and an optional
  88 + // /Matrix. The /BBox describes the bounding box of the annotation
  89 + // in unrotated page coordinates. /Matrix may be applied to the
  90 + // bounding box to transform the bounding box. The effect of this
  91 + // is that the transformed box is still fit within the area the
  92 + // annotation designates in its /Rect field.
  93 +
  94 + // The algorithm for computing the appearance matrix described in
  95 + // section 12.5.5 of the ISO-32000 PDF spec. It is as follows:
  96 +
  97 + // 1. Transform the four corners of /BBox by applying /Matrix to
  98 + // them, creating an arbitrarily transformed quadrilateral.
  99 +
  100 + // 2. Find the minimum upright rectangle that encompasses the
  101 + // resulting quadrilateral. This is the "transformed appearance
  102 + // box", T.
  103 +
  104 + // 3. Compute matrix A that maps the lower left and upper right
  105 + // corners of T to the annotation's /Rect. This can be done by
  106 + // translating the lower left corner and then scaling so that
  107 + // the upper right corner also matches.
  108 +
  109 + // 4. Concatenate /Matrix to A to get matrix AA. This matrix
  110 + // translates from appearance stream coordinates to page
  111 + // coordinates.
  112 +
  113 + // If the annotation's /F flag has bit 4 set, we modify the matrix
  114 + // to also rotate the annotation in the opposite direction, and we
  115 + // adjust the destination rectangle by rotating it about the upper
  116 + // left hand corner so that the annotation will appear upright on
  117 + // the rotated page.
  118 +
  119 + // You can see that the above algorithm works by imagining the
  120 + // following:
  121 +
  122 + // * In the simple case of where /BBox = /Rect and /Matrix is the
  123 + // identity matrix, the transformed quadrilateral in step 1 will
  124 + // be the bounding box. Since the bounding box is upright, T
  125 + // will be the bounding box. Since /BBox = /Rect, matrix A is
  126 + // the identity matrix, and matrix AA in step 4 is also the
  127 + // identity matrix.
  128 + //
  129 + // * Imagine that the rectangle is different from the bounding
  130 + // box. In this case, matrix A just transforms the bounding box
  131 + // to the rectangle by scaling and translating, effectively
  132 + // squeezing or stretching it into /Rect.
  133 + //
  134 + // * Imagine that /Matrix rotates the annotation by 30 degrees.
  135 + // The transformed bounding box would stick out, and T would be
  136 + // too big. In this case, matrix A shrinks T down until it fits
  137 + // in /Rect.
  138 +
  139 + QPDFObjectHandle rect_obj = this->oh.getKey("/Rect");
  140 + QPDFObjectHandle flags = this->oh.getKey("/F");
  141 + QPDFObjectHandle as = getAppearanceStream("/N").getDict();
  142 + QPDFObjectHandle bbox_obj = as.getKey("/BBox");
  143 + QPDFObjectHandle matrix_obj = as.getKey("/Matrix");
  144 +
  145 + if (! (bbox_obj.isRectangle() && rect_obj.isRectangle()))
  146 + {
  147 + return "";
  148 + }
  149 + QPDFMatrix matrix;
  150 + if (matrix_obj.isMatrix())
  151 + {
  152 +/// QTC::TC("qpdf", "QPDFAnnotationObjectHelper explicit matrix");
  153 + matrix = QPDFMatrix(matrix_obj.getArrayAsMatrix());
  154 + }
  155 + else
  156 + {
  157 +/// QTC::TC("qpdf", "QPDFAnnotationObjectHelper default matrix");
  158 + }
  159 + QPDFObjectHandle::Rectangle rect = rect_obj.getArrayAsRectangle();
  160 + if (rotate && flags.isInteger() && (flags.getIntValue() & 16))
  161 + {
  162 + // If the the annotation flags include the NoRotate bit and
  163 + // the page is rotated, we have to rotate the annotation about
  164 + // its upper left corner by the same amount in the opposite
  165 + // direction so that it will remain upright in absolute
  166 + // coordinates. Since the semantics of /Rotate for a page are
  167 + // to rotate the page, while the effect of rotating using a
  168 + // transformation matrix is to rotate the coordinate system,
  169 + // the opposite directionality is explicit in the code.
  170 + QPDFMatrix mr;
  171 + mr.rotatex90(rotate);
  172 + mr.concat(matrix);
  173 + matrix = mr;
  174 + double rect_w = rect.urx - rect.llx;
  175 + double rect_h = rect.ury - rect.lly;
  176 + switch (rotate)
  177 + {
  178 + case 90:
  179 +/// QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 90");
  180 + rect = QPDFObjectHandle::Rectangle(
  181 + rect.llx,
  182 + rect.ury,
  183 + rect.llx + rect_h,
  184 + rect.ury + rect_w);
  185 + break;
  186 + case 180:
  187 +/// QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 180");
  188 + rect = QPDFObjectHandle::Rectangle(
  189 + rect.llx - rect_w,
  190 + rect.ury,
  191 + rect.llx,
  192 + rect.ury + rect_h);
  193 + break;
  194 + case 270:
  195 +/// QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 270");
  196 + rect = QPDFObjectHandle::Rectangle(
  197 + rect.llx - rect_h,
  198 + rect.ury - rect_w,
  199 + rect.llx,
  200 + rect.ury);
  201 + break;
  202 + default:
  203 + // ignore
  204 + break;
  205 + }
  206 + }
  207 +
  208 + // Transform bounding box by matrix to get T
  209 + QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle();
  210 + std::vector<double> bx(4);
  211 + std::vector<double> by(4);
  212 + matrix.transform(bbox.llx, bbox.lly, bx.at(0), by.at(0));
  213 + matrix.transform(bbox.llx, bbox.ury, bx.at(1), by.at(1));
  214 + matrix.transform(bbox.urx, bbox.lly, bx.at(2), by.at(2));
  215 + matrix.transform(bbox.urx, bbox.ury, bx.at(3), by.at(3));
  216 + // Find the transformed appearance box
  217 + double t_llx = *std::min_element(bx.begin(), bx.end());
  218 + double t_urx = *std::max_element(bx.begin(), bx.end());
  219 + double t_lly = *std::min_element(by.begin(), by.end());
  220 + double t_ury = *std::max_element(by.begin(), by.end());
  221 + if ((t_urx == t_llx) || (t_ury == t_lly))
  222 + {
  223 + // avoid division by zero
  224 + return "";
  225 + }
  226 + // Compute a matrix to transform the appearance box to the rectangle
  227 + QPDFMatrix AA;
  228 + AA.translate(rect.llx, rect.lly);
  229 + AA.scale((rect.urx - rect.llx) / (t_urx - t_llx),
  230 + (rect.ury - rect.lly) / (t_ury - t_lly));
  231 + AA.translate(-t_llx, -t_lly);
  232 + // Concatenate the user-specified matrix
  233 + AA.concat(matrix);
  234 + return AA.unparse();
  235 +}
  236 +
  237 +std::string
  238 +QPDFAnnotationObjectHelper::getPageContentForAppearance(int rotate)
  239 +{
  240 + QPDFObjectHandle as = getAppearanceStream("/N");
  241 + if (! (as.isStream() && as.getDict().getKey("/BBox").isRectangle()))
  242 + {
  243 + return "";
  244 + }
  245 +
  246 + QPDFObjectHandle::Rectangle rect =
  247 + as.getDict().getKey("/BBox").getArrayAsRectangle();
  248 + std::string cm = getAnnotationAppearanceMatrix(rotate);
  249 + if (cm.empty())
  250 + {
  251 + return "";
  252 + }
  253 + std::string as_content = (
  254 + "q\n" +
  255 + cm + " cm\n" +
  256 + QUtil::double_to_string(rect.llx, 5) + " " +
  257 + QUtil::double_to_string(rect.lly, 5) + " " +
  258 + QUtil::double_to_string(rect.urx - rect.llx, 5) + " " +
  259 + QUtil::double_to_string(rect.ury - rect.lly, 5) + " " +
  260 + "re W n\n");
  261 + PointerHolder<Buffer> buf = as.getStreamData(qpdf_dl_all);
  262 + as_content += std::string(
  263 + reinterpret_cast<char *>(buf->getBuffer()),
  264 + buf->getSize());
  265 + as_content += "\nQ\n";
  266 + return as_content;
  267 +}
... ...