1 | //======================================================================== |
2 | // |
3 | // MarkedContentOutputDev.cc |
4 | // |
5 | // This file is licensed under the GPLv2 or later |
6 | // |
7 | // Copyright 2013 Igalia S.L. |
8 | // Copyright 2018-2020, 2022 Albert Astals Cid <aacid@kde.org> |
9 | // Copyright 2021, 2023 Adrian Johnson <ajohnson@redneon.com> |
10 | // Copyright 2022 Oliver Sander <oliver.sander@tu-dresden.de> |
11 | // |
12 | //======================================================================== |
13 | |
14 | #include "MarkedContentOutputDev.h" |
15 | #include "GlobalParams.h" |
16 | #include "UnicodeMap.h" |
17 | #include "GfxState.h" |
18 | #include "GfxFont.h" |
19 | #include "Annot.h" |
20 | #include <cmath> |
21 | #include <vector> |
22 | |
23 | MarkedContentOutputDev::MarkedContentOutputDev(int mcidA, const Object &stmObj) : currentFont(nullptr), currentText(nullptr), mcid(mcidA), pageWidth(0.0), pageHeight(0.0), unicodeMap(nullptr) |
24 | { |
25 | stmRef = stmObj.copy(); |
26 | currentColor.r = currentColor.g = currentColor.b = 0; |
27 | } |
28 | |
29 | MarkedContentOutputDev::~MarkedContentOutputDev() |
30 | { |
31 | delete currentText; |
32 | } |
33 | |
34 | void MarkedContentOutputDev::endSpan() |
35 | { |
36 | if (currentText && currentText->getLength()) { |
37 | // The TextSpan takes ownership of currentText and |
38 | // increases the reference count for currentFont. |
39 | textSpans.push_back(x: TextSpan(currentText, currentFont, currentColor)); |
40 | } |
41 | currentText = nullptr; |
42 | } |
43 | |
44 | void MarkedContentOutputDev::startPage(int pageNum, GfxState *state, XRef *xref) |
45 | { |
46 | if (state) { |
47 | pageWidth = state->getPageWidth(); |
48 | pageHeight = state->getPageHeight(); |
49 | } else { |
50 | pageWidth = pageHeight = 0.0; |
51 | } |
52 | } |
53 | |
54 | void MarkedContentOutputDev::endPage() |
55 | { |
56 | pageWidth = pageHeight = 0.0; |
57 | } |
58 | |
59 | void MarkedContentOutputDev::beginForm(Object * /* obj */, Ref id) |
60 | { |
61 | formStack.push_back(x: id); |
62 | } |
63 | |
64 | void MarkedContentOutputDev::endForm(Object * /* obj */, Ref id) |
65 | { |
66 | formStack.pop_back(); |
67 | } |
68 | |
69 | bool MarkedContentOutputDev::contentStreamMatch() |
70 | { |
71 | if (stmRef.isRef()) { |
72 | if (formStack.empty()) { |
73 | return false; |
74 | } |
75 | return formStack.back() == stmRef.getRef(); |
76 | } |
77 | return formStack.empty(); |
78 | } |
79 | |
80 | void MarkedContentOutputDev::beginMarkedContent(const char *name, Dict *properties) |
81 | { |
82 | int id = -1; |
83 | if (properties) { |
84 | properties->lookupInt(key: "MCID" , alt_key: nullptr, value: &id); |
85 | } |
86 | |
87 | if (id == -1) { |
88 | return; |
89 | } |
90 | |
91 | // The stack keep track of MCIDs of nested marked content. |
92 | if (inMarkedContent() || (id == mcid && contentStreamMatch())) { |
93 | mcidStack.push_back(x: id); |
94 | } |
95 | } |
96 | |
97 | void MarkedContentOutputDev::endMarkedContent(GfxState *state) |
98 | { |
99 | if (inMarkedContent()) { |
100 | mcidStack.pop_back(); |
101 | // The outer marked content sequence MCID was popped, ensure |
102 | // that the last piece of text collected ends up in a TextSpan. |
103 | if (!inMarkedContent()) { |
104 | endSpan(); |
105 | } |
106 | } |
107 | } |
108 | |
109 | bool MarkedContentOutputDev::needFontChange(const std::shared_ptr<const GfxFont> &font) const |
110 | { |
111 | if (currentFont == font) { |
112 | return false; |
113 | } |
114 | |
115 | if (!currentFont) { |
116 | return font != nullptr && font->isOk(); |
117 | } |
118 | |
119 | if (font == nullptr) { |
120 | return true; |
121 | } |
122 | |
123 | // Two non-null valid fonts are the same if they point to the same Ref |
124 | if (*currentFont->getID() == *font->getID()) { |
125 | return false; |
126 | } |
127 | |
128 | return true; |
129 | } |
130 | |
131 | void MarkedContentOutputDev::drawChar(GfxState *state, double xx, double yy, double dx, double dy, double ox, double oy, CharCode c, int nBytes, const Unicode *u, int uLen) |
132 | { |
133 | if (!inMarkedContent() || !uLen) { |
134 | return; |
135 | } |
136 | |
137 | // Color changes are tracked here so the color can be chosen depending on |
138 | // the render mode (for mode 1 stroke color is used), so there is no need |
139 | // to implement both updateFillColor() and updateStrokeColor(). |
140 | bool colorChange = false; |
141 | GfxRGB color; |
142 | if ((state->getRender() & 3) == 1) { |
143 | state->getStrokeRGB(rgb: &color); |
144 | } else { |
145 | state->getFillRGB(rgb: &color); |
146 | } |
147 | |
148 | colorChange = (color.r != currentColor.r || color.g != currentColor.g || color.b != currentColor.b); |
149 | |
150 | // Check also for font changes. |
151 | bool fontChange = needFontChange(font: state->getFont()); |
152 | |
153 | // Save a span with the current changes. |
154 | if (colorChange || fontChange) { |
155 | endSpan(); |
156 | } |
157 | |
158 | // Perform the color/font changes. |
159 | if (colorChange) { |
160 | currentColor = color; |
161 | } |
162 | |
163 | if (fontChange) { |
164 | currentFont = state->getFont(); |
165 | } |
166 | |
167 | double sp, dx2, dy2, w1, h1, x1, y1; |
168 | |
169 | // Subtract char and word spacing from the (dx,dy) values |
170 | sp = state->getCharSpace(); |
171 | if (c == (CharCode)0x20) { |
172 | sp += state->getWordSpace(); |
173 | } |
174 | state->textTransformDelta(x1: sp * state->getHorizScaling(), y1: 0, x2: &dx2, y2: &dy2); |
175 | dx -= dx2; |
176 | dy -= dy2; |
177 | state->transformDelta(x1: dx, y1: dy, x2: &w1, y2: &h1); |
178 | state->transform(x1: xx, y1: yy, x2: &x1, y2: &y1); |
179 | |
180 | // Throw away characters that are not inside the page boundaries. |
181 | if (x1 + w1 < 0 || x1 > pageWidth || y1 + h1 < 0 || y1 > pageHeight) { |
182 | return; |
183 | } |
184 | |
185 | if (std::isnan(x: x1) || std::isnan(x: y1) || std::isnan(x: w1) || std::isnan(x: h1)) { |
186 | return; |
187 | } |
188 | |
189 | for (int i = 0; i < uLen; i++) { |
190 | // Soft hyphen markers are skipped, as they are invisible unless |
191 | // rendering is done to an actual device and the hyphenation hint |
192 | // used. MarkedContentOutputDev extracts the *visible* text content. |
193 | if (u[i] != 0x00AD) { |
194 | // Add the UTF-8 sequence to the current text span. |
195 | if (!unicodeMap) { |
196 | unicodeMap = globalParams->getTextEncoding(); |
197 | } |
198 | |
199 | char buf[8]; |
200 | int n = unicodeMap->mapUnicode(u: u[i], buf, bufSize: sizeof(buf)); |
201 | if (n > 0) { |
202 | if (currentText == nullptr) { |
203 | currentText = new GooString(); |
204 | } |
205 | currentText->append(str: buf, lengthA: n); |
206 | } |
207 | } |
208 | } |
209 | } |
210 | |
211 | const TextSpanArray &MarkedContentOutputDev::getTextSpans() const |
212 | { |
213 | return textSpans; |
214 | } |
215 | |