Skip to content

Instantly share code, notes, and snippets.

@jaames
Created September 26, 2019 19:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaames/6d31f215c3e2ee3b94b785d7bff6cf1a to your computer and use it in GitHub Desktop.
Save jaames/6d31f215c3e2ee3b94b785d7bff6cf1a to your computer and use it in GitHub Desktop.
php kwz metadata parser
<?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