<?php
  
/*
   * 2010/07/24 (c) yoya@awm.jp
   */

class YSwf {
    
//
    
var $_data// input_data
    
var $_header_info null;
    var 
$_movie_info_list = array(); // offset, length, ...
    //
    
var $_tagName// all known tags
    
var $_tagHasCharaId;
    var 
$_tagMisc = array(
        
0  => 'End',
        
1  => 'ShowFrame',
        
8  => 'JPEGTables',
        
9  => 'SetBackgroundColor',
        
12 => 'DoAction',
        
28 => 'RemoveObject2',
        
34 => 'DefineButton2',
        
39 => 'DefineSprite',
        
43 => 'FrameLabel',
        );
    var 
$_tagJpeg = array(
        
6  => 'DefineBitsJPEG',
        
21 => 'DefineBitsJPEG2',
        
35 => 'DefineBitsJPEG3',
        );
    var 
$_tagLossless = array(
        
20 => 'DefineLossless',
        
36 => 'DefineLossless2',
        );
    var 
$_tagShape = array(
        
2  => 'DefineShape',
        
22 => 'DefineShape2',
        
32 => 'DefineShape3',
        
46 => 'DefineMorphShape',
        
83 => 'DefineShape4',
        
84 => 'DefineMorphShape2',
        );
    var 
$_tagPlace = array(
        
4  => 'PlaceObject',
        
26 => 'PlaceObject2',
        );
    
    
// constructor
    
function YSwf() {
        
$this->_tagName $this->_tagShape $this->_tagPlace +
            
$this->_tagJpeg $this->_tagLossless $this->_tagMisc;
        
$this->_tagHasCharaId $this->_tagShape $this->_tagPlace $this->_tagJpeg $this->_tagLossless;
    }
    
    
/*
     * utility function for byte processing
     */
    
function getData($offset$length) { // XXX
        
return substr($this->_data$offset$length);
    }
    function 
getUI8($offset) { // XXX
        
return ord($this->_data{$offset}); // UI8
                
    
}
    function 
getUI16LE($offset) {
        
$ret unpack('v'substr($this->_data$offset2)); // UI16LE
        
return $ret[1];
    }
    function 
getUI32LE($offset) {
        
$ret unpack('V'substr($this->_data$offset4)); // UI32LE
        
return $ret[1];
    }
    function 
putUI8($offset$value) { // XXX
        
$this->_data{$offset} = chr($value); // UI8
    
}
    function 
putUI16LE($offset$value) {
        
$data pack('v'$value); // UI16LE
        
$this->_data{$offset    } = $data{0};
        
$this->_data{$offset 1} = $data{1};
    }
    function 
putUI32LE($offset$value) {
        
$data pack('V'$value); // UI32LE
        
$this->_data{$offset    } = $data{0};
        
$this->_data{$offset 1} = $data{1};
        
$this->_data{$offset 2} = $data{2};
        
$this->_data{$offset 3} = $data{3};
    }
    function 
toUI8($value) { // XXX
        
return chr($value); // UI8
    
}
    function 
toUI16LE($value) {
        return 
pack('v'$value); // UI16LE
    
}
    function 
toUI32LE($value) {
        return 
pack('V'$value); // UI32LE
    
}
    
    
/*
     * input swf data
     */
    
function input($data) {
        
$this->_data $data;
        
// header
        
$ret $this->_validate_header($data);
        
$ret $this->_get_header_length($data);
        if (
$ret === false) {
            
trigger_error("get_header_length failed");
            return 
$ret;
        }
        
$this->_header_info = array('length' => $ret);
        
// movie
        
$offset $ret;
        
        
$ret $this->_input_movie($data$offsetstrlen($data) - $offset$this->_movie_info_list);
        return 
$ret;
    }
    function 
_input_movie($data$offset$length, &$movie_info_list) {
        while (
true) {
            
$ret $this->_get_movie_info($data$offset);
            if (
$ret === false) {
                
trigger_error("get_movie_info failed");
                return 
$ret;
            }
            
$movie_info $ret;
            
$tag $movie_info['tag'];
            
$length $movie_info['length'];
            
$tag_and_length_size $movie_info['tag_and_length_size'];
            if (
$tag == 39) { // SpriteTag (MovieClip)
                // Sprite is nest movie tag
                
$movie_info['chara_id'] = $this->getUI16LE($offset $tag_and_length_size);
                
$movie_info['frame_count'] = $this->getUI16LE($offset $tag_and_length_size 2);
                
$movie_offset $tag_and_length_size 2;
                
$ret $this->_input_movie($data$offset $movie_offset$length $movie_offset$movie_info['movie']);
                if (
$ret === false) {
                    return 
false;
                }
            }
            
$movie_info_list[] = $movie_info;
            if (
$tag == 0) { // End Tag
                
break;
            }
            if (
$length <= 0) { // Error
                
trigger_error("movie_info length <= 0");
                break;
            }
            
$offset += $length;
        }
    }
    function 
_validate_header($data) {
        
// validate
        
$magic $this->getData(03);
        if (
$magic != 'FWS') {
            
trigger_error("no FWS");
            return 
false;
        }
        
$version $this->getUI8(3);
        if (
$version 4) {
            
trigger_error("version($version) must be le 4");
            return 
false;
        }
        
$file_length $this->getUI32LE(4);
        
$data_length strlen($this->_data);
        if (
$file_length != $data_length) {
            
trigger_error("file_length($file_length) != data length($data_length");
            return 
false;
        }
        return 
true;
    }
    function 
_get_header_length($data) {
        
$rect_field_bit_width + (* ($this->getUI8(8) >> 3));
        
// bit width => byte length
        
$rect_field_length  ceil($rect_field_bit_width 8);
        return 
$rect_field_length 4;
    }
    function 
_get_movie_info($data$offset) {
        
$chara_id null;
        
$tag_and_length $this->getUI16LE($offset);
        
$tag = ($tag_and_length >> 6) & 0x3ff;
        
$length $tag_and_length 0x3f;
        if (
$length 0x3f) {
            
$tag_and_length_size 2;
        } else {
            
$length $this->getUI32LE($offset 2);
            
$tag_and_length_size 4;
        }
        
$length $tag_and_length_size $length;
        if (isset(
$this->_tagHasCharaId[$tag])) {
            if (isset(
$this->_tagShape[$tag]) ||
                isset(
$this->_tagJpeg[$tag])) {
                
$chara_id $this->getUI16LE($offset $tag_and_length_size);
            } else {
                switch (
$tag) {
                case 
39// DefineSprite
                
case 4:  // PlaceObject
                    
$chara_id $this->getUI16LE($offset $tag_and_length_size);
                    break;
                case 
26// PlaceObject2
                    
$flag $this->getUI8($offset $tag_and_length_size);
                    if (
$flag 2) { // place_has_id_ref 
                        
$chara_id $this->getUI16LE($offset $tag_and_length_size 3);
                    }
                    break;
                default:
                    break;
                }
            }
        }
        
$movie_info = array('tag' => $tag,
                            
'offset' => $offset'length' => $length,
                            
'tag_and_length_size' => $tag_and_length_size);
        if (! 
is_null($chara_id)) {
            
$movie_info['chara_id'] = $chara_id;
        }
        return 
$movie_info;
    }
    
/*
     * output swf data
     */
    
    
function output() {
        
// header
        
if (isset($this->_header_info['replaced'])) {
            
$header_data $this->_header_info['data'];
        } else {
            
$header_data $this->getData(0$this->_header_info['length']);
        }
        
$movie_data $this->_output_movie($this->_movie_info_list);
        
$file_length strlen($header_data) + strlen($movie_data);
        
$file_length_data $this->toUI32LE($file_length);
        
$header_data substr_replace($header_data$file_length_data44);
        return 
$header_data.$movie_data;
    }
    function 
_output_movie($movie_info_list) {
        
$data '';
        foreach (
$movie_info_list as $movie_info) {
            if (isset(
$movie_info['replaced'])) {
                
$tag $movie_info['tag'];
                if (
$tag == 39) { // Sprite
//                   $movie_data = $this->toUI16LE($movie_info['frame_count']);
                    
$movie_data $this->toUI16LE($movie_info['chara_id']);
                    
$movie_data .= $this->toUI16LE(count($movie_info['movie']));
                    
$movie_data .= $this->_output_movie($movie_info['movie']);
                    
$length strlen($movie_data);
                    if (
$length 0x3f) {
                        
$tag_and_length = ($tag << 6) | $length;
                        
$data .= $this->toUI16LE($tag_and_length);
                    } else {
                        
$tag_and_length = ($tag << 6) | 0x3f;
                        
$data .= $this->toUI16LE($tag_and_length);
                        
$data .= $this->toUI32LE($length);
                    }
                    
$data .= $movie_data;
                } else {
                    
// Sprite 以外は只の連結
                    
$data .= $movie_info['data'];
                }
            } else {
                
// 編集していない部分は元のデータを連結
                
$data .= $this->getData($movie_info['offset'], $movie_info['length']);
            }
        }
        return 
$data;
    }
    
/*
     * tag contents checksum
     */
    
function _set_checksum_tag($tag_table) {
        foreach (
$this->_movie_info_list as &$movie_info) {
            
$tag $movie_info['tag'];
            if (isset(
$tag_table[$tag])) {
                
$offset $movie_info['offset'];
                
$length $movie_info['length'];
                
$tag_and_length_size $movie_info['tag_and_length_size'];
                
$movie_info['checksum'] = crc32($this->getData($offset $tag_and_length_size 2$length $tag_and_length_size 2));
            }
        }
    }
    
// image_unique
    
function set_image_checksum() {
        
$this->_set_checksum_tag($this->_tagJpeg $this->_tagLossless);
    }
    function 
set_shape_checksum() {
        
$this->_set_checksum_tag($this->_tagShape);
    }
    function 
unique_image_by_checksum() {
        ; 
// edit id shape => jpeg or lossless
    
}
    
// shape unique
    
function _make_uniq_shape_map($movie_info_list, &$shape_checksum_table, &$unique_shape_map) {
        foreach (
$movie_info_list as $movie_info) {
            
$tag $movie_info['tag'];
            if (
$tag == 39) { // Sprite
                
$this->_make_uniq_shape_map($movie_info['movie'], $shape_checksum_table$unique_shape_map);
            } else {
                if (isset(
$this->_tagShape[$tag])) {
                    
$chara_id $movie_info['chara_id'];
                    
$checksum $movie_info['checksum'];
                    if (isset(
$shape_checksum_table[$checksum])) {
                        
// checksum が同じ id が前にある場合
                        
$unique_shape_map[$chara_id] = $shape_checksum_table[$checksum];
                    } else {
                        
$shape_checksum_table[$checksum] = $chara_id;
                    }
                }
            }
        }
        return 
$unique_shape_map;
    }
    function 
_unique_shape_by_map(&$movie_info_list, &$unique_shape_map) {
        
$replaced false;
        foreach (
$movie_info_list as $idx => &$movie_info) {
            
$tag $movie_info['tag'];
            if (isset(
$this->_tagShape[$tag])) {
                
// (重複する) Shape tag を削除する
                
$chara_id $movie_info['chara_id'];
                if (isset(
$unique_shape_map[$chara_id])) {
                    unset(
$movie_info_list[$idx]);
                    
$replaced true;
                }
            } else if (isset(
$this->_tagPlace[$tag]) && isset($movie_info['chara_id'])) {
                
// Place tag の (Shape が重複する) ref ID を入れ替える
                
$chara_id $movie_info['chara_id'];
                if (isset(
$unique_shape_map[$chara_id])) {
                    
$tag_and_length_size $movie_info['tag_and_length_size'];
                    
$replaced_chara_id $unique_shape_map[$chara_id];
                    switch (
$tag) {
                      case 
4:  // PlaceObject
                        
$chara_id_offset 0;
                        break;
                      case 
26// PlaceObject2
                        
$chara_id_offset 3;
                        break;
                    }
                    
$movie_info['chara_id'] = $replaced_chara_id;
                    
$this->putUI16LE($movie_info['offset'] + $tag_and_length_size $chara_id_offset$replaced_chara_id);
                    
$replaced true;
                }
            } else if (
$tag == 39) { // Sprite
                
$ret $this->_unique_shape_by_map($movie_info['movie'], $unique_shape_map);
                if (
$ret == true) {
                    
$movie_info['frame_count'] = count($movie_info['movie']);
                    
$movie_info['replaced'] = true;
                    
$replaced true;
                }
            }
        }
        return 
$replaced;
    }
    function 
unique_shape_by_checksum($opts) {
        
$shape_checksum_table = array();
        
$unique_shape_map = array();
        
$unique_shape_map $this->_make_uniq_shape_map($this->_movie_info_list$this->_movie_info_list$shape_checksum_table$unique_shape_map);
        if (! empty(
$opts['debug'])) {
            echo 
"unique_shape_map\n";
            foreach (
$unique_shape_map as $from => $to) {
                echo 
"$from => $to".PHP_EOL;
            }

        }
        
$this->_unique_shape_by_map($this->_movie_info_list$unique_shape_map);
    }

    
/*
     * dump swf data structure
     * TODO: replaced 対応
     */
    
function dump($opts = array()) {
        
$header_length $this->_header_info['length'];
        echo 
"head_length=".$header_length.PHP_EOL;
        if (isset(
$opts['hex_dump'])) {
            
$this->hex_dump(0$header_length);
            echo 
PHP_EOL// for look attractive
        
}
        
$this->dump_movie($this->_movie_info_list0$opts);
    }
    function 
dump_movie(&$movie_info_list$indent$opts) {
        foreach (
$movie_info_list as $movie_info) {
            echo 
str_pad(''$indent' ');
            
$tag $movie_info['tag'];
            echo 
"tag=$tag";
            if (isset(
$this->_tagName[$movie_info['tag']])) {
                echo 
'('.$this->_tagName[$movie_info['tag']].')';
            }
            echo 
" length=".$movie_info['length'];
            if (isset(
$movie_info['chara_id'])) {
                echo 
" id=".$movie_info['chara_id'];
            }
            if (isset(
$movie_info['checksum'])) {
                echo 
" checksum=".$movie_info['checksum'];
            }
            echo 
PHP_EOL;
                    
            if (isset(
$movie_info['movie'])) {
                
$this->dump_movie($movie_info['movie'], $indent+1$opts);
            } else {
                if (isset(
$opts['hex_dump'])) {
                    
$this->hex_dump($movie_info['offset'], $movie_info['length']);
                    echo 
PHP_EOL;
                }
            }
        }
    }
    
/*
     * general purpose hex_dump routine
     */
    
function hex_dump($offset$length) {
        
printf("            0  1  2  3  4  5  6  7   8  9  a  b  c  d  e  f  0123456789abcdef\n");
        
$dump_str '';
        if (
$offset 0x10) {
            
printf("0x%08x "$offset - ($offset 0x10));
            
$dump_str str_pad(' '$offset 0x10);
        }
        for (
$i 0$i $offset 0x10$i++) {
            if (
$i == 8) {
                echo(
' ');
            }
            echo(
'   ');
        }
        for (
$i $offset $i $offset $length$i++) {
            if ((
$i 0x10) == 0) {
                
printf("0x%08x "$i);
            }
            if (isset(
$this->_data[$i])) {
                
$value $this->getUI8($i);
                if ((
0x20 $value) && ($value 0x7f)) { // XXX: printable
                    
$dump_str .= $this->_data{$i};
                } else {
                    
$dump_str .= ' ';
                }
                
printf("%02x "$value);
            } else {
                
$dump_str .= ' ';
                echo 
'   ';
            }
            if ((
$i 0x10) == 0x0f) {
                echo 
" ";
                echo 
$dump_str;
                echo 
PHP_EOL;
                
$dump_str '';
            }
        }
        if ((
$i 0x10) != 0) {
            echo 
str_pad(' '* (0x10 - ($i 0x10)));
            if (
$i 8) {
                echo 
' ';
            }
            echo 
" ";
            echo 
$dump_str;
            echo 
PHP_EOL;
        }
    }
}