/* * Copyright 2014-2017 Fukurou Mishiranu * * 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. */ package com.mishiranu.dashchan.util; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Random; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorMatrixColorFilter; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.util.Pair; import android.view.Gravity; import com.mishiranu.dashchan.content.model.FileHolder; public class GraphicsUtils { public static final Typeface TYPEFACE_MEDIUM = newTypeface("sans-serif-medium"); public static final Typeface TYPEFACE_LIGHT = newTypeface("sans-serif-light"); private static final Random RANDOM = new Random(System.currentTimeMillis()); private static Typeface newTypeface(String familyName) { return Typeface.create(familyName, Typeface.NORMAL); } private static final float CONTRAST_GAIN = 2.5f; private static final ColorMatrixColorFilter CONTRAST_FILTER = new ColorMatrixColorFilter(new float[] { CONTRAST_GAIN, 0f, 0f, 0f, (1f - CONTRAST_GAIN) * 255f, 0f, CONTRAST_GAIN, 0f, 0f, (1f - CONTRAST_GAIN) * 255f, 0f, 0f, CONTRAST_GAIN, 0f, (1f - CONTRAST_GAIN) * 255f, 0f, 0f, 0f, 1f, 0f }); private static final ColorMatrixColorFilter BLACK_CHROMA_KEY_FILTER = new ColorMatrixColorFilter(new float[] { 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, -1f/3f, -1f/3f, -1f/3f, 0f, 255f }); public static final ColorMatrixColorFilter INVERT_FILTER = new ColorMatrixColorFilter(new float[] { -1f, 0f, 0f, 0f, 255f, 0f, -1f, 0f, 0f, 255f, 0f, 0f, -1f, 0f, 255f, 0f, 0f, 0f, 1f, 0f }); public static int modifyColorGain(int color, float gain) { int r = Color.red(color), g = Color.green(color), b = Color.blue(color); return Color.argb(Color.alpha(color), Math.min((int) (r * gain), 0xff), Math.min((int) (g * gain), 0xff), Math.min((int) (b * gain), 0xff)); } public static boolean isLight(int color) { return (Color.red(color) + Color.green(color) + Color.blue(color)) / 3 >= 0x80; } public static int mixColors(int background, int foreground) { int ba = Color.alpha(background), fa = Color.alpha(foreground); int a = fa + ba * (0xff - fa) / 0xff; int r = (Color.red(foreground) * fa + Color.red(background) * ba * (0xff - fa) / 0xff) / a; int g = (Color.green(foreground) * fa + Color.green(background) * ba * (0xff - fa) / 0xff) / a; int b = (Color.blue(foreground) * fa + Color.blue(background) * ba * (0xff - fa) / 0xff) / a; return Color.argb(Math.min(a, 0xff), Math.min(r, 0xff), Math.min(g, 0xff), Math.min(b, 0xff)); } @SuppressLint("RtlHardcoded") public static int getDrawableColor(Context context, Drawable drawable, int gravity) { float density = ResourceUtils.obtainDensity(context); int size = Math.max(drawable.getMinimumWidth(), drawable.getMinimumHeight()); if (size == 0) { size = (int) (64f * density); } Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); try { drawable.setBounds(0, 0, size, size); drawable.draw(new Canvas(bitmap)); int x, y; switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: { x = 0; break; } case Gravity.RIGHT: { x = size - 1; break; } default: { x = size / 2; break; } } switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: { y = 0; break; } case Gravity.BOTTOM: { y = size - 1; break; } default: { y = size / 2; break; } } return bitmap.getPixel(x, y); } finally { bitmap.recycle(); } } public static Bitmap reduceThumbnailSize(Resources resources, Bitmap bitmap) { int newSize = (int) (72f * ResourceUtils.obtainDensity(resources)); return reduceBitmapSize(bitmap, newSize, true); } public static Bitmap reduceBitmapSize(Bitmap bitmap, int newSize, boolean recycleOld) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); int oldSize = Math.min(width, height); float scale = newSize / (float) oldSize; if (scale >= 1.0) { return bitmap; } Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, (int) (width * scale), (int) (height * scale), true); if (recycleOld && resizedBitmap != bitmap) { bitmap.recycle(); } return resizedBitmap; } public static class Reencoding { public static final String FORMAT_JPEG = "jpeg"; public static final String FORMAT_PNG = "png"; public final String format; public final int quality; public final int reduce; public Reencoding(String format, int quality, int reduce) { this.format = FORMAT_JPEG.equals(format) || FORMAT_PNG.equals(format) ? format : FORMAT_JPEG; this.quality = quality > 100 ? 100 : quality < 1 ? 1 : quality; this.reduce = reduce > 8 ? 8 : reduce < 1 ? 1 : reduce; } public static boolean allowQuality(String format) { return FORMAT_JPEG.equals(format); } } public static boolean canRemoveMetadata(FileHolder fileHolder) { return fileHolder.getImageType() == FileHolder.ImageType.IMAGE_JPEG || fileHolder.getImageType() == FileHolder.ImageType.IMAGE_PNG; } public static class SkipRange { public final int start; public final int count; private SkipRange(int start, int count) { this.start = start; this.count = count; } } public static class TransformationData { public final ArrayList<SkipRange> skipRanges; public final byte[] decodedBytes; public final String newFileName; public final int newWidth; public final int newHeight; private TransformationData(ArrayList<SkipRange> skipRanges, byte[] decodedBytes, String newFileName, int newWidth, int newHeight) { this.skipRanges = skipRanges; this.decodedBytes = decodedBytes; this.newFileName = newFileName; this.newWidth = newWidth; this.newHeight = newHeight; } } public static TransformationData transformImageForPosting(FileHolder fileHolder, String fileName, boolean removeMetadata, Reencoding reencoding) { ArrayList<SkipRange> skipRanges = null; byte[] decodedBytes = null; String newFileName = null; InputStream input = null; int newWidth = -1; int newHeight = -1; try { if (reencoding != null && fileHolder.isImage()) { Bitmap bitmap; try { bitmap = fileHolder.readImageBitmap(Integer.MAX_VALUE, true, true); } catch (Exception | OutOfMemoryError e) { bitmap = null; } if (bitmap != null) { try { if (reencoding.reduce > 1) { Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, Math.max(bitmap.getWidth() / reencoding.reduce, 1), Math.max(bitmap.getHeight() / reencoding.reduce, 1), true); if (scaledBitmap != bitmap) { bitmap.recycle(); bitmap = scaledBitmap; } newWidth = bitmap.getWidth(); newHeight = bitmap.getHeight(); } boolean png = Reencoding.FORMAT_PNG.equals(reencoding.format); ByteArrayOutputStream output = new ByteArrayOutputStream(); bitmap.compress(Reencoding.FORMAT_PNG.equals(reencoding.format) ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, reencoding.quality, output); decodedBytes = output.toByteArray(); int index = fileName.lastIndexOf('.'); newFileName = (index >= 0 ? fileName.substring(0, index) : fileName) + (png ? ".png" : ".jpeg"); } finally { bitmap.recycle(); } } } else if (removeMetadata) { if (fileHolder.getImageType() == FileHolder.ImageType.IMAGE_JPEG) { input = new BufferedInputStream(fileHolder.openInputStream(), 16 * 1024); int position = 0; byte[] buffer = new byte[2]; while (true) { int oneByte = input.read(); position++; if (oneByte == 0xff) { oneByte = input.read(); position++; if ((oneByte & 0xe0) == 0xe0 || oneByte == 0xfe) { // Application data (0xe0 for JFIF, 0xe1 for EXIF) or comment (0xfe) if (!IOUtils.readExactlyCheck(input, buffer, 0, 2)) { break; } int size = IOUtils.bytesToInt(false, 0, 2, buffer); if (!IOUtils.skipExactlyCheck(input, size - 2)) { break; } if (skipRanges == null) { skipRanges = new ArrayList<>(); } skipRanges.add(new SkipRange(position - 2, size + 2)); position += size; } } if (oneByte == -1) { break; } } } else if (fileHolder.getImageType() == FileHolder.ImageType.IMAGE_PNG) { input = fileHolder.openInputStream(); if (IOUtils.skipExactlyCheck(input, 8)) { int position = 8; byte[] buffer = new byte[8]; while (true) { if (!IOUtils.readExactlyCheck(input, buffer, 0, 8)) { break; } int size = IOUtils.bytesToInt(false, 0, 4, buffer); String name = new String(buffer, 4, 4); if (!IOUtils.skipExactlyCheck(input, size + 4)) { break; } if (isUselessPngChunk(name)) { if (skipRanges == null) { skipRanges = new ArrayList<>(); } skipRanges.add(new SkipRange(position, size + 12)); } position += size + 12; if ("IEND".equals(name)) { int fileSize = fileHolder.getSize(); if (fileSize > position) { if (skipRanges == null) { skipRanges = new ArrayList<>(); } skipRanges.add(new SkipRange(position, fileSize - position)); } break; } } } } } } catch (IOException e) { // Ignore exception } finally { IOUtils.close(input); } return skipRanges != null || decodedBytes != null || newFileName != null ? new TransformationData(skipRanges, decodedBytes, newFileName, newWidth, newHeight) : null; } public static boolean isUselessPngChunk(String name) { return "iTXt".equals(name) || "tEXt".equals(name) || "tIME".equals(name) || "zTXt".equals(name); } public static boolean isBlackAndWhiteCaptchaImage(Bitmap image) { if (image != null) { int width = image.getWidth(); int height = image.getHeight(); int[] pixels = new int[width]; for (int i = 0; i < height; i++) { image.getPixels(pixels, 0, width, 0, i, width, 1); for (int j = 0; j < width; j++) { int color = pixels[j]; int a = Color.alpha(color); int r = Color.red(color); int g = Color.green(color); int b = Color.blue(color); if (a >= 0x20) { int max = Math.max(r, Math.max(g, b)); int min = Math.min(r, Math.min(g, b)); if (max - min >= 0x1a) { return false; // 10% } } } } return true; } return false; } public static Pair<Bitmap, Boolean> handleBlackAndWhiteCaptchaImage(Bitmap image) { return handleBlackAndWhiteCaptchaImage(image, null, 0, 0); } public static Pair<Bitmap, Boolean> handleBlackAndWhiteCaptchaImage(Bitmap image, Bitmap overlay, int overlayX, int overlayY) { if (image != null) { int width = image.getWidth(), height = image.getHeight(); Bitmap mask = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mask); canvas.drawColor(Color.WHITE); Paint paint = new Paint(); paint.setColorFilter(CONTRAST_FILTER); canvas.drawBitmap(image, 0, 0, paint); if (overlay != null) { canvas.drawBitmap(overlay, overlayX, overlayY, paint); } image.recycle(); paint.reset(); Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); canvas = new Canvas(result); paint.setColorFilter(BLACK_CHROMA_KEY_FILTER); canvas.drawBitmap(mask, 0, 0, paint); mask.recycle(); image = result; return new Pair<>(image, true); } return new Pair<>(null, false); } public static Bitmap generateNoise(int size, int scale, int colorFrom, int colorTo) { int aFrom = Color.alpha(colorFrom); int rFrom = Color.red(colorFrom); int gFrom = Color.green(colorFrom); int bFrom = Color.blue(colorFrom); int aTo = Color.alpha(colorTo); int rTo = Color.red(colorTo); int gTo = Color.green(colorTo); int bTo = Color.blue(colorTo); Random random = RANDOM; scale = Math.max(scale, 1); int realSize = size * scale; int[] pixels = new int[realSize * realSize]; for (int i = 0; i < pixels.length; i += realSize * scale) { for (int j = 0; j < realSize; j += scale) { int a = random.nextInt(aTo - aFrom + 1) + aFrom; int r = random.nextInt(rTo - rFrom + 1) + rFrom; int g = random.nextInt(gTo - gFrom + 1) + gFrom; int b = random.nextInt(bTo - bFrom + 1) + bFrom; for (int k = 0; k < scale; k++) { pixels[i + j + k] = Color.argb(a, r, g, b); } } for (int j = 1; j < scale; j++) { System.arraycopy(pixels, i, pixels, i + j * realSize, realSize); } } return Bitmap.createBitmap(pixels, realSize, realSize, Bitmap.Config.ARGB_8888); } }