Coverage Report

Created: 2026-06-15 06:21

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