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:
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.
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:
It's implemented as a separate ruby script that preprocesses the tex source code.
It won't work for the very first page of the document.
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
placeinswith its float barriers might help you there. Also, if you properly reference your figure in the text it is unimportant on which page it will end up. – Henri Menke Jun 11 '16 at 20:28dblfloatfix– Henri Menke Jun 11 '16 at 20:30