diff --git a/card_images.py b/card_images.py index 5148250..19781ea 100644 --- a/card_images.py +++ b/card_images.py @@ -3,7 +3,7 @@ from pathlib import Path from PIL import Image from base_proxy import LEAVE_BLEED_INCHES -from sheet_layout import SheetLayout +from sheet_layout import SheetLayout, CardImageTransfer W_INCHES_EXPECTED = 2.72 @@ -14,28 +14,44 @@ BLEED_INCHES_EXPECTED = 0.12 class InputCardImage: def __init__(self, path: Path): self.path = path + self.input_dpi = None + self.original_im = None - def crop(self) -> Image: + def load(self) -> None: with Image.open(self.path) 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') + print(f'Image is {w} x {h}, {dpi_by_w} dpi (by width)') 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})') + print(f'By height dpi is different: {dpi_by_h} dpi') + self.input_dpi = dpi_by_w + self.original_im = im - # Crop - crop_box = (crop_pixels_int, crop_pixels_int, w - crop_pixels_int, h - crop_pixels_int) - cropped_part = rgb_im.crop(crop_box) + def get_cropped_and_resized(self, layout: SheetLayout, transfer: CardImageTransfer) -> Image: + new_dpi = layout.dpi - return cropped_part + self.load() + im = self.original_im + + # Resize to match sheet DPI + new_w = layout.in_to_px(W_INCHES_EXPECTED) + new_h = layout.in_to_px(H_INCHES_EXPECTED) + im = im.resize((new_w, new_h), resample=Image.Resampling.LANCZOS) + + # Crop + top_bleed, bottom_bleed, left_bleed, right_bleed = transfer.bleeds + crop_pixels_leaving_bleed = layout.in_to_px(BLEED_INCHES_EXPECTED - LEAVE_BLEED_INCHES) + crop_pixels_no_bleed = layout.in_to_px(BLEED_INCHES_EXPECTED) + top_crop = crop_pixels_leaving_bleed if top_bleed else crop_pixels_no_bleed + bottom_crop = crop_pixels_leaving_bleed if bottom_bleed else crop_pixels_no_bleed + left_crop = crop_pixels_leaving_bleed if left_bleed else crop_pixels_no_bleed + right_crop = crop_pixels_leaving_bleed if right_bleed else crop_pixels_no_bleed + + # Crop + crop_box = (left_crop, top_crop, new_w - right_crop, new_h - bottom_crop) + im = im.crop(crop_box) - def get_cropped_and_resized(self, sheet_layout: SheetLayout) -> Image: - im = self.crop() - im = im.resize((sheet_layout.cropped_image_dest_w, sheet_layout.cropped_image_dest_h), resample=Image.Resampling.LANCZOS) return im diff --git a/crop_card.py b/crop_card.py index f0a81e5..9f840f8 100644 --- a/crop_card.py +++ b/crop_card.py @@ -8,7 +8,7 @@ from PIL import Image, ImageDraw, ImageCms from base_proxy import BASE_DIR from card_images import InputCardImage import sheet_layout -from sheet_layout import SheetLayout +from sheet_layout import SheetLayout, CardImageTransfer import util @@ -44,16 +44,20 @@ def create_sheet(input_cards: List[InputCardImage], sheet_filepath: Path, layout guide_thickness = layout.in_to_px(CUT_GUIDE_WIDTH_INCHES) for x_index in range(layout.cards_per_column): - left_x = layout.left_of_column(x_index, with_bleed=False) - right_x = layout.right_of_column(x_index, with_bleed=False) - draw.line([(left_x, 0), (left_x, layout.h)], fill=color, width=guide_thickness) - draw.line([(right_x, 0), (right_x, layout.h)], fill=color, width=guide_thickness) + left_x = layout.card_area_left_xs[x_index] + right_x = layout.card_area_right_xs[x_index] + min_y = layout.min_padding + max_y = layout.h - 1 - layout.min_padding + draw.line([(left_x, min_y), (left_x, max_y)], fill=color, width=guide_thickness) + draw.line([(right_x, min_y), (right_x, max_y)], fill=color, width=guide_thickness) for y_index in range(layout.cards_per_row): - top_y = layout.top_of_row(y_index, with_bleed=False) - bottom_y = layout.bottom_of_row(y_index, with_bleed=False) - draw.line([(0, top_y), (layout.w, top_y)], fill=color, width=guide_thickness) - draw.line([(0, bottom_y), (layout.w, bottom_y)], fill=color, width=guide_thickness) + top_y = layout.card_area_top_ys[y_index] + bottom_y = layout.card_area_bottom_ys[y_index] + min_x = layout.min_padding + max_x = layout.w - 1 - layout.min_padding + draw.line([(min_x, top_y), (max_x, top_y)], fill=color, width=guide_thickness) + draw.line([(min_x, bottom_y), (max_x, bottom_y)], fill=color, width=guide_thickness) # Draw cards input_cards_iter = input_cards.__iter__() @@ -64,18 +68,19 @@ def create_sheet(input_cards: List[InputCardImage], sheet_filepath: Path, layout except StopIteration: break - card_im = input_card.get_cropped_and_resized(layout) - sheet.paste(card_im, layout.topleft_of_card_with_bleed(y_index, x_index)) + image_transfer: CardImageTransfer = layout.create_card_image_transfer(y_index, x_index) + card_im = input_card.get_cropped_and_resized(layout, image_transfer) + sheet.paste(card_im, image_transfer.topleft_dest) # Draw internal guidelines color = 'magenta' guide_len = layout.in_to_px(LENGTH_CUT_GUIDE_INCHES) # -1 because the pixels are in [0, W - 1] and [0, H - 1] - left_x = layout.left_of_column(x_index, with_bleed=False) - right_x = layout.right_of_column(x_index, with_bleed=False) - 1 - top_y = layout.top_of_row(y_index, with_bleed=False) - bottom_y = layout.bottom_of_row(y_index, with_bleed=False) - 1 + left_x = layout.card_area_left_xs[x_index] + right_x = layout.card_area_right_xs[x_index] - 1 + top_y = layout.card_area_top_ys[y_index] + bottom_y = layout.card_area_bottom_ys[y_index] - 1 # Top left draw.line([(left_x, top_y), (left_x, top_y + guide_len)], fill = color, width = guide_thickness) diff --git a/sheet_layout.py b/sheet_layout.py index a6c60a2..e3352aa 100644 --- a/sheet_layout.py +++ b/sheet_layout.py @@ -1,10 +1,18 @@ -from typing import Tuple +import dataclasses +import math +from typing import List, Tuple from base_proxy import CARD_W_INCHES, CARD_H_INCHES, LEAVE_BLEED_INCHES +@dataclasses.dataclass +class CardImageTransfer: + topleft_dest: Tuple[int, int] + bleeds: List[int] + + class SheetLayout: - def __init__(self, dpi: int, w_inches: float, h_inches: float): + def __init__(self, dpi: int, w_inches: float, h_inches: float, internal_bleed: bool): self.dpi = dpi self.card_w_inches = CARD_W_INCHES @@ -17,66 +25,129 @@ class SheetLayout: 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_inches = 0.05 if internal_bleed else 0 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 + paste_area_left_xs = [] + paste_area_right_xs = [] + card_area_left_xs = [] + card_area_right_xs = [] + cur_x = 0 + x_index = 0 - 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 + while True: + paste_area_left = cur_x + if x_index == 0 or internal_bleed: + # Left side bleed + card_area_left = cur_x + self.leave_bleed + else: + card_area_left = cur_x + card_area_right = card_area_left + self.card_w + if internal_bleed: + # Right side bleed (added to last card of row in break block) + paste_area_right = card_area_right + self.leave_bleed + else: + paste_area_right = card_area_right + + if paste_area_right >= self.w - 2 * self.min_padding - (self.leave_bleed if not internal_bleed else 0): + if not internal_bleed: + # Right side bleed of previous card + paste_area_right_xs[-1] += self.leave_bleed + + self.cards_per_row = x_index + self.total_w_content = paste_area_right_xs[-1] + + # Center content horizontally + self.blank_space_w = (self.w - self.total_w_content) // 2 + self.paste_area_left_xs = [x + self.blank_space_w for x in paste_area_left_xs] + self.paste_area_right_xs = [x + self.blank_space_w for x in paste_area_right_xs] + self.card_area_left_xs = [x + self.blank_space_w for x in card_area_left_xs] + self.card_area_right_xs = [x + self.blank_space_w for x in card_area_right_xs] + break + + paste_area_left_xs.append(paste_area_left) + paste_area_right_xs.append(paste_area_right) + card_area_left_xs.append(card_area_left) + card_area_right_xs.append(card_area_right) + cur_x = paste_area_right + self.spacing + x_index += 1 + + paste_area_top_ys = [] + paste_area_bottom_ys = [] + card_area_top_ys = [] + card_area_bottom_ys = [] + cur_y = 0 + y_index = 0 + + while True: + paste_area_top = cur_y + if y_index == 0 or internal_bleed: + # Top bleed + card_area_top = cur_y + self.leave_bleed + else: + card_area_top = cur_y + card_area_bottom = card_area_top + self.card_h + if internal_bleed: + # Bottom bleed (added to last card of column in break block) + paste_area_bottom = card_area_bottom + self.leave_bleed + else: + paste_area_bottom = card_area_bottom + + if paste_area_bottom >= self.h - 2 * self.min_padding - (self.leave_bleed if not internal_bleed else 0): + if not internal_bleed: + # Bottom bleed of previous card + paste_area_bottom_ys[-1] += self.leave_bleed + + self.cards_per_column = y_index + self.total_h_content = paste_area_bottom_ys[-1] + + # Center content vertically + self.blank_space_h = (self.h - self.total_h_content) // 2 + self.paste_area_top_ys = [y + self.blank_space_h for y in paste_area_top_ys] + self.paste_area_bottom_ys = [y + self.blank_space_h for y in paste_area_bottom_ys] + self.card_area_top_ys = [y + self.blank_space_h for y in card_area_top_ys] + self.card_area_bottom_ys = [y + self.blank_space_h for y in card_area_bottom_ys] + break + + paste_area_top_ys.append(paste_area_top) + paste_area_bottom_ys.append(paste_area_bottom) + card_area_top_ys.append(card_area_top) + card_area_bottom_ys.append(card_area_bottom) + cur_y = paste_area_bottom + self.spacing + y_index += 1 + + self.cards_per_sheet = self.cards_per_row * self.cards_per_column 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 create_card_image_transfer(self, y_index: int, x_index: int) -> CardImageTransfer: + top_row = y_index == 0 + bottom_row = y_index == self.cards_per_column - 1 + left_col = x_index == 0 + right_col = x_index == self.cards_per_row - 1 - 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 + paste_x = self.paste_area_left_xs[x_index] + paste_y = self.paste_area_top_ys[y_index] - 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) + bleeds = [ + self.leave_bleed if top_row else 0, + self.leave_bleed if bottom_row else 0, + self.leave_bleed if left_col else 0, + self.leave_bleed if right_col else 0] + return CardImageTransfer((paste_x, paste_y), bleeds) def letter_portrait_3x3_sheet_layout() -> SheetLayout: - return SheetLayout(1200, 8.5, 11) + return SheetLayout(1200, 8.5, 11, False) def letter_landscape_2x4_sheet_layout() -> SheetLayout: - return SheetLayout(1200, 11, 8.5) + return SheetLayout(1200, 11, 8.5, False) +