Skip to content

Instantly share code, notes, and snippets.

@SirTony
Last active August 29, 2015 14:06
Show Gist options
  • Save SirTony/f0144b0105e4c1e4a109 to your computer and use it in GitHub Desktop.
Save SirTony/f0144b0105e4c1e4a109 to your computer and use it in GitHub Desktop.
UI archive compression tool for https://github.com/Evairfairy/PacketRogue
/+
Compile with:
dmd pack.d
Options:
Flag Type Required Default Description
---------------------------------------------------------
--out string yes <none> The name of the .ui file to output.
--dir string yes <none> The source directory to pack.
--width uint yes <none> Window client size width.
--height uint yes <none> Window client size height.
--silent bool no flase Do not print to the console.
--recurse bool no true Resursively search --dir
--overwrite bool no false Force overwrite of --out if it already exists.
+/
import std.path,
std.file,
std.stream,
std.zlib,
std.getopt,
std.array,
std.random,
std.string,
io = std.stdio;
string output;
string root;
bool overwrite = false;
bool silent = false;
bool recurse = true;
int width = -1;
int height = -1;
enum BUFFER_SIZE = 10_240;
enum ubyte[] MAGIC = [ 0x50, 0x52, 0x55, 0x49 ];
enum ubyte VERSION_MAJOR = 1;
enum ubyte VERSION_MINOR = 0;
void main( string[] args )
{
args.getopt(
"out", &output,
"dir", &root,
"width", &width,
"height", &height,
"silent", &silent,
"recurse", &recurse,
"overwrite", &overwrite
);
if( width <= 0 || height <= 0 )
{
io.stderr.writeln( "Width and height must be greater than zero." );
return;
}
if( output is null || output.length == 0 )
{
io.stderr.writeln( "Output file is required." );
return;
}
if( output.exists && output.isDir )
{
io.stderr.writeln( "Output is a directory, not a file." );
return;
}
if( output.exists && !overwrite )
{
io.stdout.writef( "'%s' already exists. Overwrite? [Y/n] ", output );
auto _in = io.stdin.readln().chomp.strip.toLower;
if( _in.length < 1 || _in[0] != 'y' )
return;
}
if( !output.toLower.endsWith( ".ui" ) )
output ~= ".ui";
if( root is null || ( !root.exists && !root.isDir ) )
{
io.stderr.writefln( "The directory '%s' does not exist or is not a directory.",
root is null ? "<null>" : root );
return;
}
if( output.exists )
output.remove();
auto mode = !recurse ? SpanMode.shallow : SpanMode.breadth;
auto entries = root.dirEntries( mode ).array;
auto tempName = "~" ~ randomString() ~ ".tmp";
auto arch = new File( output, FileMode.In | FileMode.Out );
auto temp = new File( tempName, FileMode.In | FileMode.OutNew );
assert( temp.seekable );
Entry[] files;
auto getHeaderSize()
{
auto size = 4 //Magic ID length
+ 2 //version length
+ 4 //width length
+ 4 //height length
+ 4; //count length
if( files.length > 0 )
{
foreach( e; files )
{
size += 4 //compressed size
+ 4 //uncompressed size
+ 4; //offset
size += 2 + e.MIME.length;
size += 2 + e.Name.length;
}
}
return size;
}
arch.write( MAGIC );
arch.write( VERSION_MAJOR );
arch.write( VERSION_MINOR );
arch.write( width );
arch.write( height );
while( entries.length )
{
auto entry = entries.back;
entries.popBack();
if( entry.isDir && recurse )
{
entries ~= entry.name.dirEntries( mode ).array;
continue;
}
string name = entry.name.normalize();
Entry file = {
0, 0, 0,
name.getMimeType(),
name,
entry
};
files ~= file;
}
arch.write( files.length );
arch.flush();
auto headerSize = getHeaderSize();
if( files.length == 0 )
goto End;
foreach( ref entry; files )
{
printfln( "Packing '%s'.", entry.Name );
auto input = new File( entry.path, FileMode.In | FileMode.Out );
auto compressor = new Compress( 9, HeaderFormat.deflate );
auto before = temp.position;
const( void )[] chunk;
while( !input.eof )
{
auto left = input.available > BUFFER_SIZE
? BUFFER_SIZE
: input.available;
ubyte[] buf = new ubyte[]( cast(uint)left );
input.read( buf );
chunk = compressor.compress( cast(void[])buf );
temp.write( cast(ubyte[])chunk );
}
chunk = compressor.flush();
temp.write( cast(ubyte[])chunk );
temp.flush();
entry.UncompressedSize = cast(int)input.size;
entry.CompressedSize = cast(int)( temp.position - before );
entry.Offset = cast(int)( headerSize + before );
input.close();
arch.write( entry.CompressedSize );
arch.write( entry.UncompressedSize );
arch.write( entry.Offset );
short len = cast(short)entry.MIME.length;
ubyte[] buf = cast(ubyte[])entry.MIME.dup;
arch.write( len );
arch.write( buf );
len = cast(short)entry.Name.length;
buf = cast(ubyte[])entry.Name.dup;
arch.write( len );
arch.write( buf );
arch.flush();
}
temp.seekSet( 0 );
arch.copyFrom( temp );
arch.flush();
End:
temp.close();
arch.close();
tempName.remove();
println( "Done." );
}
enum MimeLookup = [
"ai": "application/postscript",
"aif": "audio/x-aiff",
"aifc": "audio/x-aiff",
"aiff": "audio/x-aiff",
"asc": "text/plain",
"atom": "application/atom+xml",
"au": "audio/basic",
"avi": "video/x-msvideo",
"bcpio": "application/x-bcpio",
"bin": "application/octet-stream",
"bmp": "image/bmp",
"cdf": "application/x-netcdf",
"cgm": "image/cgm",
"class": "application/octet-stream",
"cpio": "application/x-cpio",
"cpt": "application/mac-compactpro",
"csh": "application/x-csh",
"css": "text/css",
"dcr": "application/x-director",
"dif": "video/x-dv",
"dir": "application/x-director",
"djv": "image/vnd.djvu",
"djvu": "image/vnd.djvu",
"dll": "application/octet-stream",
"dmg": "application/octet-stream",
"dms": "application/octet-stream",
"doc": "application/msword",
"dtd": "application/xml-dtd",
"dv": "video/x-dv",
"dvi": "application/x-dvi",
"dxr": "application/x-director",
"eps": "application/postscript",
"etx": "text/x-setext",
"exe": "application/octet-stream",
"ez": "application/andrew-inset",
"gif": "image/gif",
"gram": "application/srgs",
"grxml": "application/srgs+xml",
"gtar": "application/x-gtar",
"hdf": "application/x-hdf",
"hqx": "application/mac-binhex40",
"htm": "text/html",
"html": "text/html",
"ice": "x-conference/x-cooltalk",
"ico": "image/x-icon",
"ics": "text/calendar",
"ief": "image/ief",
"ifb": "text/calendar",
"iges": "model/iges",
"igs": "model/iges",
"jnlp": "application/x-java-jnlp-file",
"jp2": "image/jp2",
"jpe": "image/jpeg",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "application/x-javascript",
"kar": "audio/midi",
"latex": "application/x-latex",
"lha": "application/octet-stream",
"lzh": "application/octet-stream",
"m3u": "audio/x-mpegurl",
"m4a": "audio/mp4a-latm",
"m4b": "audio/mp4a-latm",
"m4p": "audio/mp4a-latm",
"m4u": "video/vnd.mpegurl",
"m4v": "video/x-m4v",
"mac": "image/x-macpaint",
"man": "application/x-troff-man",
"mathml": "application/mathml+xml",
"me": "application/x-troff-me",
"mesh": "model/mesh",
"mid": "audio/midi",
"midi": "audio/midi",
"mif": "application/vnd.mif",
"mov": "video/quicktime",
"movie": "video/x-sgi-movie",
"mp2": "audio/mpeg",
"mp3": "audio/mpeg",
"mp4": "video/mp4",
"mpe": "video/mpeg",
"mpeg": "video/mpeg",
"mpg": "video/mpeg",
"mpga": "audio/mpeg",
"ms": "application/x-troff-ms",
"msh": "model/mesh",
"mxu": "video/vnd.mpegurl",
"nc": "application/x-netcdf",
"oda": "application/oda",
"ogg": "application/ogg",
"pbm": "image/x-portable-bitmap",
"pct": "image/pict",
"pdb": "chemical/x-pdb",
"pdf": "application/pdf",
"pgm": "image/x-portable-graymap",
"pgn": "application/x-chess-pgn",
"pic": "image/pict",
"pict": "image/pict",
"png": "image/png",
"pnm": "image/x-portable-anymap",
"pnt": "image/x-macpaint",
"pntg": "image/x-macpaint",
"ppm": "image/x-portable-pixmap",
"ppt": "application/vnd.ms-powerpoint",
"ps": "application/postscript",
"qt": "video/quicktime",
"qti": "image/x-quicktime",
"qtif": "image/x-quicktime",
"ra": "audio/x-pn-realaudio",
"ram": "audio/x-pn-realaudio",
"ras": "image/x-cmu-raster",
"rdf": "application/rdf+xml",
"rgb": "image/x-rgb",
"rm": "application/vnd.rn-realmedia",
"roff": "application/x-troff",
"rtf": "text/rtf",
"rtx": "text/richtext",
"sgm": "text/sgml",
"sgml": "text/sgml",
"sh": "application/x-sh",
"shar": "application/x-shar",
"silo": "model/mesh",
"sit": "application/x-stuffit",
"skd": "application/x-koan",
"skm": "application/x-koan",
"skp": "application/x-koan",
"skt": "application/x-koan",
"smi": "application/smil",
"smil": "application/smil",
"snd": "audio/basic",
"so": "application/octet-stream",
"spl": "application/x-futuresplash",
"src": "application/x-wais-source",
"sv4cpio": "application/x-sv4cpio",
"sv4crc": "application/x-sv4crc",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"t": "application/x-troff",
"tar": "application/x-tar",
"tcl": "application/x-tcl",
"tex": "application/x-tex",
"texi": "application/x-texinfo",
"texinfo": "application/x-texinfo",
"tif": "image/tiff",
"tiff": "image/tiff",
"tr": "application/x-troff",
"tsv": "text/tab-separated-values",
"txt": "text/plain",
"ustar": "application/x-ustar",
"vcd": "application/x-cdlink",
"vrml": "model/vrml",
"vxml": "application/voicexml+xml",
"wav": "audio/x-wav",
"wbmp": "image/vnd.wap.wbmp",
"wbmxl": "application/vnd.wap.wbxml",
"wml": "text/vnd.wap.wml",
"wmlc": "application/vnd.wap.wmlc",
"wmls": "text/vnd.wap.wmlscript",
"wmlsc": "application/vnd.wap.wmlscriptc",
"wrl": "model/vrml",
"xbm": "image/x-xbitmap",
"xht": "application/xhtml+xml",
"xhtml": "application/xhtml+xml",
"xls": "application/vnd.ms-excel",
"xml": "application/xml",
"xpm": "image/x-xpixmap",
"xsl": "application/xml",
"xslt": "application/xslt+xml",
"xul": "application/vnd.mozilla.xul+xml",
"xwd": "image/x-xwindowdump",
"xyz": "chemical/x-xyz",
"zip": "application/zip"
];
struct Entry
{
int CompressedSize;
int UncompressedSize;
int Offset;
string MIME;
string Name;
string path; //This is an internal field and
//won't be written to the archive.
}
string getMimeType( in string file )
{
auto ext = file.extension.toLower[1 .. $];
auto mime = ext in MimeLookup;
if( !mime )
return "application/octet-stream";
else
return *mime;
}
string randomString( int length = 15, char[] chars = null )
{
if( chars is null )
chars = "ABCDEFGIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz".dup;
string result;
foreach( _; 0 .. length )
result ~= chars[uniform( 0, chars.length )];
return result;
}
void printfln( T... )( string format, T args )
{
if( silent )
return;
io.writefln( format, args );
}
void println( T... )( T args )
{
if( silent )
return;
io.writeln( args );
}
string normalize( in string str )
{
return str[root.length + 1 .. $].replace( "\\", "/" );
}
bool endsWith( in string str, in string value )
{
if( value.length > str.length )
return false;
if( value == str )
return true;
return str[str.length - value.length .. $] == value;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment