Commit 104fd6da522c8828571580cb30f324c3cbe7283f
1 parent
5059ec0d
Add matrix and annotation appearance stream handling
Generate page content fragment for rendering appearance streams including all matrix calculation.
Showing
2 changed files
with
210 additions
and
0 deletions
include/qpdf/QPDFAnnotationObjectHelper.hh
| @@ -72,6 +72,24 @@ class QPDFAnnotationObjectHelper: public QPDFObjectHelper | @@ -72,6 +72,24 @@ class QPDFAnnotationObjectHelper: public QPDFObjectHelper | ||
| 72 | QPDFObjectHandle getAppearanceStream(std::string const& which, | 72 | QPDFObjectHandle getAppearanceStream(std::string const& which, |
| 73 | std::string const& state = ""); | 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 | private: | 93 | private: |
| 76 | class Members | 94 | class Members |
| 77 | { | 95 | { |
libqpdf/QPDFAnnotationObjectHelper.cc
| 1 | #include <qpdf/QPDFAnnotationObjectHelper.hh> | 1 | #include <qpdf/QPDFAnnotationObjectHelper.hh> |
| 2 | #include <qpdf/QTC.hh> | 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 | QPDFAnnotationObjectHelper::Members::~Members() | 9 | QPDFAnnotationObjectHelper::Members::~Members() |
| 5 | { | 10 | { |
| @@ -73,3 +78,190 @@ QPDFAnnotationObjectHelper::getAppearanceStream( | @@ -73,3 +78,190 @@ QPDFAnnotationObjectHelper::getAppearanceStream( | ||
| 73 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN null"); | 78 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AN null"); |
| 74 | return QPDFObjectHandle::newNull(); | 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 | +} |