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 | 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 | +} | ... | ... |