comment #2 - Fix watermarks

This commit is contained in:
Thomas 2022-05-08 19:12:41 +02:00
parent de744fedfa
commit ed8303c8b0
23 changed files with 2755 additions and 6 deletions

View file

@ -119,4 +119,5 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1'
} }

View file

@ -33,7 +33,9 @@ import android.content.res.Resources;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -126,6 +128,8 @@ import app.fedilab.android.client.mastodon.entities.Attachment;
import app.fedilab.android.exception.DBException; import app.fedilab.android.exception.DBException;
import app.fedilab.android.sqlite.Sqlite; import app.fedilab.android.sqlite.Sqlite;
import app.fedilab.android.viewmodel.mastodon.OauthVM; import app.fedilab.android.viewmodel.mastodon.OauthVM;
import app.fedilab.android.watermark.androidwm.WatermarkBuilder;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
import app.fedilab.android.webview.CustomWebview; import app.fedilab.android.webview.CustomWebview;
import es.dmoral.toasty.Toasty; import es.dmoral.toasty.Toasty;
import okhttp3.MediaType; import okhttp3.MediaType;
@ -1242,6 +1246,60 @@ public class Helper {
return MultipartBody.Part.createFormData(paramName, attachment.filename, requestFile); return MultipartBody.Part.createFormData(paramName, attachment.filename, requestFile);
} }
/**
* Creates MultipartBody.Part from Uri
*
* @return MultipartBody.Part for the given Uri
*/
public static MultipartBody.Part getMultipartBodyWithWM(Context context, String waterMark, @NonNull String paramName, @NonNull Attachment attachment) {
File files = new File(attachment.local_path);
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
float scale = sharedpreferences.getFloat(context.getString(R.string.SET_FONT_SCALE), 1.0f);
float textSize = 15;
Paint mPaint = new Paint();
mPaint.setTextSize(textSize);
float width = mPaint.measureText(waterMark, 0, waterMark.length()) * scale;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap backgroundBitmap = BitmapFactory.decodeFile(files.getAbsolutePath(), options);
int w = options.outWidth;
int h = options.outHeight;
float valx = (float) 1.0 - ((Helper.convertDpToPixel(width, context) + 90)) / (float) w;
if (valx < 0)
valx = 0;
float valy = (h - Helper.convertDpToPixel(textSize, context) - 30) / (float) h;
WatermarkText watermarkText = new WatermarkText(waterMark)
.setPositionX(valx)
.setPositionY(valy)
.setTextColor(Color.WHITE)
.setTextShadow(0.1f, 1, 1, Color.LTGRAY)
.setTextAlpha(200)
.setRotation(0)
.setTextSize(textSize);
Bitmap bitmap = WatermarkBuilder
.create(context, backgroundBitmap)
.loadWatermarkText(watermarkText)
.getWatermark()
.getOutputImage();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 0, bos);
byte[] bitmapdata = bos.toByteArray();
RequestBody requestFile = RequestBody.create(MediaType.parse(attachment.mimeType), bitmapdata);
return MultipartBody.Part.createFormData(paramName, attachment.filename, requestFile);
} catch (Exception e) {
e.printStackTrace();
}
RequestBody requestFile = RequestBody.create(MediaType.parse(attachment.mimeType), new File(attachment.local_path));
return MultipartBody.Part.createFormData(paramName, attachment.filename, requestFile);
}
public static MultipartBody.Part getMultipartBody(Context context, @NonNull String paramName, @NonNull Uri uri) { public static MultipartBody.Part getMultipartBody(Context context, @NonNull String paramName, @NonNull Uri uri) {
byte[] imageBytes = uriToByteArray(context, uri); byte[] imageBytes = uriToByteArray(context, uri);
ContentResolver cR = context.getApplicationContext().getContentResolver(); ContentResolver cR = context.getApplicationContext().getContentResolver();

View file

@ -23,6 +23,7 @@ import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@ -31,6 +32,7 @@ import android.text.Html;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.preference.PreferenceManager;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@ -41,6 +43,7 @@ import java.util.concurrent.TimeUnit;
import app.fedilab.android.BaseMainActivity; import app.fedilab.android.BaseMainActivity;
import app.fedilab.android.R; import app.fedilab.android.R;
import app.fedilab.android.activities.ContextActivity; import app.fedilab.android.activities.ContextActivity;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.client.entities.Account; import app.fedilab.android.client.entities.Account;
import app.fedilab.android.client.entities.PostState; import app.fedilab.android.client.entities.PostState;
import app.fedilab.android.client.entities.StatusDraft; import app.fedilab.android.client.entities.StatusDraft;
@ -173,6 +176,9 @@ public class PostMessageService extends IntentService {
} }
totalMediaSize = 0; totalMediaSize = 0;
totalBitRead = 0; totalBitRead = 0;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(PostMessageService.this);
boolean watermark = sharedPreferences.getBoolean(getString(R.string.SET_WATERMARK), false);
String watermarkText = sharedPreferences.getString(getString(R.string.SET_WATERMARK_TEXT) + MainActivity.currentUserID + MainActivity.currentInstance, null);
for (int i = startingPosition; i < statuses.size(); i++) { for (int i = startingPosition; i < statuses.size(); i++) {
if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) {
for (Attachment attachment : statuses.get(i).media_attachments) { for (Attachment attachment : statuses.get(i).media_attachments) {
@ -180,6 +186,15 @@ public class PostMessageService extends IntentService {
} }
} }
} }
if (watermarkText == null) {
try {
Account account = new Account(PostMessageService.this).getAccountByToken(token);
watermarkText = account.mastodon_account.username + "@" + account.instance;
} catch (DBException e) {
e.printStackTrace();
}
}
messageToSend = statuses.size() - startingPosition; messageToSend = statuses.size() - startingPosition;
messageSent = 0; messageSent = 0;
for (int i = startingPosition; i < statuses.size(); i++) { for (int i = startingPosition; i < statuses.size(); i++) {
@ -194,7 +209,11 @@ public class PostMessageService extends IntentService {
attachmentIds = new ArrayList<>(); attachmentIds = new ArrayList<>();
for (Attachment attachment : statuses.get(i).media_attachments) { for (Attachment attachment : statuses.get(i).media_attachments) {
MultipartBody.Part fileMultipartBody; MultipartBody.Part fileMultipartBody;
fileMultipartBody = Helper.getMultipartBody("file", attachment); if (watermark && attachment.mimeType.contains("image")) {
fileMultipartBody = Helper.getMultipartBodyWithWM(PostMessageService.this, watermarkText, "file", attachment);
} else {
fileMultipartBody = Helper.getMultipartBody("file", attachment);
}
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, null, attachment.description, null); Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, null, attachment.description, null);
if (attachmentCall != null) { if (attachmentCall != null) {
try { try {

View file

@ -17,10 +17,14 @@ package app.fedilab.android.ui.fragment.settings;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Bundle; import android.os.Bundle;
import androidx.preference.EditTextPreference;
import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import androidx.preference.SwitchPreferenceCompat;
import app.fedilab.android.R; import app.fedilab.android.R;
import app.fedilab.android.activities.MainActivity;
import app.fedilab.android.helper.Helper;
public class FragmentComposeSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { public class FragmentComposeSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -31,15 +35,23 @@ public class FragmentComposeSettings extends PreferenceFragmentCompat implements
} }
private void createPref() { private void createPref() {
SwitchPreferenceCompat SET_WATERMARK = findPreference(getString(R.string.SET_WATERMARK));
if (SET_WATERMARK != null) {
SET_WATERMARK.getContext().setTheme(Helper.dialogStyle());
}
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
EditTextPreference SET_WATERMARK_TEXT = findPreference(getString(R.string.SET_WATERMARK_TEXT));
if (SET_WATERMARK_TEXT != null) {
String val = sharedPreferences.getString(getString(R.string.SET_WATERMARK_TEXT) + MainActivity.currentUserID + MainActivity.currentInstance, sharedPreferences.getString(getString(R.string.SET_WATERMARK_TEXT), null));
SET_WATERMARK_TEXT.setText(val);
}
} }
@Override @Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (getActivity() != null) { if (key.equalsIgnoreCase(getString(R.string.SET_WATERMARK_TEXT))) {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); SharedPreferences.Editor editor = sharedPreferences.edit();
SharedPreferences.Editor editor = sharedpreferences.edit(); editor.putString(getString(R.string.SET_WATERMARK_TEXT) + MainActivity.currentUserID + MainActivity.currentInstance, sharedPreferences.getString(getString(R.string.SET_WATERMARK_TEXT), null));
editor.apply(); editor.apply();
} }
} }

View file

@ -0,0 +1,278 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.resizeBitmap;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.textAsBitmap;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Shader;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import app.fedilab.android.watermark.androidwm.bean.AsyncTaskParams;
import app.fedilab.android.watermark.androidwm.bean.WatermarkImage;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
import app.fedilab.android.watermark.androidwm.listener.BuildFinishListener;
import app.fedilab.android.watermark.androidwm.task.FDWatermarkTask;
import app.fedilab.android.watermark.androidwm.task.LSBWatermarkTask;
import app.fedilab.android.watermark.androidwm.utils.BitmapUtils;
/**
* The main class for watermark processing library.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class Watermark {
private final WatermarkText watermarkText;
private final WatermarkImage watermarkImg;
private final Bitmap backgroundImg;
private final Context context;
private final boolean isTileMode;
private final boolean isInvisible;
private final boolean isLSB;
private final BuildFinishListener<Bitmap> buildFinishListener;
private Bitmap outputImage;
private Bitmap canvasBitmap;
/**
* Constructors for WatermarkImage
*/
@SuppressWarnings("PMD")
Watermark(@NonNull Context context,
@NonNull Bitmap backgroundImg,
@Nullable WatermarkImage watermarkImg,
@Nullable List<WatermarkImage> wmBitmapList,
@Nullable WatermarkText inputText,
@Nullable List<WatermarkText> wmTextList,
boolean isTileMode,
boolean isInvisible,
boolean isLSB,
@Nullable BuildFinishListener<Bitmap> buildFinishListener) {
this.context = context;
this.isTileMode = isTileMode;
this.watermarkImg = watermarkImg;
this.backgroundImg = backgroundImg;
this.watermarkText = inputText;
this.isInvisible = isInvisible;
this.buildFinishListener = buildFinishListener;
this.isLSB = isLSB;
canvasBitmap = backgroundImg;
outputImage = backgroundImg;
createWatermarkImage(watermarkImg);
createWatermarkImages(wmBitmapList);
createWatermarkText(watermarkText);
createWatermarkTexts(wmTextList);
}
/**
* interface for getting the watermark bitmap.
*
* @return {@link Bitmap} in watermark.
*/
public Bitmap getWatermarkBitmap() {
return watermarkImg.getImage();
}
/**
* interface for getting the watermark text.
*
* @return {@link Bitmap} in watermark.
*/
public String getWatermarkText() {
return watermarkText.getText();
}
/**
* Creating the composite image with {@link WatermarkImage}.
* This method cannot be called outside.
*/
private void createWatermarkImage(WatermarkImage watermarkImg) {
if (watermarkImg != null && backgroundImg != null) {
if (isInvisible) {
Bitmap scaledWMBitmap = resizeBitmap(watermarkImg.getImage(), (float) watermarkImg.getSize(), backgroundImg);
if (isLSB) {
new LSBWatermarkTask(buildFinishListener).execute(
new AsyncTaskParams(context, backgroundImg, scaledWMBitmap)
);
} else {
new FDWatermarkTask(buildFinishListener).execute(
new AsyncTaskParams(context, backgroundImg, scaledWMBitmap)
);
}
} else {
Paint watermarkPaint = new Paint();
watermarkPaint.setAlpha(watermarkImg.getAlpha());
Bitmap newBitmap = Bitmap.createBitmap(backgroundImg.getWidth(),
backgroundImg.getHeight(), backgroundImg.getConfig());
Canvas watermarkCanvas = new Canvas(newBitmap);
watermarkCanvas.drawBitmap(canvasBitmap, 0, 0, null);
Bitmap scaledWMBitmap = resizeBitmap(watermarkImg.getImage(), (float) watermarkImg.getSize(), backgroundImg);
scaledWMBitmap = adjustPhotoRotation(scaledWMBitmap,
(int) watermarkImg.getPosition().getRotation());
if (isTileMode) {
watermarkPaint.setShader(new BitmapShader(scaledWMBitmap,
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT));
Rect bitmapShaderRect = watermarkCanvas.getClipBounds();
watermarkCanvas.drawRect(bitmapShaderRect, watermarkPaint);
} else {
watermarkCanvas.drawBitmap(scaledWMBitmap,
(float) watermarkImg.getPosition().getPositionX() * backgroundImg.getWidth(),
(float) watermarkImg.getPosition().getPositionY() * backgroundImg.getHeight(),
watermarkPaint);
}
canvasBitmap = newBitmap;
outputImage = newBitmap;
}
}
}
/**
* Creating the composite image with {@link WatermarkImage}.
* The input of the method is a set of {@link WatermarkImage}s.
*/
private void createWatermarkImages(List<WatermarkImage> watermarkImages) {
if (watermarkImages != null) {
for (int i = 0; i < watermarkImages.size(); i++) {
createWatermarkImage(watermarkImages.get(i));
}
}
}
/**
* Creating the composite image with {@link WatermarkText}.
* This method cannot be called outside.
*/
private void createWatermarkText(WatermarkText watermarkText) {
if (watermarkText != null && backgroundImg != null) {
if (isInvisible) {
if (isLSB) {
new LSBWatermarkTask(buildFinishListener).execute(
new AsyncTaskParams(context, backgroundImg, watermarkText)
);
} else {
new FDWatermarkTask(buildFinishListener).execute(
new AsyncTaskParams(context, backgroundImg, watermarkText)
);
}
} else {
Paint watermarkPaint = new Paint();
watermarkPaint.setAlpha(watermarkText.getTextAlpha());
Bitmap newBitmap = Bitmap.createBitmap(backgroundImg.getWidth(),
backgroundImg.getHeight(), backgroundImg.getConfig());
Canvas watermarkCanvas = new Canvas(newBitmap);
watermarkCanvas.drawBitmap(canvasBitmap, 0, 0, null);
Bitmap scaledWMBitmap = textAsBitmap(context, watermarkText);
scaledWMBitmap = adjustPhotoRotation(scaledWMBitmap,
(int) watermarkText.getPosition().getRotation());
if (isTileMode) {
watermarkPaint.setShader(new BitmapShader(scaledWMBitmap,
Shader.TileMode.REPEAT,
Shader.TileMode.REPEAT));
Rect bitmapShaderRect = watermarkCanvas.getClipBounds();
watermarkCanvas.drawRect(bitmapShaderRect, watermarkPaint);
} else {
watermarkCanvas.drawBitmap(scaledWMBitmap,
(float) watermarkText.getPosition().getPositionX() * backgroundImg.getWidth(),
(float) watermarkText.getPosition().getPositionY() * backgroundImg.getHeight(),
watermarkPaint);
}
canvasBitmap = newBitmap;
outputImage = newBitmap;
}
}
}
/**
* Creating the composite image with {@link WatermarkText}.
* The input of the method is a set of {@link WatermarkText}s.
*/
private void createWatermarkTexts(List<WatermarkText> watermarkTexts) {
if (watermarkTexts != null) {
for (int i = 0; i < watermarkTexts.size(); i++) {
createWatermarkText(watermarkTexts.get(i));
}
}
}
/**
* The interface for getting the output image.
*
* @return {@link Bitmap} out bitmap.
*/
public Bitmap getOutputImage() {
return outputImage;
}
/**
* Save output png image to local.
*
* @param path the output path of image.
*/
public void saveToLocalPng(String path) {
BitmapUtils.saveAsPNG(outputImage, path, true);
}
/**
* You can use this function to set the composite
* image into an ImageView.
*
* @param target the target {@link ImageView}.
*/
public void setToImageView(ImageView target) {
target.setImageBitmap(outputImage);
}
/**
* Adjust the rotation of a bitmap.
*
* @param bitmap input bitmap.
* @param orientationAngle the orientation angle.
* @return {@link Bitmap} the new bitmap.
*/
private Bitmap adjustPhotoRotation(Bitmap bitmap, final int orientationAngle) {
Matrix matrix = new Matrix();
matrix.setRotate(orientationAngle,
(float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
return Bitmap.createBitmap(bitmap,
0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
}

View file

@ -0,0 +1,344 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.resizeBitmap;
import static app.fedilab.android.watermark.androidwm.utils.Constant.MAX_IMAGE_SIZE;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import app.fedilab.android.watermark.androidwm.bean.WatermarkImage;
import app.fedilab.android.watermark.androidwm.bean.WatermarkPosition;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
import app.fedilab.android.watermark.androidwm.listener.BuildFinishListener;
/**
* A builder class for setting default structural classes for watermark to use.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public final class WatermarkBuilder {
private final Context context;
private final boolean resizeBackgroundImg;
private Bitmap backgroundImg;
private boolean isTileMode = false;
private boolean isLSB = false;
private BuildFinishListener<Bitmap> buildFinishListener = null;
private WatermarkImage watermarkImage;
private WatermarkText watermarkText;
private List<WatermarkText> watermarkTexts = new ArrayList<>();
private List<WatermarkImage> watermarkBitmaps = new ArrayList<>();
/**
* Constructors for WatermarkBuilder
*/
private WatermarkBuilder(@NonNull Context context, @NonNull Bitmap backgroundImg, boolean resizeBackgroundImg) {
this.context = context;
this.resizeBackgroundImg = resizeBackgroundImg;
if (resizeBackgroundImg) {
this.backgroundImg = resizeBitmap(backgroundImg, MAX_IMAGE_SIZE);
} else {
this.backgroundImg = backgroundImg;
}
}
private WatermarkBuilder(@NonNull Context context, @NonNull ImageView backgroundImageView, boolean resizeBackgroundImg) {
this.context = context;
this.resizeBackgroundImg = resizeBackgroundImg;
backgroundFromImageView(backgroundImageView);
}
private WatermarkBuilder(@NonNull Context context, @DrawableRes int backgroundDrawable, boolean resizeBackgroundImg) {
this.context = context;
this.resizeBackgroundImg = resizeBackgroundImg;
if (resizeBackgroundImg) {
this.backgroundImg = resizeBitmap(BitmapFactory.decodeResource(context.getResources(),
backgroundDrawable), MAX_IMAGE_SIZE);
} else {
this.backgroundImg = BitmapFactory.decodeResource(context.getResources(),
backgroundDrawable);
}
}
private WatermarkBuilder(@NonNull Context context, @NonNull Bitmap backgroundImg) {
this(context, backgroundImg, true);
}
private WatermarkBuilder(@NonNull Context context, @NonNull ImageView backgroundImageView) {
this(context, backgroundImageView, true);
}
private WatermarkBuilder(@NonNull Context context, @DrawableRes int backgroundDrawable) {
this(context, backgroundDrawable, true);
}
/**
* to get an instance form class.
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, Bitmap backgroundImg) {
return new WatermarkBuilder(context, backgroundImg);
}
/**
* to get an instance form class.
* Load the background image from a {@link ImageView}
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, ImageView imageView) {
return new WatermarkBuilder(context, imageView);
}
/**
* to get an instance form class.
* Load the background image from a DrawableRes
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, @DrawableRes int backgroundDrawable) {
return new WatermarkBuilder(context, backgroundDrawable);
}
/**
* to get an instance form class.
* with background image resize option
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, Bitmap backgroundImg, boolean resizeBackgroundImg) {
return new WatermarkBuilder(context, backgroundImg, resizeBackgroundImg);
}
/**
* to get an instance form class.
* Load the background image from a {@link ImageView}
* with background image resize option
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, ImageView imageView, boolean resizeBackgroundImg) {
return new WatermarkBuilder(context, imageView, resizeBackgroundImg);
}
/**
* to get an instance form class.
* Load the background image from a DrawableRes
* with background image resize option
*
* @return instance of {@link WatermarkBuilder}
*/
@SuppressWarnings("PMD")
public static WatermarkBuilder create(Context context, @DrawableRes int backgroundDrawable, boolean resizeBackgroundImg) {
return new WatermarkBuilder(context, backgroundDrawable, resizeBackgroundImg);
}
/**
* Sets the {@link String} as the args
* which ready for adding to a watermark.
* Using the default position.
*
* @param inputText The text to add.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkText(@NonNull String inputText) {
watermarkText = new WatermarkText(inputText);
return this;
}
/**
* Sets the {@link String} as the args
* which ready for adding to a watermark.
* Using the new position.
*
* @param inputText The text to add.
* @param position The position in the background image.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkText(@NonNull String inputText,
@NonNull WatermarkPosition position) {
watermarkText = new WatermarkText(inputText, position);
return this;
}
/**
* Sets the {@link String} as the args
* which ready for adding to a watermark.
*
* @param watermarkString The {@link WatermarkText} object.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkText(@NonNull WatermarkText watermarkString) {
watermarkText = watermarkString;
return this;
}
/**
* Sets the {@link String} as the args
* which ready for adding to a watermark.
* And, this is a set of Strings.
*
* @param watermarkTexts The texts to add.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkTexts(@NonNull List<WatermarkText> watermarkTexts) {
this.watermarkTexts = watermarkTexts;
return this;
}
/**
* Sets the {@link Bitmap} as the args
* which ready for adding to a background.
* Using the default position.
*
* @param wmImg The image to add.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkImage(@NonNull Bitmap wmImg) {
watermarkImage = new WatermarkImage(wmImg);
return this;
}
/**
* Sets the {@link Bitmap} as the args
* which ready for adding to a background.
* Using the new position.
*
* @param position The position in the background image.
* @param wmImg The bitmap to add into.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkImage(@NonNull Bitmap wmImg,
@NonNull WatermarkPosition position) {
watermarkImage = new WatermarkImage(wmImg, position);
return this;
}
/**
* Sets the {@link Bitmap} as the args
* which ready for adding to a background.
*
* @param watermarkImg The {@link WatermarkImage} object.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkImage(@NonNull WatermarkImage watermarkImg) {
watermarkImage = watermarkImg;
return this;
}
/**
* Sets the {@link Bitmap} as the args
* which ready for adding into the background.
* And, this is a set of bitmaps.
*
* @param bitmapList The bitmaps to add.
* @return This {@link WatermarkBuilder}.
*/
public WatermarkBuilder loadWatermarkImages(@NonNull List<WatermarkImage> bitmapList) {
this.watermarkBitmaps = bitmapList;
return this;
}
/**
* Set mode to tile. We need to draw watermark over the
* whole background.
*/
public WatermarkBuilder setTileMode(boolean tileMode) {
this.isTileMode = tileMode;
return this;
}
/**
* set a listener for building progress.
*/
public void setInvisibleWMListener(
boolean isLSB,
BuildFinishListener<Bitmap> listener
) {
this.buildFinishListener = listener;
this.isLSB = isLSB;
new Watermark(
context,
backgroundImg,
watermarkImage,
watermarkBitmaps,
watermarkText,
watermarkTexts,
isTileMode,
true,
isLSB,
buildFinishListener
);
}
/**
* load a bitmap as background image from a ImageView.
*
* @param imageView the {@link ImageView} we need to use.
*/
private void backgroundFromImageView(ImageView imageView) {
imageView.invalidate();
if (imageView.getDrawable() != null) {
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
if (resizeBackgroundImg) {
backgroundImg = resizeBitmap(drawable.getBitmap(), MAX_IMAGE_SIZE);
} else {
backgroundImg = drawable.getBitmap();
}
}
}
/**
* let the watermark builder to build a new watermark object
*
* @return a new {@link Watermark} object
*/
public Watermark getWatermark() {
return new Watermark(
context,
backgroundImg,
watermarkImage,
watermarkBitmaps,
watermarkText,
watermarkTexts,
isTileMode,
false,
isLSB,
buildFinishListener
);
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.widget.ImageView;
import app.fedilab.android.watermark.androidwm.listener.DetectFinishListener;
import app.fedilab.android.watermark.androidwm.task.FDDetectionTask;
import app.fedilab.android.watermark.androidwm.task.LSBDetectionTask;
import io.reactivex.annotations.NonNull;
/**
* This is for detecting the invisible watermark in one picture.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public final class WatermarkDetector {
private final Bitmap imageWithWatermark;
private final boolean isLSB;
private WatermarkDetector(
@NonNull Bitmap imageWithWatermark,
boolean isLSB) {
this.imageWithWatermark = imageWithWatermark;
this.isLSB = isLSB;
}
/**
* to get an instance form class.
*
* @return instance of {@link WatermarkDetector}
*/
public static WatermarkDetector create(@NonNull Bitmap imageWithWatermark, boolean isLSB) {
return new WatermarkDetector(imageWithWatermark, isLSB);
}
/**
* to get an instance form class.
* If the imageView has no src or bitmap image, it will throws a {@link NullPointerException}.
*
* @return instance of {@link WatermarkDetector}
*/
public static WatermarkDetector create(ImageView imageView, boolean isLSB) {
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
return new WatermarkDetector(drawable.getBitmap(), isLSB);
}
/**
* The method for watermark detecting.
*/
public void detect(DetectFinishListener listener) {
if (isLSB) {
new LSBDetectionTask(listener).execute(imageWithWatermark);
} else {
new FDDetectionTask(listener).execute(imageWithWatermark);
}
}
}

View file

@ -0,0 +1,84 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.bean;
import android.content.Context;
import android.graphics.Bitmap;
/**
* This is a simple class that can help we put multiple primitive
* parameters into the task.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class AsyncTaskParams {
private Bitmap backgroundImg;
private WatermarkText watermarkText;
private Bitmap watermarkImg;
private Context context;
public AsyncTaskParams(Context context, Bitmap backgroundImg, WatermarkText watermarkText, Bitmap watermarkImg) {
this.backgroundImg = backgroundImg;
this.watermarkText = watermarkText;
this.watermarkImg = watermarkImg;
}
public AsyncTaskParams(Context context, Bitmap backgroundImg, Bitmap watermarkImg) {
this.backgroundImg = backgroundImg;
this.watermarkImg = watermarkImg;
}
public AsyncTaskParams(Context context, Bitmap backgroundImg, WatermarkText watermarkText) {
this.backgroundImg = backgroundImg;
this.watermarkText = watermarkText;
}
/**
* Getters and Setters for {@link AsyncTaskParams}.
*/
public Bitmap getBackgroundImg() {
return backgroundImg;
}
public void setBackgroundImg(Bitmap backgroundImg) {
this.backgroundImg = backgroundImg;
}
public WatermarkText getWatermarkText() {
return watermarkText;
}
public void setWatermarkText(WatermarkText watermarkText) {
this.watermarkText = watermarkText;
}
public Bitmap getWatermarkImg() {
return watermarkImg;
}
public void setWatermarkImg(Bitmap watermarkImg) {
this.watermarkImg = watermarkImg;
}
public Context getContext() {
return context;
}
public void setContext(Context context) {
this.context = context;
}
}

View file

@ -0,0 +1,159 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.bean;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.resizeBitmap;
import static app.fedilab.android.watermark.androidwm.utils.Constant.MAX_IMAGE_SIZE;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.widget.ImageView;
import androidx.annotation.DrawableRes;
import androidx.annotation.FloatRange;
/**
* It's a wrapper of the watermark image.
*
* @author huangyz0918 (huangyz0918@gmail.com)
* @since 29/08/2018
*/
public class WatermarkImage {
private Bitmap image;
@DrawableRes
private int imageDrawable;
private int alpha = 50;
private Context context;
private double size = 0.2;
// set the default values for the position.
private WatermarkPosition position = new WatermarkPosition(0, 0, 0);
/**
* Constructors for WatermarkImage.
* since we use the mobile to calculate the image, the image cannot be to large,
* we set the maxsize of an image to 1024x1024.
*/
public WatermarkImage(Bitmap image) {
this.image = resizeBitmap(image, MAX_IMAGE_SIZE);
}
public WatermarkImage(Context context, @DrawableRes int imageDrawable, WatermarkPosition position) {
this.imageDrawable = imageDrawable;
this.position = position;
this.context = context;
this.image = getBitmapFromDrawable(imageDrawable);
}
public WatermarkImage(Context context, @DrawableRes int imageDrawable) {
this.imageDrawable = imageDrawable;
this.context = context;
this.image = getBitmapFromDrawable(imageDrawable);
}
public WatermarkImage(Bitmap image, WatermarkPosition position) {
this.image = resizeBitmap(image, MAX_IMAGE_SIZE);
this.position = position;
}
public WatermarkImage(ImageView imageView) {
watermarkFromImageView(imageView);
}
/**
* Getters and Setters for those attrs.
*/
public Bitmap getImage() {
return image;
}
public int getAlpha() {
return alpha;
}
public WatermarkPosition getPosition() {
return position;
}
public WatermarkImage setPosition(WatermarkPosition position) {
this.position = position;
return this;
}
public double getSize() {
return size;
}
/**
* @param size can be set to 0-1 as the proportion of
* background image.
*/
public WatermarkImage setSize(@FloatRange(from = 0, to = 1) double size) {
this.size = size;
return this;
}
public int getImageDrawable() {
return imageDrawable;
}
public WatermarkImage setImageDrawable(@DrawableRes int imageDrawable) {
this.imageDrawable = imageDrawable;
return this;
}
public WatermarkImage setPositionX(@FloatRange(from = 0, to = 1) double x) {
this.position.setPositionX(x);
return this;
}
public WatermarkImage setPositionY(@FloatRange(from = 0, to = 1) double y) {
this.position.setPositionY(y);
return this;
}
public WatermarkImage setRotation(double rotation) {
this.position.setRotation(rotation);
return this;
}
/**
* @param imageAlpha can be set to 0-255.
*/
public WatermarkImage setImageAlpha(int imageAlpha) {
this.alpha = imageAlpha;
return this;
}
/**
* load a bitmap as watermark image from a ImageView.
*
* @param imageView the ImageView we need to use.
*/
private void watermarkFromImageView(ImageView imageView) {
imageView.invalidate();
BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
// set the limitation of input bitmap.
this.image = resizeBitmap(drawable.getBitmap(), MAX_IMAGE_SIZE);
}
private Bitmap getBitmapFromDrawable(@DrawableRes int imageDrawable) {
return resizeBitmap(BitmapFactory.decodeResource(context.getResources(),
imageDrawable), MAX_IMAGE_SIZE);
}
}

View file

@ -0,0 +1,82 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.bean;
import androidx.annotation.FloatRange;
/**
* It's a class for saving the position of watermark.
* Can be used for a single image/text or a set of
* images/texts.
*
* @author huangyz0918 (huangyz0918@gmail.com)
* @since 29/08/2018
*/
public class WatermarkPosition {
private double positionX;
private double positionY;
private double rotation;
/**
* Constructors for WatermarkImage
*/
public WatermarkPosition(@FloatRange(from = 0, to = 1) double positionX,
@FloatRange(from = 0, to = 1) double positionY) {
this.positionX = positionX;
this.positionY = positionY;
}
public WatermarkPosition(@FloatRange(from = 0, to = 1) double positionX,
@FloatRange(from = 0, to = 1) double positionY,
double rotation) {
this.positionX = positionX;
this.positionY = positionY;
this.rotation = rotation;
}
/**
* Getters and Setters for those attrs.
*/
public double getPositionX() {
return positionX;
}
public WatermarkPosition setPositionX(double positionX) {
this.positionX = positionX;
return this;
}
public double getPositionY() {
return positionY;
}
public WatermarkPosition setPositionY(double positionY) {
this.positionY = positionY;
return this;
}
public double getRotation() {
return rotation;
}
public WatermarkPosition setRotation(double rotation) {
this.rotation = rotation;
return this;
}
}

View file

@ -0,0 +1,214 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.bean;
import android.graphics.Color;
import android.graphics.Paint;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.FontRes;
/**
* It's a wrapper of the watermark text.
*
* @author huangyz0918 (huangyz0918@gmail.com)
* @since 29/08/2018
*/
public class WatermarkText {
private String text;
private int alpha = 50;
private double size = 20;
@ColorInt
private int color = Color.BLACK;
@ColorInt
private int backgroundColor = Color.TRANSPARENT;
private Paint.Style style = Paint.Style.FILL;
@FontRes
private int typeFaceId = 0;
private float textShadowBlurRadius;
private float textShadowXOffset;
private float textShadowYOffset;
@ColorInt
private int textShadowColor = Color.WHITE;
// set the default values for the position.
private WatermarkPosition position = new WatermarkPosition(0, 0, 0);
/**
* Constructors for WatermarkText
*/
public WatermarkText(String text) {
this.text = text;
}
public WatermarkText(String text, WatermarkPosition position) {
this.text = text;
this.position = position;
}
public WatermarkText(TextView textView) {
textFromTextView(textView);
}
public WatermarkText(EditText editText) {
textFromEditText(editText);
}
/**
* Getters and Setters for those attrs.
*/
public String getText() {
return text;
}
public int getTextAlpha() {
return alpha;
}
/**
* @param textAlpha can be set to 0-255.
*/
public WatermarkText setTextAlpha(int textAlpha) {
this.alpha = textAlpha;
return this;
}
public WatermarkPosition getPosition() {
return position;
}
public WatermarkText setPosition(WatermarkPosition position) {
this.position = position;
return this;
}
public double getTextSize() {
return size;
}
/**
* @param size can be set to normal text size.
*/
public WatermarkText setTextSize(double size) {
this.size = size;
return this;
}
public int getTextColor() {
return color;
}
public WatermarkText setTextColor(int color) {
this.color = color;
return this;
}
public Paint.Style getTextStyle() {
return style;
}
public WatermarkText setTextStyle(Paint.Style style) {
this.style = style;
return this;
}
public int getBackgroundColor() {
return backgroundColor;
}
public WatermarkText setBackgroundColor(int backgroundColor) {
this.backgroundColor = backgroundColor;
return this;
}
public float getTextShadowBlurRadius() {
return textShadowBlurRadius;
}
public float getTextShadowXOffset() {
return textShadowXOffset;
}
public float getTextShadowYOffset() {
return textShadowYOffset;
}
public int getTextShadowColor() {
return textShadowColor;
}
public int getTextFont() {
return typeFaceId;
}
/**
* Use the typeface path to get the text typeface.
*/
public WatermarkText setTextFont(@FontRes int typeFaceId) {
this.typeFaceId = typeFaceId;
return this;
}
public WatermarkText setPositionX(double x) {
this.position.setPositionX(x);
return this;
}
public WatermarkText setPositionY(double y) {
this.position.setPositionY(y);
return this;
}
public WatermarkText setRotation(double rotation) {
this.position.setRotation(rotation);
return this;
}
/**
* Set the shadow of the text watermark.
*/
public WatermarkText setTextShadow(final float blurRadius, final float shadowXOffset,
final float shadowYOffset, @ColorInt final int shadowColor) {
this.textShadowBlurRadius = blurRadius;
this.textShadowXOffset = shadowXOffset;
this.textShadowYOffset = shadowYOffset;
this.textShadowColor = shadowColor;
return this;
}
/**
* load a string text as watermark text from a {@link TextView}.
*
* @param textView the {@link TextView} we need to use.
*/
private void textFromTextView(TextView textView) {
this.text = textView.getText().toString();
}
/**
* load a string text as watermark text from a {@link EditText}.
*
* @param editText the {@link EditText} we need to use.
*/
private void textFromEditText(EditText editText) {
this.text = editText.getText().toString();
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.listener;
/**
* This interface is for listening if the task of
* creating invisible watermark is finished.
*
* @param <T> can be the image and the string watermarks.
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public interface BuildFinishListener<T> {
void onSuccess(T object);
void onFailure(String message);
}

View file

@ -0,0 +1,33 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.listener;
import app.fedilab.android.watermark.androidwm.task.DetectionReturnValue;
/**
* This interface is for listening if the task of
* detecting invisible watermark is finished.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public interface DetectFinishListener {
void onSuccess(DetectionReturnValue returnValue);
void onFailure(String message);
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.task;
import android.graphics.Bitmap;
/**
* This is a simple class that can help we get more than two kinds of
* return values in the task.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class DetectionReturnValue {
private Bitmap watermarkBitmap;
private String watermarkString;
public DetectionReturnValue() {
}
public DetectionReturnValue(Bitmap watermarkBitmap, String watermarkString) {
this.watermarkBitmap = watermarkBitmap;
this.watermarkString = watermarkString;
}
public Bitmap getWatermarkBitmap() {
return watermarkBitmap;
}
protected void setWatermarkBitmap(Bitmap watermarkBitmap) {
this.watermarkBitmap = watermarkBitmap;
}
public String getWatermarkString() {
return watermarkString;
}
protected void setWatermarkString(String watermarkString) {
this.watermarkString = watermarkString;
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.task;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.getBitmapPixels;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.pixel2ARGBArray;
import static app.fedilab.android.watermark.androidwm.utils.Constant.CHUNK_SIZE;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_BITMAP_NULL;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_DETECT_FAILED;
import static app.fedilab.android.watermark.androidwm.utils.Constant.MAX_IMAGE_SIZE;
import static app.fedilab.android.watermark.androidwm.utils.Constant.WARNING_BIG_IMAGE;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.copyFromIntArray;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import app.fedilab.android.watermark.androidwm.listener.DetectFinishListener;
import app.fedilab.android.watermark.androidwm.utils.FastDctFft;
/**
* This is a task for watermark image detection.
* In FD mode, all the task will return a bitmap;
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
@SuppressWarnings("PMD")
public class FDDetectionTask extends AsyncTask<Bitmap, Void, DetectionReturnValue> {
private final DetectFinishListener listener;
public FDDetectionTask(DetectFinishListener listener) {
this.listener = listener;
}
@Override
protected DetectionReturnValue doInBackground(Bitmap... bitmaps) {
Bitmap markedBitmap = bitmaps[0];
DetectionReturnValue resultValue = new DetectionReturnValue();
if (markedBitmap == null) {
listener.onFailure(ERROR_BITMAP_NULL);
return null;
}
if (markedBitmap.getWidth() > MAX_IMAGE_SIZE || markedBitmap.getHeight() > MAX_IMAGE_SIZE) {
listener.onFailure(WARNING_BIG_IMAGE);
return null;
}
int[] pixels = getBitmapPixels(markedBitmap);
// divide and conquer
if (pixels.length < CHUNK_SIZE) {
int[] watermarkRGB = pixel2ARGBArray(pixels);
double[] watermarkArray = copyFromIntArray(watermarkRGB);
FastDctFft.transform(watermarkArray);
//TODO: do some operations with colorTempArray.
} else {
int numOfChunks = (int) Math.ceil((double) pixels.length / CHUNK_SIZE);
for (int i = 0; i < numOfChunks; i++) {
int start = i * CHUNK_SIZE;
int length = Math.min(pixels.length - start, CHUNK_SIZE);
int[] temp = new int[length];
System.arraycopy(pixels, start, temp, 0, length);
double[] colorTempArray = copyFromIntArray(pixel2ARGBArray(temp));
FastDctFft.transform(colorTempArray);
//TODO: do some operations with colorTempArray.
}
}
/* TODO: new detection operations will replace this block.
String resultString;
if (binaryString.contains(LSB_TEXT_PREFIX_FLAG) && binaryString.contains(LSB_TEXT_SUFFIX_FLAG)) {
resultString = getBetweenStrings(binaryString, true, listener);
resultString = binaryToString(resultString);
resultValue.setWatermarkString(resultString);
} else if (binaryString.contains(LSB_IMG_PREFIX_FLAG) && binaryString.contains(LSB_IMG_SUFFIX_FLAG)) {
binaryString = getBetweenStrings(binaryString, false, listener);
resultString = binaryToString(binaryString);
resultValue.setWatermarkBitmap(BitmapUtils.stringToBitmap(resultString));
}*/
return resultValue;
}
@Override
protected void onPostExecute(DetectionReturnValue detectionReturnValue) {
if (detectionReturnValue == null) {
listener.onFailure(ERROR_DETECT_FAILED);
return;
}
if (detectionReturnValue.getWatermarkString() != null &&
!"".equals(detectionReturnValue.getWatermarkString()) ||
detectionReturnValue.getWatermarkBitmap() != null) {
listener.onSuccess(detectionReturnValue);
} else {
listener.onFailure(ERROR_DETECT_FAILED);
}
super.onPostExecute(detectionReturnValue);
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.task;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.getBitmapPixels;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.pixel2ARGBArray;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.textAsBitmap;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_CREATE_FAILED;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_NO_BACKGROUND;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_NO_WATERMARKS;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_PIXELS_NOT_ENOUGH;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.copyFromIntArray;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.AsyncTask;
import app.fedilab.android.watermark.androidwm.bean.AsyncTaskParams;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
import app.fedilab.android.watermark.androidwm.listener.BuildFinishListener;
import app.fedilab.android.watermark.androidwm.utils.FastDctFft;
/**
* This is a tack that use Fast Fourier Transform for an image, to
* build the image and text watermark into a frequency domain.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class FDWatermarkTask extends AsyncTask<AsyncTaskParams, Void, Bitmap> {
private final BuildFinishListener<Bitmap> listener;
public FDWatermarkTask(BuildFinishListener<Bitmap> callback) {
this.listener = callback;
}
@Override
protected Bitmap doInBackground(AsyncTaskParams... params) {
Bitmap backgroundBitmap = params[0].getBackgroundImg();
WatermarkText watermarkText = params[0].getWatermarkText();
Bitmap watermarkBitmap = params[0].getWatermarkImg();
Context context = params[0].getContext();
if (backgroundBitmap == null) {
listener.onFailure(ERROR_NO_BACKGROUND);
return null;
}
if (watermarkText != null) {
watermarkBitmap = textAsBitmap(context, watermarkText);
}
if (watermarkBitmap == null) {
listener.onFailure(ERROR_NO_WATERMARKS);
return null;
}
int[] watermarkPixels = getBitmapPixels(watermarkBitmap);
int[] watermarkColorArray = pixel2ARGBArray(watermarkPixels);
Bitmap outputBitmap = Bitmap.createBitmap(backgroundBitmap.getWidth(), backgroundBitmap.getHeight(),
backgroundBitmap.getConfig());
// convert the background bitmap into pixel array.
int[] backgroundPixels = getBitmapPixels(backgroundBitmap);
if (watermarkColorArray.length > backgroundPixels.length * 4) {
listener.onFailure(ERROR_PIXELS_NOT_ENOUGH);
} else {
// divide and conquer
// use fixed chunk size or the size of watermark image.
if (backgroundPixels.length < watermarkColorArray.length) {
int[] backgroundColorArray = pixel2ARGBArray(backgroundPixels);
double[] backgroundColorArrayD = copyFromIntArray(backgroundColorArray);
FastDctFft.transform(backgroundColorArrayD);
//TODO: do the operations.
FastDctFft.inverseTransform(backgroundColorArrayD);
for (int i = 0; i < backgroundPixels.length; i++) {
int color = Color.argb(
(int) backgroundColorArrayD[4 * i],
(int) backgroundColorArrayD[4 * i + 1],
(int) backgroundColorArrayD[4 * i + 2],
(int) backgroundColorArrayD[4 * i + 3]
);
backgroundPixels[i] = color;
}
} else {
// use fixed chunk size or the size of watermark image.
int numOfChunks = (int) Math.ceil((double) backgroundPixels.length / watermarkColorArray.length);
for (int i = 0; i < numOfChunks; i++) {
int start = i * watermarkColorArray.length;
int length = Math.min(backgroundPixels.length - start, watermarkColorArray.length);
int[] temp = new int[length];
System.arraycopy(backgroundPixels, start, temp, 0, length);
double[] colorTempD = copyFromIntArray(pixel2ARGBArray(temp));
FastDctFft.transform(colorTempD);
// for (int j = 0; j < length; j++) {
// colorTempD[4 * j] = colorTempD[4 * j] + watermarkColorArray[j];
// colorTempD[4 * j + 1] = colorTempD[4 * j + 1] + watermarkColorArray[j];
// colorTempD[4 * j + 2] = colorTempD[4 * j + 2] + watermarkColorArray[j];
// colorTempD[4 * j + 3] = colorTempD[4 * j + 3] + watermarkColorArray[j];
// }
double enhanceNum = 1;
// The energy in frequency scaled.
for (int j = 0; j < length; j++) {
colorTempD[4 * j] = colorTempD[4 * j] * enhanceNum;
colorTempD[4 * j + 1] = colorTempD[4 * j + 1] * enhanceNum;
colorTempD[4 * j + 2] = colorTempD[4 * j + 2] * enhanceNum;
colorTempD[4 * j + 3] = colorTempD[4 * j + 3] * enhanceNum;
}
//TODO: do the operations.
FastDctFft.inverseTransform(colorTempD);
for (int j = 0; j < length; j++) {
int color = Color.argb(
(int) colorTempD[4 * j],
(int) colorTempD[4 * j + 1],
(int) colorTempD[4 * j + 2],
(int) colorTempD[4 * j + 3]
);
backgroundPixels[start + j] = color;
}
}
}
outputBitmap.setPixels(backgroundPixels, 0, backgroundBitmap.getWidth(), 0, 0,
backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
return outputBitmap;
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (listener != null) {
if (bitmap != null) {
listener.onSuccess(bitmap);
} else {
listener.onFailure(ERROR_CREATE_FAILED);
}
}
super.onPostExecute(bitmap);
}
/**
* Normalize array.
*
* @param inputArray The array to be normalized.
* @return The result of the normalization.
*/
public double[] normalizeArray(double[] inputArray, double dataHigh,
double dataLow, double normalizedHigh,
double normalizedLow) {
for (int i = 0; i < inputArray.length; i++) {
inputArray[i] = ((inputArray[i] - dataLow)
/ (dataHigh - dataLow))
* (normalizedHigh - normalizedLow) + normalizedLow;
}
return inputArray;
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.task;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.getBitmapPixels;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.pixel2ARGBArray;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_BITMAP_NULL;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_DETECT_FAILED;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_SUFFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_SUFFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.MAX_IMAGE_SIZE;
import static app.fedilab.android.watermark.androidwm.utils.Constant.WARNING_BIG_IMAGE;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.binaryToString;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.getBetweenStrings;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.intArrayToStringJ;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.replaceNinesJ;
import android.graphics.Bitmap;
import android.os.AsyncTask;
import app.fedilab.android.watermark.androidwm.listener.DetectFinishListener;
import app.fedilab.android.watermark.androidwm.utils.BitmapUtils;
/**
* This is a task for watermark image detection.
* In LSB mode, all the task will return a bitmap;
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class LSBDetectionTask extends AsyncTask<Bitmap, Void, DetectionReturnValue> {
private final DetectFinishListener listener;
public LSBDetectionTask(DetectFinishListener listener) {
this.listener = listener;
}
@Override
protected DetectionReturnValue doInBackground(Bitmap... bitmaps) {
Bitmap markedBitmap = bitmaps[0];
DetectionReturnValue resultValue = new DetectionReturnValue();
if (markedBitmap == null) {
listener.onFailure(ERROR_BITMAP_NULL);
return null;
}
if (markedBitmap.getWidth() > MAX_IMAGE_SIZE || markedBitmap.getHeight() > MAX_IMAGE_SIZE) {
listener.onFailure(WARNING_BIG_IMAGE);
return null;
}
int[] pixels = getBitmapPixels(markedBitmap);
int[] colorArray = pixel2ARGBArray(pixels);
for (int i = 0; i < colorArray.length; i++) {
colorArray[i] = colorArray[i] % 10;
}
replaceNinesJ(colorArray);
String binaryString = intArrayToStringJ(colorArray);
String resultString;
if (binaryString.contains(LSB_TEXT_PREFIX_FLAG) && binaryString.contains(LSB_TEXT_SUFFIX_FLAG)) {
resultString = getBetweenStrings(binaryString, true, listener);
resultString = binaryToString(resultString);
resultValue.setWatermarkString(resultString);
} else if (binaryString.contains(LSB_IMG_PREFIX_FLAG) && binaryString.contains(LSB_IMG_SUFFIX_FLAG)) {
binaryString = getBetweenStrings(binaryString, false, listener);
resultString = binaryToString(binaryString);
resultValue.setWatermarkBitmap(BitmapUtils.stringToBitmap(resultString));
}
return resultValue;
}
@Override
protected void onPostExecute(DetectionReturnValue detectionReturnValue) {
if (detectionReturnValue == null) {
listener.onFailure(ERROR_DETECT_FAILED);
return;
}
if (detectionReturnValue.getWatermarkString() != null &&
!"".equals(detectionReturnValue.getWatermarkString()) ||
detectionReturnValue.getWatermarkBitmap() != null) {
listener.onSuccess(detectionReturnValue);
} else {
listener.onFailure(ERROR_DETECT_FAILED);
}
super.onPostExecute(detectionReturnValue);
}
}

View file

@ -0,0 +1,143 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.task;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.getBitmapPixels;
import static app.fedilab.android.watermark.androidwm.utils.BitmapUtils.pixel2ARGBArray;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_CREATE_FAILED;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_NO_BACKGROUND;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_NO_WATERMARKS;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_PIXELS_NOT_ENOUGH;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_SUFFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_SUFFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.replaceSingleDigit;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.stringToBinary;
import static app.fedilab.android.watermark.androidwm.utils.StringUtils.stringToIntArray;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.AsyncTask;
import app.fedilab.android.watermark.androidwm.bean.AsyncTaskParams;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
import app.fedilab.android.watermark.androidwm.listener.BuildFinishListener;
import app.fedilab.android.watermark.androidwm.utils.BitmapUtils;
/**
* This is a background task for adding the specific invisible text
* into the background image. We don't need to read every pixel's
* RGB value, we just read the length values that can put our encrypted
* text in.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class LSBWatermarkTask extends AsyncTask<AsyncTaskParams, Void, Bitmap> {
private final BuildFinishListener<Bitmap> listener;
public LSBWatermarkTask(BuildFinishListener<Bitmap> callback) {
this.listener = callback;
}
@Override
protected Bitmap doInBackground(AsyncTaskParams... params) {
Bitmap backgroundBitmap = params[0].getBackgroundImg();
WatermarkText watermarkText = params[0].getWatermarkText();
Bitmap watermarkBitmap = params[0].getWatermarkImg();
String watermarkString;
if (backgroundBitmap == null) {
listener.onFailure(ERROR_NO_BACKGROUND);
return null;
}
// convert the watermark bitmap into a String.
if (watermarkBitmap != null) {
watermarkString = BitmapUtils.bitmapToString(watermarkBitmap);
} else {
watermarkString = watermarkText.getText();
}
if (watermarkString == null) {
listener.onFailure(ERROR_NO_WATERMARKS);
return null;
}
Bitmap outputBitmap = Bitmap.createBitmap(backgroundBitmap.getWidth(), backgroundBitmap.getHeight(),
backgroundBitmap.getConfig());
int[] backgroundPixels = getBitmapPixels(backgroundBitmap);
int[] backgroundColorArray = pixel2ARGBArray(backgroundPixels);
// convert the Sting into a binary string, and, replace the single digit number.
// using the rebuilt pixels to create a new watermarked image.
String watermarkBinary = stringToBinary(watermarkString);
if (watermarkBitmap != null) {
watermarkBinary = LSB_IMG_PREFIX_FLAG + watermarkBinary + LSB_IMG_SUFFIX_FLAG;
} else {
watermarkBinary = LSB_TEXT_PREFIX_FLAG + watermarkBinary + LSB_TEXT_SUFFIX_FLAG;
}
int[] watermarkColorArray = stringToIntArray(watermarkBinary);
if (watermarkColorArray.length > backgroundColorArray.length) {
listener.onFailure(ERROR_PIXELS_NOT_ENOUGH);
} else {
int chunkSize = watermarkColorArray.length;
int numOfChunks = (int) Math.ceil((double) backgroundColorArray.length / chunkSize);
for (int i = 0; i < numOfChunks - 1; i++) {
int start = i * chunkSize;
for (int j = 0; j < chunkSize; j++) {
backgroundColorArray[start + j] = replaceSingleDigit(backgroundColorArray[start + j]
, watermarkColorArray[j]);
}
}
for (int i = 0; i < backgroundPixels.length; i++) {
int color = Color.argb(
backgroundColorArray[4 * i],
backgroundColorArray[4 * i + 1],
backgroundColorArray[4 * i + 2],
backgroundColorArray[4 * i + 3]
);
backgroundPixels[i] = color;
}
outputBitmap.setPixels(backgroundPixels, 0, backgroundBitmap.getWidth(), 0, 0,
backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
return outputBitmap;
}
return null;
}
@Override
protected void onPostExecute(Bitmap resultBitmap) {
if (listener != null) {
if (resultBitmap != null) {
listener.onSuccess(resultBitmap);
} else {
listener.onFailure(ERROR_CREATE_FAILED);
}
}
super.onPostExecute(resultBitmap);
}
}

View file

@ -0,0 +1,236 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.os.Environment;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.util.Base64;
import android.util.TypedValue;
import androidx.core.content.res.ResourcesCompat;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Locale;
import app.fedilab.android.watermark.androidwm.bean.WatermarkImage;
import app.fedilab.android.watermark.androidwm.bean.WatermarkText;
/**
* Util class for operations with {@link Bitmap}.
*
* @author huangyz0918
*/
public class BitmapUtils {
/**
* build a bitmap from a text.
*
* @return {@link Bitmap} the bitmap return.
*/
public static Bitmap textAsBitmap(Context context, WatermarkText watermarkText) {
TextPaint watermarkPaint = new TextPaint();
watermarkPaint.setColor(watermarkText.getTextColor());
watermarkPaint.setStyle(watermarkText.getTextStyle());
if (watermarkText.getTextAlpha() >= 0 && watermarkText.getTextAlpha() <= 255) {
watermarkPaint.setAlpha(watermarkText.getTextAlpha());
}
float value = (float) watermarkText.getTextSize();
int pixel = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
value, context.getResources().getDisplayMetrics());
watermarkPaint.setTextSize(pixel);
if (watermarkText.getTextShadowBlurRadius() != 0
|| watermarkText.getTextShadowXOffset() != 0
|| watermarkText.getTextShadowYOffset() != 0) {
watermarkPaint.setShadowLayer(watermarkText.getTextShadowBlurRadius(),
watermarkText.getTextShadowXOffset(),
watermarkText.getTextShadowYOffset(),
watermarkText.getTextShadowColor());
}
if (watermarkText.getTextFont() != 0) {
Typeface typeface = ResourcesCompat.getFont(context, watermarkText.getTextFont());
watermarkPaint.setTypeface(typeface);
}
watermarkPaint.setAntiAlias(true);
watermarkPaint.setTextAlign(Paint.Align.LEFT);
watermarkPaint.setStrokeWidth(5);
float baseline = (int) (-watermarkPaint.ascent() + 1f);
Rect bounds = new Rect();
watermarkPaint.getTextBounds(watermarkText.getText(),
0, watermarkText.getText().length(), bounds);
int boundWidth = bounds.width() + 20;
int mTextMaxWidth = (int) watermarkPaint.measureText(watermarkText.getText());
if (boundWidth > mTextMaxWidth) {
boundWidth = mTextMaxWidth;
}
StaticLayout staticLayout = new StaticLayout(watermarkText.getText(),
0, watermarkText.getText().length(),
watermarkPaint, mTextMaxWidth, android.text.Layout.Alignment.ALIGN_NORMAL, 2.0f,
2.0f, false);
int lineCount = staticLayout.getLineCount();
int height = (int) (baseline + watermarkPaint.descent() + 3) * lineCount;
Bitmap image = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
if (boundWidth > 0 && height > 0) {
image = Bitmap.createBitmap(boundWidth, height, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(image);
canvas.drawColor(watermarkText.getBackgroundColor());
staticLayout.draw(canvas);
return image;
}
/**
* this method is for image resizing, we should get
* the size from the input {@link WatermarkImage}
* objects, and, set the size from 0 to 1 ,which means:
* size = watermarkImageWidth / backgroundImageWidth
*
* @return {@link Bitmap} the new bitmap.
*/
public static Bitmap resizeBitmap(Bitmap watermarkImg, float size, Bitmap backgroundImg) {
int bitmapWidth = watermarkImg.getWidth();
int bitmapHeight = watermarkImg.getHeight();
float scale = (backgroundImg.getWidth() * size) / bitmapWidth;
Matrix matrix = new Matrix();
matrix.postScale(scale, scale);
return Bitmap.createBitmap(watermarkImg, 0, 0,
bitmapWidth, bitmapHeight, matrix, true);
}
/**
* this method is for image resizing, used in invisible watermark
* creating progress. To make the progress faster, we should do
* some pre-settings, user can set whether to do this part.
* <p>
* We set the new {@link Bitmap} to a fixed width = 512 pixels.
*
* @return {@link Bitmap} the new bitmap.
*/
public static Bitmap resizeBitmap(Bitmap inputBitmap, int maxImageSize) {
float ratio = Math.min(
(float) maxImageSize / inputBitmap.getWidth(),
(float) maxImageSize / inputBitmap.getHeight());
int width = Math.round(ratio * inputBitmap.getWidth());
int height = Math.round(ratio * inputBitmap.getHeight());
return Bitmap.createScaledBitmap(inputBitmap, width,
height, true);
}
/**
* Convert a Bitmap to a String.
*/
public static String bitmapToString(Bitmap bitmap) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] b = byteArrayOutputStream.toByteArray();
return Base64.encodeToString(b, Base64.DEFAULT);
}
/**
* Convert a String to a Bitmap.
*
* @return bitmap (from given string)
*/
public static Bitmap stringToBitmap(String encodedString) {
try {
byte[] encodeByte = Base64.decode(encodedString, Base64.DEFAULT);
return BitmapFactory.decodeByteArray(encodeByte, 0, encodeByte.length);
} catch (Exception e) {
return null;
}
}
/**
* Saving a bitmap instance into local PNG.
*/
public static void saveAsPNG(Bitmap inputBitmap, String filePath, boolean withTime) {
String sdStatus = Environment.getExternalStorageState();
@SuppressLint("SimpleDateFormat") String timeStamp =
new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US).format(Calendar.getInstance().getTime());
FileOutputStream out = null;
try {
if (withTime) {
out = new FileOutputStream(filePath + timeStamp + ".png");
} else {
out = new FileOutputStream(filePath + "watermarked" + ".png");
}
inputBitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
// PNG is a lossless format, the compression factor (100) is ignored
} catch (Exception ignored) {
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* convert the background bitmap into pixel array.
*/
public static int[] getBitmapPixels(Bitmap inputBitmap) {
int[] backgroundPixels = new int[inputBitmap.getWidth() * inputBitmap.getHeight()];
inputBitmap.getPixels(backgroundPixels, 0, inputBitmap.getWidth(), 0, 0,
inputBitmap.getWidth(), inputBitmap.getHeight());
return backgroundPixels;
}
/**
* Bitmap to Pixels then converting it to an ARGB int array.
*/
public static int[] pixel2ARGBArray(int[] inputPixels) {
int[] bitmapArray = new int[4 * inputPixels.length];
for (int i = 0; i < inputPixels.length; i++) {
bitmapArray[4 * i] = Color.alpha(inputPixels[i]);
bitmapArray[4 * i + 1] = Color.red(inputPixels[i]);
bitmapArray[4 * i + 2] = Color.green(inputPixels[i]);
bitmapArray[4 * i + 3] = Color.blue(inputPixels[i]);
}
return bitmapArray;
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.utils;
/**
* the constant pool.
*
* @author huangyz0918 (huangyz0918@gmail.com)
*/
public class Constant {
public static final String LSB_IMG_PREFIX_FLAG = "1212";
public static final String LSB_TEXT_PREFIX_FLAG = "2323";
public static final String LSB_IMG_SUFFIX_FLAG = "3434";
public static final String LSB_TEXT_SUFFIX_FLAG = "4545";
public static final int MAX_IMAGE_SIZE = 1024;
// use the watermark image's size
public static final int CHUNK_SIZE = 5000;
public static final String ERROR_NO_WATERMARKS = "No input text or image! please load an image or a text in your WatermarkBuilder!";
public static final String ERROR_CREATE_FAILED = "created watermark failed!";
public static final String ERROR_NO_BACKGROUND = "No background image! please load an image in your WatermarkBuilder!";
public static final String ERROR_PIXELS_NOT_ENOUGH = "The Pixels in background are too small to put the watermark in, " +
"the data has been lost! Please make sure the maxImageSize is bigger enough!";
public static final String ERROR_DETECT_FAILED = "Failed to detect the watermark!";
public static final String ERROR_NO_WATERMARK_FOUND = "No watermarks found in this image!";
public static final String ERROR_BITMAP_NULL = "Cannot detect the watermark! markedBitmap is null object!";
public static final String WARNING_BIG_IMAGE = "The input image may be too large to put into the memory, please be careful of the OOM!";
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.utils;
import java.util.Arrays;
import java.util.Objects;
public final class FastDctFft {
/**
* Computes the unscaled DCT type II on the specified array in place.
* The array length must be a power of 2 or zero.
*
* @param vector the vector of numbers to transform
* @throws NullPointerException if the array is {@code null}
*/
public static void transform(double[] vector) {
Objects.requireNonNull(vector);
int len = vector.length;
int halfLen = len / 2;
double[] real = new double[len];
for (int i = 0; i < halfLen; i++) {
real[i] = vector[i * 2];
real[len - 1 - i] = vector[i * 2 + 1];
}
if (len % 2 == 1) {
real[halfLen] = vector[len - 1];
}
Arrays.fill(vector, 0.0);
Fft.transform(real, vector);
for (int i = 0; i < len; i++) {
double temp = i * Math.PI / (len * 2);
vector[i] = real[i] * Math.cos(temp) + vector[i] * Math.sin(temp);
}
}
/**
* Computes the unscaled DCT type III on the specified array in place.
* The array length must be a power of 2 or zero.
*
* @param vector the vector of numbers to transform
* @throws NullPointerException if the array is {@code null}
*/
public static void inverseTransform(double[] vector) {
Objects.requireNonNull(vector);
int len = vector.length;
if (len > 0) {
vector[0] = vector[0] / 2;
}
double[] real = new double[len];
for (int i = 0; i < len; i++) {
double temp = i * Math.PI / (len * 2);
real[i] = vector[i] * Math.cos(temp);
vector[i] *= -Math.sin(temp);
}
Fft.transform(real, vector);
int halfLen = len / 2;
for (int i = 0; i < halfLen; i++) {
vector[i * 2] = real[i];
vector[i * 2 + 1] = real[len - 1 - i];
}
if (len % 2 == 1) {
vector[len - 1] = real[halfLen];
}
double scale = (double) len / 2;
for (int i = 0; i < len; i++) {
vector[i] = (int) Math.round(vector[i] / scale);
}
}
}

View file

@ -0,0 +1,202 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.utils;
public final class Fft {
/**
* Computes the discrete Fourier transform (DFT) of the given complex vector,
* storing the result back into the vector.
* <p>
* The vector can have any length. This is a wrapper function.
*/
public static void transform(double[] real, double[] imag) {
int n = real.length;
if (n != imag.length) {
throw new IllegalArgumentException("Mismatched lengths");
}
if ((n & (n - 1)) == 0) {
transformRadix2(real, imag);
} else {
transformBlueStein(real, imag);
}
}
/**
* Computes the inverse discrete Fourier transform (IDFT) of the given
* complex vector, storing the result back into the vector.
* <p>
* The vector can have any length. This is a wrapper function.
* This transform does not perform scaling, so the inverse is not a true inverse.
*/
private static void inverseTransform(double[] real, double[] imag) {
transform(imag, real);
}
/**
* Computes the discrete Fourier transform (DFT) of the given complex vector,
* storing the result back into the vector.
* <p>
* The vector's length must be a power of 2. Uses the Cooley-Tukey
* decimation-in-time radix-2 algorithm.
*/
private static void transformRadix2(double[] real, double[] imag) {
int n = real.length;
if (n != imag.length) {
throw new IllegalArgumentException("Mismatched lengths");
}
int levels = 31 - Integer.numberOfLeadingZeros(n);
if (1 << levels != n) {
throw new IllegalArgumentException("Length is not a power of 2");
}
double[] cosTable = new double[n / 2];
double[] sinTable = new double[n / 2];
for (int i = 0; i < n / 2; i++) {
cosTable[i] = Math.cos(2 * Math.PI * i / n);
sinTable[i] = Math.sin(2 * Math.PI * i / n);
}
for (int i = 0; i < n; i++) {
int j = Integer.reverse(i) >>> (32 - levels);
if (j > i) {
double temp = real[i];
real[i] = real[j];
real[j] = temp;
temp = imag[i];
imag[i] = imag[j];
imag[j] = temp;
}
}
for (int size = 2; size <= n; size *= 2) {
int halfSize = size / 2;
int tableStep = n / size;
for (int i = 0; i < n; i += size) {
for (int j = i, k = 0; j < i + halfSize; j++, k += tableStep) {
int l = j + halfSize;
double tpre = real[l] * cosTable[k] + imag[l] * sinTable[k];
double tpim = -real[l] * sinTable[k] + imag[l] * cosTable[k];
real[l] = real[j] - tpre;
imag[l] = imag[j] - tpim;
real[j] += tpre;
imag[j] += tpim;
}
}
if (size == n) {
break;
}
}
}
/**
* Computes the discrete Fourier transform (DFT) of the given complex vector,
* storing the result back into the vector.
* <p>
* The vector can have any length. This requires the convolution function,
* which in turn requires the radix-2 FFT function.
* <p>
* Uses Bluestein's chirp z-transform algorithm.
*/
private static void transformBlueStein(double[] real, double[] imag) {
int n = real.length;
if (n != imag.length) {
throw new IllegalArgumentException("Mismatched lengths");
}
if (n >= 0x20000000) {
throw new IllegalArgumentException("Array too large");
}
int m = Integer.highestOneBit(n) * 4;
double[] cosTable = new double[n];
double[] sinTable = new double[n];
for (int i = 0; i < n; i++) {
int j = (int) ((long) i * i % (n * 2));
cosTable[i] = Math.cos(Math.PI * j / n);
sinTable[i] = Math.sin(Math.PI * j / n);
}
double[] aReal = new double[m];
double[] aImag = new double[m];
for (int i = 0; i < n; i++) {
aReal[i] = real[i] * cosTable[i] + imag[i] * sinTable[i];
aImag[i] = -real[i] * sinTable[i] + imag[i] * cosTable[i];
}
double[] bReal = new double[m];
double[] bImag = new double[m];
bReal[0] = cosTable[0];
bImag[0] = sinTable[0];
for (int i = 1; i < n; i++) {
bReal[i] = bReal[m - i] = cosTable[i];
bImag[i] = bImag[m - i] = sinTable[i];
}
double[] cReal = new double[m];
double[] cImag = new double[m];
convolve(aReal, aImag, bReal, bImag, cReal, cImag);
for (int i = 0; i < n; i++) {
real[i] = cReal[i] * cosTable[i] + cImag[i] * sinTable[i];
imag[i] = -cReal[i] * sinTable[i] + cImag[i] * cosTable[i];
}
}
/**
* Computes the circular convolution of the given complex vectors.
* Each vector's length must be the same.
*/
private static void convolve(double[] xReal, double[] xImag,
double[] yReal, double[] yImag, double[] outReal, double[] outImag) {
int n = xReal.length;
if (n != xImag.length || n != yReal.length || n != yImag.length
|| n != outReal.length || n != outImag.length) {
throw new IllegalArgumentException("Mismatched lengths");
}
xReal = xReal.clone();
xImag = xImag.clone();
yReal = yReal.clone();
yImag = yImag.clone();
transform(xReal, xImag);
transform(yReal, yImag);
for (int i = 0; i < n; i++) {
double temp = xReal[i] * yReal[i] - xImag[i] * yImag[i];
xImag[i] = xImag[i] * yReal[i] + xReal[i] * yImag[i];
xReal[i] = temp;
}
inverseTransform(xReal, xImag);
for (int i = 0; i < n; i++) {
outReal[i] = xReal[i] / n;
outImag[i] = xImag[i] / n;
}
}
}

View file

@ -0,0 +1,158 @@
/*
* Copyright 2018 Yizheng Huang
*
* 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 app.fedilab.android.watermark.androidwm.utils;
import static app.fedilab.android.watermark.androidwm.utils.Constant.ERROR_NO_WATERMARK_FOUND;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_IMG_SUFFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_PREFIX_FLAG;
import static app.fedilab.android.watermark.androidwm.utils.Constant.LSB_TEXT_SUFFIX_FLAG;
import app.fedilab.android.watermark.androidwm.listener.DetectFinishListener;
/**
* Util class for operations with {@link String}.
*
* @author huangyz0918
*/
public class StringUtils {
static {
System.loadLibrary("Watermark");
}
/**
* Converting a {@link String} text into a binary text.
* <p>
* This is the native version.
*/
public static native String stringToBinary(String inputText);
/**
* String to integer array.
* <p>
* This is the native version.
*/
public static native int[] stringToIntArray(String inputString);
/**
* Converting a binary string to a ASCII string.
*/
public static native String binaryToString(String inputText);
/**
* Replace the wrong rgb number in a form of binary,
* the only case is 0 - 1 = 9, so, we need to replace
* all nines to zero.
*/
public static native void replaceNines(int[] inputArray);
public static void replaceNinesJ(int[] inputArray) {
for (int i = 0; i < inputArray.length; i++) {
if (inputArray[i] == 9) {
inputArray[i] = 0;
}
}
}
/**
* Int array to string.
*/
public static native String intArrayToString(int[] inputArray);
public static String intArrayToStringJ(int[] inputArray) {
StringBuilder binary = new StringBuilder();
for (int num : inputArray) {
binary.append(num);
}
return binary.toString();
}
/**
* native method for calculating the Convolution 1D.
*/
public static native double[] calConv1D(double[] inputArray1, double[] inputArray2);
/**
* get the single digit number and set it to the target one.
*/
public static int replaceSingleDigit(int target, int singleDigit) {
return (target / 10) * 10 + singleDigit;
}
public static int replaceSingleDigit(double target, int singleDigit) {
return ((int) target / 10) * 10 + singleDigit;
}
/**
* Get text between two strings. Passed limiting strings are not
* included into result.
*
* @param text Text to search in.
*/
public static String getBetweenStrings(String text, boolean isText, DetectFinishListener listener) {
String result = null;
if (isText) {
try {
result = text.substring(text.indexOf(LSB_TEXT_PREFIX_FLAG) + LSB_TEXT_SUFFIX_FLAG.length()
);
result = result.substring(0, result.indexOf(LSB_TEXT_SUFFIX_FLAG));
} catch (StringIndexOutOfBoundsException e) {
listener.onFailure(ERROR_NO_WATERMARK_FOUND);
}
} else {
try {
result = text.substring(text.indexOf(LSB_IMG_PREFIX_FLAG) + LSB_IMG_SUFFIX_FLAG.length()
);
result = result.substring(0, result.indexOf(LSB_IMG_SUFFIX_FLAG));
} catch (StringIndexOutOfBoundsException e) {
listener.onFailure(ERROR_NO_WATERMARK_FOUND);
}
}
return result;
}
/**
* cast an int array to a double array.
* System.arrayCopy cannot cast the int array to a double one.
*/
@SuppressWarnings("PMD")
public static double[] copyFromIntArray(int[] source) {
double[] dest = new double[source.length];
for (int i = 0; i < source.length; i++) {
dest[i] = source[i];
}
return dest;
}
/**
* cast a double array to an int array.
* System.arrayCopy cannot cast the double array to an int one.
*/
@SuppressWarnings("PMD")
public static int[] copyFromDoubleArray(double[] source) {
int[] dest = new int[source.length];
for (int i = 0; i < source.length; i++) {
dest[i] = (int) source[i];
}
return dest;
}
}