Spaces:
Sleeping
Sleeping
looks better, can still be improved
Browse files- copaint/cli.py +45 -119
- copaint/copaint.py +99 -68
- 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
|
7 |
-
actual_participants_over_participants = 0.7 # how many people actually show up compared to the number of participants
|
8 |
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10 |
default_identifiers = [
|
11 |
-
"
|
12 |
-
"
|
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 |
-
|
44 |
-
|
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 |
-
"""
|
61 |
-
#
|
62 |
-
|
63 |
-
|
64 |
-
|
|
|
|
|
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 |
-
|
90 |
-
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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 |
-
|
57 |
break
|
58 |
|
59 |
if font_path is None:
|
60 |
font_path = available_fonts[0]
|
61 |
-
|
62 |
-
|
63 |
|
64 |
if debug:
|
65 |
-
|
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 |
-
|
99 |
|
100 |
image = fromPIltoTensor(image).unsqueeze(0)
|
101 |
-
|
102 |
-
# resize to low res for testing
|
103 |
-
# image = torch.nn.functional.interpolate(image, size=1000)
|
104 |
|
105 |
if debug:
|
106 |
-
|
107 |
-
|
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 |
-
|
115 |
image = fromTensortoPIL(tensor.squeeze(0))
|
116 |
conversion_time = time.time()
|
117 |
if debug:
|
118 |
-
|
119 |
|
120 |
image.save(image_path)
|
121 |
if debug:
|
122 |
-
|
123 |
-
|
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 |
-
|
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 |
-
|
153 |
|
154 |
# Convert tensor to image
|
155 |
t1 = time.time()
|
156 |
if debug:
|
157 |
-
|
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 |
-
|
167 |
image = image.rotate(90, expand=True)
|
168 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
213 |
|
214 |
os.remove(image_path)
|
215 |
if debug:
|
216 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
302 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
392 |
-
|
393 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
408 |
else:
|
409 |
image = input_image
|
410 |
if debug:
|
411 |
-
|
412 |
|
413 |
_, c, h, w = image.shape
|
414 |
-
|
415 |
|
416 |
t1_2 = time.time()
|
417 |
-
logo_image
|
|
|
|
|
|
|
|
|
|
|
418 |
|
419 |
if debug:
|
420 |
-
|
421 |
|
422 |
t1_3 = time.time()
|
423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
424 |
if debug:
|
425 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
465 |
-
|
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 |
-
|
483 |
|
484 |
# concatenate pdfs
|
485 |
t4 = time.time()
|
486 |
-
|
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 |
-
|
495 |
|
496 |
-
|
497 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
536 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
24 |
h,w = image.shape[1:]
|
25 |
thickness = max(min(1, int(min(h,w)/100)), 1)
|
26 |
-
|
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
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
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 |
-
|
50 |
-
|
51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
if diff < min_diff:
|
53 |
min_diff = diff
|
54 |
-
|
55 |
-
|
56 |
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
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=
|
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
|
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="
|
151 |
|
152 |
-
gr.Markdown("# π€
|
153 |
-
gr.Markdown("Upload an image with your painting design and set grid parameters to generate a
|
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 |
-
|
|
|
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]:
|