diff --git a/examples/image_example.py b/examples/image_example.py index e4fdb344b759aded33faa13b2f7558bf9f297a9a..251429ff26fc43aac32ff76f4769da3c8dac5de2 100644 --- a/examples/image_example.py +++ b/examples/image_example.py @@ -25,7 +25,11 @@ IMGS_AS_LIST = [IMG1, IMG2] # Example for how to use tagplot with image files FIGS_AND_IDS = tagplot( - IMGS_AS_LIST, "image", prefix=PROJECT_ID, id_method="time", location="west" + IMGS_AS_LIST, + "image", + prefix=PROJECT_ID, + id_method="time", + qrcode=True, ) # Required arguments: tagplot(images as list, desired plot engine) diff --git a/examples/matplotlib_example.py b/examples/matplotlib_example.py index db348a5ec21a6f1cdd33bdbcb3e91edfa276636f..e9a57f3e071cb03597fcb7f95821ae8b6c26495b 100644 --- a/examples/matplotlib_example.py +++ b/examples/matplotlib_example.py @@ -40,7 +40,12 @@ FIGS_AS_LIST = [FIG1, FIG2] # Example for how to use tagplot with matplotlib figures FIGS_AND_IDS = tagplot( - FIGS_AS_LIST, "matplotlib", location="west", id_method="random", prefix=PROJECT_ID + FIGS_AS_LIST, + "matplotlib", + location="west", + id_method="random", + prefix=PROJECT_ID, + qrcode=True, ) # Required arguments: tagplot(images as list, desired plot engine) diff --git a/plotid/plotoptions.py b/plotid/plotoptions.py index 8d6abbede22da98257b1fe216ebb017fa2928d1c..274aa48abe92a7640a917741ce6154d870903a1a 100644 --- a/plotid/plotoptions.py +++ b/plotid/plotoptions.py @@ -49,12 +49,27 @@ class PlotOptions: Will be added as prefix to the ID. id_method : str, optional id_method for creating the ID. Create an ID by Unix time is referenced - as 'time', create a random ID with id_method='random'. - The default is 'time'. + as "time", create a random ID with id_method="random". + The default is "time". qrcode : bool, optional - Experimental status. Print qrcode on exported plot. Default: False. + Encode the ID in a QR code on the exported plot. Default: False. + qr_position : tuple, optional + Position of the bottom right corner of the QR Code on the plot in relative + (x, y) coordinates. Default: (1, 0). + qr_size: int or float, optional + Size of the QR code in arbitrary units. Default: 100. id_on_plot: bool, optional Print ID on the plot. Default: True. + font: str, optional + Font that will be used to print the ID. A path to an .otf or a .ttf + file has to be given. To use already installed fonts, you can search for the + standard path of fonts on your operating system and give then the path of the + desired font file to this parameter. + fontsize: int, optional + Fontsize for the displayed ID. Default: 12. + fontcolor: tuple, optional + Fontcolor for the ID. Must be given as tuple containing three rgb values with + each value between [0, 1]. Default: (0, 0, 0). """ def __init__( @@ -72,7 +87,14 @@ class PlotOptions: self.prefix = kwargs.get("prefix", "") self.id_method = kwargs.get("id_method", "time") self.qrcode = kwargs.get("qrcode", False) + self.qr_position = kwargs.get("qr_position", (1, 0)) + self.qr_size = kwargs.get("qr_size", 100) self.id_on_plot = kwargs.get("id_on_plot", True) + self.font = kwargs.get("font", False) + if self.font: + self.font = os.path.abspath(self.font) + self.fontsize = kwargs.get("fontsize", 12) + self.fontcolor = kwargs.get("fontcolor", (0, 0, 0)) def __str__(self) -> str: """Representation if an object of this class is printed.""" diff --git a/plotid/resources/OpenSans/OFL.txt b/plotid/resources/OpenSans/OFL.txt new file mode 100644 index 0000000000000000000000000000000000000000..9b448d4017f9233c21ae6db92c90bc2d229f56b2 --- /dev/null +++ b/plotid/resources/OpenSans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/plotid/resources/OpenSans/OpenSans-Bold.ttf b/plotid/resources/OpenSans/OpenSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a1398b338084cbaf0e99d89cb6089715baa78a6d Binary files /dev/null and b/plotid/resources/OpenSans/OpenSans-Bold.ttf differ diff --git a/plotid/resources/OpenSans/OpenSans-Regular.ttf b/plotid/resources/OpenSans/OpenSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1dc226ddc00a11a9813844b00fb5edaf87921c38 Binary files /dev/null and b/plotid/resources/OpenSans/OpenSans-Regular.ttf differ diff --git a/plotid/resources/__init__.py b/plotid/resources/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/plotid/tagplot.py b/plotid/tagplot.py index 25373320b4c6ed4228b543885b2b9e5164f1d480..f5dd420c5a53854fdb0b62abfa1a0e28724a484d 100644 --- a/plotid/tagplot.py +++ b/plotid/tagplot.py @@ -5,9 +5,6 @@ Tag your plot with an ID. For publishing the tagged plot along your research data have a look at the module publish. - -Functions: - tagplot(figure object, string) -> list """ import warnings @@ -23,7 +20,6 @@ from plotid.tagplot_image import tagplot_image def tagplot( figs: plt.Figure | Image | list[plt.Figure | Image], engine: Literal["matplotlib", "image"], - location: str = "east", **kwargs: Any, ) -> PlotIDTransfer: """ @@ -38,28 +34,54 @@ def tagplot( Figures that should be tagged. engine : Plot engine which should be used to tag the plot. - location : str, optional - Location for ID to be displayed on the plot. Default is 'east'. **kwargs : dict, optional Extra arguments for additional plot options. Other Parameters ---------------- + location : str, optional + Location for ID to be displayed on the plot. Default is "east". + rotation: float or int, optional + Rotation of the printed ID in degree. Overwrites the value defined by location. + position: tuple of float, optional + Position of the ID given as (x,y). x and y are relative coordinates in respect + to the figure size and must be in the intervall [0,1]. Overwrites the value + defined by location. prefix : str, optional Will be added as prefix to the ID. id_method : str, optional id_method for creating the ID. Create an ID by Unix time is referenced - as 'time', create a random ID with id_method='random'. - The default is 'time'. - qrcode : boolean, optional - Experimental support for encoding the ID in a QR Code. + as "time", create a random ID with id_method="random". + The default is "time". + qrcode : bool, optional + Encode the ID in a QR code on the exported plot. Default: False. + qr_position : tuple, optional + Position of the bottom right corner of the QR Code on the plot in relative + (x, y) coordinates. Default: (1, 0). + qr_size: int or float, optional + Size of the QR code in arbitrary units. Default: 100. + id_on_plot: bool, optional + Print ID on the plot. Default: True. + font: str, optional + Font that will be used to print the ID. An absolute path to an .otf or a .ttf + file has to be given. To use already installed fonts, you can search for the + standard path of fonts on your operating system and give then the path of the + desired font file to this parameter. + fontsize: int, optional + Fontsize for the displayed ID. Default: 12. + fontcolor: tuple, optional + Fontcolor for the ID. Must be given as tuple containing three rgb values with + each value between [0, 1]. Default: (0, 0, 0). Raises ------ TypeError If specified location is not given as string. + If rotation was not given as number. + If position was not given as tuple containing two floats. ValueError If an unsupported plot engine is given. + If position tuple does not contain two items. Returns ------- @@ -68,9 +90,8 @@ def tagplot( figures were given. The first list contains the tagged figures. The second list contains the corresponding IDs as strings. """ - if isinstance(location, str): - pass - else: + location = kwargs.get("location", "east") + if not isinstance(location, str): raise TypeError("Location is not a string.") match location: @@ -89,9 +110,6 @@ def tagplot( case "southeast": rotation = 0 position = (0.75, 0.015) - case "custom": - # TODO: Get rotation and position from user input & check if valid - pass case _: warnings.warn( f'Location "{location}" is not a defined ' @@ -101,6 +119,19 @@ def tagplot( rotation = 90 position = (0.975, 0.35) + if "rotation" in kwargs: + rotation = kwargs.pop("rotation") + if not isinstance(rotation, (int, float)): + raise TypeError("Rotation is not a float or integer.") + if "position" in kwargs: + position = kwargs.pop("position") + if not isinstance(position, tuple): + raise TypeError("Position is not a tuple of floats.") + if not len(position) == 2: + raise ValueError("Position does not contain two items.") + if not all(isinstance(item, float) for item in position): + raise TypeError("Position is not a tuple of floats.") + option_container = PlotOptions(figs, rotation, position, **kwargs) option_container.validate_input() diff --git a/plotid/tagplot_image.py b/plotid/tagplot_image.py index ca1b869ceddac31031abc795a6442361877b5122..86a92dce2dd61bae1fd1205b7f9c2d18931d93de 100644 --- a/plotid/tagplot_image.py +++ b/plotid/tagplot_image.py @@ -6,6 +6,8 @@ Functions: tagplot_image(PlotOptions instance) -> PlotIDTransfer instance """ import os +import warnings +from importlib.resources import files from PIL import Image, ImageDraw, ImageFont, ImageOps from plotid.create_id import create_id, create_qrcode from plotid.plotoptions import PlotOptions, PlotIDTransfer @@ -37,11 +39,22 @@ def tagplot_image(plotid_object: PlotOptions) -> PlotIDTransfer: if not isinstance(img, str): raise TypeError("Name of the image is not a string.") if not os.path.isfile(img): - raise TypeError("File does not exist.") + raise TypeError(f"Image '{img}' does not exist.") # Check if figs is a valid file is done by pillow internally - color = (128, 128, 128) # grey - font = ImageFont.load_default() + color = tuple(rgb_value * 255 for rgb_value in plotid_object.fontcolor) + # font = ImageFont.load_default() + font_path = ( + files("plotid.resources").joinpath("OpenSans").joinpath("OpenSans-Regular.ttf") + ) + font = ImageFont.truetype(str(font_path), plotid_object.fontsize) + + if plotid_object.font: + try: + # Absolute path to font file (.ttf or .otf) has to be given + font = ImageFont.truetype(plotid_object.font, plotid_object.fontsize) + except OSError: + warnings.warn("Font was not found.\nplotID continues with fallback font.") for i, img in enumerate(plotid_object.figs): img_id = plotid_object.prefix + create_id(plotid_object.id_method) @@ -64,8 +77,21 @@ def tagplot_image(plotid_object: PlotOptions) -> PlotIDTransfer: if plotid_object.qrcode: qrcode = create_qrcode(img_id) - qrcode.thumbnail((100, 100), Image.ANTIALIAS) - img.paste(qrcode, box=(img.width - 100, img.height - 100)) + qrcode.thumbnail( + (plotid_object.qr_size, plotid_object.qr_size), Image.ANTIALIAS + ) + img.paste( + qrcode, + box=( + int( + img.width * plotid_object.qr_position[0] - plotid_object.qr_size + ), + int( + img.height * (1 - plotid_object.qr_position[1]) + - plotid_object.qr_size + ), + ), + ) plotid_object.figs[i] = img figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) diff --git a/plotid/tagplot_matplotlib.py b/plotid/tagplot_matplotlib.py index 8d83baf82cc44fc2f9495060b99a79002a77a2bc..1da6cce9e058d1586f51047c9db0935b3abf053c 100644 --- a/plotid/tagplot_matplotlib.py +++ b/plotid/tagplot_matplotlib.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Tag your matplotlib plot with an ID.""" +from importlib.resources import files import matplotlib import matplotlib.pyplot as plt from PIL import Image @@ -35,8 +36,18 @@ def tagplot_matplotlib(plotid_object: PlotOptions) -> PlotIDTransfer: if not isinstance(figure, matplotlib.figure.Figure): raise TypeError("Figure is not a valid matplotlib-figure.") - fontsize = "small" - color = "grey" + if plotid_object.font: + # Load custom font into matplotlib + matplotlib.font_manager.fontManager.addfont(plotid_object.font) + font = matplotlib.font_manager.FontProperties(fname=plotid_object.font) + else: + font = ( + files("plotid.resources") + .joinpath("OpenSans") + .joinpath("OpenSans-Regular.ttf") + ) + matplotlib.font_manager.fontManager.addfont(str(font)) + font = matplotlib.font_manager.FontProperties(fname=font) # Loop to create and position the IDs for fig in plotid_object.figs: @@ -46,6 +57,7 @@ def tagplot_matplotlib(plotid_object: PlotOptions) -> PlotIDTransfer: plt.figure(fig) if plotid_object.id_on_plot: + plt.figtext( x=plotid_object.position[0], y=plotid_object.position[1], @@ -53,14 +65,22 @@ def tagplot_matplotlib(plotid_object: PlotOptions) -> PlotIDTransfer: ha="left", wrap=True, rotation=plotid_object.rotation, - fontsize=fontsize, - color=color, + fontsize=plotid_object.fontsize, + color=plotid_object.fontcolor, + font=font, ) if plotid_object.qrcode: qrcode = create_qrcode(fig_id) - qrcode.thumbnail((100, 100), Image.ANTIALIAS) - fig.figimage(qrcode, fig.bbox.xmax - 100, 0, cmap="bone") + qrcode.thumbnail( + (plotid_object.qr_size, plotid_object.qr_size), Image.ANTIALIAS + ) + fig.figimage( + qrcode, + fig.bbox.xmax * plotid_object.qr_position[0] - plotid_object.qr_size, + fig.bbox.ymax * plotid_object.qr_position[1], + cmap="bone", + ) fig.tight_layout() figs_and_ids = PlotIDTransfer(plotid_object.figs, plotid_object.figure_ids) diff --git a/tests/test_plotoptions.py b/tests/test_plotoptions.py index b9ba6504d18b8875fd8c4278190957d6e165e997..8d3f5e1f91a59212a85b050a57a418fdb1e5073a 100644 --- a/tests/test_plotoptions.py +++ b/tests/test_plotoptions.py @@ -48,10 +48,11 @@ class TestTagplot(unittest.TestCase): ) self.assertEqual( str(plot_obj), - "<class 'plotid.plotoptions.PlotOptions'>: {'figs': " - "'FIG', 'figure_ids': [], 'rotation': 270, 'position'" - ": (100, 200), 'prefix': 'xyz', 'id_method': " - "'random', 'qrcode': False, 'id_on_plot': False}", + "<class 'plotid.plotoptions.PlotOptions'>: {'figs': 'FIG', 'figure_ids': " + "[], 'rotation': 270, 'position': (100, 200), 'prefix': 'xyz', 'id_method':" + " 'random', 'qrcode': False, 'qr_position': (1, 0), 'qr_size': 100, " + "'id_on_plot': False, 'font': False, 'fontsize': 12, 'fontcolor': " + "(0, 0, 0)}", ) def test_str_plotidtransfer(self) -> None: diff --git a/tests/test_tagplot.py b/tests/test_tagplot.py index 5754f2b72332adfbd24bd29b10a0144103e77657..22bd8980ccd7ff754ef4877647a0b8d395a3d72a 100644 --- a/tests/test_tagplot.py +++ b/tests/test_tagplot.py @@ -36,8 +36,23 @@ class TestTagplot(unittest.TestCase): Test if tagplot runs successful. """ tagplot(FIGS_AS_LIST, PLOT_ENGINE, prefix=PROJECT_ID, id_method=METHOD) + tagplot(FIGS_AS_LIST, PLOT_ENGINE, rotation=42, position=(0.3, 0.14)) tagplot(IMGS_AS_LIST, "image", location="north") + def test_rotation(self) -> None: + """Test if Error is raised if rotation is not a number.""" + with self.assertRaises(TypeError): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, rotation="42") + + def test_position(self) -> None: + """Test if Error is raised if position is not a tuple containing two numbers.""" + with self.assertRaises(ValueError): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, position=(0.3, 0.14, 5)) + with self.assertRaises(TypeError): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, position=0.3) + with self.assertRaises(TypeError): + tagplot(FIGS_AS_LIST, PLOT_ENGINE, position=(0.3, True)) + def test_prefix(self) -> None: """Test if Error is raised if prefix is not a string.""" with self.assertRaises(TypeError): diff --git a/tests/test_tagplot_image.py b/tests/test_tagplot_image.py index 1dea02dc55d5e0a3adde55f1b1fae25bfaa598c6..727243fada6612c2a87a084646c199f38d10ee01 100644 --- a/tests/test_tagplot_image.py +++ b/tests/test_tagplot_image.py @@ -46,6 +46,9 @@ class TestTagplotImage(unittest.TestCase): prefix=PROJECT_ID, id_method=METHOD, qrcode=True, + fontsize=10, + font="plotid/resources/OpenSans/OpenSans-Bold.ttf", + fontcolor=(0, 1, 0), ) options.validate_input() figs_and_ids = tagplot_image(options) @@ -85,6 +88,13 @@ class TestTagplotImage(unittest.TestCase): with self.assertRaises(TypeError): tagplot_image("wrong_object") + def test_font_file_not_defined(self) -> None: + """Test if a Warning is raised if an invalid font file was specified.""" + options = PlotOptions(IMG1, ROTATION, POSITION, font="font") + options.validate_input() + with self.assertWarns(Warning): + tagplot_image(options) + def tearDown(self) -> None: os.remove(IMG1) os.remove(IMG2) diff --git a/tests/test_tagplot_matplotlib.py b/tests/test_tagplot_matplotlib.py index 2142698b585d91ea194fe68ed6b53f614c04f865..d7e59d9f3825a465614f3179088a8b4d501106d9 100644 --- a/tests/test_tagplot_matplotlib.py +++ b/tests/test_tagplot_matplotlib.py @@ -36,6 +36,9 @@ class TestTagplotMatplotlib(unittest.TestCase): prefix=PROJECT_ID, id_method=METHOD, qrcode=True, + fontsize=10, + font="plotid/resources/OpenSans/OpenSans-Bold.ttf", + fontcolor=(0, 0, 1), ) options.validate_input() figs_and_ids = tagplot_matplotlib(options)