import os from pathlib import Path from typing import List, Optional, Tuple from PIL import Image, ImageDraw import util W_INCHES_EXPECTED = 2.72 H_INCHES_EXPECTED = 3.7 BLEED_INCHES_EXPECTED = 0.12 LEAVE_BLEED_INCHES = 0.04 CUT_GUIDE_WIDTH_INCHES = 0.006 LENGTH_CUT_GUIDE_INCHES = 0.06 BASE_DIR = Path(__file__).parent def gather_images(directory: Path, midfix : Optional[str] = None) -> List[Path]: files_to_crop = [] for f in os.listdir(directory): possible_image_path = directory / f if not possible_image_path.is_file(): # Skip directory continue # TODO: Check if is image if midfix is None or any(s == f'.{midfix}' for s in possible_image_path.suffixes): files_to_crop.append(possible_image_path) return files_to_crop def gather_input_cards() -> List[Path]: return gather_images(directory=BASE_DIR / 'input_cards', midfix=None) def gather_cropped_cards() -> List[Path]: return gather_images(directory=BASE_DIR / 'cropped_cards', midfix='crop') def get_inputs_without_crop(all_inputs: List[Path], already_cropped: List[Path]) -> List[Path]: # For debug, always crop return all_inputs already_cropped_set = set(already_cropped) inputs_without_crop = [] for input_filepath in all_inputs: possible_cropped_filepath = input_to_crop_filepath(input_filepath) if possible_cropped_filepath not in already_cropped_set: inputs_without_crop.append(input_filepath) return inputs_without_crop def input_to_crop_filepath(input_filepath: Path) -> Path: input_filename = input_filepath.stem + '.crop' + input_filepath.suffix return BASE_DIR / 'cropped_cards' / input_filename def crop(input_filepath: Path) -> Path: out_dir = BASE_DIR / 'cropped_cards' out_dir.mkdir(parents=True, exist_ok=True) cropped_filepath = input_to_crop_filepath(input_filepath) try: with Image.open(input_filepath) as im: # print(im.info) rgb_im = im.convert('RGB') w, h = rgb_im.width, rgb_im.height dpi_by_w = w / W_INCHES_EXPECTED dpi_by_h = h / H_INCHES_EXPECTED print(f'By width dpi is {dpi_by_w} dpi') if dpi_by_h != dpi_by_w: print(f'dpi by height is different: {dpi_by_h} dpi') crop_pixels = BLEED_INCHES_EXPECTED * dpi_by_w - LEAVE_BLEED_INCHES * dpi_by_w crop_pixels_int = round(crop_pixels) print(f'Image is {w} x {h}, will trim {crop_pixels} pixels (rounded to {crop_pixels_int})') # Crop crop_box = (crop_pixels_int, crop_pixels_int, w - crop_pixels_int, h - crop_pixels_int) cropped_part = rgb_im.crop(crop_box) cropped_part.save(cropped_filepath, 'JPEG', quality=100, subsampling=0) return cropped_filepath except OSError as e: print(e) return None def crop_all_inputs(files_to_crop: List[Path]): num_files_to_crop = len(files_to_crop) for i, input_filepath in enumerate(files_to_crop, 1): print(f'[{i}/{num_files_to_crop}] Cropping {input_filepath}') crop(input_filepath) CARD_W_INCHES = 2.48031 # 63mm CARD_H_INCHES = 3.46457 # 88mm class SheetLayout: def __init__(self, dpi: int, w_inches: float, h_inches: float): self.dpi = dpi self.card_w_inches = CARD_W_INCHES self.card_h_inches = CARD_H_INCHES self.card_w = self.in_to_px(self.card_w_inches) self.card_h = self.in_to_px(self.card_h_inches) self.w_inches = w_inches self.h_inches = h_inches self.w = self.in_to_px(self.w_inches) self.h = self.in_to_px(self.h_inches) self.leave_bleed = self.in_to_px(LEAVE_BLEED_INCHES) self.cropped_image_dest_w = self.card_w + self.leave_bleed * 2 self.cropped_image_dest_h = self.card_h + self.leave_bleed * 2 self.spacing_inches = 0.05 self.spacing = self.in_to_px(self.spacing_inches) self.min_padding_inches = 0.12 self.min_padding = self.in_to_px(self.min_padding_inches) self.cards_per_row = ((self.w - self.min_padding * 2 + self.spacing) // (self.cropped_image_dest_w + self.spacing)) self.cards_per_column = ((self.h - self.min_padding * 2 + self.spacing) // (self.cropped_image_dest_h + self.spacing)) self.cards_per_sheet = self.cards_per_row * self.cards_per_column self.total_w_content = self.cropped_image_dest_w * self.cards_per_row + self.spacing * (self.cards_per_row - 1) self.total_h_content = self.cropped_image_dest_h * self.cards_per_column + self.spacing * (self.cards_per_column - 1) self.blank_space_w = (self.w - self.total_w_content) // 2 self.blank_space_h = (self.h - self.total_h_content) // 2 print(self.__dict__) def in_to_px(self, inches: float) -> float: return round(inches * self.dpi) def top_of_row(self, y_index: int, with_bleed: bool) -> int: assert 0 <= y_index < self.cards_per_column top = self.blank_space_h + (self.cropped_image_dest_h + self.spacing) * y_index if not with_bleed: top += self.leave_bleed return top def bottom_of_row(self, y_index: int, with_bleed: bool) -> int: if with_bleed: return self.top_of_row(y_index, True) + self.cropped_image_dest_h else: return self.top_of_row(y_index, False) + self.card_h def left_of_column(self, x_index: int, with_bleed: bool) -> int: assert 0 <= x_index < self.cards_per_row left = self.blank_space_w + (self.cropped_image_dest_w + self.spacing) * x_index if not with_bleed: left += self.leave_bleed return left def right_of_column(self, x_index: int, with_bleed: bool) -> int: if with_bleed: return self.left_of_column(x_index, True) + self.cropped_image_dest_w else: return self.left_of_column(x_index, False) + self.card_w def topleft_of_card_with_bleed(self, y_index: int, x_index: int) -> Tuple[int, int]: x = self.left_of_column(x_index, with_bleed=True) y = self.top_of_row(y_index, with_bleed=True) return (x, y) def create_sheet(cropped_filepaths: List[Path], sheet_filepath: Path, sheet_layout: SheetLayout): sheet = Image.new('RGB', (sheet_layout.w, sheet_layout.h), 'white') # Draw external guidelines draw = ImageDraw.Draw(sheet) color = 'black' guide_thickness = sheet_layout.in_to_px(CUT_GUIDE_WIDTH_INCHES) for x_index in range(sheet_layout.cards_per_column): left_x = sheet_layout.left_of_column(x_index, with_bleed=False) right_x = sheet_layout.right_of_column(x_index, with_bleed=False) draw.line([(left_x, 0), (left_x, sheet_layout.h)], fill=color, width=guide_thickness) draw.line([(right_x, 0), (right_x, sheet_layout.h)], fill=color, width=guide_thickness) for y_index in range(sheet_layout.cards_per_row): top_y = sheet_layout.top_of_row(y_index, with_bleed=False) bottom_y = sheet_layout.bottom_of_row(y_index, with_bleed=False) draw.line([(0, top_y), (sheet_layout.w, top_y)], fill=color, width=guide_thickness) draw.line([(0, bottom_y), (sheet_layout.w, bottom_y)], fill=color, width=guide_thickness) # Draw cards filenames_iter = cropped_filepaths.__iter__() for y_index in range(sheet_layout.cards_per_column): for x_index in range(sheet_layout.cards_per_row): try: filepath = filenames_iter.__next__() except StopIteration: break with Image.open(filepath) as card_im: card_im = card_im.resize((sheet_layout.cropped_image_dest_w, sheet_layout.cropped_image_dest_h), resample = Image.Resampling.LANCZOS) sheet.paste(card_im, sheet_layout.topleft_of_card_with_bleed(y_index, x_index)) # Draw internal guidelines color = 'magenta' guide_len = sheet_layout.in_to_px(LENGTH_CUT_GUIDE_INCHES) # -1 because the pixels are in [0, W - 1] and [0, H - 1] left_x = sheet_layout.left_of_column(x_index, with_bleed=False) right_x = sheet_layout.right_of_column(x_index, with_bleed=False) - 1 top_y = sheet_layout.top_of_row(y_index, with_bleed=False) bottom_y = sheet_layout.bottom_of_row(y_index, with_bleed=False) - 1 # Top left draw.line([(left_x, top_y), (left_x, top_y + guide_len)], fill = color, width = guide_thickness) draw.line([(left_x, top_y), (left_x + guide_len, top_y)], fill = color, width = guide_thickness) # Bottom left draw.line([(left_x, bottom_y), (left_x, bottom_y - guide_len)], fill = color, width = guide_thickness) draw.line([(left_x, bottom_y), (left_x + guide_len, bottom_y)], fill = color, width = guide_thickness) # Top right draw.line([(right_x, top_y), (right_x, top_y + guide_len)], fill = color, width = guide_thickness) draw.line([(right_x, top_y), (right_x - guide_len, top_y)], fill = color, width = guide_thickness) # Bottom right draw.line([(right_x, bottom_y), (right_x, bottom_y - guide_len)], fill = color, width = guide_thickness) draw.line([(right_x, bottom_y), (right_x - guide_len, bottom_y)], fill = color, width = guide_thickness) # Save sheet sheet.save(sheet_filepath, 'JPEG', dpi=(sheet_layout.dpi, sheet_layout.dpi), quality=100, subsampling=0) def main(): all_input_cards = gather_input_cards() print(f'Found {len(all_input_cards)} input cards:') print(all_input_cards) already_cropped_cards = gather_cropped_cards() print(f'Found {len(already_cropped_cards)} already cropped cards:') print(already_cropped_cards) new_inputs_to_crop = get_inputs_without_crop(all_input_cards, already_cropped_cards) print(f'Found {len(new_inputs_to_crop)} new cards to crop:') print(new_inputs_to_crop) crop_all_inputs(new_inputs_to_crop) all_cropped_cards = gather_cropped_cards() print(f'Now there are {len(all_cropped_cards)} cropped cards:') print(all_cropped_cards) #sheet_layout = SheetLayout(1200, 11, 8.5) sheet_layout = SheetLayout(1200, 8.5, 11) sheets_dir = BASE_DIR / 'sheets' sheets_dir.mkdir(parents=True, exist_ok=True) for i, cards_in_sheet in enumerate(util.batched(all_cropped_cards, sheet_layout.cards_per_sheet), 1): sheet_filepath = sheets_dir / f'sheet{i:03d}.jpg' create_sheet(cards_in_sheet, sheet_filepath, sheet_layout) print(f'Output {sheet_filepath}') if __name__ == '__main__': main()