mirror of
https://github.com/revanced/Apktool.git
synced 2024-06-29 16:26:32 +02:00
feat: Android implementation of 9patch decoder
This commit is contained in:
parent
d31e5275dc
commit
a87ac3a46c
|
@ -13,23 +13,24 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import org.apache.tools.ant.filters.*
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
apply plugin: 'java-library'
|
||||
|
||||
processResources {
|
||||
from('src/main/resources/properties') {
|
||||
include '**/*.properties'
|
||||
into 'properties'
|
||||
filter(ReplaceTokens, tokens: [version: project.apktool_version, gitrev: project.hash] )
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
from('src/main/resources/') {
|
||||
include '**/*.jar'
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
from('src/main/resources/properties') {
|
||||
include '**/*.properties'
|
||||
into 'properties'
|
||||
filter(ReplaceTokens, tokens: [version: project.apktool_version, gitrev: project.hash])
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
from('src/main/resources/') {
|
||||
include '**/*.jar'
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
|
||||
includeEmptyDirs = false
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
@ -39,14 +40,26 @@ dependencies {
|
|||
project(':brut.j.util'),
|
||||
project(':brut.j.common')
|
||||
|
||||
def androidJar
|
||||
def root = System.getenv('ANDROID_SDK_ROOT')
|
||||
if (root == null) {
|
||||
new GradleException("Missing ANDROID_SDK_ROOT").printStackTrace()
|
||||
androidJar = 'com.google.android:android:4.1.1.4'
|
||||
} else {
|
||||
def androidVersion = 30
|
||||
androidJar = files("$root/platforms/android-$androidVersion/android.jar")
|
||||
}
|
||||
|
||||
compileOnly androidJar
|
||||
|
||||
implementation depends.baksmali,
|
||||
depends.smali,
|
||||
depends.snakeyaml,
|
||||
depends.xmlpull,
|
||||
depends.guava,
|
||||
depends.commons_lang,
|
||||
depends.commons_io,
|
||||
depends.commons_text
|
||||
depends.smali,
|
||||
depends.snakeyaml,
|
||||
depends.xmlpull,
|
||||
depends.guava,
|
||||
depends.commons_lang,
|
||||
depends.commons_io,
|
||||
depends.commons_text
|
||||
|
||||
testImplementation depends.xmlunit
|
||||
}
|
||||
|
|
|
@ -16,9 +16,14 @@
|
|||
*/
|
||||
package brut.androlib.res.decoder;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import brut.androlib.AndrolibException;
|
||||
import brut.androlib.err.CantFind9PatchChunkException;
|
||||
import brut.util.ExtDataInput;
|
||||
import brut.util.OSDetection;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
@ -32,89 +37,23 @@ import java.io.InputStream;
|
|||
import java.io.OutputStream;
|
||||
|
||||
public class Res9patchStreamDecoder implements ResStreamDecoder {
|
||||
private static OSDecoder decoder;
|
||||
|
||||
{
|
||||
if (OSDetection.isAndroid()) {
|
||||
decoder = new AndroidImpl();
|
||||
} else {
|
||||
decoder = new OtherImpl();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decode(InputStream in, OutputStream out) throws AndrolibException {
|
||||
try {
|
||||
byte[] data = IOUtils.toByteArray(in);
|
||||
|
||||
if (data.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage im = ImageIO.read(new ByteArrayInputStream(data));
|
||||
int w = im.getWidth(), h = im.getHeight();
|
||||
|
||||
BufferedImage im2 = new BufferedImage(w + 2, h + 2, BufferedImage.TYPE_INT_ARGB);
|
||||
if (im.getType() == BufferedImage.TYPE_CUSTOM) {
|
||||
//TODO: Ensure this is gray + alpha case?
|
||||
Raster srcRaster = im.getRaster();
|
||||
WritableRaster dstRaster = im2.getRaster();
|
||||
int[] gray = null, alpha = null;
|
||||
for (int y = 0; y < im.getHeight(); y++) {
|
||||
gray = srcRaster.getSamples(0, y, w, 1, 0, gray);
|
||||
alpha = srcRaster.getSamples(0, y, w, 1, 1, alpha);
|
||||
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 0, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 1, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 2, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 3, alpha);
|
||||
}
|
||||
} else {
|
||||
im2.createGraphics().drawImage(im, 1, 1, w, h, null);
|
||||
}
|
||||
|
||||
NinePatch np = getNinePatch(data);
|
||||
drawHLine(im2, h + 1, np.padLeft + 1, w - np.padRight);
|
||||
drawVLine(im2, w + 1, np.padTop + 1, h - np.padBottom);
|
||||
|
||||
int[] xDivs = np.xDivs;
|
||||
if (xDivs.length == 0) {
|
||||
drawHLine(im2, 0, 1, w);
|
||||
} else {
|
||||
for (int i = 0; i < xDivs.length; i += 2) {
|
||||
drawHLine(im2, 0, xDivs[i] + 1, xDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
int[] yDivs = np.yDivs;
|
||||
if (yDivs.length == 0) {
|
||||
drawVLine(im2, 0, 1, h);
|
||||
} else {
|
||||
for (int i = 0; i < yDivs.length; i += 2) {
|
||||
drawVLine(im2, 0, yDivs[i] + 1, yDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Some images additionally use Optical Bounds
|
||||
// https://developer.android.com/about/versions/android-4.3.html#OpticalBounds
|
||||
try {
|
||||
OpticalInset oi = getOpticalInset(data);
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsLeft; i++) {
|
||||
int x = 1 + i;
|
||||
im2.setRGB(x, h + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsRight; i++) {
|
||||
int x = w - i;
|
||||
im2.setRGB(x, h + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsTop; i++) {
|
||||
int y = 1 + i;
|
||||
im2.setRGB(w + 1, y, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsBottom; i++) {
|
||||
int y = h - i;
|
||||
im2.setRGB(w + 1, y, OI_COLOR);
|
||||
}
|
||||
} catch (CantFind9PatchChunkException t) {
|
||||
// This chunk might not exist
|
||||
}
|
||||
|
||||
ImageIO.write(im2, "png", out);
|
||||
} catch (IOException | NullPointerException ex) {
|
||||
if (data.length == 0) return;
|
||||
decoder.decode(data, out);
|
||||
} catch (Exception ex) {
|
||||
// In my case this was triggered because a .png file was
|
||||
// containing a html document instead of an image.
|
||||
// This could be more verbose and try to MIME ?
|
||||
|
@ -153,18 +92,30 @@ public class Res9patchStreamDecoder implements ResStreamDecoder {
|
|||
}
|
||||
}
|
||||
|
||||
private void drawHLine(BufferedImage im, int y, int x1, int x2) {
|
||||
private void drawHLineJ(BufferedImage im, int y, int x1, int x2) {
|
||||
for (int x = x1; x <= x2; x++) {
|
||||
im.setRGB(x, y, NP_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawVLine(BufferedImage im, int x, int y1, int y2) {
|
||||
private void drawVLineJ(BufferedImage im, int x, int y1, int y2) {
|
||||
for (int y = y1; y <= y2; y++) {
|
||||
im.setRGB(x, y, NP_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHLineA(Bitmap bm, int y, int x1, int x2) {
|
||||
for (int x = x1; x <= x2; x++) {
|
||||
bm.setPixel(x, y, NP_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawVLineA(Bitmap bm, int x, int y1, int y2) {
|
||||
for (int y = y1; y <= y2; y++) {
|
||||
bm.setPixel(x, y, NP_COLOR);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int NP_CHUNK_TYPE = 0x6e705463; // npTc
|
||||
private static final int OI_CHUNK_TYPE = 0x6e704c62; // npLb
|
||||
private static final int NP_COLOR = 0xff000000;
|
||||
|
@ -222,4 +173,157 @@ public class Res9patchStreamDecoder implements ResStreamDecoder {
|
|||
layoutBoundsRight, layoutBoundsBottom);
|
||||
}
|
||||
}
|
||||
|
||||
// OS implementations
|
||||
private interface OSDecoder {
|
||||
void decode(byte[] data, OutputStream out) throws Exception;
|
||||
}
|
||||
|
||||
private class OtherImpl implements OSDecoder {
|
||||
public void decode(byte[] data, OutputStream out) throws Exception {
|
||||
BufferedImage im = ImageIO.read(new ByteArrayInputStream(data));
|
||||
int w = im.getWidth(), h = im.getHeight();
|
||||
|
||||
BufferedImage im2 = new BufferedImage(w + 2, h + 2, BufferedImage.TYPE_INT_ARGB);
|
||||
if (im.getType() == BufferedImage.TYPE_CUSTOM) {
|
||||
// TODO: Ensure this is gray + alpha case?
|
||||
Raster srcRaster = im.getRaster();
|
||||
WritableRaster dstRaster = im2.getRaster();
|
||||
int[] gray = null, alpha = null;
|
||||
for (int y = 0; y < im.getHeight(); y++) {
|
||||
gray = srcRaster.getSamples(0, y, w, 1, 0, gray);
|
||||
alpha = srcRaster.getSamples(0, y, w, 1, 1, alpha);
|
||||
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 0, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 1, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 2, gray);
|
||||
dstRaster.setSamples(1, y + 1, w, 1, 3, alpha);
|
||||
}
|
||||
} else {
|
||||
im2.createGraphics().drawImage(im, 1, 1, w, h, null);
|
||||
}
|
||||
|
||||
Res9patchStreamDecoder.NinePatch np = getNinePatch(data);
|
||||
drawHLineJ(im2, h + 1, np.padLeft + 1, w - np.padRight);
|
||||
drawVLineJ(im2, w + 1, np.padTop + 1, h - np.padBottom);
|
||||
|
||||
int[] xDivs = np.xDivs;
|
||||
if (xDivs.length == 0) {
|
||||
drawHLineJ(im2, 0, 1, w);
|
||||
} else {
|
||||
for (int i = 0; i < xDivs.length; i += 2) {
|
||||
drawHLineJ(im2, 0, xDivs[i] + 1, xDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
int[] yDivs = np.yDivs;
|
||||
if (yDivs.length == 0) {
|
||||
drawVLineJ(im2, 0, 1, h);
|
||||
} else {
|
||||
for (int i = 0; i < yDivs.length; i += 2) {
|
||||
drawVLineJ(im2, 0, yDivs[i] + 1, yDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Some images additionally use Optical Bounds
|
||||
// https://developer.android.com/about/versions/android-4.3.html#OpticalBounds
|
||||
try {
|
||||
OpticalInset oi = getOpticalInset(data);
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsLeft; i++) {
|
||||
int x = 1 + i;
|
||||
im2.setRGB(x, h + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsRight; i++) {
|
||||
int x = w - i;
|
||||
im2.setRGB(x, h + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsTop; i++) {
|
||||
int y = 1 + i;
|
||||
im2.setRGB(w + 1, y, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsBottom; i++) {
|
||||
int y = h - i;
|
||||
im2.setRGB(w + 1, y, OI_COLOR);
|
||||
}
|
||||
} catch (CantFind9PatchChunkException t) {
|
||||
// This chunk might not exist
|
||||
}
|
||||
|
||||
ImageIO.write(im2, "png", out);
|
||||
}
|
||||
}
|
||||
|
||||
private class AndroidImpl implements OSDecoder {
|
||||
@Override
|
||||
public void decode(byte[] data, OutputStream output) throws Exception {
|
||||
Bitmap bm = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
int width = bm.getWidth(), height = bm.getHeight();
|
||||
|
||||
Bitmap outImg = Bitmap.createBitmap(width + 2, height + 2, Bitmap.Config.ARGB_8888);
|
||||
drawImage(outImg, bm);
|
||||
|
||||
NinePatch np = getNinePatch(data);
|
||||
drawHLineA(outImg, height + 1, np.padLeft + 1, width - np.padRight);
|
||||
drawVLineA(outImg, width + 1, np.padTop + 1, height - np.padBottom);
|
||||
|
||||
int[] xDivs = np.xDivs;
|
||||
if (xDivs.length == 0) {
|
||||
drawHLineA(outImg, 0, 1, width);
|
||||
} else {
|
||||
for (int i = 0; i < xDivs.length; i += 2) {
|
||||
drawHLineA(outImg, 0, xDivs[i] + 1, xDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
int[] yDivs = np.yDivs;
|
||||
if (yDivs.length == 0) {
|
||||
drawVLineA(outImg, 0, 1, height);
|
||||
} else {
|
||||
for (int i = 0; i < yDivs.length; i += 2) {
|
||||
drawVLineA(outImg, 0, yDivs[i] + 1, yDivs[i + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Some images additionally use Optical Bounds
|
||||
// https://developer.android.com/about/versions/android-4.3.html#OpticalBounds
|
||||
try {
|
||||
OpticalInset oi = getOpticalInset(data);
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsLeft; i++) {
|
||||
int x = 1 + i;
|
||||
outImg.setPixel(x, height + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsRight; i++) {
|
||||
int x = width - i;
|
||||
outImg.setPixel(x, height + 1, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsTop; i++) {
|
||||
int y = 1 + i;
|
||||
outImg.setPixel(width + 1, y, OI_COLOR);
|
||||
}
|
||||
|
||||
for (int i = 0; i < oi.layoutBoundsBottom; i++) {
|
||||
int y = height - i;
|
||||
outImg.setPixel(width + 1, y, OI_COLOR);
|
||||
}
|
||||
} catch (CantFind9PatchChunkException t) {
|
||||
// This chunk might not exist
|
||||
}
|
||||
|
||||
outImg.compress(Bitmap.CompressFormat.PNG, 100, output);
|
||||
}
|
||||
|
||||
private void drawImage(Bitmap self, Bitmap toDraw) {
|
||||
Canvas canvas = new Canvas(self);
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.mapPoints(new float[] { 1f, 1f, toDraw.getWidth(), toDraw.getHeight() });
|
||||
canvas.drawBitmap(toDraw, matrix, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,23 @@
|
|||
*/
|
||||
package brut.util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class OSDetection {
|
||||
private static final String OS = System.getProperty("os.name").toLowerCase();
|
||||
private static final String BIT = System.getProperty("sun.arch.data.model").toLowerCase();
|
||||
private static final String BIT = System.getProperty("sun.arch.data.model");
|
||||
private static final boolean ANDROID;
|
||||
|
||||
static {
|
||||
boolean tmp;
|
||||
try {
|
||||
Class.forName("android.app.Activity");
|
||||
tmp = true;
|
||||
} catch(ClassNotFoundException e) {
|
||||
tmp = false;
|
||||
}
|
||||
ANDROID = tmp;
|
||||
}
|
||||
|
||||
public static boolean isWindows() {
|
||||
return (OS.contains("win"));
|
||||
|
@ -32,6 +46,10 @@ public class OSDetection {
|
|||
return (OS.contains("nix") || OS.contains("nux") || OS.contains("aix") || (OS.contains("sunos")));
|
||||
}
|
||||
|
||||
public static boolean isAndroid() {
|
||||
return ANDROID;
|
||||
}
|
||||
|
||||
public static boolean is64Bit() {
|
||||
if (isWindows()) {
|
||||
String arch = System.getenv("PROCESSOR_ARCHITECTURE");
|
||||
|
@ -39,7 +57,7 @@ public class OSDetection {
|
|||
|
||||
return arch != null && arch.endsWith("64") || wow64Arch != null && wow64Arch.endsWith("64");
|
||||
}
|
||||
return BIT.equalsIgnoreCase("64");
|
||||
return Objects.equals(BIT, "64");
|
||||
}
|
||||
|
||||
public static String returnOS() {
|
||||
|
|
35
build.gradle
35
build.gradle
|
@ -18,18 +18,18 @@ import java.nio.charset.StandardCharsets
|
|||
buildscript {
|
||||
ext {
|
||||
depends = [
|
||||
baksmali : 'org.smali:baksmali:2.5.2',
|
||||
commons_cli : 'commons-cli:commons-cli:1.5.0',
|
||||
commons_io : 'commons-io:commons-io:2.11.0',
|
||||
commons_lang : 'org.apache.commons:commons-lang3:3.12.0',
|
||||
commons_text : 'org.apache.commons:commons-text:1.9',
|
||||
guava : 'com.google.guava:guava:31.0.1-jre',
|
||||
junit : 'junit:junit:4.13.2',
|
||||
proguard_gradle: 'com.guardsquare:proguard-gradle:7.1.1',
|
||||
snakeyaml : 'org.yaml:snakeyaml:1.29:android',
|
||||
smali : 'org.smali:smali:2.5.2',
|
||||
xmlpull : 'xpp3:xpp3:1.1.4c',
|
||||
xmlunit : 'xmlunit:xmlunit:1.6',
|
||||
baksmali : 'org.smali:baksmali:2.5.2',
|
||||
commons_cli : 'commons-cli:commons-cli:1.5.0',
|
||||
commons_io : 'commons-io:commons-io:2.11.0',
|
||||
commons_lang : 'org.apache.commons:commons-lang3:3.12.0',
|
||||
commons_text : 'org.apache.commons:commons-text:1.9',
|
||||
guava : 'com.google.guava:guava:31.0.1-jre',
|
||||
junit : 'junit:junit:4.13.2',
|
||||
proguard_gradle: 'com.guardsquare:proguard-gradle:7.1.1',
|
||||
snakeyaml : 'org.yaml:snakeyaml:1.29:android',
|
||||
smali : 'org.smali:smali:2.5.2',
|
||||
xmlpull : 'xpp3:xpp3:1.1.4c',
|
||||
xmlunit : 'xmlunit:xmlunit:1.6',
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -78,8 +78,8 @@ allprojects {
|
|||
|
||||
// license plugin automatically fires these tasks, disable them and run them during releases
|
||||
gradle.startParameter.excludedTaskNames += [
|
||||
"licenseMain",
|
||||
"licenseTest"
|
||||
"licenseMain",
|
||||
"licenseTest"
|
||||
]
|
||||
|
||||
publishing {
|
||||
|
@ -132,9 +132,9 @@ build.doFirst {
|
|||
// fail the build if java (1.5/1.6/1.7)
|
||||
if (javaVersion.startsWith("1.5") || javaVersion.startsWith("1.6") || javaVersion.startsWith("1.7")) {
|
||||
throw new GradleException("You can fix this problem!\n" +
|
||||
"We found a " + javaVersion + " JDK\n" +
|
||||
"Please update JAVA_HOME to use at least a 1.8 JDK\n" +
|
||||
"Currently it is set to: " + System.getProperty("java.home")
|
||||
"We found a " + javaVersion + " JDK\n" +
|
||||
"Please update JAVA_HOME to use at least a 1.8 JDK\n" +
|
||||
"Currently it is set to: " + System.getProperty("java.home")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -229,7 +229,6 @@ subprojects {
|
|||
}
|
||||
|
||||
java {
|
||||
withJavadocJar()
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user