Squashed commit of the following:
commit e5832a17356cebd43846c0542ce595bba5a8cdda
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Mon Feb 13 14:08:17 2023 +0100
reduce pytest parameter combinatons
commit a1e6c9e553545ed1fc4c017e67dddaa98fc2a1c9
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:56:16 2023 +0100
clear color map cache per pytest parameter combination
commit 21a9db25cdb55b967c664f5d129a9ac35aa1da0f
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:38:52 2023 +0100
Remove obsolete line
commit 90c367cc325dd3a4d3b8f7f37e06a79c30207867
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:38:05 2023 +0100
Refactoring: Move
commit 42d285e35b82ba0f36835eff6ff70c50bd80d20c
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:33:44 2023 +0100
Refactoring: Move
Move content generator into its own module
commit ddc92461d7442e08921408707ada6963f555f708
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:29:59 2023 +0100
Refactoring: Move
Move remaining segment generation functions into segments module
commit d2cb78d38f47a8c705a82dd725e24c0540a29710
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:27:26 2023 +0100
Refactoring: Move
Move zipmap and evert_nth into utils module
commit 9c401a977ce0749463cb2af509f412007f37a084
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:26:01 2023 +0100
Refactoring: Move
Move rectangle shrinking logic into new morphing module
commit b77951d4feb1e5dacdb32f0d36a399f6f94b2293
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:22:15 2023 +0100
Refactoring: Move
Move segment generation functions into their own module
commit c7b224a98a355f93653a0d576a10fbd2507ed1d8
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:14:54 2023 +0100
Refactoring: Move
Move cell class into its own module
commit f0072b0852f34f0448d467fc4993eee3a23a6c5b
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:12:18 2023 +0100
Refactoring: Move
Move table generation related code into new table module
commit 9fd87aff8ea69404959056b3d58c7f8856527c83
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 18:07:36 2023 +0100
Refactoring: Move
- Move random plot into its own module
- Move geometric predicates into their own module
commit 6728642a4fc07ec9c47db99efe12981c18f95ee5
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:59:54 2023 +0100
Refactoring: Move
Mode random helper functions
commit cc86a79ac7bc47e5ddb68e5c95327eebc97041d9
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:33:51 2023 +0100
Refactoring: Move
Move text block generator module into text module
commit 160d5b3473d7e4f6f6dbb8fcf51cf554d6b54543
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:29:29 2023 +0100
Remove unused code
commit 7b2f921472bb47b5c5d7848393ae471664eab583
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:28:17 2023 +0100
Refactoring: Move
Move text block generators into their own module
commit e258df899f4be39beec4a0bfc01eaea105218adb
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:24:54 2023 +0100
Refactoring: Move
Move text block into its own module
commit cef97b33f920488857c308e6ebcbc5a309de4b20
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:20:30 2023 +0100
Refactoring: Move
Move page partitioners into partitioner module
commit a54ccb2fdf44595720718fef44d5d3b1b8cbfe0a
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:15:40 2023 +0100
Refactoring: Move
Move text generation funtions into their own module
commit 1de938f2faa50cb805d7ebea3075c1d6d969d254
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:07:33 2023 +0100
Refactoring: Move
Move font related functions into font module
commit de9b3bad93d91b2d1820b59403fc357e243238e6
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 17:05:47 2023 +0100
Refactoring: Move
Move font picker into new font module
commit 9480d58a8a77b3feb7206cb1b7ac5c8a25516b39
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:59:31 2023 +0100
Refactoring: Move
Move line formatters into their own module
commit cc0094d3f73b258a0b89353981529e7fa6978b53
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:54:08 2023 +0100
Refactoring: Move
Move random content rectangle into its own module
commit 93a52080df8f5aa39b3b29f2c9a8dcbc8d72ad9d
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:52:57 2023 +0100
Remove unused code
commit 4ec3429dec932cadd828376610950b8ad84a51f4
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:51:03 2023 +0100
Refactoring: Move
Move page partitioner into its own module
commit bdcb2f1bef36357ea048c4f00b9dccfa25b13bd9
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:42:55 2023 +0100
Refactoring: Move
commit 845d1691949dcba049737af29fcee735825ecb8f
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:39:39 2023 +0100
Refactoring
commit 56c10490b965ccf3ca81aa9ba0403d9068871688
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:36:21 2023 +0100
Refactoring
commit 740a9cb3c25710a46452fa28dbef011daa03d6ed
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:33:32 2023 +0100
Refactoring
commit b3cf3e44548c71e7eff90e94ce8ce671a0d8f343
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:29:03 2023 +0100
Refactoring
Add fixture for page partitioner
commit 2fb450943e74d0a2a49ca0e20c9507d0230e4373
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:25:50 2023 +0100
Refactoring: Move
commit fd76933b5ac1fbab1b508ef1f3f199d04189cf81
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:16:16 2023 +0100
Refactoring: Move
Move image operations such as blurring into their own module.
commit 809590054315266286c75fb0ef2f81b506aaf20c
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 16:10:48 2023 +0100
Fix effectless bug
commit d42f053c81105e3144fcc54a7c6e924c777b3665
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 13:22:16 2023 +0100
Refactoring: Re-order
commit 04a617b9df0ee62e73f87508c8b09c4d3817a6e3
Author: Matthias Bisping <matthias.bisping@axbit.com>
Date: Wed Feb 1 13:19:25 2023 +0100
Refactoring
Move content rectangle base class
280 lines
9.4 KiB
Python
280 lines
9.4 KiB
Python
import random
|
|
from copy import deepcopy
|
|
from enum import Enum
|
|
from functools import lru_cache, partial
|
|
from math import sqrt
|
|
from typing import List, Iterable
|
|
|
|
from PIL import Image
|
|
from funcy import chunks, mapcat, repeatedly
|
|
from loguru import logger
|
|
|
|
from cv_analysis.utils.geometric import is_square_like
|
|
from cv_analysis.utils.image_operations import superimpose
|
|
|
|
from cv_analysis.utils.rectangle import Rectangle
|
|
from cv_analysis.utils.spacial import area
|
|
from synthesis.random import rnd, possibly
|
|
from synthesis.segment.content_rectangle import ContentRectangle
|
|
from synthesis.segment.plot import pick_colormap
|
|
from synthesis.segment.random_content_rectangle import RandomContentRectangle
|
|
from synthesis.segment.segments import generate_random_plot, generate_recursive_random_table, generate_text_block
|
|
from synthesis.segment.table.cell import Cell
|
|
from synthesis.text.text import generate_random_words, generate_random_number
|
|
|
|
|
|
class RecursiveRandomTable(RandomContentRectangle):
|
|
def __init__(self, x1, y1, x2, y2, border_width=1, layout: str = None, double_rule=False):
|
|
"""A table with a random number of rows and columns, and random content in each cell.
|
|
|
|
Args:
|
|
x1: x-coordinate of the top-left corner
|
|
y1: y-coordinate of the top-left corner
|
|
x2: x-coordinate of the bottom-right corner
|
|
y2: y-coordinate of the bottom-right corner
|
|
border_width: width of the table border
|
|
layout: layout of the table, either "horizontal", "vertical", "closed", or "open"
|
|
double_rule: whether to use double rules as the top and bottom rules
|
|
"""
|
|
|
|
assert layout in [None, "horizontal", "vertical", "closed", "open"]
|
|
|
|
super().__init__(x1, y1, x2, y2)
|
|
|
|
self.double_rule = double_rule
|
|
self.double_rule_width = (3 * border_width) if self.double_rule else 0
|
|
|
|
self.n_columns = rnd.randint(1, max(self.width // 100, 1))
|
|
self.n_rows = rnd.randint(1, max((self.height - 2 * self.double_rule_width) // rnd.randint(17, 100), 1))
|
|
self.cell_size = (self.width / self.n_columns, (self.height - 2 * self.double_rule_width) / self.n_rows)
|
|
|
|
self.content = Image.new("RGBA", (self.width, self.height), (255, 255, 255, 0))
|
|
|
|
self.background_color = get_random_background_color()
|
|
|
|
self.layout = layout or self.pick_random_layout()
|
|
logger.debug(f"Layout: {self.layout}")
|
|
|
|
def pick_random_layout(self):
|
|
|
|
if self.n_columns == 1 and self.n_rows == 1:
|
|
layout = "closed"
|
|
elif self.n_columns == 1:
|
|
layout = rnd.choice(["vertical", "closed"])
|
|
elif self.n_rows == 1:
|
|
layout = rnd.choice(["horizontal", "closed"])
|
|
else:
|
|
layout = rnd.choice(["closed", "horizontal", "vertical", "open"])
|
|
|
|
return layout
|
|
|
|
def generate_random_table(self):
|
|
cells = self.generate_table()
|
|
cells = list(self.fill_cells_with_content(cells))
|
|
# FIXME: There is a bug here: Table rule is not drawn correctly, actually we want to do cells = ...
|
|
list(self.draw_cell_borders(cells))
|
|
|
|
self.content = paste_contents(self.content, cells)
|
|
assert self.content.mode == "RGBA"
|
|
|
|
def fill_cells_with_content(self, cells):
|
|
yield from map(self.build_cell, cells)
|
|
|
|
def build_cell(self, cell):
|
|
|
|
if self.__is_a_small_cell(cell):
|
|
cell = self.build_small_cell(cell)
|
|
|
|
elif self.__is_a_medium_sized_cell(cell):
|
|
cell = self.build_medium_sized_cell(cell)
|
|
|
|
elif self.__is_a_large_cell(cell):
|
|
cell = self.build_large_cell(cell)
|
|
|
|
else:
|
|
raise ValueError(f"Invalid cell size: {get_size(cell)}")
|
|
|
|
assert cell.content.mode == "RGBA"
|
|
|
|
return cell
|
|
|
|
def __is_a_small_cell(self, cell):
|
|
return get_size(cell) <= Size.SMALL.value
|
|
|
|
def __is_a_medium_sized_cell(self, cell):
|
|
return get_size(cell) <= Size.MEDIUM.value
|
|
|
|
def __is_a_large_cell(self, cell):
|
|
return get_size(cell) > Size.MEDIUM.value
|
|
|
|
def build_small_cell(self, cell):
|
|
|
|
content = (possibly() and generate_random_words(1, 3)) or (
|
|
generate_random_number()
|
|
+ ((possibly() and " " + rnd.choice(["$", "£", "%", "EUR", "USD", "CAD", "ADA"])) or "")
|
|
)
|
|
|
|
return generate_text_block(cell, content)
|
|
|
|
def build_medium_sized_cell(self, cell):
|
|
|
|
choice = rnd.choice(["plot", "recurse"])
|
|
|
|
if choice == "plot":
|
|
return generate_random_plot(cell)
|
|
|
|
elif choice == "recurse":
|
|
return generate_recursive_random_table(
|
|
cell,
|
|
border_width=1,
|
|
layout=random.choice(["open", "horizontal", "vertical"]),
|
|
double_rule=False,
|
|
)
|
|
|
|
else:
|
|
return generate_text_block(cell, f"{choice} {get_size(cell):.0f} {get_size_class(cell).name}")
|
|
|
|
def build_large_cell(self, cell):
|
|
choice = rnd.choice(["plot", "recurse"])
|
|
|
|
logger.debug(f"Generating {choice} {get_size(cell):.0f} {get_size_class(cell).name}")
|
|
|
|
if choice == "plot" and is_square_like(cell):
|
|
return generate_random_plot(cell)
|
|
|
|
else:
|
|
logger.debug(f"recurse {get_size(cell):.0f} {get_size_class(cell).name}")
|
|
return generate_recursive_random_table(
|
|
cell,
|
|
border_width=1,
|
|
layout=random.choice(["open", "horizontal", "vertical"]),
|
|
double_rule=False,
|
|
)
|
|
|
|
def draw_cell_borders(self, cells: List[ContentRectangle]):
|
|
def draw_edges_based_on_position(cell: Cell, col_idx, row_index):
|
|
# Draw the borders of the cell based on its position in the table
|
|
if col_idx < self.n_columns - 1:
|
|
cell.draw_right_border()
|
|
|
|
if row_index < self.n_rows - 1:
|
|
cell.draw_bottom_border()
|
|
|
|
columns = chunks(self.n_rows, cells)
|
|
for col_idx, column in enumerate(columns):
|
|
for row_index, cell in enumerate(column):
|
|
# TODO: Refactor
|
|
c = Cell(*cell.coords, self.background_color)
|
|
c.content = cell.content
|
|
draw_edges_based_on_position(c, col_idx, row_index)
|
|
yield cell
|
|
|
|
if self.layout == "closed":
|
|
# TODO: Refactor
|
|
c = Cell(*self.coords, self.background_color)
|
|
c.content = self.content
|
|
c.draw()
|
|
yield self
|
|
|
|
# TODO: Refactor
|
|
if self.double_rule:
|
|
c1 = Cell(*self.coords)
|
|
c1.draw_top_border(width=1)
|
|
c1.draw_bottom_border(width=1)
|
|
|
|
x1, y1, x2, y2 = self.coords
|
|
c2 = Cell(x1, y1 + self.double_rule_width, x2, y2 - self.double_rule_width)
|
|
c2.draw_top_border(width=1)
|
|
c2.draw_bottom_border(width=1)
|
|
|
|
c = superimpose(c1.content, c2.content)
|
|
|
|
self.content = superimpose(c, self.content)
|
|
|
|
yield self
|
|
|
|
def generate_table(self) -> Iterable[ContentRectangle]:
|
|
yield from mapcat(self.generate_column, range(self.n_columns))
|
|
|
|
def generate_column(self, column_index) -> Iterable[ContentRectangle]:
|
|
logger.trace(f"Generating column {column_index}.")
|
|
generate_cell_for_row_index = partial(self.generate_cell, column_index)
|
|
yield from map(generate_cell_for_row_index, range(self.n_rows))
|
|
|
|
def generate_cell(self, column_index, row_index) -> ContentRectangle:
|
|
w, h = self.cell_size
|
|
x1, y1 = (column_index * w), (row_index * h) + self.double_rule_width
|
|
x2, y2 = x1 + w, y1 + h
|
|
logger.trace(f"Generating cell ({row_index}, {column_index}) at ({x1}, {y1}, {x2}, {y2}).")
|
|
return Cell(x1, y1, x2, y2, self.background_color)
|
|
|
|
def generate_column_names(self):
|
|
column_names = repeatedly(self.generate_column_name, self.n_columns)
|
|
return column_names
|
|
|
|
def generate_column_name(self):
|
|
column_name = generate_random_words(1, 3)
|
|
return column_name
|
|
|
|
|
|
@lru_cache(maxsize=None)
|
|
def get_random_background_color():
|
|
return tuple([*get_random_color_complementing_color_map(pick_colormap()), rnd.randint(100, 210)])
|
|
|
|
|
|
def get_random_color_complementing_color_map(colormap):
|
|
def color_complement(r, g, b):
|
|
"""Reference: https://stackoverflow.com/a/40234924"""
|
|
|
|
def hilo(a, b, c):
|
|
if c < b:
|
|
b, c = c, b
|
|
if b < a:
|
|
a, b = b, a
|
|
if c < b:
|
|
b, c = c, b
|
|
return a + c
|
|
|
|
k = hilo(r, g, b)
|
|
return tuple(k - u for u in (r, g, b))
|
|
|
|
color = colormap(0.2)[:3]
|
|
color = [int(255 * v) for v in color]
|
|
color = color_complement(*color)
|
|
return color
|
|
|
|
|
|
def paste_contents(page, contents: Iterable[ContentRectangle]):
|
|
page = deepcopy(page)
|
|
for content in contents:
|
|
paste_content(page, content)
|
|
return page
|
|
|
|
|
|
def paste_content(page, content_box: ContentRectangle):
|
|
assert content_box.content.mode == "RGBA"
|
|
page.paste(content_box.content, (content_box.x1, content_box.y1), content_box.content)
|
|
return page
|
|
|
|
|
|
def get_size_class(rectangle: Rectangle):
|
|
size = get_size(rectangle)
|
|
if size < Size.SMALL.value:
|
|
return Size.SMALL
|
|
elif size < Size.LARGE.value:
|
|
return Size.MEDIUM
|
|
else:
|
|
return Size.LARGE
|
|
|
|
|
|
def get_size(rectangle: Rectangle):
|
|
size = sqrt(area(rectangle))
|
|
return size
|
|
|
|
|
|
class Size(Enum):
|
|
# FIXME: this has to scale with the DPI
|
|
SMALL = 120
|
|
MEDIUM = 180
|
|
LARGE = 300
|