[Tex/LaTex] Create a “yearbook-style” multi-page layout of photos

automationboxesgraphicsmarginspositioning

I would like to create a "yearbook-style" layout of photos in LaTeX. Each photo would have a caption and the objective would be to automatically layout the differently sized rectangular photos to minimize the whitespace on the page (with a mandatory margin around each photo and caption). If I could do this with a browser as well that would also be helpful, but that is a question for a different forum. I would have expected this problem to be solved problematically with some kind of open sourced package but I have found none.

enter image description here

Survey of 2D packing algorithms

Best Answer

This is not complete answer to this question, it's rather showcase of idea I had about generative layouts and luatex. I don't think that any of following examples is useful as is, but maybe in can serve as basis for more interesting solutions in the future, maybe something like this.

My idea is following: we have LaTeX command that stores it's content in a box ant then add list id and dimensions to the list.

\ProvidesPackage{generativelayout}
\RequirePackage{luacode}

\luaexec{%
generators = require('generativelayout')
genboxes = {}
curr_box_id =  2222
function findFreeBox(id)
  if type(tex.box[id]) == 'nil' then
    return id
  else
    return findFreeBox(id + 1)
  end
end
gen = generators.getGenerator("default")
}

\newcommand\gensavebox[1]{%
\setbox0=\hbox{\fbox{#1}}
\luaexec{%
local box = tex.box[0] 
curr_box_id= findFreeBox(curr_box_id)
tex.setbox('global',curr_box_id, node.copy_list(box))
table.insert(genboxes,{width=box.width / 65536 ,height=(box.height / 65536 + box.depth / 65536),box=curr_box_id})
curr_box_id = curr_box_id + 1
}
%\box0
}

Next we have environment genlayout that selects layout algorithm and run it on list of boxes:

\newenvironment{genlayout}[1][]{%
\luaexec{%
local gen_name = "\luatexluaescapestring{#1}" 
if gen_name == "" then gen_name = "default" end
gen = generators.getGenerator(gen_name)
gen:init()
}
}{%
\luaexec{%
gen:run(genboxes)
}
}

This package can be used like this:

\documentclass{article}
\usepackage{lmodern,generativelayout}
\begin{document}
\pagestyle{empty}
\begin{genlayout}[]
\gensavebox{%
Hello world
}
\gensavebox{%
\begin{tabular}{l l}
hello & world\\
second & line\\
\end{tabular}
}
\gensavebox{%
\begin{minipage}{.3\textwidth}
\begin{itemize}
\item First
\item Second
\end{itemize}
\end{minipage}
}
\gensavebox{%
little bit smaller box
}
\gensavebox{totaly small box}
\gensavebox{azsvm}
\end{genlayout}

\end{document}

Now the lua library generativelayout.lua

module(...,package.seeall)

generators = {}
empty_generator = {init = function() end, run = function() end}

function getGenerator(name)
  return generators[name] or empty_generator
end

printBox = function(self,box_number)
  tex.print('\\fbox{\\copy'..box_number.."}")
end    

generators.default = {
  init = empty_generator.init,
  printer = printBox,
  run = function(self,boxes)
    for k, v in pairs(boxes) do
       self:printer(v.box)
    end
  end 
}

Every generator is added as object with methods run and init, which are called form genlayout environment. Generator can be selected with optional parameter of genlayout, if empty, default is selected. The output is following:

enter image description here

We can sort the boxes:

generators.bigToSmall = {
  init = generators.default.init,
  printer = printBox,
  run = function(self,boxes)
    table.sort(boxes,function(a, b) return a.height > b.height end)
    generators.default.run(self,boxes)
  end
}

You can use functions from other generators, as generators.default.run(self,boxes) enter image description here

More funny example of putting boxes on random places on the page:

generators.randomize = {
  width = 150,
  height = 150,
  init = generators.default.init,
  printer = function(self,box,x,y) 
    tex.print("\\put(",x,",",y,"){\\copy",box,"}")
  end,
  run = function(self,boxes)
    tex.print("\\setlength\\unitlength{1pt}")
    tex.print("\\begin{picture}("..self.width..","..self.height..")")
    math.randomseed( os.time() )
    for k,v in pairs(boxes) do
      local x = math.random(self.width - v.width) 
      local y = math.random(self.height - v.height)
      self:printer(v.box,x,y)
    end  
    tex.print("\\end{picture}")
  end
}

Here we use trick with picture environment to place things on exact place on the page enter image description here

And finally, my port of http://codeincomplete.com/posts/2011/5/7/bin_packing/ to lua:

generators.binPack = {
  root = {},
  init = function(self) self.root = {x=0,y=0,w=280,h=250} end,
  printer = generators.randomize.printer,
  fit = function(self,boxes)
    local node
    for _,block in ipairs(boxes) do
        print("Velikost boxu",block.width,block.height)
        node = self:findNode(self.root, block.width,block.height)
        if node then
          block.fit = self:splitNode(node, block.width, block.height)
          end
    end
  end,
  findNode = function(self,root,w,h)
     if root.used then
       local right = self:findNode(root.right,w,h)
       if right then 
         return right 
       else 
         return  self:findNode(root.down,w,h)
      end
     elseif w <= root.w and h <= root.h then
       return root 
     else 
       return nil
     end  
  end,
  splitNode = function (self,node,w,h)
     print("Split box: ",node.x,node.y,node.w,node.h,w,h)
     node.used = true
     node.down  = { x= node.x,     y= node.y + h, w= node.w,     h= node.h - h }
     node.right = { x= node.x + w, y= node.y,     w= node.w - w, h= h}
     return node
  end,
  run = function(self,boxes)
    --generators.randomize.run(self,boxes)
    table.sort(boxes,function(a, b) return a.height > b.height end)
    tex.print("\\setlength\\unitlength{1pt}")
    tex.print("\\begin{picture}("..self.root.w..","..self.root.h..")")
    self:fit(boxes)
    for _,v in ipairs(boxes) do
      if v.fit then
        x= v.fit.x  
        y= self.root.h-v.fit.y  
        self:printer(v.box,x,y)
      end
    end
    tex.print("\\end{picture}")
  end
}

I don't think the result is really nice and I think there is some error in placing boxes on their exact place, but maybe someone will like it:

enter image description here

Related Question