proxyscripts/crop_card.py
2023-07-01 00:49:22 -04:00

217 lines
8.8 KiB
Python

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
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]:
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 - 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_w, cropped_h = cropped_part.width, cropped_part.height
# Draw where cuts should go
cut_pixels = LEAVE_BLEED_INCHES * dpi_by_w
cut_pixels_int = round(cut_pixels)
draw = ImageDraw.Draw(cropped_part)
LENGTH_CUT_GUIDE_INCHES = 0.06
CUT_GUIDE_WIDTH_INCHES = 0.004
guide_len = round(LENGTH_CUT_GUIDE_INCHES * dpi_by_w)
guide_thickness = round(CUT_GUIDE_WIDTH_INCHES * dpi_by_w)
cut_x_l = cut_pixels_int
cut_x_r = cropped_w - cut_pixels_int
cut_y_t = cut_pixels_int
cut_y_b = cropped_h - cut_pixels_int
color = 'magenta'
# Top left
draw.line([(cut_x_l, cut_y_t), (cut_x_l, cut_y_t + guide_len)], fill = color, width = guide_thickness)
draw.line([(cut_x_l, cut_y_t), (cut_x_l + guide_len, cut_y_t)], fill = color, width = guide_thickness)
# Bottom left
draw.line([(cut_x_l, cut_y_b), (cut_x_l, cut_y_b - guide_len)], fill = color, width = guide_thickness)
draw.line([(cut_x_l, cut_y_b), (cut_x_l + guide_len, cut_y_b)], fill = color, width = guide_thickness)
# Top right
draw.line([(cut_x_r, cut_y_t), (cut_x_r, cut_y_t + guide_len)], fill = color, width = guide_thickness)
draw.line([(cut_x_r, cut_y_t), (cut_x_r - guide_len, cut_y_t)], fill = color, width = guide_thickness)
# Bottom right
draw.line([(cut_x_r, cut_y_b), (cut_x_r, cut_y_b - guide_len)], fill = color, width = guide_thickness)
draw.line([(cut_x_r, cut_y_b), (cut_x_r - guide_len, cut_y_b)], fill = color, width = guide_thickness)
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.cropped_image_dest_w = self.in_to_px(self.card_w_inches + LEAVE_BLEED_INCHES)
self.cropped_image_dest_h = self.in_to_px(self.card_h_inches + LEAVE_BLEED_INCHES)
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
def in_to_px(self, inches: float) -> float:
return round(inches * self.dpi)
def topleft_of_card(self, y_index: int, x_index: int) -> Tuple[int, int]:
assert 0 <= x_index < self.cards_per_row
assert 0 <= y_index < self.cards_per_column
x = self.blank_space_w + (self.cropped_image_dest_w + self.spacing) * x_index
y = self.blank_space_h + (self.cropped_image_dest_h + self.spacing) * y_index
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 cards
filenames_iter = cropped_filepaths.__iter__()
for y in range(sheet_layout.cards_per_column):
for x 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(y, x))
# 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()