/src/qpdf/libqpdf/QPDFAnnotationObjectHelper.cc
Line | Count | Source (jump to first uncovered line) |
1 | | #include <qpdf/QPDFAnnotationObjectHelper.hh> |
2 | | |
3 | | #include <qpdf/QPDF.hh> |
4 | | #include <qpdf/QPDFMatrix.hh> |
5 | | #include <qpdf/QTC.hh> |
6 | | #include <qpdf/QUtil.hh> |
7 | | |
8 | | QPDFAnnotationObjectHelper::QPDFAnnotationObjectHelper(QPDFObjectHandle oh) : |
9 | | QPDFObjectHelper(oh) |
10 | 98.7k | { |
11 | 98.7k | } |
12 | | |
13 | | std::string |
14 | | QPDFAnnotationObjectHelper::getSubtype() |
15 | 30.1k | { |
16 | 30.1k | return this->oh.getKey("/Subtype").getName(); |
17 | 30.1k | } |
18 | | |
19 | | QPDFObjectHandle::Rectangle |
20 | | QPDFAnnotationObjectHelper::getRect() |
21 | 1.52k | { |
22 | 1.52k | return this->oh.getKey("/Rect").getArrayAsRectangle(); |
23 | 1.52k | } |
24 | | |
25 | | QPDFObjectHandle |
26 | | QPDFAnnotationObjectHelper::getAppearanceDictionary() |
27 | 79.8k | { |
28 | 79.8k | return this->oh.getKey("/AP"); |
29 | 79.8k | } |
30 | | |
31 | | std::string |
32 | | QPDFAnnotationObjectHelper::getAppearanceState() |
33 | 59.4k | { |
34 | 59.4k | if (this->oh.getKey("/AS").isName()) { |
35 | 20.7k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS present"); |
36 | 20.7k | return this->oh.getKey("/AS").getName(); |
37 | 20.7k | } |
38 | 38.6k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AS absent"); |
39 | 38.6k | return ""; |
40 | 59.4k | } |
41 | | |
42 | | int |
43 | | QPDFAnnotationObjectHelper::getFlags() |
44 | 11.4k | { |
45 | 11.4k | QPDFObjectHandle flags_obj = this->oh.getKey("/F"); |
46 | 11.4k | return flags_obj.isInteger() ? flags_obj.getIntValueAsInt() : 0; |
47 | 11.4k | } |
48 | | |
49 | | QPDFObjectHandle |
50 | | QPDFAnnotationObjectHelper::getAppearanceStream(std::string const& which, std::string const& state) |
51 | 59.4k | { |
52 | 59.4k | QPDFObjectHandle ap = getAppearanceDictionary(); |
53 | 59.4k | std::string desired_state = state.empty() ? getAppearanceState() : state; |
54 | 59.4k | if (ap.isDictionary()) { |
55 | 44.9k | QPDFObjectHandle ap_sub = ap.getKey(which); |
56 | 44.9k | if (ap_sub.isStream()) { |
57 | | // According to the spec, Appearance State is supposed to refer to a subkey of the |
58 | | // appearance stream when /AP is a dictionary, but files have been seen in the wild |
59 | | // where Appearance State is `/N` and `/AP` is a stream. Therefore, if `which` points to |
60 | | // a stream, disregard state and just use the stream. See qpdf issue #949 for details. |
61 | 28.5k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP stream"); |
62 | 28.5k | return ap_sub; |
63 | 28.5k | } |
64 | 16.4k | if (ap_sub.isDictionary() && (!desired_state.empty())) { |
65 | 14.6k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP dictionary"); |
66 | 14.6k | QPDFObjectHandle ap_sub_val = ap_sub.getKey(desired_state); |
67 | 14.6k | if (ap_sub_val.isStream()) { |
68 | 10.7k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP sub stream"); |
69 | 10.7k | return ap_sub_val; |
70 | 10.7k | } |
71 | 14.6k | } |
72 | 16.4k | } |
73 | 20.1k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper AP null"); |
74 | 20.1k | return QPDFObjectHandle::newNull(); |
75 | 59.4k | } |
76 | | |
77 | | std::string |
78 | | QPDFAnnotationObjectHelper::getPageContentForAppearance( |
79 | | std::string const& name, int rotate, int required_flags, int forbidden_flags) |
80 | 11.4k | { |
81 | 11.4k | if (!getAppearanceStream("/N").isStream()) { |
82 | 0 | return ""; |
83 | 0 | } |
84 | | |
85 | | // The appearance matrix computed by this method is the transformation matrix that needs to be |
86 | | // in effect when drawing this annotation's appearance stream on the page. The algorithm for |
87 | | // computing the appearance matrix described in section 12.5.5 of the ISO-32000 PDF spec is |
88 | | // similar but not identical to what we are doing here. |
89 | | |
90 | | // When rendering an appearance stream associated with an annotation, there are four relevant |
91 | | // components: |
92 | | // |
93 | | // * The appearance stream's bounding box (/BBox) |
94 | | // * The appearance stream's matrix (/Matrix) |
95 | | // * The annotation's rectangle (/Rect) |
96 | | // * In the case of form fields with the NoRotate flag, the page's rotation |
97 | | |
98 | | // When rendering a form xobject in isolation, just drawn with a /Do operator, there is no form |
99 | | // field, so page rotation is not relevant, and there is no annotation, so /Rect is not |
100 | | // relevant, so only /BBox and /Matrix are relevant. The effect of these are as follows: |
101 | | |
102 | | // * /BBox is treated as a clipping region |
103 | | // * /Matrix is applied as a transformation prior to rendering the appearance stream. |
104 | | |
105 | | // There is no relationship between /BBox and /Matrix in this case. |
106 | | |
107 | | // When rendering a form xobject in the context of an annotation, things are a little different. |
108 | | // In particular, a matrix is established such that /BBox, when transformed by /Matrix, would |
109 | | // fit completely inside of /Rect. /BBox is no longer a clipping region. To illustrate the |
110 | | // difference, consider a /Matrix of [2 0 0 2 0 0], which is scaling by a factor of two along |
111 | | // both axes. If the appearance stream drew a rectangle equal to /BBox, in the case of the form |
112 | | // xobject in isolation, this matrix would cause only the lower-left quadrant of the rectangle |
113 | | // to be visible since the scaling would cause the rest of it to fall outside of the clipping |
114 | | // region. In the case of the form xobject displayed in the context of an annotation, such a |
115 | | // matrix would have no effect at all because it would be applied to the bounding box first, and |
116 | | // then when the resulting enclosing quadrilateral was transformed to fit into /Rect, the effect |
117 | | // of the scaling would be undone. |
118 | | |
119 | | // Our job is to create a transformation matrix that compensates for these differences so that |
120 | | // the appearance stream of an annotation can be drawn as a regular form xobject. |
121 | | |
122 | | // To do this, we perform the following steps, which overlap significantly with the algorithm |
123 | | // in 12.5.5: |
124 | | |
125 | | // 1. Transform the four corners of /BBox by applying /Matrix to them, creating an arbitrarily |
126 | | // transformed quadrilateral. |
127 | | |
128 | | // 2. Find the minimum upright rectangle that encompasses the resulting quadrilateral. This is |
129 | | // the "transformed appearance box", T. |
130 | | |
131 | | // 3. Compute matrix A that maps the lower left and upper right corners of T to the annotation's |
132 | | // /Rect. This can be done by scaling so that the sizes match and translating so that the |
133 | | // scaled T exactly overlaps /Rect. |
134 | | |
135 | | // If the annotation's /F flag has bit 4 set, this means that annotation is to be rotated about |
136 | | // its upper left corner to counteract any rotation of the page so it remains upright. To |
137 | | // achieve this effect, we do the following extra steps: |
138 | | |
139 | | // 1. Perform the rotation on /BBox box prior to transforming it with /Matrix (by replacing |
140 | | // matrix with concatenation of matrix onto the rotation) |
141 | | |
142 | | // 2. Rotate the destination rectangle by the specified amount |
143 | | |
144 | | // 3. Apply the rotation to A as computed above to get the final appearance matrix. |
145 | | |
146 | 11.4k | QPDFObjectHandle rect_obj = this->oh.getKey("/Rect"); |
147 | 11.4k | QPDFObjectHandle as = getAppearanceStream("/N").getDict(); |
148 | 11.4k | QPDFObjectHandle bbox_obj = as.getKey("/BBox"); |
149 | 11.4k | QPDFObjectHandle matrix_obj = as.getKey("/Matrix"); |
150 | | |
151 | 11.4k | int flags = getFlags(); |
152 | 11.4k | if (flags & forbidden_flags) { |
153 | 83 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper forbidden flags"); |
154 | 83 | return ""; |
155 | 83 | } |
156 | 11.3k | if ((flags & required_flags) != required_flags) { |
157 | 0 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper missing required flags"); |
158 | 0 | return ""; |
159 | 0 | } |
160 | | |
161 | 11.3k | if (!(bbox_obj.isRectangle() && rect_obj.isRectangle())) { |
162 | 2.33k | return ""; |
163 | 2.33k | } |
164 | 9.04k | QPDFMatrix matrix; |
165 | 9.04k | if (matrix_obj.isMatrix()) { |
166 | 709 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper explicit matrix"); |
167 | 709 | matrix = QPDFMatrix(matrix_obj.getArrayAsMatrix()); |
168 | 8.34k | } else { |
169 | 8.34k | QTC::TC("qpdf", "QPDFAnnotationObjectHelper default matrix"); |
170 | 8.34k | } |
171 | 9.04k | QPDFObjectHandle::Rectangle rect = rect_obj.getArrayAsRectangle(); |
172 | 9.04k | bool do_rotate = (rotate && (flags & an_no_rotate)); |
173 | 9.04k | if (do_rotate) { |
174 | | // If the annotation flags include the NoRotate bit and the page is rotated, we have to |
175 | | // rotate the annotation about its upper left corner by the same amount in the opposite |
176 | | // direction so that it will remain upright in absolute coordinates. Since the semantics of |
177 | | // /Rotate for a page are to rotate the page, while the effect of rotating using a |
178 | | // transformation matrix is to rotate the coordinate system, the opposite directionality is |
179 | | // explicit in the code. |
180 | 0 | QPDFMatrix mr; |
181 | 0 | mr.rotatex90(rotate); |
182 | 0 | mr.concat(matrix); |
183 | 0 | matrix = mr; |
184 | 0 | double rect_w = rect.urx - rect.llx; |
185 | 0 | double rect_h = rect.ury - rect.lly; |
186 | 0 | switch (rotate) { |
187 | 0 | case 90: |
188 | 0 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 90"); |
189 | 0 | rect = QPDFObjectHandle::Rectangle( |
190 | 0 | rect.llx, rect.ury, rect.llx + rect_h, rect.ury + rect_w); |
191 | 0 | break; |
192 | 0 | case 180: |
193 | 0 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 180"); |
194 | 0 | rect = QPDFObjectHandle::Rectangle( |
195 | 0 | rect.llx - rect_w, rect.ury, rect.llx, rect.ury + rect_h); |
196 | 0 | break; |
197 | 0 | case 270: |
198 | 0 | QTC::TC("qpdf", "QPDFAnnotationObjectHelper rotate 270"); |
199 | 0 | rect = QPDFObjectHandle::Rectangle( |
200 | 0 | rect.llx - rect_h, rect.ury - rect_w, rect.llx, rect.ury); |
201 | 0 | break; |
202 | 0 | default: |
203 | | // ignore |
204 | 0 | break; |
205 | 0 | } |
206 | 0 | } |
207 | | |
208 | | // Transform bounding box by matrix to get T |
209 | 9.04k | QPDFObjectHandle::Rectangle bbox = bbox_obj.getArrayAsRectangle(); |
210 | 9.04k | QPDFObjectHandle::Rectangle T = matrix.transformRectangle(bbox); |
211 | 9.04k | if ((T.urx == T.llx) || (T.ury == T.lly)) { |
212 | | // avoid division by zero |
213 | 12 | return ""; |
214 | 12 | } |
215 | | // Compute a matrix to transform the appearance box to the rectangle |
216 | 9.03k | QPDFMatrix AA; |
217 | 9.03k | AA.translate(rect.llx, rect.lly); |
218 | 9.03k | AA.scale((rect.urx - rect.llx) / (T.urx - T.llx), (rect.ury - rect.lly) / (T.ury - T.lly)); |
219 | 9.03k | AA.translate(-T.llx, -T.lly); |
220 | 9.03k | if (do_rotate) { |
221 | 0 | AA.rotatex90(rotate); |
222 | 0 | } |
223 | | |
224 | 9.03k | as.replaceKey("/Subtype", QPDFObjectHandle::newName("/Form")); |
225 | 9.03k | return ("q\n" + AA.unparse() + " cm\n" + name + " Do\n" + "Q\n"); |
226 | 9.04k | } |