groueix commited on
Commit
255a121
Β·
1 Parent(s): 24546c1

looks better, can still be improved

Browse files
Files changed (3) hide show
  1. copaint/cli.py +45 -119
  2. copaint/copaint.py +99 -68
  3. copaint/gradio_ui.py +62 -46
copaint/cli.py CHANGED
@@ -1,93 +1,54 @@
 
 
1
  import argparse
2
- from copaint.copaint import image_to_pdf
3
- # from copaint import image_to_copaint_pdf
4
- from PIL import Image
5
  import numpy as np
6
- import random
7
- actual_participants_over_participants = 0.7 # how many people actually show up compared to the number of participants
8
 
9
- # Default list of identifiers
 
 
 
 
 
 
 
 
 
 
10
  default_identifiers = [
11
- "sunshine",
12
- "bliss",
13
- "smile",
14
- "serenity",
15
- "laughter",
16
- "breeze",
17
- "harmony",
18
- "glee",
19
- "cheer",
20
- "delight",
21
- "hope",
22
- "sparkle",
23
- "kindness",
24
- "charm",
25
- "grace",
26
- "radiance",
27
- "jubilee",
28
- "flutter",
29
- "playful",
30
- "whimsy",
31
- "gleam",
32
- "glow",
33
- "twinkle",
34
- "love",
35
- "joy",
36
- "peace",
37
- "cheeky",
38
- "amity",
39
- "blissful",
40
- "grateful"
41
  ]
42
 
43
- # Function to get a default identifier (random or specified)
44
- def default_identifier(index=None):
45
- """
46
- Get a default identifier from the list.
47
-
48
- Args:
49
- index: Optional index to get a specific identifier.
50
- If None, returns a random identifier.
51
-
52
- Returns:
53
- str: A default identifier
54
- """
55
- if index is not None and 0 <= index < len(default_identifiers):
56
- return default_identifiers[index]
57
- return random.choice(default_identifiers)
58
 
59
  def get_grid_size(nparticipants, input_image):
60
- """ Takes the number of participants and the input image and returns the grid size, with the objective of making each cell as square as possible."""
61
- # get the dimensions of the input image, load with PIL
62
- input_image = Image.open(input_image)
63
- w, h = input_image.size
64
- n_cell = nparticipants * actual_participants_over_participants
 
 
65
  aspect_ratio = w/h
66
- h_cells = np.sqrt(aspect_ratio * n_cell)
67
- w_cells = aspect_ratio * h_cells
68
-
69
- """
70
- We have the following equations:
71
- (1) w_cells/h_cells = aspect_ratio (as close as possible up to w_cells and h_cells being integers)
72
- (2) h_cells * w_cells = n_cell
73
-
74
- Solving to
75
- (1) w_cells = aspect_ratio * h_cells
76
- # replace in (2)
77
- (2) h_cells * aspect_ratio * h_cells = n_cell
78
- Leads to (3) h_cells^2 * aspect_ratio = n_cell
79
- Leads to h_cells = sqrt(n_cell / aspect_ratio)
80
- """
81
- h_cells = np.round(np.sqrt(n_cell / aspect_ratio))
82
- w_cells = np.round(aspect_ratio * h_cells)
83
-
84
- # convert to integers
85
- h_cells = int(h_cells)
86
- w_cells = int(w_cells)
87
 
 
 
 
 
 
 
 
 
 
88
 
89
- print(f"Using {h_cells} x {w_cells} = {h_cells*w_cells} grid size for a canvas of size {h}*{w} and {nparticipants} participants (actual {n_cell})")
90
- return int(h_cells), int(w_cells)
 
 
 
91
 
92
  def main():
93
  parser = argparse.ArgumentParser(description='CoPaint')
@@ -105,46 +66,19 @@ def main():
105
  parser.add_argument('--min_cell_size_in_cm', type=int, default=2, help='minimum size of cells in cm')
106
  parser.add_argument('--debug', action='store_true', help='debug mode')
107
 
108
- # done adding arguments
109
  args = parser.parse_args()
110
 
 
 
111
 
112
  if args.unique_identifier is None:
113
  # select one at random
114
  idx = np.random.choice(len(default_identifiers), 1)
115
  args.unique_identifier = default_identifiers[idx[0]]
116
-
117
- presets = [
118
- [2, 3], # 6 people
119
- [3, 3], # 9 people
120
- [3, 4], # 12 people
121
- [4, 4], # 16 people
122
- [4, 5], # 20 people
123
- [4, 6], # 24 people
124
- [5, 6], # 30 people
125
- [6, 8], # 48 people
126
- [7, 9], # 63 people
127
- [7, 10], # 70 people
128
- [8, 10], # 80 people
129
- [8, 12], # 96 people
130
- ]
131
- preset_number_of_guests = [presets[i][0]*presets[i][1] for i in range(len(presets))]
132
-
133
- # generate all presets
134
- if args.use_presets:
135
- # disregard other parameters and use the presets
136
- # assert other parameters are not set
137
- assert(args.h_cells is None), "When using presets, the number of H cells can't be set"
138
- assert(args.w_cells is None), "When using presets, the number of W cells can't be set"
139
- assert(args.nparticipants is None), "When using presets, the number of participants can't be set"
140
-
141
- for preset in presets:
142
- image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, preset[0], preset[1],
143
- unique_identifier=args.unique_identifier, cell_size_in_cm=args.cell_size_in_cm,
144
- a4=args.a4, high_res=args.high_res, min_cell_size_in_cm=args.min_cell_size_in_cm, debug=args.debug)
145
 
146
  # generate a copaint pdf based on the number of participants
147
- elif args.nparticipants:
148
  # assert other parameters are not set
149
  assert(args.h_cells is None), "When choosing via number of participants, the number of H cells can't be set"
150
  assert(args.w_cells is None ), "When choosing via number of participants, the number of W cells can't be set"
@@ -154,20 +88,12 @@ def main():
154
  image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, h_cells, w_cells,
155
  unique_identifier=args.unique_identifier, cell_size_in_cm=args.cell_size_in_cm,
156
  a4=args.a4, high_res=args.high_res, min_cell_size_in_cm=args.min_cell_size_in_cm, debug=args.debug)
157
-
158
- # # Depracated find the first preset that can accomodate the number of participants
159
- # preset_number_of_guests_inflated_by_losers = actual_participants_over_participants*args.nparticipants
160
- # for i, preset in enumerate(presets):
161
- # if preset_number_of_guests_inflated_by_losers <= preset_number_of_guests[i]:
162
- # print(f"Using preset {preset} for {args.nparticipants} participants")
163
- # image_to_copaint_pdf(args.input_image, args.copaint_logo, args.outputfolder, preset[0], preset[1])
164
- # break
165
 
166
  # Generate the copaint pdf using the specified number of cells
167
  else:
168
  image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, args.h_cells, args.w_cells,
169
  unique_identifier=args.unique_identifier, cell_size_in_cm=args.cell_size_in_cm,
170
  a4=args.a4, high_res=args.high_res, min_cell_size_in_cm=args.min_cell_size_in_cm, debug=args.debug)
171
-
172
  if __name__ == '__main__':
173
  main()
 
1
+ """Command line interface for Copaint pdf generator.
2
+ """
3
  import argparse
 
 
 
4
  import numpy as np
5
+ import logging
 
6
 
7
+ from copaint.copaint import image_to_pdf
8
+
9
+ # Configure logging
10
+ logging.basicConfig(
11
+ level=logging.INFO,
12
+ format='%(asctime)s - %(levelname)s - %(message)s',
13
+ datefmt='%Y-%m-%d %H:%M:%S'
14
+ )
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Default identifiers for unique_identifier parameter
18
  default_identifiers = [
19
+ "Mauricette", "Gertrude", "Bernadette", "Henriette", "Georgette", "Antoinette", "Colette", "Suzette", "Yvette", "Paulette",
20
+ "Juliette", "Odette", "Marinette", "Lucette", "Pierrette", "Cosette", "Rosette", "Claudette", "Violette", "Josette"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  ]
22
 
23
+ def default_identifier():
24
+ """Return a random default identifier."""
25
+ return np.random.choice(default_identifiers)
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  def get_grid_size(nparticipants, input_image):
28
+ """Get grid size based on number of participants."""
29
+ # Compute the grid size based on the number of participants
30
+ # We want to find h_cells and w_cells such that h_cells * w_cells >= nparticipants
31
+ # and h_cells/w_cells is close to the aspect ratio of the image
32
+ from PIL import Image
33
+ image = Image.open(input_image)
34
+ w, h = image.size
35
  aspect_ratio = w/h
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # Find the smallest grid that can accommodate nparticipants
38
+ min_cells = int(np.ceil(np.sqrt(nparticipants)))
39
+ for i in range(min_cells, min_cells+10):
40
+ for j in range(min_cells, min_cells+10):
41
+ if i*j >= nparticipants:
42
+ ratio = j/i
43
+ if abs(ratio - aspect_ratio) < 0.2:
44
+ logger.info(f"Found grid size {i}x{j} for {nparticipants} participants")
45
+ return i, j
46
 
47
+ # If no good ratio is found, just use the smallest grid that can accommodate nparticipants
48
+ h_cells = min_cells
49
+ w_cells = int(np.ceil(nparticipants/min_cells))
50
+ logger.warning(f"No good aspect ratio found. Using {h_cells}x{w_cells} grid for {nparticipants} participants")
51
+ return h_cells, w_cells
52
 
53
  def main():
54
  parser = argparse.ArgumentParser(description='CoPaint')
 
66
  parser.add_argument('--min_cell_size_in_cm', type=int, default=2, help='minimum size of cells in cm')
67
  parser.add_argument('--debug', action='store_true', help='debug mode')
68
 
 
69
  args = parser.parse_args()
70
 
71
+ if args.debug:
72
+ logger.setLevel(logging.DEBUG)
73
 
74
  if args.unique_identifier is None:
75
  # select one at random
76
  idx = np.random.choice(len(default_identifiers), 1)
77
  args.unique_identifier = default_identifiers[idx[0]]
78
+ logger.info(f"Using random identifier: {args.unique_identifier}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
  # generate a copaint pdf based on the number of participants
81
+ if args.nparticipants:
82
  # assert other parameters are not set
83
  assert(args.h_cells is None), "When choosing via number of participants, the number of H cells can't be set"
84
  assert(args.w_cells is None ), "When choosing via number of participants, the number of W cells can't be set"
 
88
  image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, h_cells, w_cells,
89
  unique_identifier=args.unique_identifier, cell_size_in_cm=args.cell_size_in_cm,
90
  a4=args.a4, high_res=args.high_res, min_cell_size_in_cm=args.min_cell_size_in_cm, debug=args.debug)
 
 
 
 
 
 
 
 
91
 
92
  # Generate the copaint pdf using the specified number of cells
93
  else:
94
  image_to_pdf(args.input_image, args.copaint_logo, args.outputfolder, args.h_cells, args.w_cells,
95
  unique_identifier=args.unique_identifier, cell_size_in_cm=args.cell_size_in_cm,
96
  a4=args.a4, high_res=args.high_res, min_cell_size_in_cm=args.min_cell_size_in_cm, debug=args.debug)
97
+
98
  if __name__ == '__main__':
99
  main()
copaint/copaint.py CHANGED
@@ -25,16 +25,32 @@ from reportlab.pdfgen import canvas
25
  from reportlab.lib.pagesizes import letter, A4
26
  from reportlab.lib.units import inch
27
  import PyPDF2
 
28
 
29
  from functools import lru_cache
30
  from matplotlib import font_manager
31
 
32
  from PIL import Image, ImageDraw, ImageFont
 
 
 
 
 
 
 
 
 
 
 
 
33
  Image.MAX_IMAGE_PIXELS = None # Removes the limit entirely
34
  fromPIltoTensor = torchvision.transforms.ToTensor()
35
  fromTensortoPIL = torchvision.transforms.ToPILImage()
36
 
37
 
 
 
 
38
  @lru_cache(maxsize=1)
39
  def get_font(debug=False) -> str:
40
  """
@@ -45,7 +61,7 @@ def get_font(debug=False) -> str:
45
  font_paths = ["/System/Library/Fonts/Avenir Next.ttc"]
46
  for font_path in font_paths:
47
  if os.path.exists(font_path):
48
- print(f"Found '{font_path}' font")
49
  return font_path
50
 
51
  available_fonts = font_manager.findSystemFonts(fontpaths=None, fontext='ttf')
@@ -53,16 +69,16 @@ def get_font(debug=False) -> str:
53
  for good_font in good_font_options:
54
  font_path = next((font for font in available_fonts if good_font in font), None)
55
  if font_path:
56
- print(f"Found '{good_font}' font: {font_path}")
57
  break
58
 
59
  if font_path is None:
60
  font_path = available_fonts[0]
61
- print(f"No good fonts found. Using default: {font_path}")
62
- print("Please install one of the recommended fonts.")
63
 
64
  if debug:
65
- print(f"Font loading took {time.time() - start_time:.4f} seconds")
66
  return font_path
67
 
68
  font_path = get_font()
@@ -82,8 +98,6 @@ def load_image(image_path, debug=False):
82
  png_data = cairosvg.svg2png(file_obj=svg_file)
83
  # Load the PNG data into a Pillow Image
84
  image = Image.open(io.BytesIO(png_data))
85
- # display
86
- # image.show()
87
  else:
88
  image = Image.open(image_path)
89
  # convert to RGBA
@@ -95,32 +109,30 @@ def load_image(image_path, debug=False):
95
 
96
  image_open_time = time.time()
97
  if debug:
98
- print(f"Image opening took {image_open_time - start_time:.4f} seconds")
99
 
100
  image = fromPIltoTensor(image).unsqueeze(0)
101
- print(f"Loaded image of shape {image.shape}, from {image_path}")
102
- # resize to low res for testing
103
- # image = torch.nn.functional.interpolate(image, size=1000)
104
 
105
  if debug:
106
- print(f"Image to tensor conversion took {time.time() - image_open_time:.4f} seconds")
107
- print(f"Total image loading took {time.time() - start_time:.4f} seconds")
108
  return image
109
 
110
 
111
  def save_image(tensor, image_path, debug=False):
112
  """ Save a tensor to an image file. """
113
  start_time = time.time()
114
- print(f"Saving image of shape {tensor.shape} to {image_path}")
115
  image = fromTensortoPIL(tensor.squeeze(0))
116
  conversion_time = time.time()
117
  if debug:
118
- print(f"Tensor to PIL conversion took {conversion_time - start_time:.4f} seconds")
119
 
120
  image.save(image_path)
121
  if debug:
122
- print(f"Image saving took {time.time() - conversion_time:.4f} seconds")
123
- print(f"Total save_image took {time.time() - start_time:.4f} seconds")
124
 
125
 
126
  def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_side_in_cm=None, a4=False, high_res=False, scale=None, debug=False):
@@ -128,13 +140,14 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
128
  Save a tensor to a PDF, the tensor is assumed to be a single image, and is centered on the page.
129
  """
130
  start_time = time.time()
 
131
  image = fromTensortoPIL(tensor.squeeze(0))
132
  img_width, img_height = image.size
133
  # 1 Inch = 72 Points : ad-hoc metric used in typography and the printing industry.
134
  # The US Letter format is US Letter size: 8.5 by 11 inches
135
  W, H = 8.5, 11 # the unit is inch
136
  if a4:
137
- print("Using A4 format")
138
  W, H = 8.27, 11.69 # the unit is inch
139
 
140
  page_width_in_pt = (W - 2*margin) * inch
@@ -149,12 +162,12 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
149
  assert(img_small_side_in_pt < page_width_in_pt and img_large_side_in_pt < page_height_in_pt), f"Cell size in cm is too large for the page, max in pt unit is {page_width_in_pt}x{page_height_in_pt}, got {img_small_side_in_pt}x{img_large_side_in_pt}. It looks you manually set the size of the cell in cm, but the image is too large for the page, try a smaller cell size."
150
 
151
 
152
- print(f"Saving tensor of shape {tensor.shape} to {pdf_path}")
153
 
154
  # Convert tensor to image
155
  t1 = time.time()
156
  if debug:
157
- print(f"Tensor to PIL conversion took {time.time() - t1:.4f} seconds")
158
 
159
  t2 = time.time()
160
  # Check if image should be rotated
@@ -163,9 +176,9 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
163
  scale_1, rotated = scale
164
 
165
  if image.width > image.height and rotated:
166
- print("Rotating image. Size in pixels: ", image.width, image.height)
167
  image = image.rotate(90, expand=True)
168
- print("Rotated image. Size in pixels: ", image.width, image.height)
169
  img_width, img_height = image.width, image.height
170
  rotated = True
171
  else:
@@ -190,7 +203,7 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
190
  x_offset = (page_width_in_pt - new_width) // 2
191
  y_offset = (page_height_in_pt - new_height) // 2
192
  if debug:
193
- print(f"Image calculations took {time.time() - t2:.4f} seconds")
194
 
195
  # Save image to PDF
196
  t3 = time.time()
@@ -198,7 +211,7 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
198
  image_path = "temp.png" if high_res else "temp.jpg"
199
  image.save(image_path)
200
  if debug:
201
- print(f"Temporary image saving took {time.time() - t3:.4f} seconds")
202
 
203
  # Create a PDF
204
  t4 = time.time()
@@ -209,11 +222,11 @@ def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_s
209
  c.drawImage(image_path, x_offset+margin*inch, y_offset+margin*inch, width=new_width, height=new_height, preserveAspectRatio=True)
210
  c.save()
211
  if debug:
212
- print(f"PDF creation took {time.time() - t4:.4f} seconds")
213
 
214
  os.remove(image_path)
215
  if debug:
216
- print(f"Total PDF saving took {time.time() - start_time:.4f} seconds")
217
  return pdf_path, (scale_1, rotated)
218
 
219
 
@@ -226,7 +239,7 @@ def merge_pdf_list(pdfs, output_path, debug=False):
226
  merger.write(output_path)
227
  merger.close()
228
  if debug:
229
- print(f"PDF merging took {time.time() - start_time:.4f} seconds")
230
  return output_path
231
 
232
 
@@ -273,7 +286,7 @@ def create_image_with_text(text: str = "1", size: int = 400, underline: bool = T
273
  tensor = fromPIltoTensor(image).unsqueeze(0)
274
 
275
  if debug and len(text) <= 2: # Only log for short texts (cell numbers) when debugging
276
- print(f"Creating image with text '{text}' took {time.time() - start_time:.4f} seconds")
277
  return tensor
278
 
279
 
@@ -284,7 +297,7 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
284
  The logo is in each cell, with the cell number underlined
285
  logo_image : tensor of size 1x3xhxw
286
  """
287
- print(f"Creating back image of size {h}x{w} for {h_cells}x{w_cells} cells")
288
  start_time = time.time()
289
  num_channels = 3 # do not consider the alpha channel
290
  back_image = torch.ones(1, num_channels, h, w)
@@ -298,8 +311,8 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
298
  number_size = min(cell_h, cell_w) // 2
299
 
300
  if debug:
301
- print(f"thickness of the lines: {line_thickness}")
302
- print(f"Initialization took {time.time() - start_time:.4f} seconds")
303
 
304
  # Create the grid lines
305
  grid_start_time = time.time()
@@ -320,7 +333,7 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
320
  if w1 - line_half_thickness > 0:
321
  back_image[:, :num_channels, :, (w1-line_half_thickness):w1] = 0
322
  if debug:
323
- print(f"Creating grid lines took {time.time() - grid_start_time:.4f} seconds")
324
 
325
  # Resize logo for all cells
326
  logo_resize_time = time.time()
@@ -335,7 +348,7 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
335
  new_h_insta, new_w_insta = int(h_insta * scale_insta), int(w_insta * scale_insta)
336
  logo_insta_image_resized = torch.nn.functional.interpolate(logo_insta_image, size=(new_h_insta, new_w_insta), mode='bilinear')
337
  if debug:
338
- print(f"Logo resizing took {time.time() - logo_resize_time:.4f} seconds ({time.time() - t_insta:.4f} for insta and {t_insta - logo_resize_time:.4f} for copaint logo)")
339
  # save logo_insta_image_resized
340
  save_image(logo_insta_image_resized, "logo_insta_image_resized.png", debug=debug)
341
  # Add content to cells
@@ -343,6 +356,12 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
343
  letscopaint = create_image_with_text("copaint_art", underline=False,
344
  size=(int(0.8*number_size), number_size//8),
345
  debug=debug)
 
 
 
 
 
 
346
  for i in range(h_cells):
347
  for j in range(w_cells):
348
  h0 = i * cell_h # height start
@@ -357,7 +376,7 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
357
  # add cell number at the center of the cell
358
  # invert cell number to match the order of the canvas. 1 is at the top right, and w_cells is at the top left
359
  if list_of_cell_idx is not None:
360
- print(f"list_of_cell_idx: {list_of_cell_idx}")
361
  if list_of_cell_idx is not None:
362
  cell_number = list_of_cell_idx[i*w_cells+j]
363
  else:
@@ -367,12 +386,6 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
367
  start_w_big = w0 + (w1 - w0) // 2 - number_size // 2
368
  back_image[:, :, start_h_big:start_h_big+number_size, start_w_big:start_w_big+number_size] = image_with_number[:, :num_channels, :, :]
369
 
370
- # add unique identifier
371
- unique_identifier_size_w = number_size
372
- unique_identifier_size_h = number_size // 4
373
- image_with_unique_identifier = create_image_with_text(unique_identifier, underline=False,
374
- size=(unique_identifier_size_w, unique_identifier_size_h),
375
- debug=debug)
376
  start_h = h0 + unique_identifier_size_h // 2 # Fix
377
  start_w = w0 + unique_identifier_size_h // 2 # Fix
378
  back_image[:, :, start_h:start_h+unique_identifier_size_h, start_w:start_w+unique_identifier_size_w] = image_with_unique_identifier[:, :num_channels, :, :]
@@ -388,9 +401,9 @@ def create_back_image(h, w, h_cells, w_cells, logo_image, logo_insta_image, uniq
388
  back_image[:, :, start_insta_h-(number_size//8):start_insta_h-(number_size//8)+h_insta, start_insta_w:start_insta_w+w_insta] = logo_insta_image_resized[:, :num_channels, :, :]
389
 
390
  if debug:
391
- print(f"Adding content to cells took {time.time() - cell_content_time:.4f} seconds")
392
- print(f"Created back image of shape {back_image.shape}")
393
- print(f"Total back image creation took {time.time() - start_time:.4f} seconds")
394
  return back_image
395
 
396
 
@@ -404,25 +417,43 @@ def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells,
404
  # Load image
405
  t1 = time.time()
406
  if not isinstance(input_image, torch.Tensor):
407
- image = load_image(input_image, debug=debug)
 
 
 
 
 
408
  else:
409
  image = input_image
410
  if debug:
411
- print(f"Image loading took {time.time() - t1:.4f} seconds")
412
 
413
  _, c, h, w = image.shape
414
- print(f"Image shape: {image.shape}")
415
 
416
  t1_2 = time.time()
417
- logo_image = load_image(logo_image, debug=debug)
 
 
 
 
 
418
 
419
  if debug:
420
- print(f"Logo copaint Image loading took {time.time() - t1_2:.4f} seconds")
421
 
422
  t1_3 = time.time()
423
- logo_insta_image = load_image("./copaint/static/logo_instagram.png", debug=debug)
 
 
 
 
 
 
 
 
424
  if debug:
425
- print(f"Logo instagram Image loading took {time.time() - t1_3:.4f} seconds")
426
 
427
  # # Quick check that the greatest dimension corresponds to the greatest number of cells
428
  # if h > w and h_cells < w_cells:
@@ -439,12 +470,12 @@ def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells,
439
  if scale_3 is None:
440
  scale_3 = max(multiplier_w, multiplier_h)
441
 
442
- print(f"Creating back image with {h*scale_3} x {w*scale_3} pixels for {h_cells} x {w_cells} cells")
443
  back_image = create_back_image(h*scale_3, w*scale_3, h_cells, w_cells, logo_image, logo_insta_image,
444
  unique_identifier=unique_identifier, list_of_cell_idx=list_of_cell_idx, debug=debug)
445
  if debug:
446
  save_image(back_image, os.path.join(outputfolder, "back_image.png"), debug=debug)
447
- print(f"Back image creation and saving took {time.time() - t2:.4f} seconds")
448
 
449
  # Save to PDF
450
  t3 = time.time()
@@ -455,14 +486,14 @@ def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells,
455
  img_small_side_in_cm = None
456
  if cell_size_in_cm is not None:
457
  # Why Min? Cells are not neccearily square, depending on the aspect ratio of the image, and the number of H and W cells, so we assume cell_size_in_cm is the smallest side of the cell.
458
- print(f"cell_size_in_cm: {cell_size_in_cm}")
459
  min_cells = min(h_cells, w_cells)
460
  img_small_side_in_cm = cell_size_in_cm * min_cells # smallest side in cm.
461
 
462
  # print image and back image shapes
463
  if debug:
464
- print(f"Image shape: {image.shape}")
465
- print(f"Back image shape: {back_image.shape}")
466
 
467
  # Only resize back image if not high-res
468
  if not high_res:
@@ -479,11 +510,11 @@ def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells,
479
 
480
  scale = (scale_1 , scale_2, scale_3, scale_4)
481
  if debug:
482
- print(f"PDF creation took {time.time() - t3:.4f} seconds")
483
 
484
  # concatenate pdfs
485
  t4 = time.time()
486
- print("Concatenating PDFs")
487
 
488
  output_path = os.path.join(outputfolder, f"{file_name}_{h_cells}x{w_cells}_copaint.pdf")
489
  merge_pdf_list([output_path_front, output_path_back], output_path, debug=debug)
@@ -491,10 +522,10 @@ def image_to_pdf_core(input_image, file_name, logo_image, outputfolder, h_cells,
491
  os.remove(output_path_front)
492
  os.remove(output_path_back)
493
  if debug:
494
- print(f"PDF concatenation and cleanup took {time.time() - t4:.4f} seconds")
495
 
496
- print(f"Total processing time: {time.time() - overall_start_time:.4f} seconds")
497
- print(f"Done! Output saved to {output_path}")
498
  return output_path, scale
499
 
500
 
@@ -502,7 +533,7 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
502
  """
503
  Create a copaint PDF from an image and a logo.
504
  """
505
- print(f"h_cells: {h_cells}, w_cells: {w_cells}, a4: {a4}")
506
 
507
  image = load_image(input_image, debug=debug)
508
  _, c, h, w = image.shape
@@ -517,7 +548,7 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
517
  # The US Letter format is US Letter size: 8.5 by 11 inches
518
  W, H = 8.5, 11 # the unit is inch
519
  if a4:
520
- print("Using A4 format")
521
  W, H = 8.27, 11.69 # the unit is inch
522
 
523
  margin = 0.25 # hardcoded margin
@@ -532,8 +563,8 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
532
  img_small_side_in_pt = min(max_cell_per_page_h, max_cell_per_page_w) * min_cell_size_in_cm * inch * 0.393701 # 1 cm = 0.393701 inches
533
  minimum_is_width = min(w, h) == w
534
  img_large_side_in_pt = img_small_side_in_pt * max(w, h) / min(w, h)
535
- print(f"img_small_side_in_pt: {img_small_side_in_pt}, img_large_side_in_pt: {img_large_side_in_pt}")
536
- print(f"page_width_in_pt: {page_width_in_pt}, page_height_in_pt: {page_height_in_pt}")
537
  if img_large_side_in_pt < page_height_in_pt and img_small_side_in_pt < page_width_in_pt:
538
  established_cell_size = True
539
 
@@ -541,13 +572,13 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
541
  max_cell_per_page_h = max_cell_per_page_h // 2
542
  max_cell_per_page_w = max_cell_per_page_w // 2
543
 
544
- print(f"Decreasing max_cell_per_page to {max_cell_per_page_h}x{max_cell_per_page_w}")
545
 
546
 
547
  divide_factor_h = int(np.ceil(h_cells / max_cell_per_page_h))
548
  divide_factor_w = int(np.ceil(w_cells / max_cell_per_page_w))
549
 
550
- print(f"divide_factor_h: {divide_factor_h}, divide_factor_w: {divide_factor_w}")
551
  copaint_pdfs = []
552
  scale = None
553
  for i in range(divide_factor_h):
@@ -558,7 +589,7 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
558
  cell_w_end = min((j + 1) * max_cell_per_page_w, w_cells)
559
  list_of_cell_idx = [cell_h_idx * w_cells + (w_cells-cell_w_idx) for cell_h_idx in range(cell_h_start, cell_h_end) for cell_w_idx in range(cell_w_start, cell_w_end)]
560
 
561
- print(f"cell_h_start: {cell_h_start}, cell_h_end: {cell_h_end}, cell_w_start: {cell_w_start}, cell_w_end: {cell_w_end}")
562
  h_cells_new = cell_h_end - cell_h_start
563
  w_cells_new = cell_w_end - cell_w_start
564
  file_name_new = f"{file_name}_{i}x{j}"
@@ -584,7 +615,7 @@ def image_to_pdf(input_image, logo_image, outputfolder, h_cells, w_cells, unique
584
  for pdf in copaint_pdfs:
585
  os.remove(pdf)
586
 
587
- print(f"Done! Final output saved to {output_path}")
588
  return output_path
589
 
590
 
 
25
  from reportlab.lib.pagesizes import letter, A4
26
  from reportlab.lib.units import inch
27
  import PyPDF2
28
+ import logging
29
 
30
  from functools import lru_cache
31
  from matplotlib import font_manager
32
 
33
  from PIL import Image, ImageDraw, ImageFont
34
+
35
+ # Configure logging
36
+ logging.basicConfig(
37
+ level=logging.INFO,
38
+ format='%(asctime)s - %(levelname)s - %(message)s',
39
+ datefmt='%Y-%m-%d %H:%M:%S'
40
+ )
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Configure debug logging based on environment variable
44
+ logger.setLevel(logging.DEBUG)
45
+
46
  Image.MAX_IMAGE_PIXELS = None # Removes the limit entirely
47
  fromPIltoTensor = torchvision.transforms.ToTensor()
48
  fromTensortoPIL = torchvision.transforms.ToPILImage()
49
 
50
 
51
+ pre_loaded_images = {}
52
+
53
+
54
  @lru_cache(maxsize=1)
55
  def get_font(debug=False) -> str:
56
  """
 
61
  font_paths = ["/System/Library/Fonts/Avenir Next.ttc"]
62
  for font_path in font_paths:
63
  if os.path.exists(font_path):
64
+ logger.info(f"Found '{font_path}' font")
65
  return font_path
66
 
67
  available_fonts = font_manager.findSystemFonts(fontpaths=None, fontext='ttf')
 
69
  for good_font in good_font_options:
70
  font_path = next((font for font in available_fonts if good_font in font), None)
71
  if font_path:
72
+ logger.info(f"Found '{good_font}' font: {font_path}")
73
  break
74
 
75
  if font_path is None:
76
  font_path = available_fonts[0]
77
+ logger.warning(f"No good fonts found. Using default: {font_path}")
78
+ logger.warning("Please install one of the recommended fonts.")
79
 
80
  if debug:
81
+ logger.debug(f"Font loading took {time.time() - start_time:.4f} seconds")
82
  return font_path
83
 
84
  font_path = get_font()
 
98
  png_data = cairosvg.svg2png(file_obj=svg_file)
99
  # Load the PNG data into a Pillow Image
100
  image = Image.open(io.BytesIO(png_data))
 
 
101
  else:
102
  image = Image.open(image_path)
103
  # convert to RGBA
 
109
 
110
  image_open_time = time.time()
111
  if debug:
112
+ logger.debug(f"Image opening took {image_open_time - start_time:.4f} seconds")
113
 
114
  image = fromPIltoTensor(image).unsqueeze(0)
115
+ logger.info(f"Loaded image of shape {image.shape}, from {image_path}")
 
 
116
 
117
  if debug:
118
+ logger.debug(f"Image to tensor conversion took {time.time() - image_open_time:.4f} seconds")
119
+ logger.debug(f"Total image loading took {time.time() - start_time:.4f} seconds")
120
  return image
121
 
122
 
123
  def save_image(tensor, image_path, debug=False):
124
  """ Save a tensor to an image file. """
125
  start_time = time.time()
126
+ logger.info(f"Saving image of shape {tensor.shape} to {image_path}")
127
  image = fromTensortoPIL(tensor.squeeze(0))
128
  conversion_time = time.time()
129
  if debug:
130
+ logger.debug(f"Tensor to PIL conversion took {conversion_time - start_time:.4f} seconds")
131
 
132
  image.save(image_path)
133
  if debug:
134
+ logger.debug(f"Image saving took {time.time() - conversion_time:.4f} seconds")
135
+ logger.debug(f"Total save_image took {time.time() - start_time:.4f} seconds")
136
 
137
 
138
  def save_tensor_to_pdf(tensor, pdf_path, is_front=True, margin=0.25, img_small_side_in_cm=None, a4=False, high_res=False, scale=None, debug=False):
 
140
  Save a tensor to a PDF, the tensor is assumed to be a single image, and is centered on the page.
141
  """
142
  start_time = time.time()
143
+
144
  image = fromTensortoPIL(tensor.squeeze(0))
145
  img_width, img_height = image.size
146
  # 1 Inch = 72 Points : ad-hoc metric used in typography and the printing industry.
147
  # The US Letter format is US Letter size: 8.5 by 11 inches
148
  W, H = 8.5, 11 # the unit is inch
149
  if a4:
150
+ logger.info("Using A4 format")
151
  W, H = 8.27, 11.69 # the unit is inch
152
 
153
  page_width_in_pt = (W - 2*margin) * inch
 
162
  assert(img_small_side_in_pt < page_width_in_pt and img_large_side_in_pt < page_height_in_pt), f"Cell size in cm is too large for the page, max in pt unit is {page_width_in_pt}x{page_height_in_pt}, got {img_small_side_in_pt}x{img_large_side_in_pt}. It looks you manually set the size of the cell in cm, but the image is too large for the page, try a smaller cell size."
163
 
164
 
165
+ logger.info(f"Saving tensor of shape {tensor.shape} to {pdf_path}")
166
 
167
  # Convert tensor to image
168
  t1 = time.time()
169
  if debug:
170
+ logger.debug(f"Tensor to PIL conversion took {time.time() - t1:.4f} seconds")
171
 
172
  t2 = time.time()
173
  # Check if image should be rotated
 
176
  scale_1, rotated = scale
177
 
178
  if image.width > image.height and rotated:
179
+ logger.info(f"Rotating image. Size in pixels: {image.width}, {image.height}")
180
  image = image.rotate(90, expand=True)
181
+ logger.info(f"Rotated image. Size in pixels: {image.width}, {image.height}")
182
  img_width, img_height = image.width, image.height
183
  rotated = True
184
  else:
 
203
  x_offset = (page_width_in_pt - new_width) // 2
204
  y_offset = (page_height_in_pt - new_height) // 2
205
  if debug:
206
+ logger.debug(f"Image calculations took {time.time() - t2:.4f} seconds")
207
 
208
  # Save image to PDF
209
  t3 = time.time()
 
211
  image_path = "temp.png" if high_res else "temp.jpg"
212
  image.save(image_path)
213
  if debug:
214
+ logger.debug(f"Temporary image saving took {time.time() - t3:.4f} seconds")
215
 
216
  # Create a PDF
217
  t4 = time.time()
 
222
  c.drawImage(image_path, x_offset+margin*inch, y_offset+margin*inch, width=new_width, height=new_height, preserveAspectRatio=True)
223
  c.save()
224
  if debug:
225
+ logger.debug(f"PDF creation took {time.time() - t4:.4f} seconds")
226
 
227
  os.remove(image_path)
228
  if debug:
229
+ logger.debug(f"Total PDF saving took {time.time() - start_time:.4f} seconds")
230
  return pdf_path, (scale_1, rotated)
231
 
232
 
 
239
  merger.write(output_path)
240
  merger.close()
241
  if debug:
242
+ logger.debug(f"PDF merging took {time.time() - start_time:.4f} seconds")
243
  return output_path
244
 
245
 
 
286
  tensor = fromPIltoTensor(image).unsqueeze(0)
287
 
288
  if debug and len(text) <= 2: # Only log for short texts (cell numbers) when debugging
289
+ logger.debug(f"Creating image with text '{text}' took {time.time() - start_time:.4f} seconds")
290
  return tensor
291
 
292
 
 
297
  The logo is in each cell, with the cell number underlined
298
  logo_image : tensor of size 1x3xhxw
299
  """
300
+ logger.info(f"Creating back image of size {h}x{w} for {h_cells}x{w_cells} cells")
301
  start_time = time.time()
302
  num_channels = 3 # do not consider the alpha channel
303
  back_image = torch.ones(1, num_channels, h, w)
 
311
  number_size = min(cell_h, cell_w) // 2
312
 
313
  if debug:
314
+ logger.debug(f"thickness of the lines: {line_thickness}")
315
+ logger.debug(f"Initialization took {time.time() - start_time:.4f} seconds")
316
 
317
  # Create the grid lines
318
  grid_start_time = time.time()
 
333
  if w1 - line_half_thickness > 0:
334
  back_image[:, :num_channels, :, (w1-line_half_thickness):w1] = 0
335
  if debug:
336
+ logger.debug(f"Creating grid lines took {time.time() - grid_start_time:.4f} seconds")
337
 
338
  # Resize logo for all cells
339
  logo_resize_time = time.time()
 
348
  new_h_insta, new_w_insta = int(h_insta * scale_insta), int(w_insta * scale_insta)
349
  logo_insta_image_resized = torch.nn.functional.interpolate(logo_insta_image, size=(new_h_insta, new_w_insta), mode='bilinear')
350
  if debug:
351
+ logger.debug(f"Logo resizing took {time.time() - logo_resize_time:.4f} seconds ({time.time() - t_insta:.4f} for insta and {t_insta - logo_resize_time:.4f} for copaint logo)")
352
  # save logo_insta_image_resized
353
  save_image(logo_insta_image_resized, "logo_insta_image_resized.png", debug=debug)
354
  # Add content to cells
 
356
  letscopaint = create_image_with_text("copaint_art", underline=False,
357
  size=(int(0.8*number_size), number_size//8),
358
  debug=debug)
359
+ # add unique identifier
360
+ unique_identifier_size_w = number_size
361
+ unique_identifier_size_h = number_size // 4
362
+ image_with_unique_identifier = create_image_with_text(unique_identifier, underline=False,
363
+ size=(unique_identifier_size_w, unique_identifier_size_h),
364
+ debug=debug)
365
  for i in range(h_cells):
366
  for j in range(w_cells):
367
  h0 = i * cell_h # height start
 
376
  # add cell number at the center of the cell
377
  # invert cell number to match the order of the canvas. 1 is at the top right, and w_cells is at the top left
378
  if list_of_cell_idx is not None:
379
+ logger.info(f"list_of_cell_idx: {list_of_cell_idx}")
380
  if list_of_cell_idx is not None:
381
  cell_number = list_of_cell_idx[i*w_cells+j]
382
  else:
 
386
  start_w_big = w0 + (w1 - w0) // 2 - number_size // 2
387
  back_image[:, :, start_h_big:start_h_big+number_size, start_w_big:start_w_big+number_size] = image_with_number[:, :num_channels, :, :]
388
 
 
 
 
 
 
 
389
  start_h = h0 + unique_identifier_size_h // 2 # Fix
390
  start_w = w0 + unique_identifier_size_h // 2 # Fix
391
  back_image[:, :, start_h:start_h+unique_identifier_size_h, start_w:start_w+unique_identifier_size_w] = image_with_unique_identifier[:, :num_channels, :, :]
 
401
  back_image[:, :, start_insta_h-(number_size//8):start_insta_h-(number_size//8)+h_insta, start_insta_w:start_insta_w+w_insta] = logo_insta_image_resized[:, :num_channels, :, :]
402
 
403
  if debug:
404
+ logger.debug(f"Adding content to cells took {time.time() - cell_content_time:.4f} seconds")
405
+ logger.debug(f"Created back image of shape {back_image.shape}")
406
+ logger.debug(f"Total back image creation took {time.time() - start_time:.4f} seconds")
407
  return back_image
408
 
409
 
 
417
  # Load image
418
  t1 = time.time()
419
  if not isinstance(input_image, torch.Tensor):
420
+ if input_image in pre_loaded_images:
421
+ image = pre_loaded_images[input_image]
422
+ logger.info(f"Loaded image from cache: {input_image}")
423
+ else:
424
+ image = load_image(input_image, debug=debug)
425
+ pre_loaded_images[input_image] = image
426
  else:
427
  image = input_image
428
  if debug:
429
+ logger.debug(f"Image loading took {time.time() - t1:.4f} seconds")
430
 
431
  _, c, h, w = image.shape
432
+ logger.info(f"Image shape: {image.shape}")
433
 
434
  t1_2 = time.time()
435
+ if logo_image in pre_loaded_images:
436
+ logo_image = pre_loaded_images[logo_image]
437
+ logger.info(f"Loaded logo copaint image from cache: {logo_image}")
438
+ else:
439
+ logo_image = load_image(logo_image, debug=debug)
440
+ pre_loaded_images[logo_image] = logo_image
441
 
442
  if debug:
443
+ logger.debug(f"Logo copaint Image loading took {time.time() - t1_2:.4f} seconds")
444
 
445
  t1_3 = time.time()
446
+ logo_insta_path = "./copaint/static/logo_instagram.png"
447
+
448
+ if logo_insta_path in pre_loaded_images:
449
+ logo_insta_image = pre_loaded_images[logo_insta_path]
450
+ logger.info(f"Loaded logo instagram image from cache: {logo_insta_path}")
451
+ else:
452
+ logo_insta_image = load_image(logo_insta_path, debug=debug)
453
+ pre_loaded_images[logo_insta_path] = logo_insta_image
454
+
455
  if debug:
456
+ logger.debug(f"Logo instagram Image loading took {time.time() - t1_3:.4f} seconds")
457
 
458
  # # Quick check that the greatest dimension corresponds to the greatest number of cells
459
  # if h > w and h_cells < w_cells:
 
470
  if scale_3 is None:
471
  scale_3 = max(multiplier_w, multiplier_h)
472
 
473
+ logger.info(f"Creating back image with {h*scale_3} x {w*scale_3} pixels for {h_cells} x {w_cells} cells")
474
  back_image = create_back_image(h*scale_3, w*scale_3, h_cells, w_cells, logo_image, logo_insta_image,
475
  unique_identifier=unique_identifier, list_of_cell_idx=list_of_cell_idx, debug=debug)
476
  if debug:
477
  save_image(back_image, os.path.join(outputfolder, "back_image.png"), debug=debug)
478
+ logger.debug(f"Back image creation and saving took {time.time() - t2:.4f} seconds")
479
 
480
  # Save to PDF
481
  t3 = time.time()
 
486
  img_small_side_in_cm = None
487
  if cell_size_in_cm is not None:
488
  # Why Min? Cells are not neccearily square, depending on the aspect ratio of the image, and the number of H and W cells, so we assume cell_size_in_cm is the smallest side of the cell.
489
+ logger.info(f"cell_size_in_cm: {cell_size_in_cm}")
490
  min_cells = min(h_cells, w_cells)
491
  img_small_side_in_cm = cell_size_in_cm * min_cells # smallest side in cm.
492
 
493
  # print image and back image shapes
494
  if debug:
495
+ logger.debug(f"Image shape: {image.shape}")
496
+ logger.debug(f"Back image shape: {back_image.shape}")
497
 
498
  # Only resize back image if not high-res
499
  if not high_res:
 
510
 
511
  scale = (scale_1 , scale_2, scale_3, scale_4)
512
  if debug:
513
+ logger.debug(f"PDF creation took {time.time() - t3:.4f} seconds")
514
 
515
  # concatenate pdfs
516
  t4 = time.time()
517
+ logger.info("Concatenating PDFs")
518
 
519
  output_path = os.path.join(outputfolder, f"{file_name}_{h_cells}x{w_cells}_copaint.pdf")
520
  merge_pdf_list([output_path_front, output_path_back], output_path, debug=debug)
 
522
  os.remove(output_path_front)
523
  os.remove(output_path_back)
524
  if debug:
525
+ logger.debug(f"PDF concatenation and cleanup took {time.time() - t4:.4f} seconds")
526
 
527
+ logger.info(f"Total processing time: {time.time() - overall_start_time:.4f} seconds")
528
+ logger.info(f"Done! Output saved to {output_path}")
529
  return output_path, scale
530
 
531
 
 
533
  """
534
  Create a copaint PDF from an image and a logo.
535
  """
536
+ logger.info(f"h_cells: {h_cells}, w_cells: {w_cells}, a4: {a4}")
537
 
538
  image = load_image(input_image, debug=debug)
539
  _, c, h, w = image.shape
 
548
  # The US Letter format is US Letter size: 8.5 by 11 inches
549
  W, H = 8.5, 11 # the unit is inch
550
  if a4:
551
+ logger.info("Using A4 format")
552
  W, H = 8.27, 11.69 # the unit is inch
553
 
554
  margin = 0.25 # hardcoded margin
 
563
  img_small_side_in_pt = min(max_cell_per_page_h, max_cell_per_page_w) * min_cell_size_in_cm * inch * 0.393701 # 1 cm = 0.393701 inches
564
  minimum_is_width = min(w, h) == w
565
  img_large_side_in_pt = img_small_side_in_pt * max(w, h) / min(w, h)
566
+ logger.info(f"img_small_side_in_pt: {img_small_side_in_pt}, img_large_side_in_pt: {img_large_side_in_pt}")
567
+ logger.info(f"page_width_in_pt: {page_width_in_pt}, page_height_in_pt: {page_height_in_pt}")
568
  if img_large_side_in_pt < page_height_in_pt and img_small_side_in_pt < page_width_in_pt:
569
  established_cell_size = True
570
 
 
572
  max_cell_per_page_h = max_cell_per_page_h // 2
573
  max_cell_per_page_w = max_cell_per_page_w // 2
574
 
575
+ logger.info(f"Decreasing max_cell_per_page to {max_cell_per_page_h}x{max_cell_per_page_w}")
576
 
577
 
578
  divide_factor_h = int(np.ceil(h_cells / max_cell_per_page_h))
579
  divide_factor_w = int(np.ceil(w_cells / max_cell_per_page_w))
580
 
581
+ logger.info(f"divide_factor_h: {divide_factor_h}, divide_factor_w: {divide_factor_w}")
582
  copaint_pdfs = []
583
  scale = None
584
  for i in range(divide_factor_h):
 
589
  cell_w_end = min((j + 1) * max_cell_per_page_w, w_cells)
590
  list_of_cell_idx = [cell_h_idx * w_cells + (w_cells-cell_w_idx) for cell_h_idx in range(cell_h_start, cell_h_end) for cell_w_idx in range(cell_w_start, cell_w_end)]
591
 
592
+ logger.info(f"cell_h_start: {cell_h_start}, cell_h_end: {cell_h_end}, cell_w_start: {cell_w_start}, cell_w_end: {cell_w_end}")
593
  h_cells_new = cell_h_end - cell_h_start
594
  w_cells_new = cell_w_end - cell_w_start
595
  file_name_new = f"{file_name}_{i}x{j}"
 
615
  for pdf in copaint_pdfs:
616
  os.remove(pdf)
617
 
618
+ logger.info(f"Done! Final output saved to {output_path}")
619
  return output_path
620
 
621
 
copaint/gradio_ui.py CHANGED
@@ -3,6 +3,7 @@
3
  import gradio as gr
4
  import shutil
5
  import tempfile
 
6
 
7
  from gradio_pdf import PDF
8
  from importlib.resources import files
@@ -12,6 +13,14 @@ from copaint.copaint import image_to_pdf
12
  import torchvision
13
  import torch
14
 
 
 
 
 
 
 
 
 
15
  fromPIltoTensor = torchvision.transforms.ToTensor()
16
  fromTensortoPIL = torchvision.transforms.ToPILImage()
17
 
@@ -20,10 +29,14 @@ def add_grid_to_image(image, h_cells, w_cells):
20
  # image is a torch tensor of shape (3, h, w)
21
  image = image.convert("RGB")
22
  image = fromPIltoTensor(image)
23
- grid_color = torch.tensor([16,15,46]).unsqueeze(1).unsqueeze(1) / 255.0
 
 
 
 
24
  h,w = image.shape[1:]
25
  thickness = max(min(1, int(min(h,w)/100)), 1)
26
- print("thickness, h, w", thickness, h, w)
27
  for i in range(h_cells+1):
28
  idx_i = int(i*h/h_cells)
29
  image[:, idx_i-thickness:idx_i+thickness, :] = grid_color
@@ -36,52 +49,54 @@ def add_grid_to_image(image, h_cells, w_cells):
36
  return image
37
 
38
 
39
- def canvas_ratio(image, h_cells, w_cells):
40
- w,h = image.size
41
- aspect_ratio = w/h
42
- if aspect_ratio > 1:
43
- aspect_ratio = 1/aspect_ratio
44
-
45
- # find nearest aspect ratio in the list of predefined ones
46
- predefined_aspect_ratios = [2/3, 1/2, 1/1, 5/6, 4/5, 5/7]
47
- predefined_aspect_ratios_str = ["2:3", "1:2", "1:1", "5:6", "4:5", "5:7"]
48
  min_diff = float('inf')
49
- closest_ratio_idx = None
50
- for idx, ratio in enumerate(predefined_aspect_ratios):
51
- diff = abs(aspect_ratio - ratio)
 
 
 
 
 
 
 
 
 
52
  if diff < min_diff:
53
  min_diff = diff
54
- closest_ratio_idx = idx
55
- closest_ratio_str = predefined_aspect_ratios_str[closest_ratio_idx]
56
 
57
- if min_diff > 0.1:
58
- return None
59
- else:
60
-
61
- example_str = ""
62
-
63
- if closest_ratio_str == "1:1":
64
- if h_cells == 2 and h_cells == 2:
65
- example_str = " (for example, a 6” by 6” canvas)"
66
- if h_cells == 3 and h_cells == 3:
67
- example_str = " (for example, an 8” by 8” canvas)"
68
- elif closest_ratio_str == "5:6":
69
- example_str = " (for example, a 10” by 12” canvas)"
70
- elif closest_ratio_str == "3:4":
71
- if (h_cells == 3 and w_cells == 4) or (h_cells == 4 and w_cells == 3):
72
- example_str = " (for example, a 6” by 8” canvas)"
73
- if (h_cells == 4 and w_cells == 6) or (h_cells == 6 and w_cells == 4):
74
- example_str = " (for example, an 18” by 24”, or a 12” by 16” canvas)"
75
- elif closest_ratio_str == "2:3" and ((h_cells == 6 and w_cells == 9) or (h_cells == 9 and w_cells == 6)):
76
- example_str = " (for example, a 24” by 36” canvas)"
77
-
78
- return f"Preparing your canvas: *choose a canvas with a {closest_ratio_str} ratio to respect your design’s size{example_str}.*"
79
-
80
 
81
  def add_grid_and_display_ratio(image, h_cells, w_cells):
82
  if image is None:
83
  return None, gr.update(visible=False)
84
- return add_grid_to_image(image, h_cells, w_cells), gr.update(visible=True, value=canvas_ratio(image, h_cells, w_cells))
85
 
86
 
87
  def process_copaint(
@@ -95,7 +110,7 @@ def process_copaint(
95
  copaint_name="",
96
  copaint_logo=None,
97
  ):
98
- """Process the input and generate CoPaint PDF"""
99
  # Create temporary directories for processing
100
  temp_input_dir = tempfile.mkdtemp()
101
  temp_output_dir = tempfile.mkdtemp()
@@ -147,10 +162,10 @@ def process_copaint(
147
 
148
  def build_gradio_ui():
149
  # Create Gradio Interface
150
- with gr.Blocks(title="CoPaint Generator", theme='NoCrypt/miku') as demo:
151
 
152
- gr.Markdown("# πŸ€– CoPaint Generator")
153
- gr.Markdown("Upload an image with your painting design and set grid parameters to generate a CoPaint PDF template πŸ–¨οΈπŸ“„βœ‚οΈ for your next collaborative painting activities. πŸŽ¨πŸ–ŒοΈ")
154
 
155
  # --- inputs ---
156
  with gr.Row(equal_height=True):
@@ -185,7 +200,8 @@ def build_gradio_ui():
185
 
186
  # Grid + Design preview
187
  gr.Markdown("<div style='text-align: center; font-weight: bold;'>Preview</div>")
188
- output_image = gr.Image(label="Squares' Grid Preview", interactive=False)
 
189
 
190
  # canvas ratio message
191
  canvas_msg = gr.Markdown(label="Canvas Ratio", visible=False)
@@ -229,7 +245,7 @@ def build_gradio_ui():
229
 
230
  with gr.Row():
231
  with gr.Column(scale=1):
232
- output_pdf = PDF(label="PDF Preview")
233
 
234
  # Update output_image: trigger update when any input changes
235
  for component in [input_image, h_cells, w_cells]:
 
3
  import gradio as gr
4
  import shutil
5
  import tempfile
6
+ import logging
7
 
8
  from gradio_pdf import PDF
9
  from importlib.resources import files
 
13
  import torchvision
14
  import torch
15
 
16
+ # Configure logging
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(levelname)s - %(message)s',
20
+ datefmt='%Y-%m-%d %H:%M:%S'
21
+ )
22
+ logger = logging.getLogger(__name__)
23
+
24
  fromPIltoTensor = torchvision.transforms.ToTensor()
25
  fromTensortoPIL = torchvision.transforms.ToPILImage()
26
 
 
29
  # image is a torch tensor of shape (3, h, w)
30
  image = image.convert("RGB")
31
  image = fromPIltoTensor(image)
32
+
33
+ # Calculate average image brightness
34
+ avg_brightness = image.mean()
35
+ # Use white grid for dark images, dark blue for bright ones
36
+ grid_color = torch.tensor([255,255,255]).unsqueeze(1).unsqueeze(1) / 255.0 if avg_brightness < 0.3 else torch.tensor([16,15,46]).unsqueeze(1).unsqueeze(1) / 255.0
37
  h,w = image.shape[1:]
38
  thickness = max(min(1, int(min(h,w)/100)), 1)
39
+ logger.debug(f"thickness, h, w: {thickness}, {h}, {w}")
40
  for i in range(h_cells+1):
41
  idx_i = int(i*h/h_cells)
42
  image[:, idx_i-thickness:idx_i+thickness, :] = grid_color
 
49
  return image
50
 
51
 
52
+ def get_canvas_ratio_message(h_cells, w_cells):
53
+ # Calculate aspect ratio
54
+ ratio = h_cells / w_cells if h_cells > w_cells else w_cells / h_cells
55
+ closest_ratio = None
56
+ closest_ratio_str = None
 
 
 
 
57
  min_diff = float('inf')
58
+
59
+ # Common aspect ratios
60
+ ratios = {
61
+ "1:1": 1,
62
+ "5:6": 5/6,
63
+ "3:4": 3/4,
64
+ "2:3": 2/3
65
+ }
66
+
67
+ # Find closest ratio
68
+ for ratio_str, r in ratios.items():
69
+ diff = abs(ratio - r)
70
  if diff < min_diff:
71
  min_diff = diff
72
+ closest_ratio = r
73
+ closest_ratio_str = ratio_str
74
 
75
+ example_str = ""
76
+
77
+ if closest_ratio_str == "1:1":
78
+ if h_cells == 2 and h_cells == 2:
79
+ example_str = ' (for example, a 6" by 6" canvas)'
80
+ if h_cells == 3 and h_cells == 3:
81
+ example_str = ' (for example, an 8" by 8" canvas)'
82
+ elif closest_ratio_str == "5:6":
83
+ example_str = ' (for example, a 10" by 12" canvas)'
84
+ elif closest_ratio_str == "3:4":
85
+ if (h_cells == 3 and w_cells == 4) or (h_cells == 4 and w_cells == 3):
86
+ example_str = ' (for example, a 6" by 8" canvas)'
87
+ if (h_cells == 4 and w_cells == 6) or (h_cells == 6 and w_cells == 4):
88
+ example_str = ' (for example, an 18" by 24", or a 12" by 16" canvas)'
89
+ elif closest_ratio_str == "2:3" and ((h_cells == 6 and w_cells == 9) or (h_cells == 9 and w_cells == 6)):
90
+ example_str = ' (for example, a 24" by 36" canvas)'
91
+
92
+ return_str = f"Your grid has <b>{h_cells}x{w_cells} cells</b>, which is a <b>{h_cells*w_cells}-cell CopainT</b>.\n\n"
93
+ return_str += f"Preparing your canvas: <b>choose a canvas with a {closest_ratio_str} ratio</b> to respect your design's size{example_str}."
94
+ return f"<div style='font-size: 1.2em; line-height: 1.5;'>{return_str}</div>"
 
 
 
95
 
96
  def add_grid_and_display_ratio(image, h_cells, w_cells):
97
  if image is None:
98
  return None, gr.update(visible=False)
99
+ return add_grid_to_image(image, h_cells, w_cells), gr.update(visible=True, value=get_canvas_ratio_message(h_cells, w_cells))
100
 
101
 
102
  def process_copaint(
 
110
  copaint_name="",
111
  copaint_logo=None,
112
  ):
113
+ """Process the input and generate CopainT PDF"""
114
  # Create temporary directories for processing
115
  temp_input_dir = tempfile.mkdtemp()
116
  temp_output_dir = tempfile.mkdtemp()
 
162
 
163
  def build_gradio_ui():
164
  # Create Gradio Interface
165
+ with gr.Blocks(title="CopainT Generator", theme='NoCrypt/miku') as demo:
166
 
167
+ gr.Markdown("# πŸ€– CopainT Generator")
168
+ gr.Markdown("Upload an image with your painting design and set grid parameters to generate a CopainT PDF template πŸ–¨οΈπŸ“„βœ‚οΈ for your next collaborative painting activities. πŸŽ¨πŸ–ŒοΈ")
169
 
170
  # --- inputs ---
171
  with gr.Row(equal_height=True):
 
200
 
201
  # Grid + Design preview
202
  gr.Markdown("<div style='text-align: center; font-weight: bold;'>Preview</div>")
203
+
204
+ output_image = gr.Image(label=None, show_label=False, show_fullscreen_button=False, interactive=False, show_download_button=False)
205
 
206
  # canvas ratio message
207
  canvas_msg = gr.Markdown(label="Canvas Ratio", visible=False)
 
245
 
246
  with gr.Row():
247
  with gr.Column(scale=1):
248
+ output_pdf = PDF(label="PDF Preview", show_label=False, show_fullscreen_button=False, interactive=False, show_download_button=False)
249
 
250
  # Update output_image: trigger update when any input changes
251
  for component in [input_image, h_cells, w_cells]: