[Tex/LaTex] Time travel in LaTeX

floatspositioningtwo-column

Is it possible to define a previouspage environment, so that whatever is inside it is output on the previous page?

Motivation: In a two-column document, the only way to get a full-width float to be placed on a given page is to put the float on the previous page. At best this is awkward, because it separates the source code of the float from where it logically should be in the tex code. Also, I have an application where my tex file is machine generated, so that doing this is impossible — the program generating the tex code isn't capable of looking at the pdf output and figuring out what landed on what page.

Possible implementation: I was thinking that this might be possible to implement by the following method. Use everyshi to write some kind of hook code on every page. On page 17, the code might say something like \input{page17}. When the previouspage environment is invoked on page 18, it generates a label, and after the document is compiled for the first time, the aux file will say that this label lies on page 18. When latex is run the second time, we read the aux file, we determine that the previouspage environment landed on p. 18, and therefore we write the stuff inside the previouspage environment to page17.tex. On the third compilation, this code is read back in from page17.tex.

A complication in this implementation is that we are hoping the pagination converges to what we want. To get it close to the final pagination on the initial pass, we need to make sure that the floats actually do show up in the document, although probably one page too late. There is no guarantee that the whole process will converge to the desired result after three iterations, or indeed after any number of iterations.

My tex fu is probably insufficient to do anything as fancy as implementing all of the above in pure latex, so if I were to do this myself, I would probably write an external script in some other programming language that would do some of the work. Or maybe something like this already exists…?

Related:

IEEEtran: Placing a two-column figure on desired page

How to set the position of an equation in LaTeX?

Best Answer

EDIT -- After posting the code below, I worked on it some more and got rid of some of the ugliness. Rather than continuing to update this answer, I made a project on github: https://github.com/bcrowell/timetravel

Below is the code for a proof-of-concept implementation of this idea.

The good news:

  1. It accomplishes what I wanted to do for this application. In the sample below, we have a two-column document. A floating full-page-width table occurs in the source code at page 2, and is typeset at the top of page 2.

  2. It should normally converge to a definite result after compiling the document three times. Compiling a fourth or subsequent times should not cause floats to move to different pages.

The bad news:

  1. It's implemented as a separate ruby script that preprocesses the tex source code.

  2. It won't work for the very first page of the document.

  3. The float is inserted at the beginning of the first paragraph that lands on the desired page. To accomplish this, I had to use everyhook to place a hook at the beginning of every paragraph. This would cause an error if the first paragraph on the target page wasn't in outer paragraph mode. To work around this, any material that isn't normal paragraphs has to have \prevpagedisable above it and \prevpageenable below it.

Bad-news item #3 is quite ugly, and that's the most important reason that I would consider this no more than a proof of concept. A usenet post by Donald Aseneau suggests that there is no reliable way for latex code to detect whether it's in outer paragraph mode. The original idea I had when I wrote the question was to get the necessary hook using everyshi or eso-pic, but that doesn't work, because the material typeset by those packages is not in outer paragraph mode.

Sample LaTeX file:

\documentclass[twocolumn]{article}

\usepackage{prevpage}
\usepackage{lipsum}

\begin{document}

\lipsum[1-13]

% begin-prev-page
\begin{table*}
  \begin{tabular}{p{30mm}p{30mm}p{30mm}p{30mm}}
    John & Paul & George & Ringo
  \end{tabular}
\end{table*}
% end-prev-page

\end{document}

Style file:

\RequirePackage{everyhook}

% This is a proof-of-concept package that allows us to implement "time travel" in LaTeX
% by causing a float to be invoked on the page before the page on which its source code
% occurs. This can be used in a two-column document to make a full-page-width float
% show up on the same page as the one on which it was invoked.
% http://tex.stackexchange.com/questions/314257/time-travel-in-latex    

\newcounter{prevpageparctr}% a counter that labels each paragraph in the document sequentially

\newcommand{\inputifitexists}[1]{\IfFileExists{#1.tex}{\input{#1}}{}}
\newcommand{\kirk}{\inputifitexists{prev-page/par\theprevpageparctr}}
\newcommand{\spock}{\ifdim\emergencystretch>0pt{}\kirk\fi}
% Use \ifdim\emergencystretch>0pt to attempt to detect whether we're in outer paragraph
% mode. This won't always work, and in fact doesn't actually seem to work.
% http://comp.text.tex.narkive.com/ttqVg20H/test-for-outer-par-mode

\PushPreHook{par}{\stepcounter{prevpageparctr}\label{prevpagepar\theprevpageparctr}}
\newcommand{\prevpageenable}{\PushPreHook{par}{\spock}}
\newcommand{\prevpagedisable}{\PopPreHook{par}}
\prevpageenable

Ruby code:

#!/usr/bin/ruby

# usage: prev-page.rb foo.tex bar.tex
# Reads foo.tex, writes the preprocessed version to bar.tex.

require 'fileutils'
require 'digest'
require 'json'

$freeze_at_pass = 3
  # Recompiling more than this many times should not change what pages floats land on.
  # This is normally 3, must be at least 2.

def main()
  debug = false
  in_file = ARGV[0]
  out_file = ARGV[1]
  if in_file.nil? then fatal_error("no input file specified") end
  if out_file.nil? then fatal_error("no output file specified") end
  if !(File.exist?(in_file)) then fatal_error("input file #{in_file} does not exist") end
  aux_file = File.basename(out_file, ".tex") + ".aux"
  $temp_dir = "prev-page" # subdirectory of current working directory
  $pass_file = "#{$temp_dir}/pass" # keep track of which pass we're on

  pass = 1
  if File.exist?(aux_file) then
    if !(File.directory?($temp_dir)) then fatal_error("#{aux_file} exists, but directory #{$temp_dir} doesn't") end
    pass = slurp_or_die($pass_file).to_i
    pass = pass+1
  end
  if pass==1 then # make a clean temporary directory
    FileUtils.rm_rf $temp_dir
    Dir.mkdir($temp_dir)
  end
  File.open($pass_file,'w') { |f|  f.print pass}
  if debug then $stderr.print "pass=#{pass}\n" end

  page_numbers = {}
  if pass>=2 then
    if pass<=$freeze_at_pass then 
      get_page_numbers_from_aux_file(aux_file)
      save_page_numbers
    else
      # Try to make sure it converges rather than oscillating indefinitely.
      $aux_invoked,$aux_par = remember_page_numbers()
    end
  end
  File.open(out_file,'w') { |f_out|
    inside = false # are we currently inside or outside of a % begin-prev-page ... % end-prev-page block?
    line_num = 0
    code = '' # if inside a block, start accumulating a copy of the code here
    File.readlines(in_file).each { |line|
      line_num = line_num+1
      if line=~/\s*%\s*begin-prev-page/ then
        if inside then fatal_error("begin-prev-page twice in a row at line #{line_num}") end
        inside = true
        code = "\\prevpagedisable" # Don't place a hook inside the floating content itself.
      end
      if inside then code = code+line end
      if line=~/\s*%\s*end-prev-page/ then
        if !inside then fatal_error("end-prev-page occurs when not inside a prev-page block at line #{line_num}") end
        inside = false
        key = Digest::MD5.hexdigest(code)
        #$stderr.print "hash=#{key}, code=#{code}=\n"
        if pass==1 then
          code_file = "#{$temp_dir}/#{key}.tex"
          File.open(code_file,'w') { |code_f| code_f.print code+"\n\\prevpageenable" }
        end
        if pass>=2 then
          if !$aux_invoked.key?(key) then fatal_error("aux file #{aux_file} doesn't contain key #{key}") end
          page = $aux_invoked[key]
          if page>1 then page=page-1 end
          if pass==2 then
            par = $aux_par[page]
            File.open("#{$temp_dir}/par#{par}.tex",'a') { |f_page| f_page.print "\\input{prev-page/#{key}}"}
          end
        end
        f_out.print "\\label{prevpageinvoked#{key}}" # This will be immediately followed by the % end-prev-page.
      end
      if pass<2 || !inside then f_out.print line end
           # If pass is 2 or greater, don't duplicate the content of the block.
    }
    if inside then fatal_error("begin-prev-page ended at end of file") end
  }
end

def save_page_numbers
  File.open("#{$temp_dir}/freeze_aux_invoked.json",'w') { |f|
    f.print JSON.generate($aux_invoked)
  }
  File.open("#{$temp_dir}/freeze_aux_par.json",'w') { |f|
    f.print JSON.generate($aux_par)
  }
end

def remember_page_numbers
  return [
    get_json_data_from_file_or_die("#{$temp_dir}/freeze_aux_invoked.json"),
    get_json_data_from_file_or_die("#{$temp_dir}/freeze_aux_par.json")
  ]
end

# Initializes $aux_invoked and $aux_par.
# Lines in aux file look like this: 
#   \newlabel{prevpageinvoked226d375a2efab58c0ff60b659a2b5e70}{{}{2}}
#   \newlabel{prevpagepar14}{{}{2}}
def get_page_numbers_from_aux_file(aux_file)
  $aux_invoked = {} # key=hash, value=page
  $aux_par = {}     # key=page, value=paragraph number
  File.readlines(aux_file).each { |line|
    if line=~/\\newlabel{([^}]+)}{{([^}]*)}{([^}]+)}}/ then
      label,number,page = $1,$2,$3.to_i
      if label=~/\Aprevpage(invoked|par)([^}]*)/ then
        type,key=$1,$2
        if type=="invoked" then $aux_invoked[key]=page end
        if type=="par" then
          if $aux_par.key?(page) then
            if key<$aux_par[page] then $aux_par[page]=key end
          else
            $aux_par[page] = key
          end
        end
      end
    end
  }
end

def fatal_error(message)
  $stderr.print "generate_problems.rb: #{$verb} fatal error: #{message}\n"
  exit(-1)
end

def warning(message)
  $stderr.print "generate_problems.rb: #{$verb} warning: #{message}\n"
end

def get_json_data_from_file_or_die(file)
  r = slurp_file_with_detailed_error_reporting(file)
  if !(r[1].nil?) then fatal_error(r[1]) end
  return parse_json_or_die(r[0])
end

def parse_json_or_die(json)
  begin
    return JSON.parse(json) # use minifier to get rid of comments
  rescue JSON::ParserError
    fatal_error("syntax error in JSON string '#{json}'")
  end
end

# returns contents or nil on error; for more detailed error reporting, see slurp_file_with_detailed_error_reporting()
def slurp_file(file)
  x = slurp_file_with_detailed_error_reporting(file)
  return x[0]
end

def slurp_or_die(file)
  x = slurp_file_with_detailed_error_reporting(file)
  x = x[0]
  if x.nil? then fatal_error("file #{file} not found") end
  return x
end

# returns [contents,nil] normally [nil,error message] otherwise
def slurp_file_with_detailed_error_reporting(file)
  begin
    File.open(file,'r') { |f|
      t = f.gets(nil) # nil means read whole file
      if t.nil? then t='' end # gets returns nil at EOF, which means it returns nil if file is empty
      return [t,nil]
    }
  rescue
    return [nil,"Error opening file #{file} for input: #{$!}."]
  end
end

main()

Makefile:

default:
    make clean
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2
    prev-page.rb a.tex a2.tex
    pdflatex a2

clean:
    rm -f *.aux
    rm -Rf prev-page
Related Question