From ed8303c8b0459cfab494eafc2ff1ed9c7276399d Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 8 May 2022 19:12:41 +0200 Subject: [PATCH] comment #2 - Fix watermarks --- app/build.gradle | 1 + .../app/fedilab/android/helper/Helper.java | 58 +++ .../android/services/PostMessageService.java | 21 +- .../settings/FragmentComposeSettings.java | 22 +- .../watermark/androidwm/Watermark.java | 278 ++++++++++++++ .../watermark/androidwm/WatermarkBuilder.java | 344 ++++++++++++++++++ .../androidwm/WatermarkDetector.java | 74 ++++ .../androidwm/bean/AsyncTaskParams.java | 84 +++++ .../androidwm/bean/WatermarkImage.java | 159 ++++++++ .../androidwm/bean/WatermarkPosition.java | 82 +++++ .../androidwm/bean/WatermarkText.java | 214 +++++++++++ .../listener/BuildFinishListener.java | 32 ++ .../listener/DetectFinishListener.java | 33 ++ .../androidwm/task/DetectionReturnValue.java | 56 +++ .../androidwm/task/FDDetectionTask.java | 122 +++++++ .../androidwm/task/FDWatermarkTask.java | 189 ++++++++++ .../androidwm/task/LSBDetectionTask.java | 110 ++++++ .../androidwm/task/LSBWatermarkTask.java | 143 ++++++++ .../androidwm/utils/BitmapUtils.java | 236 ++++++++++++ .../watermark/androidwm/utils/Constant.java | 45 +++ .../watermark/androidwm/utils/FastDctFft.java | 98 +++++ .../watermark/androidwm/utils/Fft.java | 202 ++++++++++ .../androidwm/utils/StringUtils.java | 158 ++++++++ 23 files changed, 2755 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/Watermark.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkBuilder.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkDetector.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/bean/AsyncTaskParams.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkImage.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkPosition.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkText.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/listener/BuildFinishListener.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/listener/DetectFinishListener.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/task/DetectionReturnValue.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDDetectionTask.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDWatermarkTask.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBDetectionTask.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBWatermarkTask.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/utils/BitmapUtils.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Constant.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/utils/FastDctFft.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Fft.java create mode 100644 app/src/main/java/app/fedilab/android/watermark/androidwm/utils/StringUtils.java diff --git a/app/build.gradle b/app/build.gradle index 6ea9495b6..7ff6a2d9a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,4 +119,5 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.8.1' + } \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java index 372d143a1..a6c9f0b72 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -33,7 +33,9 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; +import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; 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.sqlite.Sqlite; 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 es.dmoral.toasty.Toasty; import okhttp3.MediaType; @@ -1242,6 +1246,60 @@ public class Helper { 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) { byte[] imageBytes = uriToByteArray(context, uri); ContentResolver cR = context.getApplicationContext().getContentResolver(); diff --git a/app/src/main/java/app/fedilab/android/services/PostMessageService.java b/app/src/main/java/app/fedilab/android/services/PostMessageService.java index 15d6f92ce..1ba0097d4 100644 --- a/app/src/main/java/app/fedilab/android/services/PostMessageService.java +++ b/app/src/main/java/app/fedilab/android/services/PostMessageService.java @@ -23,6 +23,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.os.Build; import android.os.Bundle; @@ -31,6 +32,7 @@ import android.text.Html; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; +import androidx.preference.PreferenceManager; import java.io.IOException; import java.util.ArrayList; @@ -41,6 +43,7 @@ import java.util.concurrent.TimeUnit; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; 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.PostState; import app.fedilab.android.client.entities.StatusDraft; @@ -173,6 +176,9 @@ public class PostMessageService extends IntentService { } totalMediaSize = 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++) { if (statuses.get(i).media_attachments != null && statuses.get(i).media_attachments.size() > 0) { 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; messageSent = 0; for (int i = startingPosition; i < statuses.size(); i++) { @@ -194,7 +209,11 @@ public class PostMessageService extends IntentService { attachmentIds = new ArrayList<>(); for (Attachment attachment : statuses.get(i).media_attachments) { 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 attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, null, attachment.description, null); if (attachmentCall != null) { try { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java index 255dd8acc..d4b08e887 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentComposeSettings.java @@ -17,10 +17,14 @@ package app.fedilab.android.ui.fragment.settings; import android.content.SharedPreferences; import android.os.Bundle; +import androidx.preference.EditTextPreference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; +import androidx.preference.SwitchPreferenceCompat; 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 { @@ -31,15 +35,23 @@ public class FragmentComposeSettings extends PreferenceFragmentCompat implements } 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 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (getActivity() != null) { - SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); - SharedPreferences.Editor editor = sharedpreferences.edit(); - + if (key.equalsIgnoreCase(getString(R.string.SET_WATERMARK_TEXT))) { + 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(); } } diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/Watermark.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/Watermark.java new file mode 100644 index 000000000..094ae9843 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/Watermark.java @@ -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 buildFinishListener; + private Bitmap outputImage; + private Bitmap canvasBitmap; + + /** + * Constructors for WatermarkImage + */ + @SuppressWarnings("PMD") + Watermark(@NonNull Context context, + @NonNull Bitmap backgroundImg, + @Nullable WatermarkImage watermarkImg, + @Nullable List wmBitmapList, + @Nullable WatermarkText inputText, + @Nullable List wmTextList, + boolean isTileMode, + boolean isInvisible, + boolean isLSB, + @Nullable BuildFinishListener 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 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 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); + } + +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkBuilder.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkBuilder.java new file mode 100644 index 000000000..0b0540920 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkBuilder.java @@ -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 buildFinishListener = null; + + private WatermarkImage watermarkImage; + private WatermarkText watermarkText; + private List watermarkTexts = new ArrayList<>(); + private List 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 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 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 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 + ); + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkDetector.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkDetector.java new file mode 100644 index 000000000..ad6a171c4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/WatermarkDetector.java @@ -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); + } + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/AsyncTaskParams.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/AsyncTaskParams.java new file mode 100644 index 000000000..b1f0fed82 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/AsyncTaskParams.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkImage.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkImage.java new file mode 100644 index 000000000..a6b7c4b36 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkImage.java @@ -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); + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkPosition.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkPosition.java new file mode 100644 index 000000000..f114b7d7e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkPosition.java @@ -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; + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkText.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkText.java new file mode 100644 index 000000000..4ac425ca8 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/bean/WatermarkText.java @@ -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(); + } + +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/BuildFinishListener.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/BuildFinishListener.java new file mode 100644 index 000000000..19eb3e332 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/BuildFinishListener.java @@ -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 can be the image and the string watermarks. + * @author huangyz0918 (huangyz0918@gmail.com) + */ +public interface BuildFinishListener { + + void onSuccess(T object); + + void onFailure(String message); +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/DetectFinishListener.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/DetectFinishListener.java new file mode 100644 index 000000000..c516056ee --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/listener/DetectFinishListener.java @@ -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); +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/task/DetectionReturnValue.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/DetectionReturnValue.java new file mode 100644 index 000000000..712e7549c --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/DetectionReturnValue.java @@ -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; + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDDetectionTask.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDDetectionTask.java new file mode 100644 index 000000000..050433c7a --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDDetectionTask.java @@ -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 { + + 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDWatermarkTask.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDWatermarkTask.java new file mode 100644 index 000000000..24875404f --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/FDWatermarkTask.java @@ -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 { + + private final BuildFinishListener listener; + + public FDWatermarkTask(BuildFinishListener 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; + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBDetectionTask.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBDetectionTask.java new file mode 100644 index 000000000..bee0d03d9 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBDetectionTask.java @@ -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 { + + 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); + } + +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBWatermarkTask.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBWatermarkTask.java new file mode 100644 index 000000000..8cfe35b1b --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/task/LSBWatermarkTask.java @@ -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 { + + private final BuildFinishListener listener; + + public LSBWatermarkTask(BuildFinishListener 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); + } + +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/BitmapUtils.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/BitmapUtils.java new file mode 100644 index 000000000..2c9c8f7d0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/BitmapUtils.java @@ -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. + *

+ * 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; + } +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Constant.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Constant.java new file mode 100644 index 000000000..84c7fc435 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Constant.java @@ -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!"; +} diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/FastDctFft.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/FastDctFft.java new file mode 100644 index 000000000..688d00016 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/FastDctFft.java @@ -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); + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Fft.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Fft.java new file mode 100644 index 000000000..d3d3573a4 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/Fft.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * The vector can have any length. This requires the convolution function, + * which in turn requires the radix-2 FFT function. + *

+ * 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; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/StringUtils.java b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/StringUtils.java new file mode 100644 index 000000000..14efe3522 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/watermark/androidwm/utils/StringUtils.java @@ -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. + *

+ * This is the native version. + */ + public static native String stringToBinary(String inputText); + + /** + * String to integer array. + *

+ * 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; + } + +}