Reputation: 3
I'm trying to combine a list of images on a page, with a given height, so that they flow first down the page, then across, e.g.:
Image1 Image4 Image7
Image2 Image5 Image8
Image3 Image6 Image9
The maximum number of columns is 3. The issue is that these images are passed in dynamically, but all have a fixed width i.e. they can only span 1/2/3 columns so something like this can happen:
Image1 Image4-Image4
Image2-Image2-Image2
Image3 Image5 Image6
Also, the height of each image in my list is variable, meaning that there isn't a set number of rows. So if an image were to exceed the page or overlap another image. it has to also be stored for later. Blank spaces are allowed, e.g.:
Image1 Image4
Image2-Image2 Image4
Image2-Image2 Image5
In the example above Image3 took up the entire page, or maybe ~3/4 of the page, meaning it wouldn't fit, so it is kept for another new page.
How can I achieve this method of combining images?
Upvotes: 0
Views: 606
Reputation: 11179
You have quite a complex set of rules. I think you'll probably need to write some code if you want that exact behaviour.
I'm trying to avoid doing any actual work, so I wrote a little python to lay out pages for you:
#!/usr/bin/python3
import sys
import math
import pyvips
column_width = 256
row_height = 256
background_colour = 255 # you could use eg. [128, 255, 128] as well
# pop enough tiles from the argument to fill a page
def layout(tiles):
# we insert an image (at its top-left), or "x" if a tile is covered by an
# image up and left of itself
page = [[None for x in range(3)] for y in range(3)]
# where we put the next tile
current_x = 0
current_y = 0
# loop to fill page
page_filled = False
while not page_filled:
# used up all the tiles?
if tiles == []:
break
this_tile = tiles[0]
tiles_across = math.ceil(this_tile.width / column_width)
tiles_down = math.ceil(this_tile.height / row_height)
# image too large for page
if tiles_across > 3 or tiles_down > 3:
raise Exception(f"tile too large - {this_tile}")
# loop to find the next free space this tile fits
while True:
# too tall for this column?
if tiles_down > 3 - current_y:
current_y = 0
current_x += 1
# too wide for this row?
if tiles_across > 3 - current_x:
# we've filled the page
page_filled = True
break
# is this set of tiles clear?
all_clear = True
for y in range(tiles_down):
for x in range(tiles_across):
if page[current_y + y][current_x + x]:
all_clear = False
if all_clear:
break
# try the next slot down
current_y += 1
# did we find a spot?
if not page_filled:
# place the tile here and mark the spaces it covers in the page
for y in range(tiles_down):
for x in range(tiles_across):
page[current_y + y][current_x + x] = "x"
page[current_y][current_x] = this_tile
tiles.pop(0)
# the page has filled -- draw!
image = pyvips.Image.black(3 * column_width, 3 * row_height) \
+ background_colour
for y in range(3):
for x in range(3):
if isinstance(page[y][x], pyvips.Image):
image = image.insert(page[y][x], \
x * column_width, y * row_height)
return image
# a source of tiles .. we just load the command-line arguments
all_tiles = [pyvips.Image.new_from_file(filename, access="sequential")
for filename in sys.argv[1:]]
page_number = 0
while all_tiles != []:
filename = f"page-{page_number}.jpg"
print(f"generating {filename} ...")
page = layout(all_tiles)
page.write_to_file(filename)
page_number += 1
You can run it like this:
$ ./layout.py ~/pics/shark.jpg ~/pics/k2.jpg ~/pics/shark.jpg ~/pics/shark.jpg ~/pics/shark.jpg ~/pics/shark.jpg ~/pics/shark.jpg ~/pics/shark.jpg
generating page-0.jpg ...
generating page-1.jpg ...
$
It seems to work for me and implements all your rules (I think). I used pyvips to do the page rendering because I'm familiar with it, but it'd be simple to swap it out for something else.
Upvotes: 1
Reputation: 53089
If you do not care the order of the images in the result nor the grid numbers and just want the best fit for a given output size, then ImageMagick has a new feature for doing that. See ASHLAR:ouput.png at https://imagemagick.org/script/formats.php#pseudo
magick lena.jpg barn.jpg mandril3.jpg monet2.jpg zelda1.jpg redhat.jpg -background black -define ashlar:best-fit=true ASHLAR:x.png[600x400+0+0]
Upvotes: 1
Reputation: 53089
You can make ImageMagick put the images vertically first in montage by a trick. Transpose the images first, montage, then transpose the result.
convert logo3.jpg lena2.jpg hatching.jpg zelda3.jpg -transpose miff:- |\
montage - -geometry +2+2 -tile 2x2 miff:- |\
convert - -transpose montage_columns.jpg
Upvotes: 1
Reputation: 53089
You can do that in Python as a subprocess call to ImageMagick montage as follows:
import subprocess
cmd = '/usr/local/bin/montage lena.jpg house.jpg boats3_small.jpg barn.jpg cameraman.jpg mandril3.jpg monet2.jpg zelda1.jpg redhat.jpg -tile 3x3 -geometry +0+0 -gravity north -background black montage.jpg'
subprocess.call(cmd, shell=True)
Upvotes: 0