Coverage Report

Created: 2024-05-20 06:28

/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
}