/src/mupdf/source/pdf/pdf-clean.c
Line | Count | Source |
1 | | // Copyright (C) 2004-2026 Artifex Software, Inc. |
2 | | // |
3 | | // This file is part of MuPDF. |
4 | | // |
5 | | // MuPDF is free software: you can redistribute it and/or modify it under the |
6 | | // terms of the GNU Affero General Public License as published by the Free |
7 | | // Software Foundation, either version 3 of the License, or (at your option) |
8 | | // any later version. |
9 | | // |
10 | | // MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY |
11 | | // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
12 | | // FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more |
13 | | // details. |
14 | | // |
15 | | // You should have received a copy of the GNU Affero General Public License |
16 | | // along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html> |
17 | | // |
18 | | // Alternative licensing terms are available from the licensor. |
19 | | // For commercial licensing, see <https://www.artifex.com/> or contact |
20 | | // Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco, |
21 | | // CA 94129, USA, for further information. |
22 | | |
23 | | #include "mupdf/fitz.h" |
24 | | #include "pdf-annot-imp.h" |
25 | | |
26 | | #include <string.h> |
27 | | #include <assert.h> |
28 | | |
29 | | static void |
30 | | pdf_filter_xobject(fz_context *ctx, pdf_document *doc, pdf_obj *xobj, pdf_obj *page_res, pdf_filter_options *options, pdf_cycle_list *cycle_up); |
31 | | |
32 | | static void |
33 | | pdf_filter_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *page_res, pdf_filter_options *options, pdf_cycle_list *cycle_up); |
34 | | |
35 | | static void |
36 | | pdf_filter_resources(fz_context *ctx, pdf_document *doc, pdf_obj *in_res, pdf_obj *res, pdf_filter_options *options, pdf_cycle_list *cycle_up) |
37 | 0 | { |
38 | 0 | pdf_cycle_list cycle; |
39 | 0 | pdf_obj *obj; |
40 | 0 | int i, n; |
41 | |
|
42 | 0 | if (!options->recurse) |
43 | 0 | return; |
44 | | |
45 | 0 | if (pdf_cycle(ctx, &cycle, cycle_up, in_res)) |
46 | 0 | return; |
47 | | |
48 | | /* ExtGState */ |
49 | 0 | obj = pdf_dict_get(ctx, res, PDF_NAME(ExtGState)); |
50 | 0 | if (obj) |
51 | 0 | { |
52 | 0 | n = pdf_dict_len(ctx, obj); |
53 | 0 | for (i = 0; i < n; i++) |
54 | 0 | { |
55 | 0 | pdf_obj *smask = pdf_dict_get(ctx, pdf_dict_get_val(ctx, obj, i), PDF_NAME(SMask)); |
56 | 0 | if (smask) |
57 | 0 | { |
58 | 0 | pdf_obj *g = pdf_dict_get(ctx, smask, PDF_NAME(G)); |
59 | 0 | if (g) |
60 | 0 | { |
61 | | /* Transparency group XObject */ |
62 | 0 | pdf_filter_xobject(ctx, doc, g, in_res, options, &cycle); |
63 | 0 | } |
64 | 0 | } |
65 | 0 | } |
66 | 0 | } |
67 | | |
68 | | /* Pattern */ |
69 | 0 | obj = pdf_dict_get(ctx, res, PDF_NAME(Pattern)); |
70 | 0 | if (obj) |
71 | 0 | { |
72 | 0 | n = pdf_dict_len(ctx, obj); |
73 | 0 | for (i = 0; i < n; i++) |
74 | 0 | { |
75 | 0 | pdf_obj *pat = pdf_dict_get_val(ctx, obj, i); |
76 | 0 | if (pat && pdf_dict_get_int(ctx, pat, PDF_NAME(PatternType)) == 1) |
77 | 0 | { |
78 | 0 | pdf_filter_xobject(ctx, doc, pat, in_res, options, &cycle); |
79 | 0 | } |
80 | 0 | } |
81 | 0 | } |
82 | | |
83 | | /* XObject */ |
84 | 0 | if (!options->instance_forms) |
85 | 0 | { |
86 | 0 | obj = pdf_dict_get(ctx, res, PDF_NAME(XObject)); |
87 | 0 | if (obj) |
88 | 0 | { |
89 | 0 | n = pdf_dict_len(ctx, obj); |
90 | 0 | for (i = 0; i < n; i++) |
91 | 0 | { |
92 | 0 | pdf_obj *xobj = pdf_dict_get_val(ctx, obj, i); |
93 | 0 | if (xobj && pdf_dict_get(ctx, xobj, PDF_NAME(Subtype)) == PDF_NAME(Form)) |
94 | 0 | { |
95 | 0 | pdf_filter_xobject(ctx, doc, xobj, in_res, options, &cycle); |
96 | 0 | } |
97 | 0 | } |
98 | 0 | } |
99 | 0 | } |
100 | | |
101 | | /* Font */ |
102 | 0 | obj = pdf_dict_get(ctx, res, PDF_NAME(Font)); |
103 | 0 | if (obj) |
104 | 0 | { |
105 | 0 | n = pdf_dict_len(ctx, obj); |
106 | 0 | for (i = 0; i < n; i++) |
107 | 0 | { |
108 | 0 | pdf_obj *font = pdf_dict_get_val(ctx, obj, i); |
109 | 0 | if (font && pdf_dict_get(ctx, font, PDF_NAME(Subtype)) == PDF_NAME(Type3)) |
110 | 0 | { |
111 | 0 | pdf_filter_type3(ctx, doc, font, in_res, options, &cycle); |
112 | 0 | } |
113 | 0 | } |
114 | 0 | } |
115 | |
|
116 | 0 | } |
117 | | |
118 | | /* |
119 | | Clean a content stream's rendering operations, with an optional post |
120 | | processing step. |
121 | | |
122 | | Firstly, this filters the PDF operators used to avoid (some cases of) |
123 | | repetition, and leaves the content stream in a balanced state with an |
124 | | unchanged top level matrix etc. At the same time, the resources actually |
125 | | used are collected into a new resource dictionary. |
126 | | |
127 | | Next, the resources themselves are recursively cleaned (as appropriate) |
128 | | in the same way, if the 'recurse' flag is set. |
129 | | */ |
130 | | static void |
131 | | pdf_filter_content_stream( |
132 | | fz_context *ctx, |
133 | | pdf_document *doc, |
134 | | pdf_obj *in_stm, |
135 | | pdf_obj *in_res, |
136 | | fz_matrix transform, |
137 | | pdf_filter_options *options, |
138 | | int struct_parents, |
139 | | fz_buffer **out_buf, |
140 | | pdf_obj **out_res, |
141 | | pdf_cycle_list *cycle_up) |
142 | 0 | { |
143 | 0 | pdf_processor *proc_buffer = NULL; |
144 | 0 | pdf_processor *top = NULL; |
145 | 0 | pdf_processor **list = NULL; |
146 | 0 | int num_filters = 0; |
147 | 0 | int i; |
148 | |
|
149 | 0 | fz_var(proc_buffer); |
150 | |
|
151 | 0 | *out_buf = NULL; |
152 | 0 | *out_res = NULL; |
153 | |
|
154 | 0 | if (options->filters) |
155 | 0 | for (; options->filters[num_filters].filter != NULL; num_filters++); |
156 | |
|
157 | 0 | if (num_filters > 0) |
158 | 0 | list = fz_calloc(ctx, num_filters, sizeof(pdf_processor *)); |
159 | |
|
160 | 0 | fz_try(ctx) |
161 | 0 | { |
162 | 0 | *out_buf = fz_new_buffer(ctx, 1024); |
163 | 0 | top = proc_buffer = pdf_new_buffer_processor(ctx, *out_buf, options->ascii, options->newlines); |
164 | 0 | if (num_filters > 0) |
165 | 0 | { |
166 | 0 | for (i = num_filters - 1; i >= 0; i--) |
167 | 0 | top = list[i] = options->filters[i].filter(ctx, doc, top, struct_parents, transform, options, options->filters[i].options); |
168 | 0 | } |
169 | |
|
170 | 0 | pdf_process_contents(ctx, top, doc, in_res, in_stm, NULL, out_res); |
171 | 0 | pdf_close_processor(ctx, top); |
172 | |
|
173 | 0 | pdf_filter_resources(ctx, doc, in_res, *out_res, options, cycle_up); |
174 | 0 | } |
175 | 0 | fz_always(ctx) |
176 | 0 | { |
177 | 0 | for (i = 0; i < num_filters; i++) |
178 | 0 | pdf_drop_processor(ctx, list[i]); |
179 | 0 | pdf_drop_processor(ctx, proc_buffer); |
180 | 0 | fz_free(ctx, list); |
181 | 0 | } |
182 | 0 | fz_catch(ctx) |
183 | 0 | { |
184 | 0 | fz_drop_buffer(ctx, *out_buf); |
185 | 0 | *out_buf = NULL; |
186 | 0 | pdf_drop_obj(ctx, *out_res); |
187 | 0 | *out_res = NULL; |
188 | 0 | fz_rethrow(ctx); |
189 | 0 | } |
190 | 0 | } |
191 | | |
192 | | /* |
193 | | Clean a Type 3 font's CharProcs content streams. This works almost |
194 | | exactly like pdf_filter_content_stream, but the resource dictionary is |
195 | | shared between all off the CharProcs. |
196 | | */ |
197 | | static void |
198 | | pdf_filter_type3(fz_context *ctx, pdf_document *doc, pdf_obj *obj, pdf_obj *page_res, pdf_filter_options *options, pdf_cycle_list *cycle_up) |
199 | 0 | { |
200 | 0 | pdf_cycle_list cycle; |
201 | 0 | pdf_processor *proc_buffer = NULL; |
202 | 0 | pdf_processor *proc_filter = NULL; |
203 | 0 | pdf_obj *in_res; |
204 | 0 | pdf_obj *out_res = NULL; |
205 | 0 | pdf_obj *charprocs; |
206 | 0 | int i, n; |
207 | 0 | int num_filters = 0; |
208 | 0 | pdf_processor **list = NULL; |
209 | 0 | fz_buffer *buffer = NULL; |
210 | 0 | pdf_processor *top = NULL; |
211 | 0 | pdf_obj *res = NULL; |
212 | 0 | fz_buffer *new_buf = NULL; |
213 | |
|
214 | 0 | fz_var(out_res); |
215 | 0 | fz_var(proc_buffer); |
216 | 0 | fz_var(proc_filter); |
217 | 0 | fz_var(buffer); |
218 | 0 | fz_var(res); |
219 | 0 | fz_var(new_buf); |
220 | 0 | fz_var(top); |
221 | | |
222 | | /* We cannot combine instancing with type3 fonts. The new names for |
223 | | * instanced form/image resources would clash, since they start over for |
224 | | * each content stream. This is not a problem for now, because we only |
225 | | * use instancing with redaction, and redaction doesn't clean type3 |
226 | | * fonts. |
227 | | */ |
228 | 0 | assert(!options->instance_forms); |
229 | | |
230 | | /* Avoid recursive cycles! */ |
231 | 0 | if (pdf_cycle(ctx, &cycle, cycle_up, obj)) |
232 | 0 | return; |
233 | | |
234 | 0 | if (options->filters) |
235 | 0 | for (; options->filters[num_filters].filter != NULL; num_filters++); |
236 | |
|
237 | 0 | if (num_filters > 0) |
238 | 0 | list = fz_calloc(ctx, num_filters, sizeof(pdf_processor *)); |
239 | |
|
240 | 0 | fz_try(ctx) |
241 | 0 | { |
242 | 0 | in_res = pdf_dict_get(ctx, obj, PDF_NAME(Resources)); |
243 | 0 | if (!in_res) |
244 | 0 | in_res = page_res; |
245 | |
|
246 | 0 | buffer = fz_new_buffer(ctx, 1024); |
247 | 0 | top = proc_buffer = pdf_new_buffer_processor(ctx, buffer, options->ascii, options->newlines); |
248 | 0 | if (num_filters > 0) |
249 | 0 | { |
250 | 0 | for (i = num_filters - 1; i >= 0; i--) |
251 | 0 | top = list[i] = options->filters[i].filter(ctx, doc, top, -1, fz_identity, options, options->filters[i].options); |
252 | 0 | } |
253 | |
|
254 | 0 | pdf_processor_push_resources(ctx, top, in_res); |
255 | 0 | charprocs = pdf_dict_get(ctx, obj, PDF_NAME(CharProcs)); |
256 | 0 | n = pdf_dict_len(ctx, charprocs); |
257 | 0 | for (i = 0; i < n; i++) |
258 | 0 | { |
259 | 0 | pdf_obj *val = pdf_dict_get_val(ctx, charprocs, i); |
260 | |
|
261 | 0 | if (i > 0) |
262 | 0 | { |
263 | | // reset all chained processors (and clear the buffer) |
264 | 0 | pdf_reset_processor(ctx, top); |
265 | 0 | } |
266 | 0 | pdf_process_raw_contents(ctx, top, doc, val, NULL); |
267 | |
|
268 | 0 | pdf_close_processor(ctx, top); |
269 | |
|
270 | 0 | if (!options->no_update) |
271 | 0 | { |
272 | 0 | new_buf = fz_clone_buffer(ctx, buffer); |
273 | 0 | pdf_update_stream(ctx, doc, val, new_buf, 0); |
274 | 0 | fz_drop_buffer(ctx, new_buf); |
275 | 0 | new_buf = NULL; |
276 | 0 | } |
277 | 0 | } |
278 | |
|
279 | 0 | } |
280 | 0 | fz_always(ctx) |
281 | 0 | { |
282 | 0 | res = pdf_processor_pop_resources(ctx, top); |
283 | 0 | for (i = 0; i < num_filters; i++) |
284 | 0 | pdf_drop_processor(ctx, list[i]); |
285 | 0 | pdf_drop_processor(ctx, proc_buffer); |
286 | 0 | fz_free(ctx, list); |
287 | 0 | fz_drop_buffer(ctx, new_buf); |
288 | 0 | fz_drop_buffer(ctx, buffer); |
289 | 0 | } |
290 | 0 | fz_catch(ctx) |
291 | 0 | { |
292 | 0 | pdf_drop_obj(ctx, res); |
293 | 0 | fz_rethrow(ctx); |
294 | 0 | } |
295 | 0 | pdf_dict_put_drop(ctx, obj, PDF_NAME(Resources), res); |
296 | 0 | } |
297 | | |
298 | | static void |
299 | | pdf_filter_xobject(fz_context *ctx, pdf_document *doc, pdf_obj *stm, pdf_obj *page_res, pdf_filter_options *options, pdf_cycle_list *cycle_up) |
300 | 0 | { |
301 | 0 | pdf_cycle_list cycle; |
302 | 0 | int struct_parents; |
303 | 0 | pdf_obj *new_res = NULL; |
304 | 0 | fz_buffer *new_buf = NULL; |
305 | 0 | pdf_obj *old_res; |
306 | |
|
307 | 0 | fz_var(new_buf); |
308 | 0 | fz_var(new_res); |
309 | | |
310 | | // TODO for RJW: XObject can also be a StructParent; how do we handle that case? |
311 | |
|
312 | 0 | struct_parents = pdf_dict_get_int_default(ctx, stm, PDF_NAME(StructParents), -1); |
313 | |
|
314 | 0 | old_res = pdf_dict_get(ctx, stm, PDF_NAME(Resources)); |
315 | 0 | if (!old_res) |
316 | 0 | old_res = page_res; |
317 | | |
318 | | // TODO: don't clean objects more than once. |
319 | | |
320 | | /* Avoid recursive cycles! */ |
321 | 0 | if (pdf_cycle(ctx, &cycle, cycle_up, stm)) |
322 | 0 | return; |
323 | 0 | fz_try(ctx) |
324 | 0 | { |
325 | 0 | pdf_filter_content_stream(ctx, doc, stm, old_res, fz_identity, options, struct_parents, &new_buf, &new_res, &cycle); |
326 | 0 | if (!options->no_update) |
327 | 0 | { |
328 | 0 | pdf_update_stream(ctx, doc, stm, new_buf, 0); |
329 | 0 | pdf_dict_put(ctx, stm, PDF_NAME(Resources), new_res); |
330 | 0 | } |
331 | 0 | } |
332 | 0 | fz_always(ctx) |
333 | 0 | { |
334 | 0 | fz_drop_buffer(ctx, new_buf); |
335 | 0 | pdf_drop_obj(ctx, new_res); |
336 | 0 | } |
337 | 0 | fz_catch(ctx) |
338 | 0 | fz_rethrow(ctx); |
339 | 0 | } |
340 | | |
341 | | pdf_obj * |
342 | | pdf_filter_xobject_instance(fz_context *ctx, pdf_obj *old_xobj, pdf_obj *page_res, fz_matrix transform, pdf_filter_options *options, pdf_cycle_list *cycle_up) |
343 | 0 | { |
344 | 0 | pdf_cycle_list cycle; |
345 | 0 | pdf_document *doc = pdf_get_bound_document(ctx, old_xobj); |
346 | 0 | pdf_obj *new_xobj = NULL; |
347 | 0 | pdf_obj *new_res = NULL, *old_res; |
348 | 0 | fz_buffer *new_buf = NULL; |
349 | 0 | int struct_parents; |
350 | 0 | fz_matrix matrix; |
351 | |
|
352 | 0 | fz_var(new_xobj); |
353 | 0 | fz_var(new_buf); |
354 | 0 | fz_var(new_res); |
355 | | |
356 | | // TODO for RJW: XObject can also be a StructParent; how do we handle that case? |
357 | | // TODO for RJW: will we run into trouble by duplicating StructParents stuff? |
358 | |
|
359 | 0 | struct_parents = pdf_dict_get_int_default(ctx, old_xobj, PDF_NAME(StructParents), -1); |
360 | |
|
361 | 0 | old_res = pdf_dict_get(ctx, old_xobj, PDF_NAME(Resources)); |
362 | 0 | if (!old_res) |
363 | 0 | old_res = page_res; |
364 | |
|
365 | 0 | if (pdf_cycle(ctx, &cycle, cycle_up, old_xobj)) |
366 | 0 | return pdf_keep_obj(ctx, old_xobj); |
367 | | |
368 | 0 | matrix = pdf_dict_get_matrix(ctx, old_xobj, PDF_NAME(Matrix)); |
369 | 0 | transform = fz_concat(matrix, transform); |
370 | |
|
371 | 0 | fz_try(ctx) |
372 | 0 | { |
373 | 0 | new_xobj = pdf_add_object_drop(ctx, doc, pdf_copy_dict(ctx, old_xobj)); |
374 | 0 | pdf_filter_content_stream(ctx, doc, old_xobj, old_res, transform, options, struct_parents, &new_buf, &new_res, &cycle); |
375 | 0 | if (!options->no_update) |
376 | 0 | { |
377 | 0 | pdf_update_stream(ctx, doc, new_xobj, new_buf, 0); |
378 | 0 | pdf_dict_put(ctx, new_xobj, PDF_NAME(Resources), new_res); |
379 | 0 | } |
380 | 0 | } |
381 | 0 | fz_always(ctx) |
382 | 0 | { |
383 | 0 | fz_drop_buffer(ctx, new_buf); |
384 | 0 | pdf_drop_obj(ctx, new_res); |
385 | 0 | } |
386 | 0 | fz_catch(ctx) |
387 | 0 | { |
388 | 0 | pdf_drop_obj(ctx, new_xobj); |
389 | 0 | fz_rethrow(ctx); |
390 | 0 | } |
391 | | |
392 | 0 | return new_xobj; |
393 | 0 | } |
394 | | |
395 | | void pdf_filter_page_contents(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_filter_options *options) |
396 | 0 | { |
397 | 0 | pdf_obj *contents, *old_res; |
398 | 0 | pdf_obj *new_res; |
399 | 0 | fz_buffer *buffer; |
400 | 0 | int struct_parents; |
401 | |
|
402 | 0 | struct_parents = pdf_dict_get_int_default(ctx, page->obj, PDF_NAME(StructParents), -1); |
403 | |
|
404 | 0 | contents = pdf_page_contents(ctx, page); |
405 | 0 | old_res = pdf_page_resources(ctx, page); |
406 | |
|
407 | 0 | pdf_filter_content_stream(ctx, doc, contents, old_res, fz_identity, options, struct_parents, &buffer, &new_res, NULL); |
408 | |
|
409 | 0 | fz_try(ctx) |
410 | 0 | { |
411 | 0 | if (options->complete) |
412 | 0 | options->complete(ctx, buffer, options->opaque); |
413 | 0 | if (!options->no_update) |
414 | 0 | { |
415 | | /* Always create a new stream object to replace the page contents. This is useful |
416 | | both if the contents is an array of streams, is entirely missing or if the contents |
417 | | are shared between pages. */ |
418 | 0 | contents = pdf_add_object_drop(ctx, doc, pdf_new_dict(ctx, doc, 1)); |
419 | 0 | pdf_dict_put_drop(ctx, page->obj, PDF_NAME(Contents), contents); |
420 | 0 | pdf_update_stream(ctx, doc, contents, buffer, 0); |
421 | 0 | pdf_dict_put(ctx, page->obj, PDF_NAME(Resources), new_res); |
422 | 0 | } |
423 | 0 | } |
424 | 0 | fz_always(ctx) |
425 | 0 | { |
426 | 0 | fz_drop_buffer(ctx, buffer); |
427 | 0 | pdf_drop_obj(ctx, new_res); |
428 | 0 | } |
429 | 0 | fz_catch(ctx) |
430 | 0 | fz_rethrow(ctx); |
431 | 0 | } |
432 | | |
433 | | void pdf_filter_annot_contents(fz_context *ctx, pdf_document *doc, pdf_annot *annot, pdf_filter_options *options) |
434 | 0 | { |
435 | 0 | pdf_obj *ap = pdf_dict_get(ctx, annot->obj, PDF_NAME(AP)); |
436 | 0 | if (pdf_is_dict(ctx, ap)) |
437 | 0 | { |
438 | 0 | int i, n = pdf_dict_len(ctx, ap); |
439 | 0 | for (i = 0; i < n; i++) |
440 | 0 | { |
441 | 0 | pdf_obj *stm = pdf_dict_get_val(ctx, ap, i); |
442 | 0 | if (pdf_is_stream(ctx, stm)) |
443 | 0 | { |
444 | 0 | pdf_filter_xobject(ctx, doc, stm, NULL, options, NULL); |
445 | 0 | } |
446 | 0 | } |
447 | 0 | } |
448 | 0 | } |
449 | | |
450 | | /* REDACTIONS */ |
451 | | |
452 | | struct redact_filter_state { |
453 | | pdf_filter_options filter_opts; |
454 | | pdf_sanitize_filter_options sanitize_opts; |
455 | | pdf_filter_factory filter_list[2]; |
456 | | pdf_page *page; |
457 | | pdf_annot *target; // NULL if all |
458 | | int line_art; |
459 | | int text; |
460 | | }; |
461 | | |
462 | | |
463 | | static void pdf_run_obj_to_buf(fz_context *ctx, fz_buffer *buffer, pdf_obj *obj, pdf_page *page) |
464 | 0 | { |
465 | 0 | pdf_processor *proc = pdf_new_buffer_processor(ctx, buffer, 0, 0); |
466 | 0 | pdf_obj *res; |
467 | | |
468 | |
|
469 | 0 | fz_try(ctx) |
470 | 0 | { |
471 | 0 | res = pdf_xobject_resources(ctx, obj); |
472 | 0 | if (res == NULL) |
473 | 0 | res = pdf_page_resources(ctx, page); |
474 | |
|
475 | 0 | pdf_process_contents(ctx, proc, page->doc, res, obj, NULL, NULL); |
476 | 0 | pdf_close_processor(ctx, proc); |
477 | 0 | } |
478 | 0 | fz_always(ctx) |
479 | 0 | pdf_drop_processor(ctx, proc); |
480 | 0 | fz_catch(ctx) |
481 | 0 | fz_rethrow(ctx); |
482 | 0 | } |
483 | | |
484 | | static void |
485 | | pdf_redact_end_page(fz_context *ctx, fz_buffer *buf, void *opaque) |
486 | 0 | { |
487 | 0 | struct redact_filter_state *red = opaque; |
488 | 0 | pdf_page *page = red->page; |
489 | 0 | pdf_annot *annot; |
490 | 0 | pdf_obj *qp; |
491 | 0 | int i, n; |
492 | |
|
493 | 0 | fz_append_string(ctx, buf, " 0 g\n"); |
494 | |
|
495 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
496 | 0 | { |
497 | 0 | if (red->target != NULL && red->target != annot) |
498 | 0 | continue; |
499 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
500 | 0 | { |
501 | 0 | pdf_obj *ro = pdf_dict_get(ctx, annot->obj, PDF_NAME(RO)); |
502 | 0 | if (ro) |
503 | 0 | { |
504 | 0 | pdf_run_obj_to_buf(ctx, buf, ro, page); |
505 | 0 | } |
506 | 0 | else |
507 | 0 | { |
508 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
509 | 0 | n = pdf_array_len(ctx, qp); |
510 | 0 | if (n > 0) |
511 | 0 | { |
512 | 0 | for (i = 0; i < n; i += 8) |
513 | 0 | { |
514 | 0 | fz_quad q = pdf_to_quad(ctx, qp, i); |
515 | 0 | fz_append_printf(ctx, buf, "%g %g m\n", q.ll.x, q.ll.y); |
516 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", q.lr.x, q.lr.y); |
517 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", q.ur.x, q.ur.y); |
518 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", q.ul.x, q.ul.y); |
519 | 0 | fz_append_string(ctx, buf, "f\n"); |
520 | 0 | } |
521 | 0 | } |
522 | 0 | else |
523 | 0 | { |
524 | 0 | fz_rect r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
525 | 0 | fz_append_printf(ctx, buf, "%g %g m\n", r.x0, r.y0); |
526 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", r.x1, r.y0); |
527 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", r.x1, r.y1); |
528 | 0 | fz_append_printf(ctx, buf, "%g %g l\n", r.x0, r.y1); |
529 | 0 | fz_append_string(ctx, buf, "f\n"); |
530 | 0 | } |
531 | 0 | } |
532 | 0 | } |
533 | 0 | } |
534 | 0 | } |
535 | | |
536 | | static int |
537 | | pdf_redact_text_filter(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, fz_matrix trm, fz_matrix ctm, fz_rect bbox, int tr, float ca, float CA) |
538 | 0 | { |
539 | 0 | struct redact_filter_state *red = opaque; |
540 | 0 | pdf_page *page = red->page; |
541 | 0 | pdf_annot *annot; |
542 | 0 | pdf_obj *qp; |
543 | 0 | fz_rect r; |
544 | 0 | fz_quad q; |
545 | 0 | int i, n; |
546 | 0 | float w, h; |
547 | |
|
548 | 0 | trm = fz_concat(trm, ctm); |
549 | 0 | bbox = fz_transform_rect(bbox, trm); |
550 | | |
551 | | /* Shrink character bbox a bit */ |
552 | 0 | w = bbox.x1 - bbox.x0; |
553 | 0 | h = bbox.y1 - bbox.y0; |
554 | 0 | bbox.x0 += w / 10; |
555 | 0 | bbox.x1 -= w / 10; |
556 | 0 | bbox.y0 += h / 10; |
557 | 0 | bbox.y1 -= h / 10; |
558 | |
|
559 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
560 | 0 | { |
561 | 0 | if (red->target != NULL && red->target != annot) |
562 | 0 | continue; |
563 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
564 | 0 | { |
565 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
566 | 0 | n = pdf_array_len(ctx, qp); |
567 | | /* Note, we test for the intersection being a valid rectangle, NOT |
568 | | * a non-empty one. This is because we can have 'empty' character |
569 | | * boxes (say for diacritics), that while 0 width, do have a defined |
570 | | * position on the plane, and hence inclusion makes sense. */ |
571 | 0 | if (n > 0) |
572 | 0 | { |
573 | 0 | for (i = 0; i < n; i += 8) |
574 | 0 | { |
575 | 0 | q = pdf_to_quad(ctx, qp, i); |
576 | 0 | r = fz_rect_from_quad(q); |
577 | 0 | if (fz_is_valid_rect(fz_intersect_rect(bbox, r))) |
578 | 0 | return 1; |
579 | 0 | } |
580 | 0 | } |
581 | 0 | else |
582 | 0 | { |
583 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
584 | 0 | if (fz_is_valid_rect(fz_intersect_rect(bbox, r))) |
585 | 0 | return 1; |
586 | 0 | } |
587 | 0 | } |
588 | 0 | } |
589 | | |
590 | 0 | return 0; |
591 | 0 | } |
592 | | |
593 | | static int |
594 | | pdf_redact_invisible_text_filter(fz_context *ctx, void *opaque, int *ucsbuf, int ucslen, fz_matrix trm, fz_matrix ctm, fz_rect bbox, int tr, float ca, float CA) |
595 | 0 | { |
596 | 0 | int invisible = 0; |
597 | |
|
598 | 0 | switch (tr) |
599 | 0 | { |
600 | 0 | case 0: /* Fill */ |
601 | 0 | invisible = (ca == 0); |
602 | 0 | break; |
603 | 0 | case 1: /* Stroke */ |
604 | 0 | invisible = (CA == 0); |
605 | 0 | break; |
606 | 0 | case 2: /* Fill + Stroke */ |
607 | 0 | invisible = (ca == 0 && CA == 0); |
608 | 0 | break; |
609 | 0 | case 3: /* Neither Fill nor stroke */ |
610 | 0 | invisible = 1; |
611 | 0 | break; |
612 | 0 | } |
613 | | |
614 | 0 | if (!invisible) |
615 | 0 | return 0; |
616 | | |
617 | 0 | return pdf_redact_text_filter(ctx, opaque, ucsbuf, ucslen, trm, ctm, bbox, tr, ca, CA); |
618 | 0 | } |
619 | | |
620 | | static fz_pixmap * |
621 | | pdf_redact_image_imp(fz_context *ctx, fz_matrix ctm, fz_image *image, fz_pixmap *pixmap, fz_pixmap **pmask, fz_quad q) |
622 | 0 | { |
623 | 0 | fz_matrix inv_ctm; |
624 | 0 | fz_irect r; |
625 | 0 | int x, y, k, n, bpp; |
626 | 0 | unsigned char white; |
627 | 0 | fz_pixmap *mask = *pmask; |
628 | 0 | int pixmap_cloned = 0; |
629 | |
|
630 | 0 | if (!pixmap) |
631 | 0 | { |
632 | 0 | fz_pixmap *original = fz_get_pixmap_from_image(ctx, image, NULL, NULL, NULL, NULL); |
633 | 0 | int imagemask = image->imagemask; |
634 | |
|
635 | 0 | fz_try(ctx) |
636 | 0 | { |
637 | 0 | enum fz_colorspace_type type = fz_colorspace_type(ctx, original ? original->colorspace : NULL); |
638 | 0 | retry_with_base: |
639 | 0 | switch (type) |
640 | 0 | { |
641 | 0 | case FZ_COLORSPACE_NONE: |
642 | 0 | case FZ_COLORSPACE_RGB: |
643 | 0 | case FZ_COLORSPACE_GRAY: |
644 | 0 | case FZ_COLORSPACE_CMYK: |
645 | 0 | case FZ_COLORSPACE_LAB: |
646 | 0 | pixmap = fz_clone_pixmap(ctx, original); |
647 | 0 | break; |
648 | 0 | case FZ_COLORSPACE_INDEXED: |
649 | 0 | type = original->colorspace->u.indexed.base ? original->colorspace->u.indexed.base->type : FZ_COLORSPACE_NONE; |
650 | 0 | goto retry_with_base; |
651 | 0 | case FZ_COLORSPACE_SEPARATION: |
652 | 0 | pixmap = fz_convert_pixmap(ctx, original, fz_device_cmyk(ctx), NULL, NULL, fz_default_color_params, 1); |
653 | 0 | break; |
654 | 0 | default: |
655 | 0 | pixmap = fz_convert_pixmap(ctx, original, fz_device_rgb(ctx), NULL, NULL, fz_default_color_params, 1); |
656 | 0 | break; |
657 | 0 | } |
658 | 0 | if (imagemask) |
659 | 0 | fz_invert_pixmap_alpha(ctx, pixmap); |
660 | 0 | } |
661 | 0 | fz_always(ctx) |
662 | 0 | fz_drop_pixmap(ctx, original); |
663 | 0 | fz_catch(ctx) |
664 | 0 | fz_rethrow(ctx); |
665 | 0 | pixmap_cloned = 1; |
666 | 0 | } |
667 | | |
668 | 0 | if (!mask && image->mask) |
669 | 0 | { |
670 | 0 | fz_pixmap *original = fz_get_pixmap_from_image(ctx, image->mask, NULL, NULL, NULL, NULL); |
671 | |
|
672 | 0 | fz_try(ctx) |
673 | 0 | { |
674 | 0 | mask = fz_clone_pixmap(ctx, original); |
675 | 0 | *pmask = mask; |
676 | 0 | } |
677 | 0 | fz_always(ctx) |
678 | 0 | { |
679 | 0 | fz_drop_pixmap(ctx, original); |
680 | 0 | } |
681 | 0 | fz_catch(ctx) |
682 | 0 | { |
683 | 0 | if (pixmap_cloned) |
684 | 0 | fz_drop_pixmap(ctx, pixmap); |
685 | 0 | fz_rethrow(ctx); |
686 | 0 | } |
687 | 0 | } |
688 | | |
689 | | /* If we have a 1x1 image, to which a mask is being applied |
690 | | * then it's the mask we really want to change, not the |
691 | | * image. We might have just a small section of the image |
692 | | * being covered, and setting the whole thing to white |
693 | | * will blank stuff outside the desired area. */ |
694 | 0 | if (!mask || pixmap->w > 1 || pixmap->h > 1) |
695 | 0 | { |
696 | 0 | n = pixmap->n - pixmap->alpha; |
697 | 0 | bpp = pixmap->n; |
698 | 0 | if (fz_colorspace_is_subtractive(ctx, pixmap->colorspace)) |
699 | 0 | white = 0; |
700 | 0 | else |
701 | 0 | white = 255; |
702 | |
|
703 | 0 | inv_ctm = fz_post_scale(fz_invert_matrix(ctm), pixmap->w, pixmap->h); |
704 | 0 | r = fz_round_rect(fz_transform_rect(fz_rect_from_quad(q), inv_ctm)); |
705 | 0 | r.x0 = fz_clampi(r.x0, 0, pixmap->w); |
706 | 0 | r.x1 = fz_clampi(r.x1, 0, pixmap->w); |
707 | 0 | r.y1 = fz_clampi(pixmap->h - r.y1, 0, pixmap->h); |
708 | 0 | r.y0 = fz_clampi(pixmap->h - r.y0, 0, pixmap->h); |
709 | 0 | for (y = r.y1; y < r.y0; ++y) |
710 | 0 | { |
711 | 0 | for (x = r.x0; x < r.x1; ++x) |
712 | 0 | { |
713 | 0 | unsigned char *s = &pixmap->samples[(size_t)y * pixmap->stride + (size_t)x * bpp]; |
714 | 0 | for (k = 0; k < n; ++k) |
715 | 0 | s[k] = white; |
716 | 0 | if (pixmap->alpha) |
717 | 0 | s[k] = 255; |
718 | 0 | } |
719 | 0 | } |
720 | 0 | } |
721 | |
|
722 | 0 | if (mask) |
723 | 0 | { |
724 | 0 | inv_ctm = fz_post_scale(fz_invert_matrix(ctm), mask->w, mask->h); |
725 | 0 | r = fz_round_rect(fz_transform_rect(fz_rect_from_quad(q), inv_ctm)); |
726 | 0 | r.x0 = fz_clampi(r.x0, 0, mask->w); |
727 | 0 | r.x1 = fz_clampi(r.x1, 0, mask->w); |
728 | 0 | r.y1 = fz_clampi(mask->h - r.y1, 0, mask->h); |
729 | 0 | r.y0 = fz_clampi(mask->h - r.y0, 0, mask->h); |
730 | 0 | for (y = r.y1; y < r.y0; ++y) |
731 | 0 | { |
732 | 0 | unsigned char *s = &mask->samples[(size_t)y * mask->stride + (size_t)r.x0]; |
733 | 0 | memset(s, 0xff, r.x1-r.x0); |
734 | 0 | } |
735 | 0 | } |
736 | |
|
737 | 0 | return pixmap; |
738 | 0 | } |
739 | | |
740 | | static fz_image * |
741 | | pdf_redact_image_filter_remove(fz_context *ctx, void *opaque, fz_matrix ctm, const char *name, fz_image *image, fz_rect clip) |
742 | 0 | { |
743 | 0 | fz_pixmap *redacted = NULL; |
744 | 0 | struct redact_filter_state *red = opaque; |
745 | 0 | pdf_page *page = red->page; |
746 | 0 | pdf_annot *annot; |
747 | 0 | pdf_obj *qp; |
748 | 0 | fz_rect area; |
749 | 0 | fz_rect r; |
750 | 0 | int i, n; |
751 | |
|
752 | 0 | fz_var(redacted); |
753 | |
|
754 | 0 | area = fz_transform_rect(fz_unit_rect, ctm); |
755 | |
|
756 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
757 | 0 | { |
758 | 0 | if (red->target != NULL && red->target != annot) |
759 | 0 | continue; |
760 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
761 | 0 | { |
762 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
763 | 0 | n = pdf_array_len(ctx, qp); |
764 | 0 | if (n > 0) |
765 | 0 | { |
766 | 0 | for (i = 0; i < n; i += 8) |
767 | 0 | { |
768 | 0 | r = fz_rect_from_quad(pdf_to_quad(ctx, qp, i)); |
769 | 0 | r = fz_intersect_rect(r, area); |
770 | 0 | if (!fz_is_empty_rect(r)) |
771 | 0 | return NULL; |
772 | 0 | } |
773 | 0 | } |
774 | 0 | else |
775 | 0 | { |
776 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
777 | 0 | r = fz_intersect_rect(r, area); |
778 | 0 | if (!fz_is_empty_rect(r)) |
779 | 0 | return NULL; |
780 | 0 | } |
781 | 0 | } |
782 | 0 | } |
783 | | |
784 | 0 | return fz_keep_image(ctx, image); |
785 | 0 | } |
786 | | |
787 | | static fz_image * |
788 | | pdf_redact_image_filter_remove_invisible(fz_context *ctx, void *opaque, fz_matrix ctm, const char *name, fz_image *image, fz_rect clip) |
789 | 0 | { |
790 | 0 | fz_pixmap *redacted = NULL; |
791 | 0 | struct redact_filter_state *red = opaque; |
792 | 0 | pdf_page *page = red->page; |
793 | 0 | pdf_annot *annot; |
794 | 0 | pdf_obj *qp; |
795 | 0 | fz_rect area; |
796 | 0 | fz_rect r; |
797 | 0 | int i, n; |
798 | |
|
799 | 0 | fz_var(redacted); |
800 | |
|
801 | 0 | area = fz_transform_rect(fz_unit_rect, ctm); |
802 | | |
803 | | /* Restrict the are of the image to that which can actually be seen. */ |
804 | 0 | area = fz_intersect_rect(area, clip); |
805 | |
|
806 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
807 | 0 | { |
808 | 0 | if (red->target != NULL && red->target != annot) |
809 | 0 | continue; |
810 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
811 | 0 | { |
812 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
813 | 0 | n = pdf_array_len(ctx, qp); |
814 | 0 | if (n > 0) |
815 | 0 | { |
816 | 0 | for (i = 0; i < n; i += 8) |
817 | 0 | { |
818 | 0 | r = fz_rect_from_quad(pdf_to_quad(ctx, qp, i)); |
819 | 0 | r = fz_intersect_rect(r, area); |
820 | 0 | if (!fz_is_empty_rect(r)) |
821 | 0 | return NULL; |
822 | 0 | } |
823 | 0 | } |
824 | 0 | else |
825 | 0 | { |
826 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
827 | 0 | r = fz_intersect_rect(r, area); |
828 | 0 | if (!fz_is_empty_rect(r)) |
829 | 0 | return NULL; |
830 | 0 | } |
831 | 0 | } |
832 | 0 | } |
833 | | |
834 | 0 | return fz_keep_image(ctx, image); |
835 | 0 | } |
836 | | |
837 | | static fz_image * |
838 | | pdf_redact_image_filter_pixels(fz_context *ctx, void *opaque, fz_matrix ctm, const char *name, fz_image *image, fz_rect clip) |
839 | 0 | { |
840 | 0 | fz_pixmap *redacted = NULL; |
841 | 0 | fz_pixmap *mask = NULL; |
842 | 0 | struct redact_filter_state *red = opaque; |
843 | 0 | pdf_page *page = red->page; |
844 | 0 | pdf_annot *annot; |
845 | 0 | pdf_obj *qp; |
846 | 0 | fz_quad area, q; |
847 | 0 | fz_rect r; |
848 | 0 | int i, n; |
849 | |
|
850 | 0 | fz_var(redacted); |
851 | 0 | fz_var(mask); |
852 | |
|
853 | 0 | area = fz_transform_quad(fz_quad_from_rect(fz_unit_rect), ctm); |
854 | | |
855 | | /* First see if we can redact the image completely */ |
856 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
857 | 0 | { |
858 | 0 | if (red->target != NULL && red->target != annot) |
859 | 0 | continue; |
860 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
861 | 0 | { |
862 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
863 | 0 | n = pdf_array_len(ctx, qp); |
864 | 0 | if (n > 0) |
865 | 0 | { |
866 | 0 | for (i = 0; i < n; i += 8) |
867 | 0 | { |
868 | 0 | q = pdf_to_quad(ctx, qp, i); |
869 | 0 | if (fz_is_quad_inside_quad(area, q)) |
870 | 0 | return NULL; |
871 | 0 | } |
872 | 0 | } |
873 | 0 | else |
874 | 0 | { |
875 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
876 | 0 | q = fz_quad_from_rect(r); |
877 | 0 | if (fz_is_quad_inside_quad(area, q)) |
878 | 0 | return NULL; |
879 | 0 | } |
880 | 0 | } |
881 | 0 | } |
882 | | |
883 | | /* Blank out redacted parts of the image if necessary */ |
884 | 0 | fz_try(ctx) |
885 | 0 | { |
886 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
887 | 0 | { |
888 | 0 | if (red->target != NULL && red->target != annot) |
889 | 0 | continue; |
890 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
891 | 0 | { |
892 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
893 | 0 | n = pdf_array_len(ctx, qp); |
894 | 0 | if (n > 0) |
895 | 0 | { |
896 | 0 | for (i = 0; i < n; i += 8) |
897 | 0 | { |
898 | 0 | q = pdf_to_quad(ctx, qp, i); |
899 | 0 | if (fz_is_quad_intersecting_quad(area, q)) |
900 | 0 | redacted = pdf_redact_image_imp(ctx, ctm, image, redacted, &mask, q); |
901 | 0 | } |
902 | 0 | } |
903 | 0 | else |
904 | 0 | { |
905 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
906 | 0 | q = fz_quad_from_rect(r); |
907 | 0 | if (fz_is_quad_intersecting_quad(area, q)) |
908 | 0 | redacted = pdf_redact_image_imp(ctx, ctm, image, redacted, &mask, q); |
909 | 0 | } |
910 | 0 | } |
911 | 0 | } |
912 | 0 | } |
913 | 0 | fz_catch(ctx) |
914 | 0 | { |
915 | 0 | fz_drop_pixmap(ctx, redacted); |
916 | 0 | fz_drop_pixmap(ctx, mask); |
917 | 0 | fz_rethrow(ctx); |
918 | 0 | } |
919 | | |
920 | 0 | if (redacted) |
921 | 0 | { |
922 | 0 | int imagemask = image->imagemask; |
923 | 0 | fz_image *imask = fz_keep_image(ctx, image->mask); |
924 | |
|
925 | 0 | fz_var(imask); |
926 | |
|
927 | 0 | fz_try(ctx) |
928 | 0 | { |
929 | 0 | if (mask) |
930 | 0 | { |
931 | 0 | fz_drop_image(ctx, imask); |
932 | 0 | imask = NULL; |
933 | 0 | imask = fz_new_image_from_pixmap(ctx, mask, NULL); |
934 | 0 | } |
935 | 0 | image = fz_new_image_from_pixmap(ctx, redacted, NULL); |
936 | 0 | image->imagemask = imagemask; |
937 | 0 | image->mask = imask; |
938 | 0 | imask = NULL; |
939 | 0 | } |
940 | 0 | fz_always(ctx) |
941 | 0 | { |
942 | 0 | fz_drop_pixmap(ctx, redacted); |
943 | 0 | fz_drop_pixmap(ctx, mask); |
944 | 0 | fz_drop_image(ctx, imask); |
945 | 0 | } |
946 | 0 | fz_catch(ctx) |
947 | 0 | fz_rethrow(ctx); |
948 | 0 | return image; |
949 | 0 | } |
950 | | |
951 | 0 | return fz_keep_image(ctx, image); |
952 | 0 | } |
953 | | |
954 | | /* Returns 0 if area does not intersect with any of our redactions. |
955 | | * Returns 2 if area is completely included within one of our redactions. |
956 | | * Returns 1 otherwise. */ |
957 | | static int |
958 | | rect_touches_redactions(fz_context *ctx, fz_rect area, struct redact_filter_state *red) |
959 | 0 | { |
960 | 0 | pdf_annot *annot; |
961 | 0 | pdf_obj *qp; |
962 | 0 | fz_quad q; |
963 | 0 | fz_rect r, s; |
964 | 0 | int i, n; |
965 | 0 | pdf_page *page = red->page; |
966 | |
|
967 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) |
968 | 0 | { |
969 | 0 | if (red->target != NULL && red->target != annot) |
970 | 0 | continue; |
971 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
972 | 0 | { |
973 | 0 | qp = pdf_dict_get(ctx, annot->obj, PDF_NAME(QuadPoints)); |
974 | 0 | n = pdf_array_len(ctx, qp); |
975 | 0 | if (n > 0) |
976 | 0 | { |
977 | 0 | for (i = 0; i < n; i += 8) |
978 | 0 | { |
979 | 0 | q = pdf_to_quad(ctx, qp, i); |
980 | 0 | r = fz_rect_from_quad(q); |
981 | 0 | s = fz_intersect_rect(r, area); |
982 | 0 | if (!fz_is_empty_rect(s)) |
983 | 0 | { |
984 | 0 | if (fz_contains_rect(r, area)) |
985 | 0 | return 2; |
986 | 0 | return 1; |
987 | 0 | } |
988 | 0 | } |
989 | 0 | } |
990 | 0 | else |
991 | 0 | { |
992 | 0 | r = pdf_dict_get_rect(ctx, annot->obj, PDF_NAME(Rect)); |
993 | 0 | s = fz_intersect_rect(r, area); |
994 | 0 | if (!fz_is_empty_rect(s)) |
995 | 0 | { |
996 | 0 | if (fz_contains_rect(r, area)) |
997 | 0 | return 2; |
998 | 0 | return 1; |
999 | 0 | } |
1000 | 0 | } |
1001 | 0 | } |
1002 | 0 | } |
1003 | 0 | return 0; |
1004 | 0 | } |
1005 | | |
1006 | | static void |
1007 | | remove_page_link(fz_context *ctx, pdf_page *page, pdf_obj *obj) |
1008 | 0 | { |
1009 | 0 | pdf_link **linkp = (pdf_link **)&page->links; |
1010 | 0 | pdf_link *link; |
1011 | |
|
1012 | 0 | while ((link = *linkp) != NULL) |
1013 | 0 | { |
1014 | 0 | if (link->obj == obj) |
1015 | 0 | { |
1016 | 0 | *linkp = (pdf_link *)link->super.next; |
1017 | 0 | link->super.next = NULL; |
1018 | 0 | fz_drop_link(ctx, &link->super); |
1019 | 0 | break; |
1020 | 0 | } |
1021 | 0 | else |
1022 | 0 | { |
1023 | 0 | linkp = (pdf_link **)&link->super.next; |
1024 | 0 | } |
1025 | 0 | } |
1026 | 0 | } |
1027 | | |
1028 | | static void |
1029 | | pdf_redact_page_links(fz_context *ctx, struct redact_filter_state *red) |
1030 | 0 | { |
1031 | 0 | pdf_obj *annots; |
1032 | 0 | pdf_obj *link; |
1033 | 0 | fz_rect area; |
1034 | 0 | int k; |
1035 | |
|
1036 | 0 | annots = pdf_dict_get(ctx, red->page->obj, PDF_NAME(Annots)); |
1037 | 0 | k = 0; |
1038 | 0 | while (k < pdf_array_len(ctx, annots)) |
1039 | 0 | { |
1040 | 0 | link = pdf_array_get(ctx, annots, k); |
1041 | 0 | if (pdf_dict_get(ctx, link, PDF_NAME(Subtype)) == PDF_NAME(Link)) |
1042 | 0 | { |
1043 | 0 | area = pdf_dict_get_rect(ctx, link, PDF_NAME(Rect)); |
1044 | 0 | if (rect_touches_redactions(ctx, area, red)) |
1045 | 0 | { |
1046 | 0 | pdf_array_delete(ctx, annots, k); |
1047 | 0 | remove_page_link(ctx, red->page, link); |
1048 | 0 | continue; |
1049 | 0 | } |
1050 | 0 | } |
1051 | 0 | ++k; |
1052 | 0 | } |
1053 | 0 | } |
1054 | | |
1055 | | static void |
1056 | | pdf_redact_page_annotations(fz_context *ctx, struct redact_filter_state *red) |
1057 | 0 | { |
1058 | 0 | pdf_annot *annot; |
1059 | 0 | fz_rect area; |
1060 | |
|
1061 | 0 | restart: |
1062 | 0 | for (annot = pdf_first_annot(ctx, red->page); annot; annot = pdf_next_annot(ctx, annot)) |
1063 | 0 | { |
1064 | 0 | if (pdf_annot_type(ctx, annot) == PDF_ANNOT_FREE_TEXT) |
1065 | 0 | { |
1066 | 0 | area = pdf_dict_get_rect(ctx, pdf_annot_obj(ctx, annot), PDF_NAME(Rect)); |
1067 | 0 | if (rect_touches_redactions(ctx, area, red)) |
1068 | 0 | { |
1069 | 0 | pdf_delete_annot(ctx, red->page, annot); |
1070 | 0 | goto restart; |
1071 | 0 | } |
1072 | 0 | } |
1073 | 0 | } |
1074 | 0 | } |
1075 | | |
1076 | | static int culler(fz_context *ctx, void *opaque, fz_rect bbox, fz_cull_type type) |
1077 | 0 | { |
1078 | 0 | struct redact_filter_state *red = opaque; |
1079 | |
|
1080 | 0 | switch (type) |
1081 | 0 | { |
1082 | 0 | case FZ_CULL_PATH_FILL: |
1083 | 0 | case FZ_CULL_PATH_STROKE: |
1084 | 0 | case FZ_CULL_PATH_FILL_STROKE: |
1085 | 0 | case FZ_CULL_CLIP_PATH_FILL: |
1086 | 0 | case FZ_CULL_CLIP_PATH_STROKE: |
1087 | 0 | case FZ_CULL_CLIP_PATH_FILL_STROKE: |
1088 | 0 | if (red->line_art == PDF_REDACT_LINE_ART_REMOVE_IF_COVERED) |
1089 | 0 | return (rect_touches_redactions(ctx, bbox, red) == 2); |
1090 | 0 | else if (red->line_art == PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED) |
1091 | 0 | return (rect_touches_redactions(ctx, bbox, red) != 0); |
1092 | 0 | return 0; |
1093 | 0 | default: |
1094 | 0 | return 0; |
1095 | 0 | } |
1096 | 0 | } |
1097 | | |
1098 | | static |
1099 | | void init_redact_filter(fz_context *ctx, pdf_redact_options *redact_opts, struct redact_filter_state *red, pdf_page *page, pdf_annot *target) |
1100 | 0 | { |
1101 | 0 | int black_boxes = redact_opts ? redact_opts->black_boxes : 0; |
1102 | 0 | int image_method = redact_opts ? redact_opts->image_method : PDF_REDACT_IMAGE_PIXELS; |
1103 | 0 | int line_art = redact_opts ? redact_opts->line_art : PDF_REDACT_LINE_ART_NONE; |
1104 | 0 | int text = redact_opts ? redact_opts->text : PDF_REDACT_TEXT_REMOVE; |
1105 | |
|
1106 | 0 | memset(&red->filter_opts, 0, sizeof red->filter_opts); |
1107 | 0 | memset(&red->sanitize_opts, 0, sizeof red->sanitize_opts); |
1108 | |
|
1109 | 0 | red->filter_opts.recurse = 0; /* don't redact patterns, softmasks, and type3 fonts */ |
1110 | 0 | red->filter_opts.instance_forms = 1; /* redact xobjects with instancing */ |
1111 | 0 | red->filter_opts.ascii = 1; |
1112 | 0 | red->filter_opts.opaque = red; |
1113 | 0 | red->filter_opts.filters = red->filter_list; |
1114 | 0 | if (black_boxes) |
1115 | 0 | red->filter_opts.complete = pdf_redact_end_page; |
1116 | 0 | red->line_art = line_art; |
1117 | 0 | red->text = text; |
1118 | |
|
1119 | 0 | red->sanitize_opts.opaque = red; |
1120 | 0 | if (text == PDF_REDACT_TEXT_REMOVE) |
1121 | 0 | red->sanitize_opts.text_filter = pdf_redact_text_filter; |
1122 | 0 | if (text == PDF_REDACT_TEXT_REMOVE_INVISIBLE) |
1123 | 0 | red->sanitize_opts.text_filter = pdf_redact_invisible_text_filter; |
1124 | 0 | if (image_method == PDF_REDACT_IMAGE_PIXELS) |
1125 | 0 | red->sanitize_opts.image_filter = pdf_redact_image_filter_pixels; |
1126 | 0 | if (image_method == PDF_REDACT_IMAGE_REMOVE) |
1127 | 0 | red->sanitize_opts.image_filter = pdf_redact_image_filter_remove; |
1128 | 0 | if (image_method == PDF_REDACT_IMAGE_REMOVE_UNLESS_INVISIBLE) |
1129 | 0 | red->sanitize_opts.image_filter = pdf_redact_image_filter_remove_invisible; |
1130 | 0 | red->sanitize_opts.culler = culler; |
1131 | |
|
1132 | 0 | red->filter_list[0].filter = pdf_new_sanitize_filter; |
1133 | 0 | red->filter_list[0].options = &red->sanitize_opts; |
1134 | 0 | red->filter_list[1].filter = NULL; |
1135 | 0 | red->filter_list[1].options = NULL; |
1136 | |
|
1137 | 0 | red->page = page; |
1138 | 0 | red->target = target; |
1139 | 0 | } |
1140 | | |
1141 | | static int |
1142 | | pdf_apply_redaction_imp(fz_context *ctx, pdf_page *page, pdf_annot *target, pdf_redact_options *redact_opts) |
1143 | 0 | { |
1144 | 0 | pdf_annot *annot; |
1145 | 0 | int has_redactions = 0; |
1146 | 0 | struct redact_filter_state red; |
1147 | 0 | pdf_document *doc = page->doc; |
1148 | |
|
1149 | 0 | for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot)) { |
1150 | 0 | if (target != NULL && target != annot) |
1151 | 0 | continue; |
1152 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
1153 | 0 | has_redactions = 1; |
1154 | 0 | } |
1155 | |
|
1156 | 0 | if (!has_redactions) |
1157 | 0 | return 0; |
1158 | | |
1159 | 0 | init_redact_filter(ctx, redact_opts, &red, page, target); |
1160 | |
|
1161 | 0 | if (target) |
1162 | 0 | pdf_begin_operation(ctx, doc, "Apply redaction"); |
1163 | 0 | else |
1164 | 0 | pdf_begin_operation(ctx, doc, "Apply redactions on page"); |
1165 | 0 | fz_try(ctx) |
1166 | 0 | { |
1167 | 0 | pdf_filter_page_contents(ctx, doc, page, &red.filter_opts); |
1168 | 0 | pdf_redact_page_links(ctx, &red); |
1169 | 0 | pdf_redact_page_annotations(ctx, &red); |
1170 | |
|
1171 | 0 | annot = pdf_first_annot(ctx, page); |
1172 | 0 | while (annot) |
1173 | 0 | { |
1174 | 0 | if (target == NULL || annot == target) |
1175 | 0 | { |
1176 | 0 | if (pdf_dict_get(ctx, annot->obj, PDF_NAME(Subtype)) == PDF_NAME(Redact)) |
1177 | 0 | { |
1178 | 0 | pdf_delete_annot(ctx, page, annot); |
1179 | 0 | annot = pdf_first_annot(ctx, page); |
1180 | 0 | continue; |
1181 | 0 | } |
1182 | 0 | } |
1183 | 0 | annot = pdf_next_annot(ctx, annot); |
1184 | 0 | } |
1185 | |
|
1186 | 0 | doc->redacted = 1; |
1187 | 0 | pdf_end_operation(ctx, doc); |
1188 | 0 | } |
1189 | 0 | fz_catch(ctx) |
1190 | 0 | { |
1191 | 0 | pdf_abandon_operation(ctx, doc); |
1192 | 0 | fz_rethrow(ctx); |
1193 | 0 | } |
1194 | | |
1195 | 0 | return 1; |
1196 | 0 | } |
1197 | | |
1198 | | int |
1199 | | pdf_redact_page(fz_context *ctx, pdf_document *doc, pdf_page *page, pdf_redact_options *redact_opts) |
1200 | 0 | { |
1201 | 0 | if (page == NULL || page->doc != doc) |
1202 | 0 | fz_throw(ctx, FZ_ERROR_ARGUMENT, "Can't redact a page not from the doc"); |
1203 | 0 | return pdf_apply_redaction_imp(ctx, page, NULL, redact_opts); |
1204 | 0 | } |
1205 | | |
1206 | | int |
1207 | | pdf_apply_redaction(fz_context *ctx, pdf_annot *annot, pdf_redact_options *redact_opts) |
1208 | 0 | { |
1209 | 0 | return pdf_apply_redaction_imp(ctx, annot->page, annot, redact_opts); |
1210 | 0 | } |
1211 | | |
1212 | | /* Hard clipping of pages */ |
1213 | | |
1214 | | struct clip_filter_state { |
1215 | | pdf_filter_options filter_opts; |
1216 | | pdf_sanitize_filter_options sanitize_opts; |
1217 | | pdf_filter_factory filter_list[2]; |
1218 | | pdf_page *page; |
1219 | | fz_rect clip; |
1220 | | }; |
1221 | | |
1222 | | static int clip_culler(fz_context *ctx, void *opaque, fz_rect bbox, fz_cull_type type) |
1223 | 0 | { |
1224 | 0 | struct clip_filter_state *hc = opaque; |
1225 | |
|
1226 | 0 | switch (type) |
1227 | 0 | { |
1228 | 0 | case FZ_CULL_PATH_FILL: |
1229 | 0 | case FZ_CULL_PATH_STROKE: |
1230 | 0 | case FZ_CULL_PATH_FILL_STROKE: |
1231 | 0 | case FZ_CULL_CLIP_PATH_FILL: |
1232 | 0 | case FZ_CULL_CLIP_PATH_STROKE: |
1233 | 0 | case FZ_CULL_CLIP_PATH_FILL_STROKE: |
1234 | 0 | case FZ_CULL_GLYPH: |
1235 | 0 | case FZ_CULL_IMAGE: |
1236 | 0 | case FZ_CULL_SHADING: |
1237 | 0 | return fz_is_empty_rect(fz_intersect_rect(bbox, hc->clip)); |
1238 | 0 | default: |
1239 | 0 | return 0; |
1240 | 0 | } |
1241 | 0 | } |
1242 | | |
1243 | | static |
1244 | | void init_clip_filter(fz_context *ctx, struct clip_filter_state *hc, pdf_page *page, fz_rect clip) |
1245 | 0 | { |
1246 | 0 | memset(&hc->filter_opts, 0, sizeof hc->filter_opts); |
1247 | 0 | memset(&hc->sanitize_opts, 0, sizeof hc->sanitize_opts); |
1248 | |
|
1249 | 0 | hc->filter_opts.recurse = 0; /* don't redact patterns, softmasks, and type3 fonts */ |
1250 | 0 | hc->filter_opts.instance_forms = 1; /* redact xobjects with instancing */ |
1251 | 0 | hc->filter_opts.ascii = 0; |
1252 | 0 | hc->filter_opts.opaque = hc; |
1253 | 0 | hc->filter_opts.filters = hc->filter_list; |
1254 | 0 | hc->clip = clip; |
1255 | |
|
1256 | 0 | hc->sanitize_opts.opaque = hc; |
1257 | 0 | hc->sanitize_opts.culler = clip_culler; |
1258 | |
|
1259 | 0 | hc->filter_list[0].filter = pdf_new_sanitize_filter; |
1260 | 0 | hc->filter_list[0].options = &hc->sanitize_opts; |
1261 | 0 | hc->filter_list[1].filter = NULL; |
1262 | 0 | hc->filter_list[1].options = NULL; |
1263 | |
|
1264 | 0 | hc->page = page; |
1265 | 0 | } |
1266 | | |
1267 | | static void |
1268 | | pdf_clip_page_links(fz_context *ctx, struct clip_filter_state *hc) |
1269 | 0 | { |
1270 | 0 | pdf_obj *annots; |
1271 | 0 | pdf_obj *link; |
1272 | 0 | fz_rect area; |
1273 | 0 | int k; |
1274 | |
|
1275 | 0 | annots = pdf_dict_get(ctx, hc->page->obj, PDF_NAME(Annots)); |
1276 | 0 | k = 0; |
1277 | 0 | while (k < pdf_array_len(ctx, annots)) |
1278 | 0 | { |
1279 | 0 | link = pdf_array_get(ctx, annots, k); |
1280 | 0 | if (pdf_dict_get(ctx, link, PDF_NAME(Subtype)) == PDF_NAME(Link)) |
1281 | 0 | { |
1282 | 0 | area = pdf_dict_get_rect(ctx, link, PDF_NAME(Rect)); |
1283 | 0 | if (fz_is_empty_rect(fz_intersect_rect(area, hc->clip))) |
1284 | 0 | { |
1285 | 0 | pdf_array_delete(ctx, annots, k); |
1286 | 0 | continue; |
1287 | 0 | } |
1288 | 0 | } |
1289 | 0 | ++k; |
1290 | 0 | } |
1291 | 0 | } |
1292 | | |
1293 | | static void |
1294 | | pdf_clip_page_annotations(fz_context *ctx, struct clip_filter_state *hc) |
1295 | 0 | { |
1296 | 0 | pdf_annot *annot; |
1297 | 0 | fz_rect area; |
1298 | |
|
1299 | 0 | restart: |
1300 | 0 | for (annot = pdf_first_annot(ctx, hc->page); annot; annot = pdf_next_annot(ctx, annot)) |
1301 | 0 | { |
1302 | 0 | if (pdf_annot_type(ctx, annot) == PDF_ANNOT_FREE_TEXT) |
1303 | 0 | { |
1304 | 0 | area = pdf_dict_get_rect(ctx, pdf_annot_obj(ctx, annot), PDF_NAME(Rect)); |
1305 | 0 | if (fz_is_empty_rect(fz_intersect_rect(area, hc->clip))) |
1306 | 0 | { |
1307 | 0 | pdf_delete_annot(ctx, hc->page, annot); |
1308 | 0 | goto restart; |
1309 | 0 | } |
1310 | 0 | } |
1311 | 0 | } |
1312 | 0 | } |
1313 | | |
1314 | | void |
1315 | | pdf_clip_page(fz_context *ctx, pdf_page *page, fz_rect clip) |
1316 | 0 | { |
1317 | 0 | pdf_document *doc; |
1318 | 0 | struct clip_filter_state hc; |
1319 | 0 | fz_matrix page_ctm, inv_page_ctm; |
1320 | |
|
1321 | 0 | if (page == NULL) |
1322 | 0 | return; |
1323 | | |
1324 | 0 | doc = page->doc; |
1325 | |
|
1326 | 0 | pdf_page_transform(ctx, page, NULL, &page_ctm); |
1327 | 0 | inv_page_ctm = fz_invert_matrix(page_ctm); |
1328 | 0 | clip = fz_transform_rect(clip, inv_page_ctm); |
1329 | |
|
1330 | 0 | init_clip_filter(ctx, &hc, page, clip); |
1331 | |
|
1332 | 0 | pdf_begin_operation(ctx, doc, "Apply hard clip to page"); |
1333 | 0 | fz_try(ctx) |
1334 | 0 | { |
1335 | 0 | pdf_filter_page_contents(ctx, doc, page, &hc.filter_opts); |
1336 | 0 | pdf_clip_page_links(ctx, &hc); |
1337 | 0 | pdf_clip_page_annotations(ctx, &hc); |
1338 | 0 | pdf_end_operation(ctx, doc); |
1339 | 0 | } |
1340 | 0 | fz_catch(ctx) |
1341 | 0 | { |
1342 | 0 | pdf_abandon_operation(ctx, doc); |
1343 | 0 | fz_rethrow(ctx); |
1344 | 0 | } |
1345 | 0 | } |
1346 | | |
1347 | | /* Vectorisation of pages */ |
1348 | | |
1349 | | struct vectorize_filter_state { |
1350 | | pdf_filter_options filter_opts; |
1351 | | pdf_vectorize_filter_options vectorize_opts; |
1352 | | pdf_filter_factory filter_list[2]; |
1353 | | pdf_page *page; |
1354 | | }; |
1355 | | |
1356 | | static |
1357 | | void init_vectorize_filter(fz_context *ctx, struct vectorize_filter_state *hc, pdf_page *page) |
1358 | 0 | { |
1359 | 0 | memset(&hc->filter_opts, 0, sizeof hc->filter_opts); |
1360 | 0 | memset(&hc->vectorize_opts, 0, sizeof hc->vectorize_opts); |
1361 | |
|
1362 | 0 | hc->filter_opts.recurse = 0; |
1363 | 0 | hc->filter_opts.instance_forms = 0; |
1364 | 0 | hc->filter_opts.ascii = 0; |
1365 | 0 | hc->filter_opts.opaque = hc; |
1366 | 0 | hc->filter_opts.filters = hc->filter_list; |
1367 | 0 | hc->filter_opts.recurse = 1; |
1368 | |
|
1369 | 0 | hc->vectorize_opts.opaque = hc; |
1370 | |
|
1371 | 0 | hc->filter_list[0].filter = pdf_new_vectorize_filter; |
1372 | 0 | hc->filter_list[0].options = &hc->vectorize_opts; |
1373 | 0 | hc->filter_list[1].filter = NULL; |
1374 | 0 | hc->filter_list[1].options = NULL; |
1375 | |
|
1376 | 0 | hc->page = page; |
1377 | 0 | } |
1378 | | |
1379 | | void |
1380 | | pdf_vectorize_page(fz_context *ctx, pdf_page *page) |
1381 | 0 | { |
1382 | 0 | pdf_document *doc; |
1383 | 0 | struct vectorize_filter_state hv; |
1384 | |
|
1385 | 0 | if (page == NULL) |
1386 | 0 | return; |
1387 | | |
1388 | 0 | doc = page->doc; |
1389 | |
|
1390 | 0 | init_vectorize_filter(ctx, &hv, page); |
1391 | |
|
1392 | 0 | pdf_begin_operation(ctx, doc, "Vectorize text to page"); |
1393 | 0 | fz_try(ctx) |
1394 | 0 | { |
1395 | 0 | pdf_filter_page_contents(ctx, doc, page, &hv.filter_opts); |
1396 | 0 | pdf_end_operation(ctx, doc); |
1397 | 0 | } |
1398 | 0 | fz_catch(ctx) |
1399 | 0 | { |
1400 | 0 | pdf_abandon_operation(ctx, doc); |
1401 | 0 | fz_rethrow(ctx); |
1402 | 0 | } |
1403 | 0 | } |