Skip to content

Commit

Permalink
Fixes
Browse files Browse the repository at this point in the history
Address bug 3207:
Multiple sub-paths in a path were not correctly handled when "closepath" commands were present.
When encountering "closepath", this fix generates a line that connects the current point with the first point of the current sub-path - however only if these two points are different.
The "firstpoint" value is being set exclusively by the "moveto" command.
  • Loading branch information
JorjMcKie committed Mar 6, 2024
1 parent 2bfccf6 commit 4ddcaa3
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 80 deletions.
11 changes: 7 additions & 4 deletions docs/page.rst
Original file line number Diff line number Diff line change
Expand Up @@ -511,16 +511,18 @@ In a nutshell, this is what you can do with PyMuPDF:

|history_end|

.. method:: apply_redactions(images=PDF_REDACT_IMAGE_PIXELS)
.. method:: apply_redactions(images=PDF_REDACT_IMAGE_PIXELS, graphics=PDF_REDACT_LINE_ART_IF_TOUCHED)

PDF only: Remove all **text content** contained in any redaction rectangle.
PDF only: Remove all **content** contained in any redaction rectangle on the page.

**This method applies and then deletes all redactions from the page.**

:arg int images: How to redact overlapping images. The default (2) blanks out overlapping pixels. *PDF_REDACT_IMAGE_NONE* (0) ignores, and *PDF_REDACT_IMAGE_REMOVE* (1) completely removes all overlapping images.
:arg int images: How to redact overlapping images. The default (2) blanks out overlapping pixels. `PDF_REDACT_IMAGE_NONE` (0) ignores, and `PDF_REDACT_IMAGE_REMOVE` (1) completely removes images overlapping any redaction annotation. Option `PDF_REDACT_IMAGE_REMOVE_UNLESS_INVISIBLE` (3) only removes images that are actually visible.

:arg int graphics: How to redact overlapping vector graphics (also called "line art" or "drawings"). The default (2) removes any overlapping vector graphics. `PDF_REDACT_LINE_ART_NONE` (0) ignores, and `PDF_REDACT_LINE_ART_IF_COVERED` (1) removes graphics fully contained in a redaction annotation.

:returns: *True* if at least one redaction annotation has been processed, *False* otherwise.

:returns: `True` if at least one redaction annotation has been processed, `False` otherwise.

.. note::
* Text contained in a redaction rectangle will be **physically** removed from the page (assuming :meth:`Document.save` with a suitable garbage option) and will no longer appear in e.g. text extractions or anywhere else. All redaction annotations will also be removed. Other annotations are unaffected.
Expand All @@ -546,6 +548,7 @@ In a nutshell, this is what you can do with PyMuPDF:
* New in v1.16.11
* Changed in v1.16.12: The previous *mark* parameter is gone. Instead, the respective rectangles are filled with the individual *fill* color of each redaction annotation. If a *text* was given in the annotation, then :meth:`insert_textbox` is invoked to insert it, using parameters provided with the redaction.
* Changed in v1.18.0: added option for handling images that overlap redaction areas.
* Changed in v1.23.27: added option for removing graphics as well.

|history_end|

Expand Down
25 changes: 22 additions & 3 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18839,10 +18839,23 @@ def closepath(self, ctx): # trace_close().
if jm_checkrect(self.dev):
#log(f'end1: {self.dev.pathdict=}')
return
#log('setting self.dev.pathdict[ "closePath"] to true')
self.dev.pathdict[ "closePath"] = True
self.dev.linecount = 0 # reset # of consec. lines
#log(f'end2: {self.dev.pathdict=}')

if self.dev.havemove:
if self.dev.lastpoint != self.dev.firstpoint:
item = ("l", JM_py_from_point(self.dev.lastpoint),
JM_py_from_point(self.dev.firstpoint))
self.dev.pathdict[dictkey_items].append(item)
self.dev.lastpoint = self.dev.firstpoint
self.dev.pathdict["closePath"] = False

else:
#log('setting self.dev.pathdict[ "closePath"] to true')
self.dev.pathdict[ "closePath"] = True
#log(f'end2: {self.dev.pathdict=}')

self.dev.havemove = 0

except Exception:
if g_exceptions_verbose: exception_info()
raise
Expand Down Expand Up @@ -18900,7 +18913,9 @@ def moveto(self, ctx, x, y): # trace_moveto().
log(f'self.dev.pathdict:')
for n, v in self.dev.pathdict.items():
log( ' {type(n)=} {len(n)=} {n!r} {n}: {v!r}: {v}')

#log(f'Walker(): {type(self.dev.pathdict)=} {self.dev.pathdict=}')

try:
#log( '{=dev.ctm type(dev.ctm)}')
self.dev.lastpoint = mupdf.fz_transform_point(
Expand All @@ -18914,6 +18929,8 @@ def moveto(self, ctx, x, y): # trace_moveto().
self.dev.lastpoint.x,
self.dev.lastpoint.y,
)
self.dev.firstpoint = self.dev.lastpoint
self.dev.havemove = 1
self.dev.linecount = 0 # reset # of consec. lines
except Exception:
if g_exceptions_verbose: exception_info()
Expand Down Expand Up @@ -19282,6 +19299,8 @@ def __init__(self, out, clips, method):
self.ctm = mupdf.FzMatrix()
self.rot = mupdf.FzMatrix()
self.lastpoint = mupdf.FzPoint()
self.firstpoint = mupdf.FzPoint()
self.havemove = 0
self.pathrect = mupdf.FzRect()
self.pathfactor = 0
self.linecount = 0
Expand Down
21 changes: 20 additions & 1 deletion src/extra.i
Original file line number Diff line number Diff line change
Expand Up @@ -1957,6 +1957,8 @@ struct jm_lineart_device
fz_matrix ptm = {};
fz_matrix rot = {};
fz_point lastpoint = {};
fz_point firstpoint = {};
int havemove = 0;
fz_rect pathrect = {};
int clips = {};
int linecount = {};
Expand Down Expand Up @@ -2830,6 +2832,8 @@ trace_moveto(fz_context *ctx, void *dev_, float x, float y)
dev->lastpoint.y
);
}
dev->firstpoint = dev->lastpoint;
dev->havemove = 1;
dev->linecount = 0; // reset # of consec. lines
}

Expand Down Expand Up @@ -2887,8 +2891,22 @@ trace_close(fz_context *ctx, void *dev_)
return;
}
}
DICT_SETITEMSTR_DROP(dev->pathdict, "closePath", JM_BOOL(1));
dev->linecount = 0; // reset # of consec. lines
if (dev->havemove) {
if (dev->firstpoint.x != dev->lastpoint.x || dev->firstpoint.y != dev->lastpoint.y) {
PyObject *list = PyTuple_New(3);
PyTuple_SET_ITEM(list, 0, PyUnicode_FromString("l"));
PyTuple_SET_ITEM(list, 1, JM_py_from_point(dev->lastpoint));
PyTuple_SET_ITEM(list, 2, JM_py_from_point(dev->firstpoint));
dev->lastpoint = dev->firstpoint;
PyObject *items = PyDict_GetItem(dev->pathdict, dictkey_items);
LIST_APPEND_DROP(items, list);
}
dev->havemove = 0;
DICT_SETITEMSTR_DROP(dev->pathdict, "closePath", JM_BOOL(0));
} else {
DICT_SETITEMSTR_DROP(dev->pathdict, "closePath", JM_BOOL(1));
}
}

static const fz_path_walker trace_path_walker =
Expand All @@ -2915,6 +2933,7 @@ jm_lineart_path(jm_lineart_device *dev, const fz_path *path)
dev->pathrect = fz_infinite_rect;
dev->linecount = 0;
dev->lastpoint = fz_make_point(0, 0);
dev->firstpoint = fz_make_point(0, 0);
if (dev->pathdict) {
Py_CLEAR(dev->pathdict);
}
Expand Down
22 changes: 21 additions & 1 deletion src_classic/helper-devices.i
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ static fz_matrix trace_device_ptm; // page transformation matrix
static fz_matrix trace_device_ctm; // trace device matrix
static fz_matrix trace_device_rot;
static fz_point dev_lastpoint = {0, 0};
static fz_point dev_firstpoint = {0, 0};
static int dev_havemove = 0;
static fz_rect dev_pathrect;
static float dev_pathfactor = 0;
static int dev_linecount = 0;
Expand All @@ -47,6 +49,8 @@ static void trace_device_reset()
trace_device_rot = fz_identity;
dev_lastpoint.x = 0;
dev_lastpoint.y = 0;
dev_firstpoint.x = 0;
dev_firstpoint.y = 0;
dev_pathrect.x0 = 0;
dev_pathrect.y0 = 0;
dev_pathrect.x1 = 0;
Expand Down Expand Up @@ -218,6 +222,8 @@ trace_moveto(fz_context *ctx, void *dev_, float x, float y)
dev_pathrect = fz_make_rect(dev_lastpoint.x, dev_lastpoint.y,
dev_lastpoint.x, dev_lastpoint.y);
}
dev_firstpoint = dev_lastpoint;
dev_havemove = 1;
dev_linecount = 0; // reset # of consec. lines
}

Expand Down Expand Up @@ -272,8 +278,22 @@ trace_close(fz_context *ctx, void *dev_)
return;
}
}
DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(1));
dev_linecount = 0; // reset # of consec. lines
if (dev_havemove) {
if (dev_firstpoint.x != dev_lastpoint.x || dev_firstpoint.y != dev_lastpoint.y) {
PyObject *list = PyTuple_New(3);
PyTuple_SET_ITEM(list, 0, PyUnicode_FromString("l"));
PyTuple_SET_ITEM(list, 1, JM_py_from_point(dev_lastpoint));
PyTuple_SET_ITEM(list, 2, JM_py_from_point(dev_firstpoint));
dev_lastpoint = dev_firstpoint;
PyObject *items = PyDict_GetItem(dev_pathdict, dictkey_items);
LIST_APPEND_DROP(items, list);
}
dev_havemove = 0;
DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0));
} else {
DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(1));
}
}

static const fz_path_walker trace_path_walker =
Expand Down
59 changes: 37 additions & 22 deletions tests/resources/symbols.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
[{'closePath': True,
[{'closePath': False,
'color': (1.0, 1.0, 1.0),
'dashes': '[] 0',
'even_odd': False,
'fill': (1.0, 0.0, 0.0),
'fill_opacity': 1.0,
'items': [('l', (50.0, 50.0), (50.0, 100.0)),
('l', (50.0, 100.0), (100.0, 75.0))],
('l', (50.0, 100.0), (100.0, 75.0)),
('l', (100.0, 75.0), (50.0, 50.0))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand All @@ -14,7 +15,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 1.0},
{'closePath': True,
{'closePath': False,
'color': (1.0, 1.0, 1.0),
'dashes': '[] 0',
'even_odd': False,
Expand Down Expand Up @@ -48,7 +49,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 1.0},
{'closePath': True,
{'closePath': False,
'color': (0.0, 1.0, 0.0),
'dashes': '[] 0',
'even_odd': False,
Expand All @@ -66,15 +67,16 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 0.30000001192092896},
{'closePath': True,
{'closePath': False,
'color': (1.0, 1.0, 1.0),
'dashes': '[] 0',
'even_odd': False,
'fill': (1.0, 0.0, 0.0),
'fill_opacity': 1.0,
'items': [('l', (75.0, 230.0), (100.0, 255.0)),
('l', (100.0, 255.0), (75.0, 280.0)),
('l', (75.0, 280.0), (50.0, 255.0))],
('l', (75.0, 280.0), (50.0, 255.0)),
('l', (50.0, 255.0), (75.0, 230.0))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand All @@ -83,7 +85,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 1.0},
{'closePath': True,
{'closePath': False,
'color': (1.0, 1.0, 1.0),
'dashes': '[] 0',
'even_odd': False,
Expand Down Expand Up @@ -117,7 +119,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 2.0},
{'closePath': True,
{'closePath': False,
'color': (0.0, 0.0, 0.0),
'dashes': '[] 0',
'items': [('c',
Expand Down Expand Up @@ -163,7 +165,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 3.0},
{'closePath': True,
{'closePath': False,
'even_odd': False,
'fill': (1.0, 1.0, 0.0),
'fill_opacity': 1.0,
Expand Down Expand Up @@ -191,7 +193,7 @@
'rect': (50.0, 350.0, 100.0, 400.0),
'seqno': 13,
'type': 'f'},
{'closePath': True,
{'closePath': False,
'even_odd': False,
'fill': (0.0, 0.0, 0.0),
'fill_opacity': 1.0,
Expand Down Expand Up @@ -317,14 +319,15 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 1.0},
{'closePath': True,
{'closePath': False,
'color': (1.0, 0.0, 0.0),
'dashes': '[] 0',
'even_odd': False,
'fill': (1.0, 0.0, 0.0),
'fill_opacity': 1.0,
'items': [('c', (75.0, 485.0), (62.5, 470.0), (50.0, 490.0), (75.0, 510.0)),
('c', (75.0, 485.0), (87.5, 470.0), (100.0, 490.0), (75.0, 510.0))],
('c', (75.0, 485.0), (87.5, 470.0), (100.0, 490.0), (75.0, 510.0)),
('l', (75.0, 510.0), (75.0, 485.0))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand Down Expand Up @@ -451,7 +454,7 @@
'rect': (85.5072021484375, 547.7540283203125, 100.0, 562.2459716796875),
'seqno': 27,
'type': 'f'},
{'closePath': True,
{'closePath': False,
'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391),
'dashes': '[] 0',
'even_odd': False,
Expand All @@ -461,7 +464,10 @@
(85.5072021484375, 547.7540283203125),
(86.30770111083984, 548.553955078125),
(85.00990295410156, 549.8519897460938),
(82.60870361328125, 550.6519775390625))],
(82.60870361328125, 550.6519775390625)),
('l',
(82.60870361328125, 550.6519775390625),
(85.5072021484375, 547.7540283203125))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand All @@ -473,7 +479,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 0.07246380299329758},
{'closePath': True,
{'closePath': False,
'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391),
'dashes': '[] 0',
'even_odd': False,
Expand All @@ -483,7 +489,10 @@
(82.60870361328125, 550.6519775390625),
(87.2510986328125, 553.052978515625),
(87.2510986328125, 556.947021484375),
(82.60870361328125, 559.3480224609375))],
(82.60870361328125, 559.3480224609375)),
('l',
(82.60870361328125, 559.3480224609375),
(82.60870361328125, 550.6519775390625))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand All @@ -495,7 +504,7 @@
'stroke_opacity': 1.0,
'type': 'fs',
'width': 0.07246380299329758},
{'closePath': True,
{'closePath': False,
'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391),
'dashes': '[] 0',
'even_odd': False,
Expand All @@ -505,7 +514,10 @@
(82.60870361328125, 559.3480224609375),
(85.00990295410156, 560.1480102539062),
(86.30770111083984, 561.446044921875),
(85.5072021484375, 562.2459716796875))],
(85.5072021484375, 562.2459716796875)),
('l',
(85.5072021484375, 562.2459716796875),
(82.60870361328125, 559.3480224609375))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand Down Expand Up @@ -599,15 +611,18 @@
557.1740112304688),
'seqno': 37,
'type': 'f'},
{'closePath': True,
{'closePath': False,
'color': (1.0, 1.0, 1.0),
'dashes': '[] 0',
'items': [('l',
(58.937198638916016, 548.47802734375),
(58.937198638916016, 561.52197265625)),
('l',
(61.352699279785156, 548.47802734375),
(61.352699279785156, 561.52197265625))],
(61.352699279785156, 561.52197265625)),
('l',
(61.352699279785156, 561.52197265625),
(61.352699279785156, 548.47802734375))],
'layer': '',
'lineCap': (0, 0, 0),
'lineJoin': 0.0,
Expand All @@ -619,7 +634,7 @@
'stroke_opacity': 1.0,
'type': 's',
'width': 1.1594200134277344},
{'closePath': True,
{'closePath': False,
'even_odd': False,
'fill': (1.0, 1.0, 0.0),
'fill_opacity': 1.0,
Expand Down Expand Up @@ -647,7 +662,7 @@
'rect': (50.0, 590.0, 100.0, 640.0),
'seqno': 39,
'type': 'f'},
{'closePath': True,
{'closePath': False,
'even_odd': False,
'fill': (0.0, 0.0, 0.0),
'fill_opacity': 1.0,
Expand Down
Binary file added tests/resources/test-3207.pdf
Binary file not shown.
Loading

0 comments on commit 4ddcaa3

Please sign in to comment.