diff options
author | Adam Lesinski <adamlesinski@google.com> | 2015-04-06 11:46:52 -0700 |
---|---|---|
committer | Adam Lesinski <adamlesinski@google.com> | 2015-04-09 17:19:06 -0700 |
commit | 98aa3ad6e46e3b0270785c8b3f9798e37e8af140 (patch) | |
tree | 268c3dbfbf8fb92e2e13610700e475f565c760ad /tools | |
parent | 0dfd7fba38d0ae5172903ada322e76ed99002008 (diff) | |
download | frameworks_base-98aa3ad6e46e3b0270785c8b3f9798e37e8af140.zip frameworks_base-98aa3ad6e46e3b0270785c8b3f9798e37e8af140.tar.gz frameworks_base-98aa3ad6e46e3b0270785c8b3f9798e37e8af140.tar.bz2 |
Add PNG and 9-patch support
Change-Id: I9ecdfdf82b82d59084490da518e167e256afd5f2
Diffstat (limited to 'tools')
-rw-r--r-- | tools/aapt2/Android.mk | 10 | ||||
-rw-r--r-- | tools/aapt2/Flag.cpp | 109 | ||||
-rw-r--r-- | tools/aapt2/Flag.h | 28 | ||||
-rw-r--r-- | tools/aapt2/Main.cpp | 1042 | ||||
-rw-r--r-- | tools/aapt2/Png.cpp | 1284 | ||||
-rw-r--r-- | tools/aapt2/Png.h | 38 | ||||
-rw-r--r-- | tools/aapt2/data/res/drawable/icon.png | bin | 0 -> 2341 bytes | |||
-rw-r--r-- | tools/aapt2/data/res/drawable/test.9.png | bin | 0 -> 124 bytes |
8 files changed, 1792 insertions, 719 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index 9cea176..14f558e 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -29,12 +29,14 @@ sources := \ BinaryResourceParser.cpp \ ConfigDescription.cpp \ Files.cpp \ + Flag.cpp \ JavaClassGenerator.cpp \ Linker.cpp \ Locale.cpp \ Logger.cpp \ ManifestParser.cpp \ ManifestValidator.cpp \ + Png.cpp \ ResChunkPullParser.cpp \ Resolver.cpp \ Resource.cpp \ @@ -69,7 +71,10 @@ testSources := \ XliffXmlPullParser_test.cpp \ XmlFlattener_test.cpp -cIncludes := +cIncludes := \ + external/libpng \ + external/libz + hostLdLibs := hostStaticLibs := \ @@ -78,7 +83,8 @@ hostStaticLibs := \ liblog \ libcutils \ libexpat \ - libziparchive-host + libziparchive-host \ + libpng ifneq ($(strip $(USE_MINGW)),) hostStaticLibs += libz diff --git a/tools/aapt2/Flag.cpp b/tools/aapt2/Flag.cpp new file mode 100644 index 0000000..b1ee8e7 --- /dev/null +++ b/tools/aapt2/Flag.cpp @@ -0,0 +1,109 @@ +#include "Flag.h" +#include "StringPiece.h" + +#include <functional> +#include <iomanip> +#include <iostream> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +struct Flag { + std::string name; + std::string description; + std::function<void(const StringPiece&)> action; + bool required; + bool* flagResult; + bool parsed; +}; + +static std::vector<Flag> sFlags; +static std::vector<std::string> sArgs; + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), action, false, nullptr, false }); +} + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), action, true, nullptr, false }); +} + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool* result) { + sFlags.push_back( + Flag{ name.toString(), description.toString(), {}, false, result, false }); +} + +static void usageAndDie(const StringPiece& command) { + std::cerr << command << " [options]"; + for (const Flag& flag : sFlags) { + if (flag.required) { + std::cerr << " " << flag.name << " arg"; + } + } + std::cerr << " files..." << std::endl << std::endl << "Options:" << std::endl; + + for (const Flag& flag : sFlags) { + std::string command = flag.name; + if (!flag.flagResult) { + command += " arg "; + } + std::cerr << " " << std::setw(30) << std::left << command + << flag.description << std::endl; + } + exit(1); +} + +void parse(int argc, char** argv, const StringPiece& command) { + for (int i = 0; i < argc; i++) { + const StringPiece arg(argv[i]); + if (*arg.data() != '-') { + sArgs.emplace_back(arg.toString()); + continue; + } + + bool match = false; + for (Flag& flag : sFlags) { + if (arg == flag.name) { + match = true; + flag.parsed = true; + if (flag.flagResult) { + *flag.flagResult = true; + } else { + i++; + if (i >= argc) { + std::cerr << flag.name << " missing argument." << std::endl + << std::endl; + usageAndDie(command); + } + flag.action(argv[i]); + } + break; + } + } + + if (!match) { + std::cerr << "unknown option '" << arg << "'." << std::endl << std::endl; + usageAndDie(command); + } + } + + for (const Flag& flag : sFlags) { + if (flag.required && !flag.parsed) { + std::cerr << "missing required flag " << flag.name << std::endl << std::endl; + usageAndDie(command); + } + } +} + +const std::vector<std::string>& getArgs() { + return sArgs; +} + +} // namespace flag +} // namespace aapt diff --git a/tools/aapt2/Flag.h b/tools/aapt2/Flag.h new file mode 100644 index 0000000..32f5f2c --- /dev/null +++ b/tools/aapt2/Flag.h @@ -0,0 +1,28 @@ +#ifndef AAPT_FLAG_H +#define AAPT_FLAG_H + +#include "StringPiece.h" + +#include <functional> +#include <string> +#include <vector> + +namespace aapt { +namespace flag { + +void requiredFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void optionalFlag(const StringPiece& name, const StringPiece& description, + std::function<void(const StringPiece&)> action); + +void optionalSwitch(const StringPiece& name, const StringPiece& description, bool* result); + +void parse(int argc, char** argv, const StringPiece& command); + +const std::vector<std::string>& getArgs(); + +} // namespace flag +} // namespace aapt + +#endif // AAPT_FLAG_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index cfc5874..3a4b444 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -18,10 +18,12 @@ #include "BigBuffer.h" #include "BinaryResourceParser.h" #include "Files.h" +#include "Flag.h" #include "JavaClassGenerator.h" #include "Linker.h" #include "ManifestParser.h" #include "ManifestValidator.h" +#include "Png.h" #include "ResourceParser.h" #include "ResourceTable.h" #include "ResourceValues.h" @@ -41,6 +43,7 @@ #include <iostream> #include <sstream> #include <sys/stat.h> +#include <utils/Errors.h> using namespace aapt; @@ -107,12 +110,12 @@ std::unique_ptr<FileReference> makeFileReference(StringPool& pool, const StringP * Collect files from 'root', filtering out any files that do not * match the FileFilter 'filter'. */ -bool walkTree(const StringPiece& root, const FileFilter& filter, - std::vector<Source>& outEntries) { +bool walkTree(const Source& root, const FileFilter& filter, + std::vector<Source>* outEntries) { bool error = false; - for (const std::string& dirName : listFiles(root)) { - std::string dir(root.toString()); + for (const std::string& dirName : listFiles(root.path)) { + std::string dir = root.path; appendPath(&dir, dirName); FileType ft = getFileType(dir); @@ -134,13 +137,11 @@ bool walkTree(const StringPiece& root, const FileFilter& filter, } if (ft != FileType::kRegular) { - Logger::error(Source{ file }) - << "not a regular file." - << std::endl; + Logger::error(Source{ file }) << "not a regular file." << std::endl; error = true; continue; } - outEntries.emplace_back(Source{ file }); + outEntries->push_back(Source{ file }); } } return !error; @@ -171,9 +172,6 @@ bool loadBinaryResourceTable(std::shared_ptr<ResourceTable> table, const Source& } bool loadResTable(android::ResTable* table, const Source& source) { - // For NO_ERROR (which on Windows is a MACRO). - using namespace android; - std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); if (!ifs) { Logger::error(source) << strerror(errno) << std::endl; @@ -190,7 +188,7 @@ bool loadResTable(android::ResTable* table, const Source& source) { char* buf = new char[dataSize]; ifs.read(buf, dataSize); - bool result = table->add(buf, dataSize, -1, true) == NO_ERROR; + bool result = table->add(buf, dataSize, -1, true) == android::NO_ERROR; delete [] buf; return result; @@ -323,13 +321,6 @@ bool collectXml(std::shared_ptr<ResourceTable> table, const Source& source, } } - std::unique_ptr<FileReference> fileResource = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(name.entry) + ".xml", - name.type, - config); - table->addResource(name, config, source.line(0), std::move(fileResource)); - for (size_t level : sdkLevels) { Logger::note(source) << "creating v" << level << " versioned file." @@ -347,14 +338,15 @@ bool collectXml(std::shared_ptr<ResourceTable> table, const Source& source, return true; } -struct CompileXml { +struct CompileItem { Source source; ResourceName name; ConfigDescription config; + std::string extension; }; -bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, - const Source& outputSource, std::queue<CompileXml>* queue) { +bool compileXml(std::shared_ptr<Resolver> resolver, const CompileItem& item, + const Source& outputSource, std::queue<CompileItem>* queue) { std::ifstream in(item.source.path, std::ifstream::binary); if (!in) { Logger::error(item.source) << strerror(errno) << std::endl; @@ -376,7 +368,7 @@ bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, if (minStrippedSdk.value() > 0) { // Something was stripped, so let's generate a new file // with the version of the smallest SDK version stripped. - CompileXml newWork = item; + CompileItem newWork = item; newWork.config.sdkVersion = minStrippedSdk.value(); queue->push(newWork); } @@ -394,9 +386,51 @@ bool compileXml(std::shared_ptr<Resolver> resolver, const CompileXml& item, return true; } +bool compilePng(const Source& source, const Source& output) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::ofstream out(output.path, std::ofstream::binary); + if (!out) { + Logger::error(output) << strerror(errno) << std::endl; + return false; + } + + std::string err; + Png png; + if (!png.process(source, in, out, {}, &err)) { + Logger::error(source) << err << std::endl; + return false; + } + return true; +} + +bool copyFile(const Source& source, const Source& output) { + std::ifstream in(source.path, std::ifstream::binary); + if (!in) { + Logger::error(source) << strerror(errno) << std::endl; + return false; + } + + std::ofstream out(output.path, std::ofstream::binary); + if (!out) { + Logger::error(output) << strerror(errno) << std::endl; + return false; + } + + if (out << in.rdbuf()) { + Logger::error(output) << strerror(errno) << std::endl; + return true; + } + return false; +} + struct AaptOptions { enum class Phase { - LegacyFull, + Full, Collect, Link, Compile, @@ -411,16 +445,26 @@ struct AaptOptions { // The location of the manifest file. Source manifest; - // The files to process. - std::vector<Source> sources; + // The source directories to walk and find resource files. + std::vector<Source> sourceDirs; + + // The resource files to process and collect. + std::vector<Source> collectFiles; + + // The binary table files to link. + std::vector<Source> linkFiles; + + // The resource files to compile. + std::vector<Source> compileFiles; // The libraries these files may reference. std::vector<Source> libraries; - // Output directory. + // Output path. This can be a directory or file + // depending on the phase. Source output; - // Whether to generate a Java Class. + // Directory to in which to generate R.java. Maybe<Source> generateJavaClass; // Whether to output verbose details about @@ -428,9 +472,8 @@ struct AaptOptions { bool verbose = false; }; -bool compileAndroidManifest(std::shared_ptr<Resolver> resolver, const AaptOptions& options) { - using namespace android; - +bool compileAndroidManifest(const std::shared_ptr<Resolver>& resolver, + const AaptOptions& options) { Source outSource = options.output; appendPath(&outSource.path, "AndroidManifest.xml"); @@ -461,8 +504,8 @@ bool compileAndroidManifest(std::shared_ptr<Resolver> resolver, const AaptOption p += b.size; } - ResXMLTree tree; - if (tree.setTo(data.get(), outBuffer.size()) != NO_ERROR) { + android::ResXMLTree tree; + if (tree.setTo(data.get(), outBuffer.size()) != android::NO_ERROR) { return false; } @@ -496,246 +539,117 @@ bool loadAppInfo(const Source& source, AppInfo* outInfo) { return parser.parse(source, pullParser, outInfo); } -/** - * Parses legacy options and walks the source directories collecting - * files to process. - */ -bool prepareLegacy(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions &options) { - options.phase = AaptOptions::Phase::LegacyFull; - - std::vector<StringPiece> sourceDirs; - while (argsIter != argsEndIter) { - if (*argsIter == "-S") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-S missing argument." << std::endl; - return false; - } - sourceDirs.push_back(*argsIter); - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "-M") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-M missing argument." << std::endl; - return false; - } +static AaptOptions prepareArgs(int argc, char** argv) { + if (argc < 2) { + std::cerr << "no command specified." << std::endl; + exit(1); + } - if (!options.manifest.path.empty()) { - Logger::error() << "multiple -M flags are not allowed." << std::endl; - return false; - } - options.manifest.path = argsIter->toString(); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-J") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-J missing argument." << std::endl; - return false; - } - options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else { - Logger::error() << "unrecognized option '" << *argsIter << "'." << std::endl; - return false; - } + const StringPiece command(argv[1]); + argc -= 2; + argv += 2; - ++argsIter; - } + AaptOptions options; - if (options.manifest.path.empty()) { - Logger::error() << "must specify manifest file with -M." << std::endl; - return false; + StringPiece outputDescription = "place output in file"; + if (command == "package") { + options.phase = AaptOptions::Phase::Full; + outputDescription = "place output in directory"; + } else if (command == "collect") { + options.phase = AaptOptions::Phase::Collect; + } else if (command == "link") { + options.phase = AaptOptions::Phase::Link; + } else if (command == "compile") { + options.phase = AaptOptions::Phase::Compile; + outputDescription = "place output in directory"; + } else { + std::cerr << "invalid command '" << command << "'." << std::endl; + exit(1); } - // Load the App's package name, etc. - if (!loadAppInfo(options.manifest, &options.appInfo)) { - return false; - } + if (options.phase == AaptOptions::Phase::Full) { + flag::requiredFlag("-S", "add a directory in which to find resources", + [&options](const StringPiece& arg) { + options.sourceDirs.push_back(Source{ arg.toString() }); + }); + + flag::requiredFlag("-M", "path to AndroidManifest.xml", + [&options](const StringPiece& arg) { + options.manifest = Source{ arg.toString() }; + }); + + flag::optionalFlag("-I", "add an Android APK to link against", + [&options](const StringPiece& arg) { + options.libraries.push_back(Source{ arg.toString() }); + }); + + flag::optionalFlag("--java", "directory in which to generate R.java", + [&options](const StringPiece& arg) { + options.generateJavaClass = Source{ arg.toString() }; + }); - /** - * Set up the file filter to ignore certain files. - */ - const char* customIgnore = getenv("ANDROID_AAPT_IGNORE"); - FileFilter fileFilter; - if (customIgnore && customIgnore[0]) { - fileFilter.setPattern(customIgnore); } else { - fileFilter.setPattern( - "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"); - } + flag::requiredFlag("--package", "Android package name", + [&options](const StringPiece& arg) { + options.appInfo.package = util::utf8ToUtf16(arg); + }); - /* - * Enumerate the files in each source directory. - */ - for (const StringPiece& source : sourceDirs) { - if (!walkTree(source, fileFilter, options.sources)) { - return false; + if (options.phase != AaptOptions::Phase::Collect) { + flag::optionalFlag("-I", "add an Android APK to link against", + [&options](const StringPiece& arg) { + options.libraries.push_back(Source{ arg.toString() }); + }); } - } - return true; -} - -bool prepareCollect(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Collect; - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; + if (options.phase == AaptOptions::Phase::Link) { + flag::optionalFlag("--java", "directory in which to generate R.java", + [&options](const StringPiece& arg) { + options.generateJavaClass = Source{ arg.toString() }; + }); } - ++argsIter; } - return true; -} -bool prepareLink(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Link; + // Common flags for all steps. + flag::requiredFlag("-o", outputDescription, [&options](const StringPiece& arg) { + options.output = Source{ arg.toString() }; + }); + flag::optionalSwitch("-v", "enables verbose logging", &options.verbose); - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "--java") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--java missing argument." << std::endl; - return false; - } - options.generateJavaClass = make_value<Source>(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; - } - ++argsIter; - } - return true; -} + // Build the command string for output (eg. "aapt2 compile"). + std::string fullCommand = "aapt2"; + fullCommand += " "; + fullCommand += command.toString(); -bool prepareCompile(std::vector<StringPiece>::const_iterator argsIter, - const std::vector<StringPiece>::const_iterator argsEndIter, - AaptOptions& options) { - options.phase = AaptOptions::Phase::Compile; + // Actually read the command line flags. + flag::parse(argc, argv, fullCommand); - while (argsIter != argsEndIter) { - if (*argsIter == "--package") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "--package missing argument." << std::endl; - return false; - } - options.appInfo.package = util::utf8ToUtf16(*argsIter); - } else if (*argsIter == "-o") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-o missing argument." << std::endl; - return false; - } - options.output = Source{ argsIter->toString() }; - } else if (*argsIter == "-I") { - ++argsIter; - if (argsIter == argsEndIter) { - Logger::error() << "-I missing argument." << std::endl; - return false; - } - options.libraries.push_back(Source{ argsIter->toString() }); - } else if (*argsIter == "-v") { - options.verbose = true; - } else if (argsIter->data()[0] != '-') { - options.sources.push_back(Source{ argsIter->toString() }); - } else { - Logger::error() - << "unknown option '" - << *argsIter - << "'." - << std::endl; - return false; + // Copy all the remaining arguments. + if (options.phase == AaptOptions::Phase::Collect) { + for (const std::string& arg : flag::getArgs()) { + options.collectFiles.push_back(Source{ arg }); + } + } else if (options.phase == AaptOptions::Phase::Compile) { + for (const std::string& arg : flag::getArgs()) { + options.compileFiles.push_back(Source{ arg }); + } + } else if (options.phase == AaptOptions::Phase::Link) { + for (const std::string& arg : flag::getArgs()) { + options.linkFiles.push_back(Source{ arg }); } - ++argsIter; } - return true; + return options; } -struct CollectValuesItem { - Source source; - ConfigDescription config; -}; - -bool collectValues(std::shared_ptr<ResourceTable> table, const CollectValuesItem& item) { - std::ifstream in(item.source.path, std::ifstream::binary); +static bool collectValues(const std::shared_ptr<ResourceTable>& table, const Source& source, + const ConfigDescription& config) { + std::ifstream in(source.path, std::ifstream::binary); if (!in) { - Logger::error(item.source) << strerror(errno) << std::endl; + Logger::error(source) << strerror(errno) << std::endl; return false; } std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); - ResourceParser parser(table, item.source, item.config, xmlParser); + ResourceParser parser(table, source, config, xmlParser); return parser.parse(); } @@ -750,7 +664,7 @@ struct ResourcePathData { * Resource file paths are expected to look like: * [--/res/]type[-config]/name */ -Maybe<ResourcePathData> extractResourcePathData(const Source& source) { +static Maybe<ResourcePathData> extractResourcePathData(const Source& source) { std::vector<std::string> parts = util::splitAndLowercase(source.path, '/'); if (parts.size() < 2) { Logger::error(source) << "bad resource path." << std::endl; @@ -792,16 +706,58 @@ Maybe<ResourcePathData> extractResourcePathData(const Source& source) { }; } -static bool doLegacy(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { +bool doAll(AaptOptions* options, const std::shared_ptr<ResourceTable>& table, + const std::shared_ptr<Resolver>& resolver) { + const bool versionStyles = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Link); + const bool verifyNoMissingSymbols = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Link); + const bool compileFiles = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Compile); + const bool flattenTable = (options->phase == AaptOptions::Phase::Full || + options->phase == AaptOptions::Phase::Collect || + options->phase == AaptOptions::Phase::Link); + const bool useExtendedChunks = options->phase == AaptOptions::Phase::Collect; + + // Build the output table path. + Source outputTable = options->output; + if (options->phase == AaptOptions::Phase::Full) { + appendPath(&outputTable.path, "resources.arsc"); + } + bool error = false; - std::queue<CompileXml> xmlCompileQueue; + std::queue<CompileItem> compileQueue; + + // If source directories were specified, walk them looking for resource files. + if (!options->sourceDirs.empty()) { + const char* customIgnore = getenv("ANDROID_AAPT_IGNORE"); + FileFilter fileFilter; + if (customIgnore && customIgnore[0]) { + fileFilter.setPattern(customIgnore); + } else { + fileFilter.setPattern( + "!.svn:!.git:!.ds_store:!*.scc:.*:<dir>_*:!CVS:!thumbs.db:!picasa.ini:!*~"); + } + + for (const Source& source : options->sourceDirs) { + if (!walkTree(source, fileFilter, &options->collectFiles)) { + return false; + } + } + } + + // Load all binary resource tables. + for (const Source& source : options->linkFiles) { + error |= !loadBinaryResourceTable(table, source); + } + + if (error) { + return false; + } - // - // Read values XML files and XML/PNG files. + // Collect all the resource files. // Need to parse the resource type/config/filename. - // - for (const Source& source : options.sources) { + for (const Source& source : options->collectFiles) { Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); if (!maybePathData) { return false; @@ -809,351 +765,154 @@ static bool doLegacy(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resol const ResourcePathData& pathData = maybePathData.value(); if (pathData.resourceDir == u"values") { - if (options.verbose) { + if (options->verbose) { Logger::note(source) << "collecting values..." << std::endl; } - error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); + error |= !collectValues(table, source, pathData.config); continue; } const ResourceType* type = parseResourceType(pathData.resourceDir); if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; + Logger::error(source) << "invalid resource type '" << pathData.resourceDir << "'." + << std::endl; return false; } ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - if (options.verbose) { - Logger::note(source) << "collecting XML..." << std::endl; - } - error |= !collectXml(table, source, resourceName, pathData.config); - xmlCompileQueue.push(CompileXml{ - source, - resourceName, - pathData.config - }); - } else { - std::unique_ptr<FileReference> fileReference = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(pathData.name) + "." + pathData.extension, - *type, pathData.config); + // Add the file name to the resource table. + std::unique_ptr<FileReference> fileReference = makeFileReference( + table->getValueStringPool(), + util::utf16ToUtf8(pathData.name) + "." + pathData.extension, + *type, pathData.config); + error |= !table->addResource(resourceName, pathData.config, source.line(0), + std::move(fileReference)); - error |= !table->addResource(resourceName, pathData.config, source.line(0), - std::move(fileReference)); + if (pathData.extension == "xml") { + error |= !collectXml(table, source, resourceName, pathData.config); } + + compileQueue.push( + CompileItem{ source, resourceName, pathData.config, pathData.extension }); } if (error) { return false; } - versionStylesForCompat(table); + // Version all styles referencing attributes outside of their specified SDK version. + if (versionStyles) { + versionStylesForCompat(table); + } - // - // Verify all references and data types. - // + // Verify that all references are valid. Linker linker(table, resolver); if (!linker.linkAndValidate()) { - Logger::error() - << "linking failed." - << std::endl; return false; } - const auto& unresolvedRefs = linker.getUnresolvedReferences(); - if (!unresolvedRefs.empty()) { - for (const auto& entry : unresolvedRefs) { - for (const auto& source : entry.second) { - Logger::error(source) - << "unresolved symbol '" - << entry.first - << "'." - << std::endl; + // Verify that all symbols exist. + if (verifyNoMissingSymbols) { + const auto& unresolvedRefs = linker.getUnresolvedReferences(); + if (!unresolvedRefs.empty()) { + for (const auto& entry : unresolvedRefs) { + for (const auto& source : entry.second) { + Logger::error(source) << "unresolved symbol '" << entry.first << "'." + << std::endl; + } } - } - return false; - } - - // - // Compile the XML files. - // - while (!xmlCompileQueue.empty()) { - const CompileXml& item = xmlCompileQueue.front(); - - // Create the output path from the resource name. - std::stringstream outputPath; - outputPath << item.name.type; - if (item.config != ConfigDescription{}) { - outputPath << "-" << item.config.toString(); - } - - Source outSource = options.output; - appendPath(&outSource.path, "res"); - appendPath(&outSource.path, outputPath.str()); - - if (!mkdirs(outSource.path)) { - Logger::error(outSource) << strerror(errno) << std::endl; return false; } - - appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); - - if (options.verbose) { - Logger::note(outSource) << "compiling XML file." << std::endl; - } - - error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); - xmlCompileQueue.pop(); - } - - if (error) { - return false; - } - - // - // Compile the AndroidManifest.xml file. - // - if (!compileAndroidManifest(resolver, options)) { - return false; } - // - // Generate the Java R class. - // - if (options.generateJavaClass) { - Source outPath = options.generateJavaClass.value(); - if (options.verbose) { - Logger::note() - << "writing symbols to " - << outPath - << "." - << std::endl; - } - - for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { - appendPath(&outPath.path, part); - } - - if (!mkdirs(outPath.path)) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - appendPath(&outPath.path, "R.java"); - - std::ofstream fout(outPath.path); - if (!fout) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - JavaClassGenerator generator(table, JavaClassGenerator::Options{}); - if (!generator.generate(fout)) { - Logger::error(outPath) - << generator.getError() - << "." - << std::endl; - return false; - } - } - - // - // Flatten resource table. - // - if (table->begin() != table->end()) { - BigBuffer buffer(1024); - TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = false; - TableFlattener flattener(tableOptions); - if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; - return false; - } - - if (options.verbose) { - Logger::note() - << "Final resource table size=" - << util::formatSize(buffer.size()) - << std::endl; - } + // Compile files. + if (compileFiles) { + // First process any input compile files. + for (const Source& source : options->compileFiles) { + Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); + if (!maybePathData) { + return false; + } - std::string outTable(options.output.path); - appendPath(&outTable, "resources.arsc"); + const ResourcePathData& pathData = maybePathData.value(); + const ResourceType* type = parseResourceType(pathData.resourceDir); + if (!type) { + Logger::error(source) << "invalid resource type '" << pathData.resourceDir + << "'." << std::endl; + return false; + } - std::ofstream fout(outTable, std::ofstream::binary); - if (!fout) { - Logger::error(Source{outTable}) - << strerror(errno) - << "." - << std::endl; - return false; + ResourceName resourceName = { table->getPackage(), *type, pathData.name }; + compileQueue.push( + CompileItem{ source, resourceName, pathData.config, pathData.extension }); } - if (!util::writeAll(fout, buffer)) { - Logger::error(Source{outTable}) - << strerror(errno) - << "." - << std::endl; - return false; - } - fout.flush(); - } - return true; -} - -static bool doCollect(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - bool error = false; - - // - // Read values XML files and XML/PNG files. - // Need to parse the resource type/config/filename. - // - for (const Source& source : options.sources) { - Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); - if (!maybePathData) { - return false; - } + // Now process the actual compile queue. + for (; !compileQueue.empty(); compileQueue.pop()) { + const CompileItem& item = compileQueue.front(); - const ResourcePathData& pathData = maybePathData.value(); - if (pathData.resourceDir == u"values") { - if (options.verbose) { - Logger::note(source) << "collecting values..." << std::endl; + // Create the output directory path from the resource type and config. + std::stringstream outputPath; + outputPath << item.name.type; + if (item.config != ConfigDescription{}) { + outputPath << "-" << item.config.toString(); } - error |= !collectValues(table, CollectValuesItem{ source, pathData.config }); - continue; - } + Source outSource = options->output; + appendPath(&outSource.path, "res"); + appendPath(&outSource.path, outputPath.str()); - const ResourceType* type = parseResourceType(pathData.resourceDir); - if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; - return false; - } - - ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - if (options.verbose) { - Logger::note(source) << "collecting XML..." << std::endl; + // Make the directory. + if (!mkdirs(outSource.path)) { + Logger::error(outSource) << strerror(errno) << std::endl; + return false; } - error |= !collectXml(table, source, resourceName, pathData.config); - } else { - std::unique_ptr<FileReference> fileReference = makeFileReference( - table->getValueStringPool(), - util::utf16ToUtf8(pathData.name) + "." + pathData.extension, - *type, - pathData.config); - error |= !table->addResource(resourceName, pathData.config, source.line(0), - std::move(fileReference)); - } - } - - if (error) { - return false; - } + // Add the file name to the directory path. + appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + "." + item.extension); - Linker linker(table, resolver); - if (!linker.linkAndValidate()) { - return false; - } + if (item.extension == "xml") { + if (options->verbose) { + Logger::note(outSource) << "compiling XML file." << std::endl; + } - // - // Flatten resource table-> - // - if (table->begin() != table->end()) { - BigBuffer buffer(1024); - TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = true; - TableFlattener flattener(tableOptions); - if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; - return false; - } + error |= !compileXml(resolver, item, outSource, &compileQueue); + } else if (item.extension == "png" || item.extension == "9.png") { + if (options->verbose) { + Logger::note(outSource) << "compiling png file." << std::endl; + } - std::ofstream fout(options.output.path, std::ofstream::binary); - if (!fout) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; - return false; + error |= !compilePng(item.source, outSource); + } else { + error |= !copyFile(item.source, outSource); + } } - if (!util::writeAll(fout, buffer)) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + if (error) { return false; } - fout.flush(); - } - return true; -} - -static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - bool error = false; - - for (const Source& source : options.sources) { - error |= !loadBinaryResourceTable(table, source); - } - - if (error) { - return false; } - versionStylesForCompat(table); - - Linker linker(table, resolver); - if (!linker.linkAndValidate()) { - return false; - } - - const auto& unresolvedRefs = linker.getUnresolvedReferences(); - if (!unresolvedRefs.empty()) { - for (const auto& entry : unresolvedRefs) { - for (const auto& source : entry.second) { - Logger::error(source) - << "unresolved symbol '" - << entry.first - << "'." - << std::endl; - } + // Compile and validate the AndroidManifest.xml. + if (!options->manifest.path.empty()) { + if (!compileAndroidManifest(resolver, *options)) { + return false; } - return false; } - // - // Generate the Java R class. - // - if (options.generateJavaClass) { - Source outPath = options.generateJavaClass.value(); - if (options.verbose) { - Logger::note() - << "writing symbols to " - << outPath - << "." - << std::endl; + // Generate the Java class file. + if (options->generateJavaClass) { + Source outPath = options->generateJavaClass.value(); + if (options->verbose) { + Logger::note() << "writing symbols to " << outPath << "." << std::endl; } - for (std::string& part : util::split(util::utf16ToUtf8(table->getPackage()), '.')) { + // Build the output directory from the package name. + // Eg. com.android.app -> com/android/app + const std::string packageUtf8 = util::utf16ToUtf8(table->getPackage()); + for (StringPiece part : util::tokenize<char>(packageUtf8, '.')) { appendPath(&outPath.path, part); } @@ -1170,52 +929,37 @@ static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolve return false; } - JavaClassGenerator generator(table, JavaClassGenerator::Options{}); + JavaClassGenerator generator(table, {}); if (!generator.generate(fout)) { - Logger::error(outPath) - << generator.getError() - << "." - << std::endl; + Logger::error(outPath) << generator.getError() << "." << std::endl; return false; } } - // - // Flatten resource table. - // - if (table->begin() != table->end()) { + // Flatten the resource table. + if (flattenTable && table->begin() != table->end()) { BigBuffer buffer(1024); TableFlattener::Options tableOptions; - tableOptions.useExtendedChunks = false; + tableOptions.useExtendedChunks = useExtendedChunks; TableFlattener flattener(tableOptions); if (!flattener.flatten(&buffer, *table)) { - Logger::error() - << "failed to flatten resource table->" - << std::endl; + Logger::error() << "failed to flatten resource table." << std::endl; return false; } - if (options.verbose) { - Logger::note() - << "Final resource table size=" - << util::formatSize(buffer.size()) - << std::endl; + if (options->verbose) { + Logger::note() << "Final resource table size=" << util::formatSize(buffer.size()) + << std::endl; } - std::ofstream fout(options.output.path, std::ofstream::binary); + std::ofstream fout(outputTable.path, std::ofstream::binary); if (!fout) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + Logger::error(outputTable) << strerror(errno) << "." << std::endl; return false; } if (!util::writeAll(fout, buffer)) { - Logger::error(options.output) - << strerror(errno) - << "." - << std::endl; + Logger::error(outputTable) << strerror(errno) << "." << std::endl; return false; } fout.flush(); @@ -1223,134 +967,24 @@ static bool doLink(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolve return true; } -static bool doCompile(std::shared_ptr<ResourceTable> table, std::shared_ptr<Resolver> resolver, - const AaptOptions& options) { - std::queue<CompileXml> xmlCompileQueue; - - for (const Source& source : options.sources) { - Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); - if (!maybePathData) { - return false; - } - - ResourcePathData& pathData = maybePathData.value(); - const ResourceType* type = parseResourceType(pathData.resourceDir); - if (!type) { - Logger::error(source) - << "invalid resource type '" - << pathData.resourceDir - << "'." - << std::endl; - return false; - } - - ResourceName resourceName = { table->getPackage(), *type, pathData.name }; - if (pathData.extension == "xml") { - xmlCompileQueue.push(CompileXml{ - source, - resourceName, - pathData.config - }); - } else { - // TODO(adamlesinski): Handle images here. - } - } - - bool error = false; - while (!xmlCompileQueue.empty()) { - const CompileXml& item = xmlCompileQueue.front(); - - // Create the output path from the resource name. - std::stringstream outputPath; - outputPath << item.name.type; - if (item.config != ConfigDescription{}) { - outputPath << "-" << item.config.toString(); - } - - Source outSource = options.output; - appendPath(&outSource.path, "res"); - appendPath(&outSource.path, outputPath.str()); - - if (!mkdirs(outSource.path)) { - Logger::error(outSource) << strerror(errno) << std::endl; - return false; - } - - appendPath(&outSource.path, util::utf16ToUtf8(item.name.entry) + ".xml"); - - if (options.verbose) { - Logger::note(outSource) << "compiling XML file." << std::endl; - } - - error |= !compileXml(resolver, item, outSource, &xmlCompileQueue); - xmlCompileQueue.pop(); - } - return !error; -} - int main(int argc, char** argv) { Logger::setLog(std::make_shared<Log>(std::cerr, std::cerr)); + AaptOptions options = prepareArgs(argc, argv); - std::vector<StringPiece> args; - args.reserve(argc - 1); - for (int i = 1; i < argc; i++) { - args.emplace_back(argv[i], strlen(argv[i])); - } - - if (args.empty()) { - Logger::error() << "no command specified." << std::endl; - return 1; - } - - AaptOptions options; - - // Check the command we're running. - const StringPiece& command = args.front(); - if (command == "package") { - if (!prepareLegacy(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "collect") { - if (!prepareCollect(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "link") { - if (!prepareLink(std::begin(args) + 1, std::end(args), options)) { - return 1; - } - } else if (command == "compile") { - if (!prepareCompile(std::begin(args) + 1, std::end(args), options)) { - return 1; + // If we specified a manifest, go ahead and load the package name from the manifest. + if (!options.manifest.path.empty()) { + if (!loadAppInfo(options.manifest, &options.appInfo)) { + return false; } - } else { - Logger::error() << "unknown command '" << command << "'." << std::endl; - return 1; } - // // Verify we have some common options set. - // - - if (options.sources.empty()) { - Logger::error() << "no sources specified." << std::endl; - return false; - } - - if (options.output.path.empty()) { - Logger::error() << "no output directory specified." << std::endl; - return false; - } - if (options.appInfo.package.empty()) { Logger::error() << "no package name specified." << std::endl; return false; } - - // - // Every phase needs a resource table and a resolver/linker. - // - + // Every phase needs a resource table. std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); table->setPackage(options.appInfo.package); if (options.appInfo.package == u"android") { @@ -1359,9 +993,7 @@ int main(int argc, char** argv) { table->setPackageId(0x7f); } - // // Load the included libraries. - // std::shared_ptr<android::AssetManager> libraries = std::make_shared<android::AssetManager>(); for (const Source& source : options.libraries) { if (util::stringEndsWith(source.path, ".arsc")) { @@ -1393,33 +1025,9 @@ int main(int argc, char** argv) { // Make the resolver that will cache IDs for us. std::shared_ptr<Resolver> resolver = std::make_shared<Resolver>(table, libraries); - // - // Dispatch to the real phase here. - // - - bool result = true; - switch (options.phase) { - case AaptOptions::Phase::LegacyFull: - result = doLegacy(table, resolver, options); - break; - - case AaptOptions::Phase::Collect: - result = doCollect(table, resolver, options); - break; - - case AaptOptions::Phase::Link: - result = doLink(table, resolver, options); - break; - - case AaptOptions::Phase::Compile: - result = doCompile(table, resolver, options); - break; - } - - if (!result) { - Logger::error() - << "aapt exiting with failures." - << std::endl; + // Do the work. + if (!doAll(&options, table, resolver)) { + Logger::error() << "aapt exiting with failures." << std::endl; return 1; } return 0; diff --git a/tools/aapt2/Png.cpp b/tools/aapt2/Png.cpp new file mode 100644 index 0000000..dd753f1 --- /dev/null +++ b/tools/aapt2/Png.cpp @@ -0,0 +1,1284 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Logger.h" +#include "Png.h" +#include "Source.h" +#include "Util.h" + +#include <androidfw/ResourceTypes.h> +#include <iostream> +#include <png.h> +#include <sstream> +#include <string> +#include <vector> +#include <zlib.h> + +namespace aapt { + +constexpr bool kDebug = false; +constexpr size_t kPngSignatureSize = 8u; + +struct PngInfo { + ~PngInfo() { + for (png_bytep row : rows) { + if (row != nullptr) { + delete[] row; + } + } + + delete[] xDivs; + delete[] yDivs; + } + + void* serialize9Patch() { + void* serialized = android::Res_png_9patch::serialize(info9Patch, xDivs, yDivs, + colors.data()); + reinterpret_cast<android::Res_png_9patch*>(serialized)->deviceToFile(); + return serialized; + } + + uint32_t width = 0; + uint32_t height = 0; + std::vector<png_bytep> rows; + + bool is9Patch = false; + android::Res_png_9patch info9Patch; + int32_t* xDivs = nullptr; + int32_t* yDivs = nullptr; + std::vector<uint32_t> colors; + + // Layout padding. + bool haveLayoutBounds = false; + int32_t layoutBoundsLeft; + int32_t layoutBoundsTop; + int32_t layoutBoundsRight; + int32_t layoutBoundsBottom; + + // Round rect outline description. + int32_t outlineInsetsLeft; + int32_t outlineInsetsTop; + int32_t outlineInsetsRight; + int32_t outlineInsetsBottom; + float outlineRadius; + uint8_t outlineAlpha; +}; + +static void readDataFromStream(png_structp readPtr, png_bytep data, png_size_t length) { + std::istream* input = reinterpret_cast<std::istream*>(png_get_io_ptr(readPtr)); + if (!input->read(reinterpret_cast<char*>(data), length)) { + png_error(readPtr, strerror(errno)); + } +} + +static void writeDataToStream(png_structp writePtr, png_bytep data, png_size_t length) { + std::ostream* output = reinterpret_cast<std::ostream*>(png_get_io_ptr(writePtr)); + if (!output->write(reinterpret_cast<const char*>(data), length)) { + png_error(writePtr, strerror(errno)); + } +} + +static void flushDataToStream(png_structp writePtr) { + std::ostream* output = reinterpret_cast<std::ostream*>(png_get_io_ptr(writePtr)); + if (!output->flush()) { + png_error(writePtr, strerror(errno)); + } +} + +static void logWarning(png_structp readPtr, png_const_charp warningMessage) { + SourceLogger* logger = reinterpret_cast<SourceLogger*>(png_get_error_ptr(readPtr)); + logger->warn() << warningMessage << "." << std::endl; +} + + +static bool readPng(png_structp readPtr, png_infop infoPtr, PngInfo* outInfo, + std::string* outError) { + if (setjmp(png_jmpbuf(readPtr))) { + *outError = "failed reading png"; + return false; + } + + png_set_sig_bytes(readPtr, kPngSignatureSize); + png_read_info(readPtr, infoPtr); + + int colorType, bitDepth, interlaceType, compressionType; + png_get_IHDR(readPtr, infoPtr, &outInfo->width, &outInfo->height, &bitDepth, &colorType, + &interlaceType, &compressionType, nullptr); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(readPtr); + } + + if (colorType == PNG_COLOR_TYPE_GRAY && bitDepth < 8) { + png_set_expand_gray_1_2_4_to_8(readPtr); + } + + if (png_get_valid(readPtr, infoPtr, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(readPtr); + } + + if (bitDepth == 16) { + png_set_strip_16(readPtr); + } + + if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + png_set_add_alpha(readPtr, 0xFF, PNG_FILLER_AFTER); + } + + if (colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + png_set_gray_to_rgb(readPtr); + } + + png_set_interlace_handling(readPtr); + png_read_update_info(readPtr, infoPtr); + + const uint32_t rowBytes = png_get_rowbytes(readPtr, infoPtr); + outInfo->rows.resize(outInfo->height); + for (size_t i = 0; i < outInfo->height; i++) { + outInfo->rows[i] = new png_byte[rowBytes]; + } + + png_read_image(readPtr, outInfo->rows.data()); + png_read_end(readPtr, infoPtr); + return true; +} + +static void checkNinePatchSerialization(android::Res_png_9patch* inPatch, void* data) { + size_t patchSize = inPatch->serializedSize(); + void* newData = malloc(patchSize); + memcpy(newData, data, patchSize); + android::Res_png_9patch* outPatch = inPatch->deserialize(newData); + outPatch->fileToDevice(); + // deserialization is done in place, so outPatch == newData + assert(outPatch == newData); + assert(outPatch->numXDivs == inPatch->numXDivs); + assert(outPatch->numYDivs == inPatch->numYDivs); + assert(outPatch->paddingLeft == inPatch->paddingLeft); + assert(outPatch->paddingRight == inPatch->paddingRight); + assert(outPatch->paddingTop == inPatch->paddingTop); + assert(outPatch->paddingBottom == inPatch->paddingBottom); +/* for (int i = 0; i < outPatch->numXDivs; i++) { + assert(outPatch->getXDivs()[i] == inPatch->getXDivs()[i]); + } + for (int i = 0; i < outPatch->numYDivs; i++) { + assert(outPatch->getYDivs()[i] == inPatch->getYDivs()[i]); + } + for (int i = 0; i < outPatch->numColors; i++) { + assert(outPatch->getColors()[i] == inPatch->getColors()[i]); + }*/ + free(newData); +} + +/*static void dump_image(int w, int h, const png_byte* const* rows, int color_type) { + int i, j, rr, gg, bb, aa; + + int bpp; + if (color_type == PNG_COLOR_TYPE_PALETTE || color_type == PNG_COLOR_TYPE_GRAY) { + bpp = 1; + } else if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { + bpp = 2; + } else if (color_type == PNG_COLOR_TYPE_RGB || color_type == PNG_COLOR_TYPE_RGB_ALPHA) { + // We use a padding byte even when there is no alpha + bpp = 4; + } else { + printf("Unknown color type %d.\n", color_type); + } + + for (j = 0; j < h; j++) { + const png_byte* row = rows[j]; + for (i = 0; i < w; i++) { + rr = row[0]; + gg = row[1]; + bb = row[2]; + aa = row[3]; + row += bpp; + + if (i == 0) { + printf("Row %d:", j); + } + switch (bpp) { + case 1: + printf(" (%d)", rr); + break; + case 2: + printf(" (%d %d", rr, gg); + break; + case 3: + printf(" (%d %d %d)", rr, gg, bb); + break; + case 4: + printf(" (%d %d %d %d)", rr, gg, bb, aa); + break; + } + if (i == (w - 1)) { + printf("\n"); + } + } + } +}*/ + +#define MAX(a,b) ((a)>(b)?(a):(b)) +#define ABS(a) ((a)<0?-(a):(a)) + +static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int grayscaleTolerance, + png_colorp rgbPalette, png_bytep alphaPalette, + int *paletteEntries, bool *hasTransparency, int *colorType, + png_bytepp outRows) { + int w = imageInfo.width; + int h = imageInfo.height; + int i, j, rr, gg, bb, aa, idx; + uint32_t colors[256], col; + int num_colors = 0; + int maxGrayDeviation = 0; + + bool isOpaque = true; + bool isPalette = true; + bool isGrayscale = true; + + // Scan the entire image and determine if: + // 1. Every pixel has R == G == B (grayscale) + // 2. Every pixel has A == 255 (opaque) + // 3. There are no more than 256 distinct RGBA colors + + if (kDebug) { + printf("Initial image data:\n"); + //dump_image(w, h, imageInfo.rows.data(), PNG_COLOR_TYPE_RGB_ALPHA); + } + + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + int odev = maxGrayDeviation; + maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(gg - bb), maxGrayDeviation); + maxGrayDeviation = MAX(ABS(bb - rr), maxGrayDeviation); + if (maxGrayDeviation > odev) { + if (kDebug) { + printf("New max dev. = %d at pixel (%d, %d) = (%d %d %d %d)\n", + maxGrayDeviation, i, j, rr, gg, bb, aa); + } + } + + // Check if image is really grayscale + if (isGrayscale) { + if (rr != gg || rr != bb) { + if (kDebug) { + printf("Found a non-gray pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isGrayscale = false; + } + } + + // Check if image is really opaque + if (isOpaque) { + if (aa != 0xff) { + if (kDebug) { + printf("Found a non-opaque pixel at %d, %d = (%d %d %d %d)\n", + i, j, rr, gg, bb, aa); + } + isOpaque = false; + } + } + + // Check if image is really <= 256 colors + if (isPalette) { + col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); + bool match = false; + for (idx = 0; idx < num_colors; idx++) { + if (colors[idx] == col) { + match = true; + break; + } + } + + // Write the palette index for the pixel to outRows optimistically + // We might overwrite it later if we decide to encode as gray or + // gray + alpha + *out++ = idx; + if (!match) { + if (num_colors == 256) { + if (kDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; + } else { + colors[num_colors++] = col; + } + } + } + } + } + + *paletteEntries = 0; + *hasTransparency = !isOpaque; + int bpp = isOpaque ? 3 : 4; + int paletteSize = w * h + bpp * num_colors; + + if (kDebug) { + printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); + printf("isOpaque = %s\n", isOpaque ? "true" : "false"); + printf("isPalette = %s\n", isPalette ? "true" : "false"); + printf("Size w/ palette = %d, gray+alpha = %d, rgb(a) = %d\n", + paletteSize, 2 * w * h, bpp * w * h); + printf("Max gray deviation = %d, tolerance = %d\n", maxGrayDeviation, grayscaleTolerance); + } + + // Choose the best color type for the image. + // 1. Opaque gray - use COLOR_TYPE_GRAY at 1 byte/pixel + // 2. Gray + alpha - use COLOR_TYPE_PALETTE if the number of distinct combinations + // is sufficiently small, otherwise use COLOR_TYPE_GRAY_ALPHA + // 3. RGB(A) - use COLOR_TYPE_PALETTE if the number of distinct colors is sufficiently + // small, otherwise use COLOR_TYPE_RGB{_ALPHA} + if (isGrayscale) { + if (isOpaque) { + *colorType = PNG_COLOR_TYPE_GRAY; // 1 byte/pixel + } else { + // Use a simple heuristic to determine whether using a palette will + // save space versus using gray + alpha for each pixel. + // This doesn't take into account chunk overhead, filtering, LZ + // compression, etc. + if (isPalette && (paletteSize < 2 * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; // 1 byte/pixel + 4 bytes/color + } else { + *colorType = PNG_COLOR_TYPE_GRAY_ALPHA; // 2 bytes per pixel + } + } + } else if (isPalette && (paletteSize < bpp * w * h)) { + *colorType = PNG_COLOR_TYPE_PALETTE; + } else { + if (maxGrayDeviation <= grayscaleTolerance) { + logger->note() << "forcing image to gray (max deviation = " << maxGrayDeviation + << ")." + << std::endl; + *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; + } else { + *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; + } + } + + // Perform postprocessing of the image or palette data based on the final + // color type chosen + + if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Create separate RGB and Alpha palettes and set the number of colors + *paletteEntries = num_colors; + + // Create the RGB and alpha palettes + for (int idx = 0; idx < num_colors; idx++) { + col = colors[idx]; + rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff); + rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff); + rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff); + alphaPalette[idx] = (png_byte) (col & 0xff); + } + } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { + // If the image is gray or gray + alpha, compact the pixels into outRows + for (j = 0; j < h; j++) { + const png_byte* row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + rr = *row++; + gg = *row++; + bb = *row++; + aa = *row++; + + if (isGrayscale) { + *out++ = rr; + } else { + *out++ = (png_byte) (rr * 0.2126f + gg * 0.7152f + bb * 0.0722f); + } + if (!isOpaque) { + *out++ = aa; + } + } + } + } +} + +static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance, SourceLogger* logger, std::string* outError) { + if (setjmp(png_jmpbuf(writePtr))) { + *outError = "failed to write png"; + return false; + } + + uint32_t width, height; + int colorType, bitDepth, interlaceType, compressionType; + + png_unknown_chunk unknowns[3]; + unknowns[0].data = nullptr; + unknowns[1].data = nullptr; + unknowns[2].data = nullptr; + + png_bytepp outRows = (png_bytepp) malloc((int) info->height * sizeof(png_bytep)); + if (outRows == (png_bytepp) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + for (uint32_t i = 0; i < info->height; i++) { + outRows[i] = (png_bytep) malloc(2 * (int) info->width); + if (outRows[i] == (png_bytep) 0) { + printf("Can't allocate output buffer!\n"); + exit(1); + } + } + + png_set_compression_level(writePtr, Z_BEST_COMPRESSION); + + if (kDebug) { + logger->note() << "writing image: w = " << info->width + << ", h = " << info->height + << std::endl; + } + + png_color rgbPalette[256]; + png_byte alphaPalette[256]; + bool hasTransparency; + int paletteEntries; + + analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette, + &paletteEntries, &hasTransparency, &colorType, outRows); + + // If the image is a 9-patch, we need to preserve it as a ARGB file to make + // sure the pixels will not be pre-dithered/clamped until we decide they are + if (info->is9Patch && (colorType == PNG_COLOR_TYPE_RGB || + colorType == PNG_COLOR_TYPE_GRAY || colorType == PNG_COLOR_TYPE_PALETTE)) { + colorType = PNG_COLOR_TYPE_RGB_ALPHA; + } + + if (kDebug) { + switch (colorType) { + case PNG_COLOR_TYPE_PALETTE: + logger->note() << "has " << paletteEntries + << " colors" << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE." + << std::endl; + break; + case PNG_COLOR_TYPE_GRAY: + logger->note() << "is opaque gray, using PNG_COLOR_TYPE_GRAY." << std::endl; + break; + case PNG_COLOR_TYPE_GRAY_ALPHA: + logger->note() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA." << std::endl; + break; + case PNG_COLOR_TYPE_RGB: + logger->note() << "is opaque RGB, using PNG_COLOR_TYPE_RGB." << std::endl; + break; + case PNG_COLOR_TYPE_RGB_ALPHA: + logger->note() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA." << std::endl; + break; + } + } + + png_set_IHDR(writePtr, infoPtr, info->width, info->height, 8, colorType, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + + if (colorType == PNG_COLOR_TYPE_PALETTE) { + png_set_PLTE(writePtr, infoPtr, rgbPalette, paletteEntries); + if (hasTransparency) { + png_set_tRNS(writePtr, infoPtr, alphaPalette, paletteEntries, (png_color_16p) 0); + } + png_set_filter(writePtr, 0, PNG_NO_FILTERS); + } else { + png_set_filter(writePtr, 0, PNG_ALL_FILTERS); + } + + if (info->is9Patch) { + int chunkCount = 2 + (info->haveLayoutBounds ? 1 : 0); + int pIndex = info->haveLayoutBounds ? 2 : 1; + int bIndex = 1; + int oIndex = 0; + + // Chunks ordered thusly because older platforms depend on the base 9 patch data being last + png_bytep chunkNames = info->haveLayoutBounds + ? (png_bytep)"npOl\0npLb\0npTc\0" + : (png_bytep)"npOl\0npTc"; + + // base 9 patch data + if (kDebug) { + logger->note() << "adding 9-patch info..." << std::endl; + } + strcpy((char*)unknowns[pIndex].name, "npTc"); + unknowns[pIndex].data = (png_byte*) info->serialize9Patch(); + unknowns[pIndex].size = info->info9Patch.serializedSize(); + // TODO: remove the check below when everything works + checkNinePatchSerialization(&info->info9Patch, unknowns[pIndex].data); + + // automatically generated 9 patch outline data + int chunkSize = sizeof(png_uint_32) * 6; + strcpy((char*)unknowns[oIndex].name, "npOl"); + unknowns[oIndex].data = (png_byte*) calloc(chunkSize, 1); + png_byte outputData[chunkSize]; + memcpy(&outputData, &info->outlineInsetsLeft, 4 * sizeof(png_uint_32)); + ((float*) outputData)[4] = info->outlineRadius; + ((png_uint_32*) outputData)[5] = info->outlineAlpha; + memcpy(unknowns[oIndex].data, &outputData, chunkSize); + unknowns[oIndex].size = chunkSize; + + // optional optical inset / layout bounds data + if (info->haveLayoutBounds) { + int chunkSize = sizeof(png_uint_32) * 4; + strcpy((char*)unknowns[bIndex].name, "npLb"); + unknowns[bIndex].data = (png_byte*) calloc(chunkSize, 1); + memcpy(unknowns[bIndex].data, &info->layoutBoundsLeft, chunkSize); + unknowns[bIndex].size = chunkSize; + } + + for (int i = 0; i < chunkCount; i++) { + unknowns[i].location = PNG_HAVE_PLTE; + } + png_set_keep_unknown_chunks(writePtr, PNG_HANDLE_CHUNK_ALWAYS, + chunkNames, chunkCount); + png_set_unknown_chunks(writePtr, infoPtr, unknowns, chunkCount); + +#if PNG_LIBPNG_VER < 10600 + // Deal with unknown chunk location bug in 1.5.x and earlier. + png_set_unknown_chunk_location(writePtr, infoPtr, 0, PNG_HAVE_PLTE); + if (info->haveLayoutBounds) { + png_set_unknown_chunk_location(writePtr, infoPtr, 1, PNG_HAVE_PLTE); + } +#endif + } + + png_write_info(writePtr, infoPtr); + + png_bytepp rows; + if (colorType == PNG_COLOR_TYPE_RGB || colorType == PNG_COLOR_TYPE_RGB_ALPHA) { + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(writePtr, 0, PNG_FILLER_AFTER); + } + rows = info->rows.data(); + } else { + rows = outRows; + } + png_write_image(writePtr, rows); + + if (kDebug) { + printf("Final image data:\n"); + //dump_image(info->width, info->height, rows, colorType); + } + + png_write_end(writePtr, infoPtr); + + for (uint32_t i = 0; i < info->height; i++) { + free(outRows[i]); + } + free(outRows); + free(unknowns[0].data); + free(unknowns[1].data); + free(unknowns[2].data); + + png_get_IHDR(writePtr, infoPtr, &width, &height, &bitDepth, &colorType, &interlaceType, + &compressionType, nullptr); + + if (kDebug) { + logger->note() << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType + << std::endl; + } + return true; +} + +constexpr uint32_t kColorWhite = 0xffffffffu; +constexpr uint32_t kColorTick = 0xff000000u; +constexpr uint32_t kColorLayoutBoundsTick = 0xff0000ffu; + +enum class TickType { + kNone, + kTick, + kLayoutBounds, + kBoth +}; + +static TickType tickType(png_bytep p, bool transparent, const char** outError) { + png_uint_32 color = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + + if (transparent) { + if (p[3] == 0) { + return TickType::kNone; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + if (color == kColorTick) { + return TickType::kTick; + } + + // Error cases + if (p[3] != 0xff) { + *outError = "Frame pixels must be either solid or transparent " + "(not intermediate alphas)"; + return TickType::kNone; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in transparent frame must be black or red"; + } + return TickType::kTick; + } + + if (p[3] != 0xFF) { + *outError = "White frame must be a solid color (no alpha)"; + } + if (color == kColorWhite) { + return TickType::kNone; + } + if (color == kColorTick) { + return TickType::kTick; + } + if (color == kColorLayoutBoundsTick) { + return TickType::kLayoutBounds; + } + + if (p[0] != 0 || p[1] != 0 || p[2] != 0) { + *outError = "Ticks in white frame must be black or red"; + return TickType::kNone; + } + return TickType::kTick; +} + +enum class TickState { + kStart, + kInside1, + kOutside1 +}; + +static bool getHorizontalTicks(png_bytep row, int width, bool transparent, bool required, + int32_t* outLeft, int32_t* outRight, const char** outError, + uint8_t* outDivs, bool multipleAllowed) { + *outLeft = *outRight = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < width - 1; i++) { + if (tickType(row+i*4, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outLeft = i-1; + *outRight = width-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outLeft = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outRight = i-1; + outRight += 2; + outLeft += 2; + state = TickState::kOutside1; + } + } else { + *outLeft = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outLeft = -1; + return false; + } + return true; +} + +static bool getVerticalTicks(png_bytepp rows, int offset, int height, bool transparent, + bool required, int32_t* outTop, int32_t* outBottom, + const char** outError, uint8_t* outDivs, bool multipleAllowed) { + *outTop = *outBottom = -1; + TickState state = TickState::kStart; + bool found = false; + + for (int i = 1; i < height - 1; i++) { + if (tickType(rows[i]+offset, transparent, outError) == TickType::kTick) { + if (state == TickState::kStart || + (state == TickState::kOutside1 && multipleAllowed)) { + *outTop = i-1; + *outBottom = height-2; + found = true; + if (outDivs != NULL) { + *outDivs += 2; + } + state = TickState::kInside1; + } else if (state == TickState::kOutside1) { + *outError = "Can't have more than one marked region along edge"; + *outTop = i; + return false; + } + } else if (!*outError) { + if (state == TickState::kInside1) { + // We're done with this div. Move on to the next. + *outBottom = i-1; + outTop += 2; + outBottom += 2; + state = TickState::kOutside1; + } + } else { + *outTop = i; + return false; + } + } + + if (required && !found) { + *outError = "No marked region found along edge"; + *outTop = -1; + return false; + } + return true; +} + +static bool getHorizontalLayoutBoundsTicks(png_bytep row, int width, bool transparent, + bool /* required */, int32_t* outLeft, + int32_t* outRight, const char** outError) { + *outLeft = *outRight = 0; + + // Look for left tick + if (tickType(row + 4, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < width - 1) { + (*outLeft)++; + i++; + if (tickType(row + i * 4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for right tick + if (tickType(row + (width - 2) * 4, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = width - 2; + while (i > 1) { + (*outRight)++; + i--; + if (tickType(row+i*4, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static bool getVerticalLayoutBoundsTicks(png_bytepp rows, int offset, int height, bool transparent, + bool /* required */, int32_t* outTop, int32_t* outBottom, + const char** outError) { + *outTop = *outBottom = 0; + + // Look for top tick + if (tickType(rows[1] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Starting with a layout padding tick + int i = 1; + while (i < height - 1) { + (*outTop)++; + i++; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + + // Look for bottom tick + if (tickType(rows[height - 2] + offset, transparent, outError) == TickType::kLayoutBounds) { + // Ending with a layout padding tick + int i = height - 2; + while (i > 1) { + (*outBottom)++; + i--; + if (tickType(rows[i] + offset, transparent, outError) != TickType::kLayoutBounds) { + break; + } + } + } + return true; +} + +static void findMaxOpacity(png_bytepp rows, int startX, int startY, int endX, int endY, + int dX, int dY, int* outInset) { + uint8_t maxOpacity = 0; + int inset = 0; + *outInset = 0; + for (int x = startX, y = startY; x != endX && y != endY; x += dX, y += dY, inset++) { + png_byte* color = rows[y] + x * 4; + uint8_t opacity = color[3]; + if (opacity > maxOpacity) { + maxOpacity = opacity; + *outInset = inset; + } + if (opacity == 0xff) return; + } +} + +static uint8_t maxAlphaOverRow(png_bytep row, int startX, int endX) { + uint8_t maxAlpha = 0; + for (int x = startX; x < endX; x++) { + uint8_t alpha = (row + x * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static uint8_t maxAlphaOverCol(png_bytepp rows, int offsetX, int startY, int endY) { + uint8_t maxAlpha = 0; + for (int y = startY; y < endY; y++) { + uint8_t alpha = (rows[y] + offsetX * 4)[3]; + if (alpha > maxAlpha) maxAlpha = alpha; + } + return maxAlpha; +} + +static void getOutline(PngInfo* image) { + int midX = image->width / 2; + int midY = image->height / 2; + int endX = image->width - 2; + int endY = image->height - 2; + + // find left and right extent of nine patch content on center row + if (image->width > 4) { + findMaxOpacity(image->rows.data(), 1, midY, midX, -1, 1, 0, &image->outlineInsetsLeft); + findMaxOpacity(image->rows.data(), endX, midY, midX, -1, -1, 0, + &image->outlineInsetsRight); + } else { + image->outlineInsetsLeft = 0; + image->outlineInsetsRight = 0; + } + + // find top and bottom extent of nine patch content on center column + if (image->height > 4) { + findMaxOpacity(image->rows.data(), midX, 1, -1, midY, 0, 1, &image->outlineInsetsTop); + findMaxOpacity(image->rows.data(), midX, endY, -1, midY, 0, -1, + &image->outlineInsetsBottom); + } else { + image->outlineInsetsTop = 0; + image->outlineInsetsBottom = 0; + } + + int innerStartX = 1 + image->outlineInsetsLeft; + int innerStartY = 1 + image->outlineInsetsTop; + int innerEndX = endX - image->outlineInsetsRight; + int innerEndY = endY - image->outlineInsetsBottom; + int innerMidX = (innerEndX + innerStartX) / 2; + int innerMidY = (innerEndY + innerStartY) / 2; + + // assuming the image is a round rect, compute the radius by marching + // diagonally from the top left corner towards the center + image->outlineAlpha = std::max( + maxAlphaOverRow(image->rows[innerMidY], innerStartX, innerEndX), + maxAlphaOverCol(image->rows.data(), innerMidX, innerStartY, innerStartY)); + + int diagonalInset = 0; + findMaxOpacity(image->rows.data(), innerStartX, innerStartY, innerMidX, innerMidY, 1, 1, + &diagonalInset); + + /* Determine source radius based upon inset: + * sqrt(r^2 + r^2) = sqrt(i^2 + i^2) + r + * sqrt(2) * r = sqrt(2) * i + r + * (sqrt(2) - 1) * r = sqrt(2) * i + * r = sqrt(2) / (sqrt(2) - 1) * i + */ + image->outlineRadius = 3.4142f * diagonalInset; + + if (kDebug) { + printf("outline insets %d %d %d %d, rad %f, alpha %x\n", + image->outlineInsetsLeft, + image->outlineInsetsTop, + image->outlineInsetsRight, + image->outlineInsetsBottom, + image->outlineRadius, + image->outlineAlpha); + } +} + +static uint32_t getColor(png_bytepp rows, int left, int top, int right, int bottom) { + png_bytep color = rows[top] + left*4; + + if (left > right || top > bottom) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + + while (top <= bottom) { + for (int i = left; i <= right; i++) { + png_bytep p = rows[top]+i*4; + if (color[3] == 0) { + if (p[3] != 0) { + return android::Res_png_9patch::NO_COLOR; + } + } else if (p[0] != color[0] || p[1] != color[1] || + p[2] != color[2] || p[3] != color[3]) { + return android::Res_png_9patch::NO_COLOR; + } + } + top++; + } + + if (color[3] == 0) { + return android::Res_png_9patch::TRANSPARENT_COLOR; + } + return (color[3]<<24) | (color[0]<<16) | (color[1]<<8) | color[2]; +} + +static bool do9Patch(PngInfo* image, std::string* outError) { + image->is9Patch = true; + + int W = image->width; + int H = image->height; + int i, j; + + const int maxSizeXDivs = W * sizeof(int32_t); + const int maxSizeYDivs = H * sizeof(int32_t); + int32_t* xDivs = image->xDivs = new int32_t[W]; + int32_t* yDivs = image->yDivs = new int32_t[H]; + uint8_t numXDivs = 0; + uint8_t numYDivs = 0; + + int8_t numColors; + int numRows; + int numCols; + int top; + int left; + int right; + int bottom; + memset(xDivs, -1, maxSizeXDivs); + memset(yDivs, -1, maxSizeYDivs); + image->info9Patch.paddingLeft = image->info9Patch.paddingRight = -1; + image->info9Patch.paddingTop = image->info9Patch.paddingBottom = -1; + image->layoutBoundsLeft = image->layoutBoundsRight = 0; + image->layoutBoundsTop = image->layoutBoundsBottom = 0; + + png_bytep p = image->rows[0]; + bool transparent = p[3] == 0; + bool hasColor = false; + + const char* errorMsg = nullptr; + int errorPixel = -1; + const char* errorEdge = nullptr; + + int colorIndex = 0; + std::vector<png_bytep> newRows; + + // Validate size... + if (W < 3 || H < 3) { + errorMsg = "Image must be at least 3x3 (1x1 without frame) pixels"; + goto getout; + } + + // Validate frame... + if (!transparent && + (p[0] != 0xFF || p[1] != 0xFF || p[2] != 0xFF || p[3] != 0xFF)) { + errorMsg = "Must have one-pixel frame that is either transparent or white"; + goto getout; + } + + // Find left and right of sizing areas... + if (!getHorizontalTicks(p, W, transparent, true, &xDivs[0], &xDivs[1], &errorMsg, &numXDivs, + true)) { + errorPixel = xDivs[0]; + errorEdge = "top"; + goto getout; + } + + // Find top and bottom of sizing areas... + if (!getVerticalTicks(image->rows.data(), 0, H, transparent, true, &yDivs[0], &yDivs[1], + &errorMsg, &numYDivs, true)) { + errorPixel = yDivs[0]; + errorEdge = "left"; + goto getout; + } + + // Copy patch size data into image... + image->info9Patch.numXDivs = numXDivs; + image->info9Patch.numYDivs = numYDivs; + + // Find left and right of padding area... + if (!getHorizontalTicks(image->rows[H-1], W, transparent, false, + &image->info9Patch.paddingLeft, &image->info9Patch.paddingRight, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingLeft; + errorEdge = "bottom"; + goto getout; + } + + // Find top and bottom of padding area... + if (!getVerticalTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->info9Patch.paddingTop, &image->info9Patch.paddingBottom, + &errorMsg, nullptr, false)) { + errorPixel = image->info9Patch.paddingTop; + errorEdge = "right"; + goto getout; + } + + // Find left and right of layout padding... + getHorizontalLayoutBoundsTicks(image->rows[H-1], W, transparent, false, + &image->layoutBoundsLeft, &image->layoutBoundsRight, &errorMsg); + + getVerticalLayoutBoundsTicks(image->rows.data(), (W-1)*4, H, transparent, false, + &image->layoutBoundsTop, &image->layoutBoundsBottom, &errorMsg); + + image->haveLayoutBounds = image->layoutBoundsLeft != 0 + || image->layoutBoundsRight != 0 + || image->layoutBoundsTop != 0 + || image->layoutBoundsBottom != 0; + + if (image->haveLayoutBounds) { + if (kDebug) { + printf("layoutBounds=%d %d %d %d\n", image->layoutBoundsLeft, image->layoutBoundsTop, + image->layoutBoundsRight, image->layoutBoundsBottom); + } + } + + // use opacity of pixels to estimate the round rect outline + getOutline(image); + + // If padding is not yet specified, take values from size. + if (image->info9Patch.paddingLeft < 0) { + image->info9Patch.paddingLeft = xDivs[0]; + image->info9Patch.paddingRight = W - 2 - xDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingRight = W - 2 - image->info9Patch.paddingRight; + } + if (image->info9Patch.paddingTop < 0) { + image->info9Patch.paddingTop = yDivs[0]; + image->info9Patch.paddingBottom = H - 2 - yDivs[1]; + } else { + // Adjust value to be correct! + image->info9Patch.paddingBottom = H - 2 - image->info9Patch.paddingBottom; + } + +/* if (kDebug) { + printf("Size ticks for %s: x0=%d, x1=%d, y0=%d, y1=%d\n", imageName, + xDivs[0], xDivs[1], + yDivs[0], yDivs[1]); + printf("padding ticks for %s: l=%d, r=%d, t=%d, b=%d\n", imageName, + image->info9Patch.paddingLeft, image->info9Patch.paddingRight, + image->info9Patch.paddingTop, image->info9Patch.paddingBottom); + }*/ + + // Remove frame from image. + newRows.resize(H - 2); + for (i = 0; i < H - 2; i++) { + newRows[i] = image->rows[i + 1]; + memmove(newRows[i], newRows[i] + 4, (W - 2) * 4); + } + image->rows.swap(newRows); + + image->width -= 2; + W = image->width; + image->height -= 2; + H = image->height; + + // Figure out the number of rows and columns in the N-patch + numCols = numXDivs + 1; + if (xDivs[0] == 0) { // Column 1 is strechable + numCols--; + } + if (xDivs[numXDivs - 1] == W) { + numCols--; + } + numRows = numYDivs + 1; + if (yDivs[0] == 0) { // Row 1 is strechable + numRows--; + } + if (yDivs[numYDivs - 1] == H) { + numRows--; + } + + // Make sure the amount of rows and columns will fit in the number of + // colors we can use in the 9-patch format. + if (numRows * numCols > 0x7F) { + errorMsg = "Too many rows and columns in 9-patch perimeter"; + goto getout; + } + + numColors = numRows * numCols; + image->info9Patch.numColors = numColors; + image->colors.resize(numColors); + + // Fill in color information for each patch. + + uint32_t c; + top = 0; + + // The first row always starts with the top being at y=0 and the bottom + // being either yDivs[1] (if yDivs[0]=0) of yDivs[0]. In the former case + // the first row is stretchable along the Y axis, otherwise it is fixed. + // The last row always ends with the bottom being bitmap.height and the top + // being either yDivs[numYDivs-2] (if yDivs[numYDivs-1]=bitmap.height) or + // yDivs[numYDivs-1]. In the former case the last row is stretchable along + // the Y axis, otherwise it is fixed. + // + // The first and last columns are similarly treated with respect to the X + // axis. + // + // The above is to help explain some of the special casing that goes on the + // code below. + + // The initial yDiv and whether the first row is considered stretchable or + // not depends on whether yDiv[0] was zero or not. + for (j = (yDivs[0] == 0 ? 1 : 0); j <= numYDivs && top < H; j++) { + if (j == numYDivs) { + bottom = H; + } else { + bottom = yDivs[j]; + } + left = 0; + // The initial xDiv and whether the first column is considered + // stretchable or not depends on whether xDiv[0] was zero or not. + for (i = xDivs[0] == 0 ? 1 : 0; i <= numXDivs && left < W; i++) { + if (i == numXDivs) { + right = W; + } else { + right = xDivs[i]; + } + c = getColor(image->rows.data(), left, top, right - 1, bottom - 1); + image->colors[colorIndex++] = c; + if (kDebug) { + if (c != android::Res_png_9patch::NO_COLOR) { + hasColor = true; + } + } + left = right; + } + top = bottom; + } + + assert(colorIndex == numColors); + + if (kDebug && hasColor) { + for (i = 0; i < numColors; i++) { + if (i == 0) printf("Colors:\n"); + printf(" #%08x", image->colors[i]); + if (i == numColors - 1) printf("\n"); + } + } +getout: + if (errorMsg) { + std::stringstream err; + err << "9-patch malformed: " << errorMsg; + if (!errorEdge) { + err << "." << std::endl; + if (errorPixel >= 0) { + err << "Found at pixel #" << errorPixel << " along " << errorEdge << " edge"; + } else { + err << "Found along " << errorEdge << " edge"; + } + } + *outError = err.str(); + return false; + } + return true; +} + + +bool Png::process(const Source& source, std::istream& input, std::ostream& output, + const Options& options, std::string* outError) { + png_byte signature[kPngSignatureSize]; + + // Read the PNG signature first. + if (!input.read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + *outError = strerror(errno); + return false; + } + + // If the PNG signature doesn't match, bail early. + if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { + *outError = "not a valid png file"; + return false; + } + + SourceLogger logger(source); + bool result = false; + png_structp readPtr = nullptr; + png_infop infoPtr = nullptr; + png_structp writePtr = nullptr; + png_infop writeInfoPtr = nullptr; + PngInfo pngInfo = {}; + + readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!readPtr) { + *outError = "failed to allocate read ptr"; + goto bail; + } + + infoPtr = png_create_info_struct(readPtr); + if (!infoPtr) { + *outError = "failed to allocate info ptr"; + goto bail; + } + + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(&logger), nullptr, logWarning); + + // Set the read function to read from std::istream. + png_set_read_fn(readPtr, (png_voidp)&input, readDataFromStream); + + if (!readPng(readPtr, infoPtr, &pngInfo, outError)) { + goto bail; + } + + if (util::stringEndsWith(source.path, ".9.png")) { + if (!do9Patch(&pngInfo, outError)) { + goto bail; + } + } + + writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); + if (!writePtr) { + *outError = "failed to allocate write ptr"; + goto bail; + } + + writeInfoPtr = png_create_info_struct(writePtr); + if (!writeInfoPtr) { + *outError = "failed to allocate write info ptr"; + goto bail; + } + + png_set_error_fn(writePtr, nullptr, nullptr, logWarning); + + // Set the write function to write to std::ostream. + png_set_write_fn(writePtr, (png_voidp)&output, writeDataToStream, flushDataToStream); + + if (!writePng(writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance, &logger, + outError)) { + goto bail; + } + + result = true; +bail: + if (readPtr) { + png_destroy_read_struct(&readPtr, &infoPtr, nullptr); + } + + if (writePtr) { + png_destroy_write_struct(&writePtr, &writeInfoPtr); + } + return result; +} + +} // namespace aapt diff --git a/tools/aapt2/Png.h b/tools/aapt2/Png.h new file mode 100644 index 0000000..bc80754 --- /dev/null +++ b/tools/aapt2/Png.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_PNG_H +#define AAPT_PNG_H + +#include "Source.h" + +#include <iostream> +#include <string> + +namespace aapt { + +struct Png { + struct Options { + int grayScaleTolerance = 0; + }; + + bool process(const Source& source, std::istream& input, std::ostream& output, + const Options& options, std::string* outError); +}; + +} // namespace aapt + +#endif // AAPT_PNG_H diff --git a/tools/aapt2/data/res/drawable/icon.png b/tools/aapt2/data/res/drawable/icon.png Binary files differnew file mode 100644 index 0000000..4bff9b9 --- /dev/null +++ b/tools/aapt2/data/res/drawable/icon.png diff --git a/tools/aapt2/data/res/drawable/test.9.png b/tools/aapt2/data/res/drawable/test.9.png Binary files differnew file mode 100644 index 0000000..33daa11 --- /dev/null +++ b/tools/aapt2/data/res/drawable/test.9.png |