diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4bc34b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2024 Joseph Barbier + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..1e01631 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include gifing/fonts/* \ No newline at end of file diff --git a/OFL.md b/OFL.md new file mode 100644 index 0000000..ff64345 --- /dev/null +++ b/OFL.md @@ -0,0 +1,116 @@ +Copyright (c) _\_, _\ (\)_, +with Reserved Font Name _\_. + +Copyright (c) _\_, _\ (\)_, +with Reserved Font Name _\_. + +Copyright (c) _\_, _\ (\)_. +  + + +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: +https\://openfontlicense.org +  + +\---------------------------------------------------------------------- + +#### 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/README.md b/README.md index ca20cae..c69a3da 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,29 @@ from gifing import Gif path = "tests/img" +file_path = [f"{path}/image{i}.jpg" for i in range(1, 4)] gif = Gif( - [f"{path}/image{i}.jpg" for i in range(1, 4)], + file_path, frame_duration=800, n_repeat_last_frame=3, ) +gif.set_labels( + ["print", "hello", "world"], font_size=60, padding=20, loc="bottom right" +) gif.set_background_color("red") -gif.set_size((900, 600)) -gif.make() +gif.set_size((900, 700), scale=1) +gif.make("img/output.gif") ``` ![](img/output.gif) +This package offers: + +- a streamlined approach to creating GIFs +- automatic image resizing +- ability to set a background color during resizing +- frame-by-frame label customization +
### Installation @@ -64,3 +75,10 @@ gif.make() ``` By default, the GIF will be saved as `./output.gif`. You can customize the output path by passing it as an argument to the `make()` method. + +

+ +## License + +- The source code in this repository is licensed under the [MIT License](./LICENSE). +- The included fonts are licensed under the [SIL Open Font License, Version 1.1](./OFL.md). diff --git a/gifing/fonts/Urbanist-Bold.ttf b/gifing/fonts/Urbanist-Bold.ttf new file mode 100644 index 0000000..0063afc Binary files /dev/null and b/gifing/fonts/Urbanist-Bold.ttf differ diff --git a/gifing/main.py b/gifing/main.py index 2e02f41..fd3bbf0 100644 --- a/gifing/main.py +++ b/gifing/main.py @@ -1,8 +1,9 @@ import numpy as np import imageio -from PIL import Image, ImageFile +from PIL import Image, ImageFile, ImageDraw, ImageFont from PIL.Image import Resampling -from typing import Union, Tuple, List +from typing import Union, Tuple, List, Optional +import importlib.resources as pkg_resources import warnings from .utils.colors import _strcolor_to_rgb @@ -18,17 +19,19 @@ def __init__( """ Initialize the GIF maker. - :param file_path: List of file paths to the images to be included in the GIF. - :param frame_duration: Duration of each frame in milliseconds. - :param n_repeat_last_frame: The number of additional frames to append with the last image. + Parameters + - file_path: List of file paths to the images to be included in the GIF. + - frame_duration: Duration of each frame in milliseconds. + - n_repeat_last_frame: The number of additional frames to append with the last image. """ self.file_path = file_path self.frame_duration = frame_duration self.n_repeat_last_frame = n_repeat_last_frame - self.size = (1000, 1000) + self.labels = None + self.size = (500, 500) self.scale = 1 self.background_color = (255, 255, 255) - + self.label_loc = "top left" # Default location ImageFile.LOAD_TRUNCATED_IMAGES = True def set_size( @@ -37,8 +40,11 @@ def set_size( scale: int = 1, ): """ - :param size: The size of the output GIF (width, height) in pixels. - :param scale: Scaling factor to adjust the size of the images in the GIF. + Set the size (width, height) and scale (default 1) of each image. + + Parameters + - size: The size of the output GIF (width, height) in pixels. + - scale: Scaling factor to adjust the size of the images in the GIF. """ self.size = size self.scale = scale @@ -48,24 +54,51 @@ def set_background_color( background_color: Union[str, Tuple[int, int, int]], ) -> None: """ - :param background_color: The RGB color or string name of the background for each frame. + Set the background color to use when images have different size. + + Parameters + - background_color: The RGB color, hex color or string name of the background for each frame. Default is (255, 255, 255) (white). Strings can be names of colors such as "white", "black", "red", "green", "blue", "yellow", "cyan", "magenta", "gray", "orange", "purple" or "pink". - :returns: None """ if isinstance(background_color, str): background_color = _strcolor_to_rgb(background_color) self.background_color = background_color + def set_labels( + self, + labels: List[str], + font_size: int = 20, + padding: int = 10, + loc: str = "top left", + ) -> None: + """ + Set labels for specific frames in the GIF. + + Parameters + - labels: a list of strings of the same length as the number of images. + - font_size: size of the font in pixels. + - padding: text padding in pixels + - loc: one of "top left", "top right", "bottom left", "bottom right". + """ + if loc not in ["top left", "top right", "bottom left", "bottom right"]: + raise ValueError( + "Invalid loc. Choose from: " + "'top left', 'top right', 'bottom left', 'bottom right'." + ) + self.labels = labels + self.padding = padding + self.font_size = font_size + self.label_loc = loc + def make( self, output_path: str = "./output.gif", ) -> None: """ - Creates and saves a GIF. + Make (and save) a GIF. - :param output_path: Path where the output GIF will be saved. Default is "./output.gif". - :returns: None + - output_path: path where the output GIF will be saved. """ self.output_path = output_path @@ -77,9 +110,11 @@ def make( warnings.warn("The output path does not have a '.gif' extension.") self.output_path += ".gif" - for filename in self.file_path: + for i, filename in enumerate(self.file_path): with Image.open(filename) as img: img = self._format_image(img) + if self.labels is not None: + self._draw_label(img, frame_idx=i) img_array = np.array(img) images_for_gif.append(img_array) @@ -99,14 +134,16 @@ def make( print(f"GIF created and saved at {output_path}") def get_images(self) -> List: + """ + Retrieve a list with all the images + """ return self.images_for_gif def _format_image(self, image): - """ - :param image: The image to be resized and placed on a background. - """ img_w, img_h = image.size bg_w, bg_h = self.size + self.bg_w = bg_w + self.bg_h = bg_h scale = min(bg_w / img_w, bg_h / img_h) new_w = int(img_w * scale) new_h = int(img_h * scale) @@ -114,3 +151,30 @@ def _format_image(self, image): new_image = Image.new("RGB", self.size, self.background_color) new_image.paste(image, ((bg_w - new_w) // 2, (bg_h - new_h) // 2)) return new_image + + def _draw_label(self, img, frame_idx: Optional[int] = None) -> None: + label_text = self.labels[frame_idx] + draw = ImageDraw.Draw(img) + with pkg_resources.path("gifing.fonts", "Urbanist-Bold.ttf") as font_path: + font = ImageFont.truetype(font_path, self.font_size) + + text_bbox = draw.textbbox( + (0, 0), label_text, font=font, font_size=self.font_size + ) + text_width = text_bbox[2] - text_bbox[0] + text_height = text_bbox[3] - text_bbox[1] + + if self.label_loc == "top left": + x = self.padding + y = self.padding + elif self.label_loc == "top right": + x = self.bg_w - text_width - self.padding + y = self.padding + elif self.label_loc == "bottom left": + x = self.padding + y = self.bg_h - text_height - self.padding + elif self.label_loc == "bottom right": + x = self.bg_w - text_width - self.padding + y = self.bg_h - text_height - self.padding + + draw.text((x, y), label_text, font=font, fill=(0, 0, 0)) diff --git a/img/output.gif b/img/output.gif index e1a8964..8533ed6 100644 Binary files a/img/output.gif and b/img/output.gif differ diff --git a/pyproject.toml b/pyproject.toml index d3a334a..f805b7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)", "Operating System :: OS Independent", "Framework :: Matplotlib" ] @@ -18,11 +19,8 @@ dependencies = [ "numpy" ] -[tool.pytest.ini_options] -filterwarnings = [ - "error", - "ignore::UserWarning", -] +[tool.setuptools] +license_files = ["LICENSE", "OFL.md"] [build-system] requires = ["setuptools"] diff --git a/req.py b/req.py index 56c79c1..460f9d9 100644 --- a/req.py +++ b/req.py @@ -1,6 +1,6 @@ import subprocess -packages = ["imageio", "numpy"] +packages = ["imageio", "numpy", "Pillow"] REQUIREMENTS_FILE = "requirements.txt" diff --git a/requirements.txt b/requirements.txt index 06c3f3e..a6f6294 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ imageio==2.36.1 numpy==2.1.3 +Pillow==11.0.0 diff --git a/tests/test_get_images.py b/tests/test_get_images.py index ffc132e..5978f24 100644 --- a/tests/test_get_images.py +++ b/tests/test_get_images.py @@ -1,3 +1,4 @@ +import os from gifing import Gif @@ -11,7 +12,8 @@ def test_get_images(): ) gif.set_size((500, 500), scale=2) gif.set_background_color("yellow") - gif.make(output_path="test_output.gif") + gif.make("test_output.gif") + os.remove(gif.output_path) images = gif.get_images() assert len(images) == 4