Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,30 @@ pi.mark(page, outline=True, label=True,

![Annotations](./docs/page298-outlines.png)

There are even more options! For now you will need to look at the
source code, documentation is Coming Soon.
By default, PAVÉS will assign a new colour to each distinct label based
on a colour cycle [borrowed from
Matplotlib](https://matplotlib.org/stable/gallery/color/color_cycle_default.html)
(no actual Matplotlib was harmed in the making of this library). You
can use Matplotlib's colour cycles if you like:

```
import matplotlib
pi.box(page, color=matplotlib.color_sequences["Dark2"])
```

![Color Cycles](./docs/page2-color-cycles.png)

Or just any list (it must be a `list`) of color specifications (which
are either strings, 3-tuples of integers in the range `[0, 255]`, or
3-tuples of floats in the range `[0.0, 1.0]`):

```
pi.mark(page, color=["blue", "magenta", (0.0, 0.5, 0.32), (233, 222, 111)], labelfunc=repr)
```

![Cycle Harder](./docs/page298-color-cycles.png)

(yes, that just cycles through the colors for each new object)

## Working in the PDF mine

Expand Down
Binary file modified docs/page2-annotations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/page2-color-cycles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/page298-color-cycles.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/page3-elements.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
95 changes: 83 additions & 12 deletions src/paves/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

import functools
import itertools
import subprocess
import tempfile
from os import PathLike
Expand All @@ -15,6 +16,7 @@
Iterable,
Iterator,
List,
Tuple,
Union,
cast,
)
Expand Down Expand Up @@ -411,6 +413,77 @@ def _render(
return show(page, dpi=dpi)


Color = Union[str, Tuple[int, int, int], Tuple[float, float, float]]
"""Type alias for things that can be used as colors."""
Colors = Union[Color, List[Color], Dict[str, Color]]
"""Type alias for colors or collections of colors."""
PillowColor = Union[str, Tuple[int, int, int]]
"""Type alias for things Pillow accepts as colors."""
ColorMaker = Callable[[str], PillowColor]
"""Function that makes a Pillow color for a string label."""
DEFAULT_COLOR_CYCLE: Colors = [
"blue",
"orange",
"green",
"red",
"purple",
"brown",
"pink",
"gray",
"olive",
"cyan",
]
"""Default color cycle (same as matplotlib)"""


def pillow_color(color: Color) -> PillowColor:
"""Convert colors to a form acceptable to Pillow."""
if isinstance(color, str):
return color
r, g, b = color
# Would sure be nice if MyPy understood all()
if isinstance(r, int) and isinstance(g, int) and isinstance(b, int):
return (r, g, b)
r, g, b = (int(x * 255) for x in color)
return (r, g, b)


@functools.singledispatch
def color_maker(spec: Colors, default: Color = "red") -> ColorMaker:
"""Create a function that makes colors."""
return lambda _: pillow_color(default)


@color_maker.register(str)
@color_maker.register(tuple)
def _color_maker_string(spec: Color, default: Color = "red") -> ColorMaker:
return lambda _: pillow_color(spec)


@color_maker.register(dict)
def _color_maker_dict(spec: Dict[str, Color], default: Color = "red") -> ColorMaker:
colors: Dict[str, PillowColor] = {k: pillow_color(v) for k, v in spec.items()}
pdefault: PillowColor = pillow_color(default)

def maker(label: str) -> PillowColor:
return colors.get(label, pdefault)

return maker


@color_maker.register(list)
def _color_maker_list(spec: List[Color], default: Color = "UNUSED") -> ColorMaker:
itor = itertools.cycle(spec)
colors: Dict[str, PillowColor] = {}

def maker(label: str) -> PillowColor:
if label not in colors:
colors[label] = pillow_color(next(itor))
return colors[label]

return maker


def box(
objs: Union[
Annotation,
Expand All @@ -420,9 +493,9 @@ def box(
Iterable[Union[Annotation, ContentObject, Element, Rect]],
],
*,
color: Union[str, Dict[str, str]] = "red",
color: Colors = DEFAULT_COLOR_CYCLE,
label: bool = True,
label_color: str = "white",
label_color: Color = "white",
label_size: float = 9,
label_margin: float = 1,
label_fill: bool = True,
Expand All @@ -437,6 +510,7 @@ def box(
scale = dpi / 72
font = ImageFont.load_default(label_size * scale)
label_margin *= scale
make_color = color_maker(color)
for obj in _make_boxes(objs):
if image is None:
image = _render(obj, page, dpi)
Expand All @@ -445,12 +519,10 @@ def box(
except ValueError: # it has no content and no box
continue
draw = ImageDraw.ImageDraw(image)
obj_color = (
color if isinstance(color, str) else color.get(labelfunc(obj), "red")
)
text = labelfunc(obj)
obj_color = make_color(text)
draw.rectangle((left, top, right, bottom), outline=obj_color)
if label:
text = labelfunc(obj)
tl, tt, tr, tb = font.getbbox(text)
label_box = (
left,
Expand Down Expand Up @@ -482,10 +554,10 @@ def mark(
Iterable[Union[Annotation, ContentObject, Element, Rect]],
],
*,
color: Union[str, Dict[str, str]] = "red",
color: Colors = DEFAULT_COLOR_CYCLE,
transparency: float = 0.75,
label: bool = False,
label_color: str = "white",
label_color: Color = "white",
label_size: float = 9,
label_margin: float = 1,
outline: bool = False,
Expand All @@ -503,6 +575,7 @@ def mark(
font = ImageFont.load_default(label_size * scale)
alpha = min(255, int(transparency * 255))
label_margin *= scale
make_color = color_maker(color)
for obj in _make_boxes(objs):
if image is None:
image = _render(obj, page, dpi)
Expand All @@ -515,17 +588,15 @@ def mark(
except ValueError: # it has no content and no box
continue
draw = ImageDraw.ImageDraw(overlay)
obj_color = (
color if isinstance(color, str) else color.get(labelfunc(obj), "red")
)
text = labelfunc(obj)
obj_color = make_color(text)
draw.rectangle((left, top, right, bottom), fill=obj_color)
mask_draw = ImageDraw.ImageDraw(mask)
mask_draw.rectangle((left, top, right, bottom), fill=alpha)
if outline:
draw.rectangle((left, top, right, bottom), outline="black")
mask_draw.rectangle((left, top, right, bottom), outline=0)
if label:
text = labelfunc(obj)
tl, tt, tr, tb = font.getbbox(text)
label_box = (
left,
Expand Down
12 changes: 12 additions & 0 deletions tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ def test_box():
page = pdf.pages[0]
img = pi.box(page)
assert img
img = pi.box(page, color="red")
assert img
img = pi.box(page, color=["green", "orange", "purple"])
assert img
img = pi.box(page, dpi=100, color={"text": "red", "image": "green"})
assert img


def test_mark():
Expand All @@ -57,3 +63,9 @@ def test_mark():
page = pdf.pages[0]
img = pi.mark(page)
assert img
img = pi.mark(page, color="red")
assert img
img = pi.mark(page, color=["green", "orange", "purple"])
assert img
img = pi.mark(page, dpi=100, color={"text": "red", "image": "green"})
assert img
Loading