"Fossies" - the Fresh Open Source Software Archive

Member "PhotoCollage-1.4.5/photocollage/collage.py" (9 Jul 2021, 18151 Bytes) of package /linux/privat/PhotoCollage-1.4.5.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "collage.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 1.4.4_vs_1.4.5.

    1 # Copyright (C) 2014 Adrien Vergé
    2 #
    3 # This program is free software; you can redistribute it and/or modify
    4 # it under the terms of the GNU General Public License as published by
    5 # the Free Software Foundation; either version 2 of the License, or
    6 # (at your option) any later version.
    7 #
    8 # This program is distributed in the hope that it will be useful,
    9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   11 # GNU General Public License for more details.
   12 #
   13 # You should have received a copy of the GNU General Public License along
   14 # with this program; if not, write to the Free Software Foundation, Inc.,
   15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
   16 
   17 import random
   18 
   19 """
   20 Summary of collage objects:
   21 
   22 ----------------------
   23 |                    |
   24 |                    |
   25 |       Page         |       The "Page" object represents the whole page that
   26 |                    |       will give the final assembled image.
   27 |                    |
   28 ----------------------
   29 
   30 ----------------------
   31 |      |      |      |
   32 |      |Column|      |
   33 |      |      |Column|       A page is divided into columns.
   34 |Column|      |      |
   35 |      |      |      |
   36 ----------------------
   37 
   38 ----------------------
   39 | Cell | Cell        |                   Each column contains cells. When a
   40 |------|          x--|----- CellExtent   is located in several columns,
   41 | Cell |-------------|                   its "extended" flag is set, and a
   42 |------| Cell |      |                   CellExtent object is added to the
   43 | Cell |      | Cell |                   column on the right to reserve the
   44 ----------------------                   place.
   45 
   46       ,------> Photo
   47      /          ,---------> Photo
   48 ----------------------
   49 |      |             |                   Each cell is associated
   50 |------|             |                   to a photo.
   51 |      |-------------|
   52 |------|      |      |----> Photo
   53 |      |      |      |
   54 ----------------------
   55              `----------> Photo
   56 
   57 The layout placing process is divided in three phases.
   58 
   59 Phase A: Fill columns with photos.
   60   Photos are added to columns, one by one, until there are no more photos.
   61   Each new photo is put in the smallest column, so as to have balanced
   62   columns. If two columns have approximately the same height, a photo can be
   63   "extended" to fit two columns. In this case, the "Cell" object is put in
   64   the first column, and the second column takes a "CellExtent" object to
   65   reserve the taken space.
   66 
   67 Phase B: Set all columns to same height.
   68   A global common height is computed for all columns, and every of them is
   69   stressed or extended to this common length. This can result in a decrease
   70   or increase in columns' width.
   71 
   72 Phase C: Adapt columns' width.
   73   Since cells in a column may have different widths, each column width is set
   74   to the smallest width amongst its images.
   75 
   76 """
   77 
   78 
   79 class Photo:
   80     def __init__(self, filename, w, h, orientation=0):
   81         self.filename = filename
   82         self.w = w
   83         self.h = h
   84         self.orientation = orientation
   85         self.offset_w = 0.5
   86         self.offset_h = 0.5
   87 
   88     @property
   89     def ratio(self):
   90         return float(self.h) / float(self.w)
   91 
   92     def move(self, x, y):
   93         self.offset_w = self.calculate_new_offset(self.offset_w, x)
   94         self.offset_h = self.calculate_new_offset(self.offset_h, y)
   95 
   96     @staticmethod
   97     def calculate_new_offset(offset, value):
   98         new_offset = offset + value
   99         if new_offset < 0:
  100             new_offset = 0
  101         elif new_offset > 1:
  102             new_offset = 1
  103         return new_offset
  104 
  105 
  106 class Cell:
  107     """Represents a cell in a column
  108 
  109     Properties:
  110     <- x -><- w ->
  111     ---------------------- ^
  112     |      |             | |y
  113     |------|             | |
  114     |      |-------------| v
  115     |------| Cell |      | ^
  116     |      |      |      | |h
  117     ---------------------- v
  118 
  119     """
  120     def __init__(self, parents, photo):
  121         self.parents = parents
  122         self.photo = photo
  123         self.extent = None
  124         self.h = self.w * self.wanted_ratio
  125 
  126     def __repr__(self):
  127         """Representation of the cell in ASCII art"""
  128         end = "]"
  129         if self.extent is not None:
  130             end = "--"
  131         return "[%d %d%s" % (self.w, self.h, end)
  132 
  133     @property
  134     def x(self):
  135         return self.parents[0].x
  136 
  137     @property
  138     def y(self):
  139         """Returns the cell's y coordinate
  140 
  141         It assumes that the cell is in a single column, so it is the previous
  142         cell's y + h.
  143 
  144         """
  145         prev = None
  146         for c in self.parents[0].cells:
  147             if self is c:
  148                 if prev:
  149                     return prev.y + prev.h
  150                 return 0
  151             prev = c
  152 
  153     @property
  154     def w(self):
  155         return sum(c.w for c in self.parents)
  156 
  157     @property
  158     def ratio(self):
  159         return self.h / self.w
  160 
  161     @property
  162     def wanted_ratio(self):
  163         return self.photo.ratio
  164 
  165     def scale(self, alpha):
  166         self.h *= alpha
  167 
  168     def is_extended(self):
  169         return hasattr(self, 'extent') and self.extent is not None
  170 
  171     def is_extension(self):
  172         return isinstance(self, CellExtent)
  173 
  174     def content_coords(self):
  175         """Returns the coordinates of the contained image
  176 
  177         These are computed in order not to loose space, so the content area
  178         will always be greater than the cell itself. It is the space taken by
  179         the contained image if it wasn't cropped.
  180 
  181         """
  182         # If the contained image is too thick to fit
  183         if self.wanted_ratio < self.ratio:
  184             h = self.h
  185             w = self.h / self.wanted_ratio
  186             y = self.y
  187             x = self.x - (w - self.w) / 2.0
  188         # If the contained image is too tall to fit
  189         elif self.wanted_ratio > self.ratio:
  190             w = self.w
  191             h = self.w * self.wanted_ratio
  192             x = self.x
  193             y = self.y - (h - self.h) / 2.0
  194         else:
  195             w = self.w
  196             h = self.h
  197             x = self.x
  198             y = self.y
  199         return x, y, w, h
  200 
  201     def top_neighbor(self):
  202         """Returns the cell above this one"""
  203         prev = None
  204         for c in self.parents[0].cells:
  205             if self is c:
  206                 return prev
  207             prev = c
  208 
  209     def bottom_neighbor(self):
  210         """Returns the cell below this one"""
  211         prev = None
  212         for c in reversed(self.parents[0].cells):
  213             if self is c:
  214                 return prev
  215             prev = c
  216 
  217 
  218 class CellExtent(Cell):
  219     def __init__(self, cell):
  220         self.origin = cell
  221         self.origin.extent = self
  222 
  223     def __repr__(self):
  224         """Representation of the cell in ASCII art"""
  225         return "------]"
  226 
  227     @property
  228     def parents(self):
  229         return (self.origin.parents[1],)
  230 
  231     @property
  232     def photo(self):
  233         return self.origin.photo
  234 
  235     @property
  236     def y(self):
  237         return self.origin.y
  238 
  239     @property
  240     def h(self):
  241         return self.origin.h
  242 
  243     def scale(self, alpha):
  244         pass
  245 
  246 
  247 class Column:
  248     """Represents a column in a page
  249 
  250     Properties:
  251     <----- x ----><-- w ->
  252     ---------------------- ^
  253     |      |      |      | |
  254     |      |      |      |
  255     |      |      |Column| h
  256     |      |      |      |
  257     -------|      |      | |
  258            |      |------- v
  259            --------
  260 
  261     """
  262     def __init__(self, parent, w):
  263         self.parent = parent
  264         self.cells = []
  265         self.w = w
  266 
  267     def __repr__(self):
  268         """Representation of the column in ASCII art"""
  269         return "\n".join(c.__repr__() for c in self.cells)
  270 
  271     @property
  272     def h(self):
  273         """Returns the column's total height
  274 
  275         This is not simply the sum of its cells heights, because there can be
  276         empty spaces between cells.
  277 
  278         """
  279         if not self.cells:
  280             return 0
  281         return self.cells[-1].y + self.cells[-1].h
  282 
  283     @property
  284     def x(self):
  285         x = 0
  286         for c in self.parent.cols:
  287             if self is c:
  288                 break
  289             x += c.w
  290         return x
  291 
  292     def scale(self, alpha):
  293         self.w *= alpha
  294         for c in self.cells:
  295             c.scale(alpha)
  296 
  297     def left_neighbor(self):
  298         """Returns the column on the left of this one"""
  299         prev = None
  300         for c in self.parent.cols:
  301             if self is c:
  302                 return prev
  303             prev = c
  304 
  305     def right_neighbor(self):
  306         """Returns the column on the right of this one"""
  307         prev = None
  308         for c in reversed(self.parent.cols):
  309             if self is c:
  310                 return prev
  311             prev = c
  312 
  313     def adjust_height(self, target_h):
  314         """Set the column's height to a given value by resizing cells"""
  315         # First, make groups of "movable" cells. Since cell extents are not
  316         # movable, these groups only contain pure cell objects. We only resize
  317         # those groups.
  318         class Group:
  319             def __init__(self, y):
  320                 self.y = y
  321                 self.h = 0
  322                 self.cells = []
  323 
  324         groups = []
  325         groups.append(Group(0))
  326         for c in self.cells:
  327             # While a cell extent is not reached, keep add cells to the group
  328             if not c.is_extension():
  329                 groups[-1].cells.append(c)
  330             else:
  331                 # Close current group and create a new one
  332                 groups[-1].h = c.y - groups[-1].y
  333                 groups.append(Group(c.y + c.h))
  334         groups[-1].h = target_h - groups[-1].y
  335 
  336         # Adjust height for each group independently
  337         for group in groups:
  338             if not group.cells:
  339                 continue
  340             alpha = group.h / sum(c.h for c in group.cells)
  341             for c in group.cells:
  342                 c.h = c.h * alpha
  343 
  344 
  345 class Page:
  346     """Represents a whole page
  347 
  348     Properties:
  349     <-------- w -------->
  350     ---------------------- ^
  351     |                    | |
  352     |                    |
  353     |        Page        | h
  354     |                    |
  355     |                    | |
  356     ---------------------- v
  357 
  358     """
  359     def __init__(self, w, target_ratio, no_cols):
  360         self.target_ratio = target_ratio
  361         col_w = float(w)/no_cols
  362         self.cols = []
  363         for i in range(no_cols):
  364             self.cols.append(Column(self, col_w))
  365 
  366     def __repr__(self):
  367         """Representation of the page in ASCII art
  368 
  369         Returns something like:
  370         [62 52]    [125 134-- ------]    [62 87]
  371         [62 47]    [62 66]    [125 132-- [62 45]
  372         [62 46]    ------]    [62 49]    ------]
  373         [62 78]    ------]    [62 49]    [62 45]
  374         [125 102-- ------]    [62 49]    [62 65]
  375         [125 135--            [62 85]    [62 53]
  376         [125 91--             [125 89--  [62 64]
  377                                  ------]
  378         """
  379         lines = []
  380         n = 0
  381         end = False
  382         while not end:
  383             lines.append("")
  384             end = True
  385             for col in self.cols:
  386                 cells = col.__repr__().split("\n")
  387                 w = max(len(cell) for cell in cells)
  388                 if col != self.cols[-1]:
  389                     w += 1
  390                 cell = w * " "
  391                 if n < len(cells):
  392                     cell = cells[n] + (w - len(cells[n])) * " "
  393                     if n < len(cells) - 1:
  394                         end = False
  395                 lines[-1] += cell
  396             n += 1
  397         return "\n".join(lines)
  398 
  399     @property
  400     def no_cols(self):
  401         return len(self.cols)
  402 
  403     @property
  404     def w(self):
  405         return sum(c.w for c in self.cols)
  406 
  407     @property
  408     def h(self):
  409         return max(c.h for c in self.cols)
  410 
  411     @property
  412     def ratio(self):
  413         return self.h / self.w
  414 
  415     def scale(self, alpha):
  416         for c in self.cols:
  417             c.scale(alpha)
  418 
  419     def scale_to_fit(self, max_w, max_h=None):
  420         if max_h is None or self.w * max_h > self.h * max_w:
  421             self.scale(max_w / self.w)
  422         else:
  423             self.scale(max_h / self.h)
  424 
  425     def next_free_col(self):
  426         """Returns the column with lowest height"""
  427         minimum = min(c.h for c in self.cols)
  428         candidates = []
  429         for c in self.cols:
  430             if c.h == minimum:
  431                 candidates.append(c)
  432         return random.choice(candidates)
  433 
  434     def add_cell_single_col(self, col, photo):
  435         col.cells.append(Cell((col,), photo))
  436 
  437     def add_cell_multi_col(self, col1, col2, photo):
  438         cell = Cell((col1, col2), photo)
  439         extent = CellExtent(cell)
  440         col1.cells.append(cell)
  441         col2.cells.append(extent)
  442 
  443     def add_cell(self, photo):
  444         """Add a new cell in the best computed place
  445 
  446         If possible, and if it's worth, make a "multiple-column" cell.
  447 
  448         """
  449         col = self.next_free_col()
  450         left = col.left_neighbor()
  451         right = col.right_neighbor()
  452         if 2 * random.random() > photo.ratio:
  453             if left and abs(col.h - left.h) < 0.5 * col.w:
  454                 return self.add_cell_multi_col(left, col, photo)
  455             elif right and abs(col.h - right.h) < 0.5 * col.w:
  456                 return self.add_cell_multi_col(col, right, photo)
  457 
  458         self.add_cell_single_col(col, photo)
  459 
  460     def remove_empty_cols(self):
  461         i = 0
  462         while i < len(self.cols):
  463             if len(self.cols[i].cells) == 0:
  464                 self.cols.pop(i)
  465             else:
  466                 i += 1
  467 
  468     def remove_bottom_holes(self):
  469         """Remove holes created by extended cells
  470 
  471         Example (case A):
  472         The bottom-right cell should be extended to fill the hole.
  473         ----------------------             ----------------------
  474         |      |      |      |             |      |      |      |
  475         |      |-------------|             |      |-------------|
  476         |------|             |             |------|             |
  477         |      |--------------             |      |--------------
  478         |      |      |  ^                 |      |  ^   |      |
  479         --------------- hole               -------- hole --------
  480 
  481         Example (case B):
  482         The bottom cell should be moved under the other extended cell.
  483         ----------------------             ----------------------
  484         |      |      |      |             |      |      |      |
  485         |------|-------------|             |-------------|------|
  486         |      |             |             |             |      |
  487         |---------------------             ---------------------|
  488         |             |   <-- hole      hole ->   |             |
  489         ---------------                           ---------------
  490 
  491         """
  492         for col in self.cols:
  493             cell = col.cells[-1]
  494             if cell == col.cells[0]:
  495                 continue
  496 
  497             # Case A
  498             # If cell is not extended, is below an extended cell and has no
  499             # neighbour under the latter, it should be extended.
  500             if not cell.is_extended() and not cell.is_extension():
  501                 # Case A1
  502                 if cell.top_neighbor().is_extended() \
  503                         and cell.top_neighbor().extent \
  504                         .bottom_neighbor() is None:
  505                     # Extend cell to right
  506                     extent = CellExtent(cell)
  507                     col.right_neighbor().cells.append(extent)
  508                     cell.parents = (col, col.right_neighbor())
  509                 # Case A2
  510                 elif cell.top_neighbor().is_extension() \
  511                         and cell.top_neighbor().origin \
  512                         .bottom_neighbor() is None:
  513                     # Extend cell to left
  514                     col.cells.remove(cell)
  515                     col.left_neighbor().cells.append(cell)
  516                     extent = CellExtent(cell)
  517                     col.cells.append(extent)
  518                     cell.parents = (col.left_neighbor(), col)
  519             # Case B
  520             # If cell is extended and one of the cells above is extended too,
  521             # the bottom cell should be placed right below the top one.
  522             elif cell.is_extended() and cell.extent.bottom_neighbor() is None:
  523                 # Case B1
  524                 if cell.extent.top_neighbor().is_extended() \
  525                         and cell.extent.top_neighbor().extent \
  526                         .bottom_neighbor() is None:
  527                     # Move cell to right
  528                     col.cells.remove(cell)
  529                     col.right_neighbor().cells.remove(cell.extent)
  530                     col.right_neighbor().cells.append(cell)
  531                     col.right_neighbor().right_neighbor().cells \
  532                         .append(cell.extent)
  533                     cell.parents = (col.right_neighbor(),
  534                                     col.right_neighbor().right_neighbor())
  535                 # Case B2
  536                 elif cell.top_neighbor().is_extension() \
  537                         and cell.top_neighbor().origin \
  538                         .bottom_neighbor() is None:
  539                     # Move cell to left
  540                     col.cells.remove(cell)
  541                     col.right_neighbor().cells.remove(cell.extent)
  542                     col.left_neighbor().cells.append(cell)
  543                     col.cells.append(cell.extent)
  544                     cell.parents = (col.left_neighbor(), col)
  545 
  546     def adjust_cols_heights(self):
  547         """Set all columns' heights to same value by shrinking them"""
  548         target_h = self.w * self.target_ratio
  549         for c in self.cols:
  550             c.adjust_height(target_h)
  551 
  552     def adjust(self):
  553         self.remove_empty_cols()
  554         self.remove_bottom_holes()
  555         self.adjust_cols_heights()
  556 
  557     def get_cell_at_position(self, x, y):
  558         for col in self.cols:
  559             if x >= col.x and x < col.x + col.w:
  560                 for cell in col.cells:
  561                     if y >= cell.y and y < cell.y + cell.h:
  562                         if cell.is_extension():
  563                             return cell.origin
  564                         return cell
  565         return None
  566 
  567     def swap_photos(self, cell1, cell2):
  568         cell1.photo, cell2.photo = cell2.photo, cell1.photo