commit ad0f4eabb612e03b50e4f0b5ac72ffab1634bb96
Author: u-## <u.numbernumber@gmail.com>
Date:   Sat Mar 19 21:41:43 2011 -0500

    Add ShiPainter animation support
    
    Adds support for saving and viewing Shi Painter PCH animation files in
    the oekaki extra. The PCH files are saved in the same dir as images,
    and can be viewed with the pchview.html. The PCH viewer requires
    JavaScript, because otherwise we'd have to go through CGI for every
    viewer request (to set the PCH filename applet parameter), which seems
    ridiculous.
    
    This adds two directives to oekaki_config.pl:
    
     - OEKAKI_ENABLE_ANIM: Set to a true or false value to enable or
       disable the PCH animation functionality entirely.
    
     - OEKAKI_ANIM_MAX: Set to the maxiumum file size (in KiB) that you
       want the PCH files to grow to. This is handled by a parameter
       passed to Shi Painter, but is also checked by wakaba in getpic.pl
       when the animation data is uploaded.
    
    You also need a few more files for the viewer. Namely: PCHViewer.jar,
    res.zip, and tt.zip. These go in the same directory as
    spainter_all.jar (typically the board root), and you can get them from
    the same site as Shi Painter (they're in the "viewer" zip file).
    
    You also need to rebuild caches on any Oekaki-enabled board, in order
    to get the "animation" tickybox.

diff --git a/extras/oekaki/finish.pl b/extras/oekaki/finish.pl
index 97a0cb4..eb28550 100755
--- a/extras/oekaki/finish.pl
+++ b/extras/oekaki/finish.pl
@@ -51,11 +51,15 @@ $oek_ip=$ip unless($oek_ip);
 die unless($oek_ip=~/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/);
 
 my $tmpname=TMP_DIR.$oek_ip.'.png';
+my $tmpname_pch=TMP_DIR.$oek_ip.'.pch';
 
 if(!$task)
 {
 	my $oek_parent=$query->param("oek_parent");
 	my $srcinfo=$query->param("srcinfo");
+	my $animated=$query->param("animated");
+	if (!OEKAKI_ENABLE_ANIM) { $animated = undef; }
+	$animated = $tmpname_pch if ($animated);
 
 	make_http_header();
 
@@ -64,7 +68,8 @@ if(!$task)
 		oek_parent=>clean_string($oek_parent),
 		oek_ip=>$oek_ip,
 		srcinfo=>clean_string($srcinfo),
-		decodedinfo=>OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo)),
+		decodedinfo=>OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo, $animated)),
+		animated=>$animated,
 	);
 }
 elsif($task eq "post")
@@ -79,13 +84,26 @@ elsif($task eq "post")
 	my $password=$query->param("password");
 	my $captcha=$query->param("captcha");
 	my $srcinfo=$query->param("srcinfo");
+	my $animated=$query->param("animated");
+	if (!OEKAKI_ENABLE_ANIM) { $animated = undef; }
 
 	$ENV{SCRIPT_NAME}=~s/\w+\.pl$/wakaba.pl/;
 
+	if ($animated) {
+		my $anim_file;
+		open $anim_file,$tmpname_pch or die "Can't read pch file";
+
+		my ($pch_newname,$pch_md5,$pch_width,$pch_height,$pch_thumb,$pch_twidth,$pch_theight) =
+			process_file($anim_file,"internal_animated.pch",time(),1);
+
+		$animated = $pch_newname;
+		unlink $tmpname_pch;
+	}
+
 	open TMPFILE,$tmpname or die "Can't read uploaded file";
 
 	post_stuff($parent,$name,$email,$subject,$comment,\*TMPFILE,$tmpname,$password,
-	0,$captcha,ADMIN_PASS,0,0,OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo)));
+	0,$captcha,ADMIN_PASS,0,0,OEKAKI_INFO_TEMPLATE->(decode_srcinfo($srcinfo, $animated)));
 
 	unlink $tmpname;
 }
@@ -111,9 +129,9 @@ sub expand_filename($)
 	return $self_path.$filename;
 }
 
-sub decode_srcinfo($)
+sub decode_srcinfo($;$)
 {
-	my ($srcinfo)=@_;
+	my ($srcinfo,$animated)=@_;
 	my @info=split /,/,$srcinfo;
 	my @stat=stat $tmpname;
 	my $fileage=$stat[9];
@@ -123,6 +141,7 @@ sub decode_srcinfo($)
 		time=>clean_string(pretty_age($fileage-$info[0])),
 		painter=>clean_string($$painter{name}),
 		source=>clean_string($info[2]),
+		anim=>$animated,
 	);
 }
 
diff --git a/extras/oekaki/getpic.pl b/extras/oekaki/getpic.pl
old mode 100644
new mode 100755
index e5e0b5d..015ee02
--- a/extras/oekaki/getpic.pl
+++ b/extras/oekaki/getpic.pl
@@ -5,12 +5,20 @@ use CGI::Carp qw(fatalsToBrowser);
 use strict;
 
 use lib '.';
+BEGIN { require 'config.pl'; }
+BEGIN { require 'config_defaults.pl'; }
 BEGIN { require 'oekaki_config.pl'; }
 
 my $metadata=<STDIN>;
 my $oek_ip;
+my $image_len = -1;
 
-if($metadata=~/^S[0-9]{8}(.*)[0-9]{8}$/) { $oek_ip=$1; }
+if($metadata=~/^S[0-9]{8}(.*)([0-9]{8})\r?$/) {
+	$oek_ip = $1;
+	$image_len = $2;
+	die("Invalid image len") unless ($image_len =~ /^[0-9]+$/);
+	$image_len = int($image_len);
+}
 else { $oek_ip=$ENV{REMOTE_ADDR}; }
 
 die unless($oek_ip=~/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/);
@@ -22,11 +30,50 @@ open FILE,">$tmpname" or die("Couldn't write to directory");
 binmode FILE;
 binmode STDIN;
 
-my $buffer;
-while(read(STDIN,$buffer,1024)) { print FILE $buffer; }
+writedata(\*STDIN, \*FILE, $image_len);
 
 close FILE;
 
+my $thumb_len;
+
+if (OEKAKI_ENABLE_ANIM and read(STDIN, $thumb_len, 8)) {
+	die("Invalid thumb len") unless ($thumb_len =~ /^[0-9]+$/);
+	$thumb_len = int($thumb_len);
+
+	if ((OEKAKI_ANIM_MAX && $thumb_len > OEKAKI_ANIM_MAX*1024) ||
+	    $thumb_len > MAX_KB*1024) {
+		die("Animation data too large");
+	}
+
+	$tmpname = TMP_DIR.$oek_ip.'.pch';
+	open FILE,">$tmpname" or die("Couldn't write to directory for pch");
+	binmode FILE;
+
+	writedata(\*STDIN, \*FILE, $thumb_len);
+
+	close FILE;
+}
+
 print "Content-Type: text/plain\n";
 print "\n";
 print "ok";
+
+sub writedata($$$) {
+	# read $total bytes from $inf, and write it to $outf
+	# if $total == -1, just read until EOF
+	my ($inf,$outf,$total) = @_;
+	while ($total != 0) {
+		my $buffer;
+		my $len = $total;
+		if ($len > 4096 || $len < 0) {
+			$len = 4096;
+		}
+		if (read($inf, $buffer, $len)) {
+			print $outf $buffer;
+			$total -= $len if ($total > 0);
+		} else {
+			# error or EOF on read
+			return;
+		}
+	}
+}
diff --git a/extras/oekaki/oekaki_config.pl b/extras/oekaki/oekaki_config.pl
index 85361e9..0dfb714 100644
--- a/extras/oekaki/oekaki_config.pl
+++ b/extras/oekaki/oekaki_config.pl
@@ -1,6 +1,7 @@
 use constant TMP_DIR => 'tmp/';						# Temp file directory (needs to be writeable by the script)
 
 use constant OEKAKI_ENABLE_MODIFY => 1;				# Enable image modification
+use constant OEKAKI_ENABLE_ANIM => 1;				# Enable image animation (recording the image creation process)
 
 use constant OEKAKI_DEFAULT_X => 300;               # Default X dimension for oekaki drawings
 use constant OEKAKI_DEFAULT_Y => 300;               # Default Y dimension for oekaki drawings
@@ -8,6 +9,7 @@ use constant OEKAKI_MAX_X => 800;					# Max X dimension allowed for oekaki drawi
 use constant OEKAKI_MAX_Y => 800;					# Max Y dimension allowed for oekaki drawings
 use constant OEKAKI_MIN_X => 100;   	            # Min X dimension allowed for oekaki drawings
 use constant OEKAKI_MIN_Y => 100;   	            # Min Y dimension allowed for oekaki drawings
+use constant OEKAKI_ANIM_MAX => 0;              # Max file size (in KiB) for animation recording
 
 use constant OEKAKI_DEFAULT_PAINTER => "shi_norm";	# Default painter selection
 
diff --git a/extras/oekaki/oekaki_style.pl b/extras/oekaki/oekaki_style.pl
index 9cf04a5..432264b 100644
--- a/extras/oekaki/oekaki_style.pl
+++ b/extras/oekaki/oekaki_style.pl
@@ -53,6 +53,11 @@ use constant PAGE_TEMPLATE => compile_template(NORMAL_HEAD_INCLUDE.q{
 	</select>
 </if>
 
+<if OEKAKI_ENABLE_ANIM>
+	<br />
+	<input type="checkbox" name="oek_anim" checked />Animation
+</if>
+
 <input type="submit" value="<const S_OEKSUBMIT>" />
 </form>
 </div><hr />
@@ -255,8 +260,12 @@ table.nospace tr td { margin:0px; }
 <param name="tools" value="<var $mode>" />
 <param name="layer_count" value="3" />
 <param name="url_save" value="getpic.pl" />
-<param name="url_exit" value="finish.pl?oek_parent=<var $oek_parent>&amp;oek_ip=<var $ip>&amp;srcinfo=<var $time>,<var $oek_painter>,<var $oek_src>" />
+<param name="url_exit" value="finish.pl?animated=<var $oek_anim>&amp;oek_parent=<var $oek_parent>&amp;oek_ip=<var $ip>&amp;srcinfo=<var $time>,<var $oek_painter>,<var $oek_src>" />
 <param name="send_header" value="<var $ip>" />
+<if $oek_anim>
+	<param name="thumbnail_type" value="animation" />
+	<param name="animation_max" value="<const OEKAKI_ANIM_MAX>" />
+</if>
 </applet>
 </td>
 <if $selfy>
@@ -269,10 +278,9 @@ table.nospace tr td { margin:0px; }
 </html>
 });
 
-
 use constant OEKAKI_INFO_TEMPLATE => compile_template(q{
 <p><small><strong>
-Oekaki post</strong> (Time: <var $time>, Painter: <var $painter><if $source>, Source: <a href="<var $path><var $source>"><var $source></a></if>)
+Oekaki post</strong> (Time: <var $time>, Painter: <var $painter><if $source>, Source: <a href="<var $path><var $source>"><var $source></a></if><if $anim>, Animation: <a href="<var $path>pchview.html?pchfile=<var $anim>">View</a>, <a href="<var $path><var $anim>">Download</a></if>)
 </small></p>
 });
 
@@ -287,6 +295,7 @@ use constant OEKAKI_FINISH_TEMPLATE => compile_template(NORMAL_HEAD_INCLUDE.q{
 <input type="hidden" name="task" value="post" />
 <input type="hidden" name="oek_ip" value="<var $oek_ip>" />
 <input type="hidden" name="srcinfo" value="<var $srcinfo>" />
+<if $animated><input type="hidden" name="animated" value="1" /></if>
 <table><tbody>
 <tr><td class="postblock"><const S_NAME></td><td><input type="text" name="field1" size="28" /></td></tr>
 <tr><td class="postblock"><const S_EMAIL></td><td><input type="text" name="field2" size="28" /></td></tr>
diff --git a/extras/oekaki/paint.pl b/extras/oekaki/paint.pl
old mode 100644
new mode 100755
index c0f3def..7c2637e
--- a/extras/oekaki/paint.pl
+++ b/extras/oekaki/paint.pl
@@ -23,10 +23,12 @@ my $oek_x=$query->param("oek_x");
 my $oek_y=$query->param("oek_y");
 my $oek_parent=$query->param("oek_parent");
 my $oek_src=$query->param("oek_src");
+my $oek_anim=$query->param("oek_anim");
 my $ip=$ENV{REMOTE_ADDR};
 
 make_error(S_HAXORING) if($oek_x=~/[^0-9]/ or $oek_y=~/[^0-9]/ or $oek_parent=~/[^0-9]/);
 make_error(S_HAXORING) if($oek_src and !OEKAKI_ENABLE_MODIFY);
+make_error(S_HAXORING) if($oek_anim and !OEKAKI_ENABLE_ANIM);
 make_error(S_HAXORING) if($oek_src=~m![^0-9a-zA-Z/\.]!);
 make_error(S_OEKTOOBIG) if($oek_x>OEKAKI_MAX_X or $oek_y>OEKAKI_MAX_Y);
 make_error(S_OEKTOOSMALL) if($oek_x<OEKAKI_MIN_X or $oek_y<OEKAKI_MIN_Y);
@@ -50,6 +52,7 @@ if($oek_painter=~/shi/)
 		oek_y=>clean_string($oek_y),
 		oek_parent=>clean_string($oek_parent),
 		oek_src=>clean_string($oek_src),
+		oek_anim=>clean_string($oek_anim),
 		ip=>$ip,
 		time=>$time,
 		mode=>$mode,
diff --git a/extras/oekaki/pchview.html b/extras/oekaki/pchview.html
new file mode 100755
index 0000000..adc139b
--- /dev/null
+++ b/extras/oekaki/pchview.html
@@ -0,0 +1,71 @@
+
+<html>
+<head>
+<style type="text/css">
+body { background: #9999BB; font-family: sans-serif; }
+input,textarea { background-color:#CFCFFF; font-size: small; }
+table.nospace { border-collapse:collapse; }
+table.nospace tr td { margin:0px; }
+.menu { background-color:#CFCFFF; border: 1px solid #666666; padding: 2px; margin-bottom: 2px; }
+</style>
+<script language="javascript">
+<!--
+function onLoad() {
+	var varstr = "?pchfile=";
+	var pch = document.pch;
+	var href = window.location.href;
+
+	var start = href.indexOf(varstr);
+	if (start < 0) {
+		document.getElementById("errormessage").innerHTML = "No PCH file specified?";
+		return;
+	}
+	start += varstr.length;
+
+	var amp = href.indexOf("&", start);
+	var hash = href.indexOf("#", start);
+	if (hash < 0) {
+		hash = amp;
+	}
+	if (amp < 0) {
+		amp = hash;
+	}
+	var end = amp;
+	if (hash < amp) {
+		end = hash;
+	}
+	var filename;
+	if (end < 0) {
+		filename = href.slice(start);
+	} else {
+		filename = href.slice(start, end);
+	}
+	if (filename.length < 1) {
+		document.getElementById("errormessage").innerHTML = "No PCH file specified?";
+		return;
+	}
+	document.title = "PCHViewer: Playing " + filename;
+	pch.loadPCH(filename);
+	pch.setSpeed("1");
+	pch.playPCH();
+}
+//-->
+</script>
+</head><body onload="onLoad();">
+
+<noscript>Javascript is required to use this PCH animation viewer. Sorry! You
+can still get the PCH file directly, if you can tell where it is.</noscript>
+
+<span id="errormessage"></span>
+
+<table class="nospace" width="100%" height="100%"><tbody><tr>
+<td width="100%">
+<applet name="pch" code="pch2.PCHViewer.class" codebase="./" archive="PCHViewer.jar" width="100%" height="100%">
+<param name="speed" value="1">
+<param name="run" value="false">
+<param name="res.zip" value="res.zip">
+</applet>
+</td>
+</tr></tbody></table>
+</body>
+</html>
diff --git a/wakaba.pl b/wakaba.pl
index 8c6892e..96fe451 100755
--- a/wakaba.pl
+++ b/wakaba.pl
@@ -947,21 +947,25 @@ sub get_file_size($)
 	return($size);
 }
 
-sub process_file($$$)
+sub process_file($$$;$)
 {
-	my ($file,$uploadname,$time)=@_;
+	# if $force is true, this is an internal request, so force it to succeed
+	# if we can (don't e.g. fail because of an unknown file format)
+	my ($file,$uploadname,$time,$force)=@_;
 	my %filetypes=FILETYPES;
 
+	$force = $force ? 1 : 0;
+
 	# make sure to read file in binary mode on platforms that care about such things
 	binmode $file;
 
 	# analyze file and check that it's in a supported format
 	my ($ext,$width,$height)=analyze_image($file,$uploadname);
 
-	my $known=($width or $filetypes{$ext});
+	my $known=($width or $filetypes{$ext} or $force);
 
 	make_error(S_BADFORMAT) unless(ALLOW_UNKNOWN or $known);
-	make_error(S_BADFORMAT) if(grep { $_ eq $ext } FORBIDDEN_EXTENSIONS);
+	make_error(S_BADFORMAT) if(!$force and grep { $_ eq $ext } FORBIDDEN_EXTENSIONS);
 	make_error(S_TOOBIG) if(MAX_IMAGE_WIDTH and $width>MAX_IMAGE_WIDTH);
 	make_error(S_TOOBIG) if(MAX_IMAGE_HEIGHT and $height>MAX_IMAGE_HEIGHT);
 	make_error(S_TOOBIG) if(MAX_IMAGE_PIXELS and $width*$height>MAX_IMAGE_PIXELS);
@@ -972,6 +976,10 @@ sub process_file($$$)
 	my $thumbnail=THUMB_DIR.$filebase."s.jpg";
 	$filename.=MUNGE_UNKNOWN unless($known);
 
+	# make sure we don't get thumbnails, and we don't rename the file to
+	# its original name, or anything like that
+	($ext,$width,$height) = ('',0,0) if ($force);
+
 	# do copying and MD5 checksum
 	my ($md5,$md5ctx,$buffer);
 
