commit 04cc338691a55631add9803eb6ebd356343cddad Author: Bertrand Benjamin Date: Fri Jun 26 17:05:17 2020 +0200 Feat: import original work diff --git a/README.md b/README.md new file mode 100644 index 0000000..c09f2a0 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# 3ans à Mayotte + +Sources pour faire l'album photo des 3ans passé à Mayotte. + +## Notes + +Crop une image avec le plus grand carré possible au milieu + + convert -gravity center -crop `identify -format "%[fx:min(w,h)]x%[fx:min(w,h)]+0+0" rose:` +repage + + diff --git a/photobook/__init__.py b/photobook/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/photobook/cropping.py b/photobook/cropping.py new file mode 100644 index 0000000..4a05cac --- /dev/null +++ b/photobook/cropping.py @@ -0,0 +1,149 @@ +from PIL import Image, ExifTags +from math import sqrt +from pathlib import Path + +def largest_ratio_box(ratio, img): + """ Return a center rectangle box respecting the ratio inside the image. + + :param ratio: + :param img: the img (from PIL.Image) + :return: (left, upper, right, lower) + """ + try: + ratio[0] + except TypeError: + r = ratio + else: + r = ratio[0]/ratio[1] + + img_ratio = img.size[0] / img.size[1] + if img_ratio > r: + return (int(img.size[0]/2-r*img.size[1]/2), + 0, + int(img.size[0]/2+r*img.size[1]/2), + img.size[1]) + else: + return (0, + int(img.size[1]/2-img.size[0]/(r*2)), + img.size[0], + int(img.size[1]/2+img.size[0]/(r*2))) + +def auto_rotate_exif(img): + """ Rotate the img corresponding to its exifdatas""" + for orientation in ExifTags.TAGS.keys(): + if ExifTags.TAGS[orientation] == 'Orientation': + break + exif = dict(img._getexif().items()) + + if exif[orientation] == 3: + return img.rotate(180, expand=True) + elif exif[orientation] == 6: + return img.rotate(270, expand=True) + elif exif[orientation] == 8: + return img.rotate(90, expand=True) + else: + return img + +def resize_to(img, megapixel=4): + """ Resize the image to an image with total_pixel number of pixels + + :param img: the image + :param megapixel: number of mega pixels + """ + pixels = megapixel * 10**6 + org_ratio = img.size[0]/img.size[1] + new_height = sqrt(pixels/org_ratio) + new_width = org_ratio*new_height + return img.resize((int(new_width), int(new_height))) + +def fit_to(img, ratio, megapixel=4): + """ auto_rotate then crop largest_ratio_box then resize_to + if ration = 0, the image is not cropped + """ + try: + rot = auto_rotate_exif(img) + except (AttributeError, KeyError): + rot = img + if ratio == 0: + cropped = rot + else: + cropped = rot.crop(largest_ratio_box(ratio, rot)) + return resize_to(cropped, megapixel) + +def cut_save(src, dest, ratio=0, megapixel=1): + """ + if no ratio is given, the image is not cropped + """ + img = Image.open(src) + dest_filename = dest / rename(src, ratio) + if not dest_filename.exists(): + print(f"Processing {dest_filename}") + dest_filename.parent.mkdir(parents=True, exist_ok=True) + out = fit_to(img, ratio, megapixel) + out.save(dest_filename) + else: + print(f"{dest_filename} exists skipping") + return dest_filename + +def rename(src_filename, ratio): + p_src = Path(src_filename) + path = p_src.parent + filename = p_src.stem + ext = p_src.suffix + return path / (filename + f"_{int(ratio[0])}by{int(ratio[1])}" + ext) + + +if __name__ == "__main__": + img = Image.open("DSCN0119.JPG") + filename = img.filename.split(".")[0] + print(filename) + print("img.size -> ", img.size) + img = auto_rotate_exif(img) + ratio = (1, 1) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") + + ratio = (2, 1) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") + + ratio = (1, 2) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") + + print("--------------") + + img = Image.open("IMG_2284.JPG") + filename = img.filename.split(".")[0] + print(filename) + print("img.size -> ", img.size) + img = auto_rotate_exif(img) + ratio = (1, 1) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") + + ratio = (2, 1) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") + + ratio = (1, 2) + print(ratio) + cropped = img.crop(largest_ratio_box(ratio, img)) + cropped.save(filename+f"_crop_{ratio[0]}by{ratio[1]}.jpg") + crop_res = resize_to(cropped, 4) + crop_res.save(filename+f"_resi_{ratio[0]}by{ratio[1]}.jpg") diff --git a/photobook/photobook.py b/photobook/photobook.py new file mode 100644 index 0000000..79db986 --- /dev/null +++ b/photobook/photobook.py @@ -0,0 +1,336 @@ +from fpdf import FPDF +from cropping import cut_save +from pathlib import Path + +class Photobook(FPDF): + FIG = Path("./fig/") + OUT = Path("./build/") + + def __init__(self, name, + bg_color = (0, 0, 0), + txt_color = (255, 255, 255), + sep = 3, + pdf_size= (24.447, 20.955), + page_size = (24.13, 20.32), + security_margin_out = 0.635, + security_margin_binding = 1.27, + base_fig = "./fig/", + margin = -1, + img_px=1, + ): + super().__init__("P", "mm", pdf_size) + self.name = name + self.bg_color = bg_color + self.txt_color = txt_color + self.set_fill_color(*self.bg_color) + self.set_text_color(*self.txt_color) + self._sep = sep + + if margin >= 0: + self.t_margin_ori = margin + self.l_margin_ori = margin + self.r_margin_ori = margin + self.restore_margin() + else: + self.t_margin_ori = self.t_margin + self.l_margin_ori = self.l_margin + self.r_margin_ori = self.r_margin + + self._fig_folder = Path('') + self._base_fig = Path(base_fig) + + self.img_px = img_px + + @property + def fig_path(self): + return self._base_fig / self._fig_folder + + def set_fig_folder(self, fig_folder): + self._fig_folder = fig_folder + + def set_sep(self, sep): + self._sep = sep + + def restore_margin(self): + self.set_top_margin(self.t_margin_ori) + self.set_right_margin(self.r_margin_ori) + self.set_left_margin(self.l_margin_ori) + + @property + def sep(self): + return self._sep + + @property + def dest(self): + return self.OUT / (self.name.replace(" ", "_") + ".pdf") + + @property + def fig_src(self): + return self._base_fig / self._fig_folder + #return self.FIG + + @property + def size(self): + return (self.w, self.h) + + @property + def epw(self): + """ Effective page width """ + return self.w - self.r_margin - self.l_margin + + @property + def eph(self): + """ Effective page height """ + return self.h - 2 * self.t_margin + + def add_page(self, orientation='P'): + super().add_page(orientation) + self.rect(0, 0, self.w, self.h, style="F") + + def img_process(self, img, ratio=0): + """ Process the image. + if no ratio is given, the image is not cropped + """ + img_src = self.fig_src / Path(img) + img_dest = str(cut_save(img_src, self.OUT, ratio, self.img_px)) + return img_dest + + def one_fullpage(self, img): + """ Display the picture fullpage """ + self.add_page() + self.append_content(img, 0, 0, *self.size) + # img_dest = self.img_process(img, self.size) + # self.image(img_dest, 0, 0, *self.size) + + def one_centered(self, img, text=""): + """ Display the picture centered with text """ + self.add_page() + + if text == "": + text_size = (0, 0) + else: + text_size = (self.epw, (self.eph-self.sep)/6-self.sep) + + img_size = (self.epw, self.eph - text_size[1]) + img_dest = self.img_process(img, img_size) + + self.image(img_dest, self.l_margin, self.t_margin, *img_size) + + self.set_xy(self.l_margin, self.t_margin+img_size[1]+self.sep) + if '\n' in text: + self.multi_cell(text_size[0], self.font_size, text, align='J', border=1) + else: + self.cell(text_size[0], text_size[1]-self.sep, text, align='C', border=1) + + def one_side(self, img, txt=""): + """ Display the image on the outside of the page + along with text on the other side + """ + self.add_page() + p_no = self.page_no() + win_dim = (self.size[0]*2/3, self.size[1]) + img_dest = self.img_process(img, win_dim) + if p_no % 2 == 1: + self.set_xy(win_dim[0], self.size[1]/2) + self.multi_cell(self.size[0]/3, self.font_size, txt, align="C") + self.image(img_dest, 0, 0, *win_dim) + else: + self.set_xy(0, self.size[1]/2) + self.multi_cell(self.size[0]/3, self.font_size, txt, align="C") + self.image(img_dest, self.size[0]/3, 0, *win_dim) + + def one_side_nocut(self, img, txt=""): + """ Display the image on the outside of the page without resize it + """ + self.add_page() + p_no = self.page_no() + win_dim = (self.size[0], self.size[1]) + img_dest = self.img_process(img, win_dim) + if p_no % 2 == 1: + self.set_xy(win_dim[0], self.size[1]/2) + self.image(img_dest, 0, 0, *win_dim) + else: + self.set_xy(0, self.size[1]/2) + self.image(img_dest, self.size[0]/3, 0, *win_dim) + + def rows(self, imgs, with_margin=True, with_sep=True): + """ Pictures in rows """ + self.add_page() + + if with_margin: + pg_size = (self.epw, self.eph) + top = self.t_margin + left = self.l_margin + else: + pg_size = self.size + top = 0 + left = 0 + + if with_sep: + sep = self.sep + else: + sep = 0 + + img_number = len(imgs) + win_dim = (pg_size[0], + (pg_size[1] - (img_number - 1) * sep) / img_number) + + for img in imgs: + self.append_content(img, left, top, *win_dim) + # img_dest = self.img_process(img, win_dim) + # self.image(img_dest, left, top, *win_dim) + top += win_dim[1] + sep + + def grid_row(self, content, layout=[], with_margin=True, with_sep=True): + """ Custom layout define by rows + + :param content: img or text to display in layout's cells + :param layout: cell layout with weight (need same shape than content) + :param with_margin: Put margins around pictures + :param with_sep: Put separation between pictures + """ + self.add_page() + + if with_margin: + pg_size = (self.epw, self.eph) + ori_top = self.t_margin + ori_left = self.l_margin + else: + pg_size = self.size + ori_top = 0 + ori_left = 0 + + if with_sep: + sep = self.sep + else: + sep = 0 + + if layout == []: + layout = [[1 for c in row] for row in content] + else: + if len(content) != len(layout): + raise ValueError("Content and Layout need to have same number of rows") + for (r, row) in enumerate(content): + if len(row) != len(layout[r]): + raise ValueError(f"Content and Layout need to have same number of columns at row {r}") + + top = ori_top + left = ori_left + height_unit = (pg_size[1] - (len(layout) - 1) * sep) / len(layout) + + for (r, row) in enumerate(layout): + width_unit = (pg_size[0] - (len(row) - 1) * sep) / sum(row) + + for (c, weight) in enumerate(row): + dim = (width_unit * weight, height_unit) + self.append_content(content[r][c], left, top, *dim) + + left += dim[0] + sep + top += height_unit + sep + left = ori_left + + def grid_column(self, content, layout=[], with_margin=True, with_sep=True): + """ Custom layout define by column + + :param content: img or text to display in layout's cells + :param layout: cell layout with weight (need same shape than content) + :param with_margin: Put margins around pictures + :param with_sep: Put separation between pictures + """ + self.add_page() + + if with_margin: + pg_size = (self.epw, self.eph) + ori_top = self.t_margin + ori_left = self.l_margin + else: + pg_size = self.size + ori_top = 0 + ori_left = 0 + + if with_sep: + sep = self.sep + else: + sep = 0 + + if layout == []: + layout = [[1 for r in column] for column in content] + else: + if len(content) != len(layout): + raise ValueError("Content and Layout need to have same number of columns") + for (r, column) in enumerate(content): + if len(column) != len(layout[r]): + raise ValueError(f"Content and Layout need to have same number of columns at column {r}") + + top = ori_top + left = ori_left + width_unit = (pg_size[0] - (len(layout) - 1) * sep) / len(layout) + + for (c, column) in enumerate(layout): + height_unit = (pg_size[1] - (len(column) - 1) * sep) / sum(column) + + for (r, weight) in enumerate(column): + dim = (width_unit, height_unit * weight) + self.append_content(content[c][r], left, top, *dim) + + top += dim[1] + sep + left += width_unit + sep + top = ori_top + + def append_content(self, content, left, top, width, height): + try: + img_dest = self.img_process(content, (width, height)) + self.image(img_dest, left, top, width, height) + except (FileNotFoundError, IsADirectoryError): + self.set_xy(left, top) + if '\n' in content: + self.multi_cell(width, self.font_size, content, align='J', border=1) + else: + self.cell(width, height, content, align='C', border=1) + +if __name__ == "__main__": + name = "annee3" + pagesize = (250, 200) + src_fig = Path("./fig/") + output = Path("./build/") + dest = output / (name + ".pdf") + out_fig = output / "fig" + + photobook = Photobook(name, pdf_size=pagesize) + photobook.set_font('Arial', 'B', 20) + photobook.set_auto_page_break(False) + + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg"]) + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"]) + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], with_margin=False) + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], with_sep=False) + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], with_margin=False, with_sep=False) + photobook.rows(["chronologie/annee3/1-DD/DD-01.jpg", + "Tralalala", + "chronologie/annee3/1-DD/DD-02.jpg", + "chronologie/annee3/1-DD/DD-02.jpg"]) + + photobook.grid_row([["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], + ["Coucou c'est moi!!", "chronologie/annee3/1-DD/DD-04.jpg"]], + [[1, 2], [3, 1]], + ) + photobook.grid_row([["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], + ["Coucou c'est moi!! \ncjfkldsq", "chronologie/annee3/1-DD/DD-04.jpg"]], + [[1, 2], [3, 1]]) + photobook.grid_row([["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], + ["chronologie/annee3/1-DD/DD-03.jpg", "chronologie/annee3/1-DD/DD-04.jpg"]], + [[1, 2], [3, 1]]) + photobook.set_top_margin(40) + photobook.grid_row([["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], + ["chronologie/annee3/1-DD/DD-03.jpg", "chronologie/annee3/1-DD/DD-04.jpg"]], + [[1, 2], [3, 1]],) + photobook.restore_margin() + photobook.grid_row([["chronologie/annee3/1-DD/DD-01.jpg", "chronologie/annee3/1-DD/DD-02.jpg"], + ["chronologie/annee3/1-DD/DD-03.jpg", "chronologie/annee3/1-DD/DD-04.jpg"]], + [[1, 1], [1, 1]], + with_margin=False, with_sep=False + ) + + + photobook.output(dest) + diff --git a/requirement.txt b/requirement.txt new file mode 100644 index 0000000..69ec59f --- /dev/null +++ b/requirement.txt @@ -0,0 +1,16 @@ +backcall==0.1.0 +decorator==4.3.2 +fpdf==1.7.2 +ipython==7.3.0 +ipython-genutils==0.2.0 +jedi==0.13.3 +parso==0.3.4 +pexpect==4.6.0 +pickleshare==0.7.5 +Pillow==5.4.1 +prompt-toolkit==2.0.9 +ptyprocess==0.6.0 +Pygments==2.3.1 +six==1.12.0 +traitlets==4.3.2 +wcwidth==0.1.7 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3949d29 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +try: + from setuptools import setup, find_packages +except ImportError: + from distutils.core import setup + +setup( + name='photobook', + version='1.0', + description='Li', + author='Benjamin Bertrand', + author_email='benjamin.bertrand@opytex.org', + url='http://git.opytex.org/lafrite/photobook.git', + packages=find_packages(), + include_package_data = True, + install_requires=[ + ], + )