Created
September 26, 2019 19:06
-
-
Save jaames/6d31f215c3e2ee3b94b785d7bff6cf1a to your computer and use it in GitHub Desktop.
php kwz metadata parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// Project Kaeru KWZ / KWC parser | |
// ver 1.0 | |
// | |
// Written by James Daniel | |
// github.com/jaames | rakujira.jp | |
// ----- USAGE ----- | |
// instanciate | |
// $flipnote = new kaeruMemoParser(); | |
// load flipnote from an already open file buffer | |
// $flipnote->loadFromBuffer($file); | |
// get flipnote metadata | |
// $flipnote->getMeta(); | |
// get flipnote thumbnail (raw jpg data) | |
// $flipnote->getThumbnail(); | |
// close memo buffer | |
// $flipnote->close(); | |
class kaeruMemoParser { | |
var $memo = false; | |
var $isValid = false; | |
var $type = false; | |
// load a memo from a string | |
function loadFromString($data){ | |
$this->memo = fopen("php://memory", "r+"); | |
fwrite($this->memo, $data); | |
$this->_readSectionHeaders(); | |
} | |
// load a memo from a buffer object | |
function loadFromBuffer($buffer){ | |
$this->memo = $buffer; | |
$this->_readSectionHeaders(); | |
} | |
// load a memo from a buffer object | |
function close(){ | |
fclose($this->memo); | |
$this->memo = false; | |
} | |
function __destruct() { | |
if ($this->memo){ | |
// should, hopefully, auto-close the buffer when finished | |
$this->close(); | |
} | |
} | |
function _getFormatString($spec){ | |
$formatString = []; | |
foreach ($spec as $var => $format) { | |
array_push($formatString, $format . $var); | |
} | |
return join("/", $formatString); | |
} | |
function _readSectionHeaders(){ | |
$sectionTable = array(); | |
$offset = 0; | |
// end of memo data -- the last 256 bytes are a signature | |
$end = fstat($this->memo)["size"] - 256; | |
// we're also going to limit the max number of sections that can be read; | |
// the while loop could potentially loop forever of someone submits a maliciously coded file | |
$section = 0; | |
// jump through all the section headers and build an array of section offsets | |
// we can also use section information to determine where the input is a comment or flipnote | |
while (($offset < $end) && ($section < 6)) { | |
// seek to offset | |
fseek($this->memo, $offset); | |
// read header | |
$header = unpack("a3magic/x/Vlength", fread($this->memo, 8)); | |
// add section offset and length to the offset table | |
$sectionTable[$header["magic"]] = [ | |
"offset" => $offset, | |
"length" => $header["length"] | |
]; | |
$offset += $header["length"] + 8; | |
$section ++; | |
} | |
// now we check validity and type | |
if (!(isset($sectionTable["KFH"]) && isset($sectionTable["KMC"]) && isset($sectionTable["KMI"]))){ | |
// this file isn't a valid flipnote or comment | |
$this->isValid = false; | |
// go no further | |
return false; | |
} | |
$this->isValid = true; | |
// determine whether it is a flipnote or comment by checking for the presence of KTN and KSN sections | |
$this->type = (isset($sectionTable["KTN"]) && isset($sectionTable["KSN"])) ? "flipnote" : "comment"; | |
$this->sectionTable = $sectionTable; | |
} | |
function _prettyUsername($username){ | |
return trim(mb_convert_encoding($username, "UTF-8", "UTF-16LE"), "\0"); | |
} | |
function _prettyFSID($fsid){ | |
//remove trailing 00 | |
return substr($fsid, 0, 18); | |
} | |
function getMeta(){ | |
if ($this->isValid){ | |
// jump to start of KFH section | |
fseek($this->memo, $this->sectionTable["KFH"]["offset"] + 8); | |
$spec = [ | |
"ignore" => "x4", | |
"createdTimestamp" => "V", | |
"modifiedTimestamp" => "V", | |
"ignore2" => "x4", | |
"rootAuthorID" => "H20", | |
"parentAuthorID" => "H20", | |
"currentAuthorID" => "H20", | |
"rootAuthorName" => "a22", | |
"parentAuthorName" => "a22", | |
"currentAuthorName" => "a22", | |
"rootFilename" => "a28", | |
"parentFilename" => "a28", | |
"currentFilename" => "a28", | |
"frameCount" => "v", | |
"thumbIndex" => "v", | |
"flags" => "v", | |
"frameSpeed" => "C", | |
"ignore3" => "x" | |
]; | |
// unpack | |
$meta = unpack($this->_getFormatString($spec), fread($this->memo, 204)); | |
$frameMeta = $this->getFrameMeta(); | |
return [ | |
"type" => $this->type, | |
"created_timestamp" => $meta["createdTimestamp"] + 946684800, | |
"modified_timestamp" => $meta["modifiedTimestamp"] + 946684800, | |
"root" => [ | |
"author_ID" => $this->_prettyFSID($meta["rootAuthorID"]), | |
"author_name" => $this->_prettyUsername($meta["rootAuthorName"]), | |
"filename" => $meta["rootFilename"], | |
], | |
"parent" => [ | |
"author_ID" => $this->_prettyFSID($meta["parentAuthorID"]), | |
"author_name" => $this->_prettyUsername($meta["parentAuthorName"]), | |
"filename" => $meta["parentFilename"], | |
], | |
"current" => [ | |
"author_ID" => $this->_prettyFSID($meta["currentAuthorID"]), | |
"author_name" => $this->_prettyUsername($meta["currentAuthorName"]), | |
"filename" => $meta["currentFilename"], | |
], | |
"frame_count" => $meta["frameCount"], | |
"frame_index" => $meta["thumbIndex"], // depreciated as this was originally a dumb error; use thumb_index instead | |
"thumb_index" => $meta["thumbIndex"], | |
"frame_speed" => $meta["frameSpeed"], | |
"loop" => (($meta["flags"] >> 1) & 0x01), | |
"lock" => ($meta["flags"] & 0x01), | |
"frame_author_IDs" => $frameMeta["frameAuthorIDs"], | |
"camera_usage" => $frameMeta["cameraUsage"], | |
"spinoff" => ($meta["currentFilename"] == $meta["parentFilename"]) ? 0 : 1 | |
]; | |
} | |
} | |
function getThumbnail(){ | |
if ($this->isValid && isset($this->sectionTable["KTN"])){ | |
// jump to start of KTN section | |
fseek($this->memo, $this->sectionTable["KTN"]["offset"] + 12); | |
// return jpg image data | |
return (fread($this->memo, $this->sectionTable["KTN"]["length"])); | |
} | |
} | |
function getFrameMeta(){ | |
if ($this->isValid && isset($this->sectionTable["KMI"])){ | |
$ret = [ | |
"frameAuthorIDs" => array(), | |
"cameraUsage" => 0, | |
]; | |
// jump to start of KMI section | |
fseek($this->memo, $this->sectionTable["KMI"]["offset"] + 8); | |
// struct format for each frame's metadata | |
$formatString = $this->_getFormatString([ | |
"ignore" => "x10", | |
"authorID" => "H20", | |
"ignore2" => "x6", | |
"cameraUsage" => "v" | |
]); | |
$frameCount = $this->sectionTable["KMI"]["length"] / 28; | |
$frameIndex = 0; | |
// loop through the metadata for each frame | |
while ($frameIndex < $frameCount){ | |
// unpack | |
$frame = unpack($formatString, fread($this->memo, 28)); | |
// add frame author ID to list | |
array_push($ret["frameAuthorIDs"], $this->_prettyFSID($frame["authorID"])); | |
// check camera usage | |
if ($frame["cameraUsage"] == 1) { | |
$ret["cameraUsage"] = 1; | |
} | |
$frameIndex ++; | |
} | |
// remove duplicate frame author IDs | |
$ret["frameAuthorIDs"] = array_unique($ret["frameAuthorIDs"]); | |
return $ret; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment