pokesprite/pokesprite.php
2018-09-09 21:27:57 +02:00

438 lines
14 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
// PokéSprite
// ----------
// This simple script generates an image sprite containing small icons of
// all Pokémon in the National Pokédex (along with several other types of
// icons), and the pertaining SCSS and JS files. It was built for people
// working on websites or online applications related to Pokémon. The main
// Pokémon icons are arranged sequentially, and a growing packer algorithm
// is used to arrange other images inside the sprite.
//
// Aside from the regular box sprites (which have been sourced from
// Pokémon X/Y), this package includes unofficial custom designed shiny
// versions of those sprites.
//
// The data for the Pokémon icons is taken from the `./data/pkmn.json` file.
// The other icon sets have their data generated based on the directory
// structure and file names.
//
// ---------------------------------------------------------------------------
//
// The MIT License (MIT)
//
// (C) 2014-2016 Michiel Sikma <michiel@sikma.org> and PokéSprite contributors
//
// For a full list of contributors, view the project commit history.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
// ---------------------------------------------------------------------------
//
// All files in the `icons` directory are included in this package for reasons
// of convenience and are (C) 1995-2014 Nintendo/Creatures Inc./GAME FREAK Inc.
namespace PkSpr;
error_reporting(0);
require_once 'includes/templateformatter.php';
require_once 'includes/terminalformatter.php';
require_once 'includes/settings.php';
require_once 'includes/scaffolding.php';
require_once 'includes/i18n.php';
require_once 'includes/usage.php';
require_once 'includes/iconstack.php';
require_once 'includes/functions.php';
require_once 'includes/iconoverview.php';
require_once 'includes/iconstyler.php';
require_once 'includes/iconjs.php';
require_once 'includes/iconsprite.php';
// 1. Parsing of command line arguments and basic program feedback
// ---------------------------------------------------------------------------
// Load the default settings.
Settings::load_settings_file('includes/defaults.php');
// Parse the user's command line arguments and determine whether the user
// is in need of seeing the usage guide.
$usage = new Usage();
$cl_settings = $usage->get_user_settings();
// Check the current revision number for user feedback and for
// inclusion in the output files.
$revision = trim(shell_exec('git rev-list HEAD --count 2> /dev/null'));
$revision = $revision ? 'r'.$revision : '[unknown]';
$cl_settings['revision'] = $revision;
// Retrieve the version number from the pkg-info.json file.
$pkg_info = json_decode(file_get_contents(Settings::get('resources_dir').'pkg-info.json'), true);
$version = $pkg_info['version'];
$cl_settings['version'] = $version;
// Merge the user's command line arguments into the settings.
Settings::load_settings($cl_settings);
if ($usage->needs_version) {
// Display version and exit.
$usage->load_tpl_file(
Settings::get('resources_dir').
Settings::get('version_tpl')
);
$usage->display_version();
exit;
}
if ($usage->needs_usage) {
// Display usage and exit.
$usage->load_tpl_file(
Settings::get('resources_dir').
Settings::get('usage_tpl')
);
$usage->display_usage();
exit;
}
// Export the settings variables so that we can use them here.
$vars = array_keys(Settings::$settings);
foreach ($vars as $var) {
$$var = Settings::get($var);
}
// Set the default lines.
I18n::set_language($i18n_language);
I18n::set_default_lines();
$termfrm = new TerminalFormatter();
I18n::add_output_filter(array('PkSpr\TerminalFormatter', 'format'));
// Check if we have GD.
if (!function_exists('imagecreatefrompng')) {
print(I18n::l('gd_error'));
die();
}
// Print basic program info.
print(I18n::lf('info', array(
$title_str,
$version,
$revision,
$website_txt,
$copyright_gf
)));
$generated_on = I18n::lf('generated_on', array($script_date));
// Check if we're running via anything other than the command line.
if (php_sapi_name() != 'cli') {
print(I18n::l('no_cli'));
}
// Check if pngcrush exists, in case we need it.
if ($generate_optimized === true) {
if (!is_file($pngcrush_path)) {
print(I18n::lf('pngcrush_missing', array($pngcrush_path)));
$generate_optimized = false;
}
}
// Check if the output directory exist and create them if necessary.
if (!is_dir($dir_output)) {
print(I18n::l('dir_create'));
if (!mkdir($dir_output, $dir_mode, true)) {
print(I18n::l('dir_error'));
die();
}
print(I18n::l('dir_success'));
}
// Print a quick overview of what we'll be doing.
print(I18n::l('tasks_overview'));
if ($include_pkmn) {
print(I18n::lf('task_pkmn', array(
$include_pkmn ? 'true' : 'false',
$include_pkmn_nonshiny ? 'true' : 'false',
$include_pkmn_shiny ? 'true' : 'false',
$include_pkmn_forms ? 'true' : 'false',
$include_pkmn_right,
$pkmn_language
)));
}
if ($include_icon_sets) {
print(I18n::l('task_icon_sets'));
}
if ($generate_optimized) {
print(I18n::l('task_optimize'));
}
print(I18n::l('task_html'));
if ($generate_markdown) {
print(I18n::l('task_markdown'));
}
print(I18n::l('task_scss'));
print(I18n::l('task_js'));
// 2. Pokémon data parsing and icon stack generation
// ---------------------------------------------------------------------------
$icon_stack = new IconStack();
// Retrieve Pokémon name and idx information from the data file.
if ($include_pkmn) {
$icon_stack->parse_data_file($dir_data.$file_pkmn_data, $pkmn_range);
$pkmn_data = $icon_stack->get_pkmn_data();
// Check if the data is there.
$has_data = $icon_stack->has_pkmn_data();
$has_imgs = $icon_stack->has_pkmn_images();
if (!$has_data) {
print(I18n::lf('no_data', array($file_pkmn_data, $dir_data)));
die();
}
if (!$has_imgs) {
print(I18n::l('no_images'));
die();
}
}
else {
// If we aren't including Pokémon, keep an empty array
// to avoid iteration warnings.
$pkmn_data = array();
}
// Retrieve icons from the set.
if ($include_icon_sets) {
$icon_stack->parse_icons_data_file($dir_data.$file_icon_data, $icon_range);
$icon_data = $icon_stack->get_icon_data();
// Check if the data is there.
$has_data = $icon_stack->has_icon_data();
$has_imgs = $icon_stack->has_icon_images();
if (!$has_data) {
print(I18n::lf('no_icons_data', array($file_icon_data, $dir_data)));
die();
}
if (!$has_imgs) {
print(I18n::l('no_icon_images'));
die();
}
}
// Generate and return the entire Pokémon icon stack.
$pkmn_icons = $icon_stack->get_pkmn_icon_stack();
$pkmn_std_icons = $icon_stack->get_pkmn_std_icons();
// Get the total size of the Pokémon icons. We'll position the rest of the
// images using the same width, and an arbitrary height.
$pkmn_sect_fit = $icon_stack->get_pkmn_icon_stack_size();
// We'll keep images other than Pokémon icons inside of a separate array.
// Position them using a binary tree packing algorithm.
$etc_icons = $icon_stack->get_etc_icon_stack();
$etc_sets = $icon_stack->get_etc_icon_sets();
$etc_sect_fit = $icon_stack->get_etc_icon_stack_size();
// Full sprite attributes
$stack_size = $icon_stack->get_combined_stack_size();
$sprite_width = $stack_size['w'];
$sprite_height = $stack_size['h'];
// Now we'll generate the sprite sheet.
// About how much memory will this use?
$mb_estimate = format_bytes($sprite_width * $sprite_height * 32);
print(I18n::lf('sprite_stats', array(
$sprite_width,
$sprite_height,
count($etc_icons) + count($pkmn_icons),
$mb_estimate,
)));
// 3. Generation of the icon sprite
// ---------------------------------------------------------------------------
$sprite_sections = array(
'pkmn' => $pkmn_sect_fit['h'],
'etc' => $etc_sect_fit['h'],
);
$icon_sprite = new IconSprite(
$sprite_width,
$sprite_height,
$sprite_sections,
$debug_img_inclusion
);
// Pass on data on which icons are the standard variants.
$icon_sprite->set_pkmn_std_icons($pkmn_std_icons);
// Array of both the Pokémon icons and the other icons.
$all_icons = $icon_stack->get_all_icons();
// For the sprite image itself, iterate over all icons and add them.
$added = 0;
$total = count($all_icons);
for ($a = 0; $a < count($all_icons); ++$a) {
// IconSprite returns true if successful, false if the image
// could not be used somehow.
$icon = $all_icons[$a];
if ($icon_sprite->add($icon)) {
$added += 1;
}
}
print(I18n::lf('icons_added', array($added)));
// Send some feedback in case we couldn't add all icons.
if (($added - $total) > 0) {
if ($debug_img_inclusion) {
print(I18n::lf('icons_skipped', array($added - $total)));
}
else {
print(I18n::lf('icons_skipped_hint', array($added - $total)));
}
}
print(I18n::l('sprite_ready'));
print(I18n::lf('sprite_saving', array(
$generate_optimized ? $dir_output.$img_output_tmp : $dir_output.$img_output,
)));
// Save the initial result to output directory.
if (file_exists($dir_output.$img_output_tmp)) {
unlink($dir_output.$img_output_tmp);
}
$icon_sprite->output($dir_output.$img_output_tmp, $generate_optimized !== true);
$icon_sprite->destroy();
// Use pngcrush to minimize the image.
if ($generate_optimized === true) {
print(I18n::l('pngcrush_start'));
if (file_exists($dir_output.$img_output)) {
unlink($dir_output.$img_output);
}
$crush_cmd = $pngcrush_path.' -l 9 -q -text b author "Pokémon Sprite Generator v'.$version.' r'.$revision.'" -text b copyright "'.$copyright_gf.'" '.$dir_output.$img_output_tmp.' '.$dir_output.$img_output;
exec($crush_cmd);
if (file_exists($dir_output.$img_output)) {
// Seems like pngcrush was successful. Let's review the results.
$size_before = filesize($dir_output.$img_output_tmp);
$size_after = filesize($dir_output.$img_output);
print(I18n::lf('pngcrush_success', array(
format_bytes($size_before),
format_bytes($size_after),
)));
// Delete the unoptimized image.
print(I18n::l('sprite_del_old'));
unlink($dir_output.$img_output_tmp);
}
else {
// We couldn't use pngcrush for some reason.
print(I18n::l('pngcrush_error'));
exit;
}
}
else {
// If we're not using pngcrush, just rename it to the final filename.
rename($dir_output.$img_output_tmp, $dir_output.$img_output);
}
// 4. Generation of the HTML, SCSS and JS files pertaining to the sprites
// ---------------------------------------------------------------------------
// We're going to be making a map of positioning values, styling information
// as well as an HTML overview of every icon.
$icon_overview = new IconOverview();
$icon_styler = new IconStyler();
$icon_js = new IconJS();
// Create a tree structure of our icon sets based on their subtype information.
// This will be given to the SCSS, JS and overview generators.
$icon_tree = $icon_stack->get_icon_type_tree();
$set_sizes = $icon_stack->get_set_sizes();
$icon_overview->set_icon_list($all_icons);
$icon_styler->set_icon_list($all_icons);
$icon_styler->set_icon_sizes($set_sizes);
$icon_js->set_sprite_sections($sprite_sections);
$icon_js->set_icon_tree($icon_tree);
$icon_js->set_icon_sizes($set_sizes);
// For the generation of the HTML, SCSS and JS files, a number of
// base variables are used.
$base_vars = array(
'title_str' => $title_str,
'revision' => $revision,
'version' => $version,
'website_txt' => $website_txt,
'copyright_str' => $copyright_str,
'copyright_gf' => $copyright_gf,
'copyright_contrib_notice' => $copyright_contrib_notice,
'generated_on' => $generated_on,
'css_base_selector' => $css_base_selector,
);
// Generate the HTML overview of all icons we've just added.
print(I18n::l('html_generating'));
$icon_overview->register_vars($base_vars);
$icon_overview->register_tpl($resources_dir.$html_tpl);
$overview = $icon_overview->get_overview('html');
if (file_exists($dir_output.$html_output)) {
unlink($dir_output.$html_output);
}
file_put_contents($dir_output.$html_output, $overview);
// If requested, generate a Markdown overview too.
if ($generate_markdown) {
print(I18n::l('markdown_generating'));
$overview_md = $icon_overview->get_overview('markdown');
if (file_exists($dir_output.$md_output)) {
unlink($dir_output.$md_output);
}
file_put_contents($dir_output.$md_output, $overview_md);
}
// Generate a page about the build too.
$icon_overview->register_tpl($resources_dir.$html_build_tpl);
$overview = $icon_overview->get_build_overview('html');
if (file_exists($dir_output.$html_build_output)) {
unlink($dir_output.$html_build_output);
}
file_put_contents($dir_output.$html_build_output, $overview);
// With the image and the overview done, let's generate the SCSS.
print(I18n::l('scss_generating'));
$icon_styler->register_vars($base_vars);
$icon_styler->register_tpl($resources_dir.$scss_tpl);
$scss = $icon_styler->get_scss();
if (file_exists($dir_output.$scss_output)) {
unlink($dir_output.$scss_output);
}
file_put_contents($dir_output.$scss_output, $scss);
// Finally, the JS.
print(I18n::l('js_generating'));
$icon_js->register_vars($base_vars);
$icon_js->register_tpl($resources_dir.$js_tpl);
$js = $icon_js->get_js();
if (file_exists($dir_output.$js_output)) {
unlink($dir_output.$js_output);
}
file_put_contents($dir_output.$js_output, $js);
// All done!
print(I18n::l('all_done'));
exit;