#include #include #include #include #include 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()) { // According to the spec, Appearance State is supposed to refer to a subkey of the // appearance stream when /AP is a dictionary, but files have been seen in the wild // where Appearance State is `/N` and `/AP` is a stream. Therefore, if `which` points to // a stream, disregard state and just use the stream. See qpdf issue #949 for details. 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 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"); }