From 6a5cf6b221dd31210614c92a897b8a6dff2f60c0 Mon Sep 17 00:00:00 2001 From: Olga Miller Date: Tue, 3 Jan 2017 18:32:45 +0100 Subject: [PATCH] Initial commit --- .gitignore | 8 + LICENSE | 202 +++++++++ NOTICE | 8 + README | 42 ++ app/.gitignore | 1 + app/build.gradle | 24 + app/proguard-rules.pro | 17 + app/src/main/AndroidManifest.xml | 45 ++ .../ColorPalette/ColorPaletteView.java | 96 ++++ .../ColorPalette/GridColorPalette.java | 203 +++++++++ .../ColorPalette/IColorPalette.java | 30 ++ .../main/java/om/sstvencoder/CropView.java | 422 ++++++++++++++++++ .../java/om/sstvencoder/EditTextActivity.java | 122 +++++ app/src/main/java/om/sstvencoder/Encoder.java | 137 ++++++ .../java/om/sstvencoder/MainActivity.java | 334 ++++++++++++++ .../om/sstvencoder/ModeInterfaces/IMode.java | 24 + .../sstvencoder/ModeInterfaces/IModeInfo.java | 24 + .../sstvencoder/ModeInterfaces/ModeSize.java | 29 ++ .../sstvencoder/Modes/ImageFormats/NV21.java | 60 +++ .../Modes/ImageFormats/YUV440P.java | 61 +++ .../sstvencoder/Modes/ImageFormats/YUY2.java | 53 +++ .../sstvencoder/Modes/ImageFormats/YV12.java | 65 +++ .../sstvencoder/Modes/ImageFormats/Yuv.java | 46 ++ .../Modes/ImageFormats/YuvConverter.java | 45 ++ .../Modes/ImageFormats/YuvFactory.java | 35 ++ .../Modes/ImageFormats/YuvImageFormat.java | 22 + .../java/om/sstvencoder/Modes/Martin.java | 100 +++++ .../java/om/sstvencoder/Modes/Martin1.java | 33 ++ .../java/om/sstvencoder/Modes/Martin2.java | 33 ++ .../main/java/om/sstvencoder/Modes/Mode.java | 133 ++++++ .../om/sstvencoder/Modes/ModeDescription.java | 27 ++ .../om/sstvencoder/Modes/ModeFactory.java | 74 +++ .../java/om/sstvencoder/Modes/ModeInfo.java | 39 ++ .../main/java/om/sstvencoder/Modes/PD.java | 87 ++++ .../main/java/om/sstvencoder/Modes/PD120.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD160.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD180.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD240.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD290.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD50.java | 34 ++ .../main/java/om/sstvencoder/Modes/PD90.java | 34 ++ .../java/om/sstvencoder/Modes/Robot36.java | 129 ++++++ .../java/om/sstvencoder/Modes/Robot72.java | 123 +++++ .../java/om/sstvencoder/Modes/Scottie.java | 102 +++++ .../java/om/sstvencoder/Modes/Scottie1.java | 33 ++ .../java/om/sstvencoder/Modes/Scottie2.java | 33 ++ .../java/om/sstvencoder/Modes/ScottieDX.java | 33 ++ .../java/om/sstvencoder/Modes/Wraase.java | 91 ++++ .../om/sstvencoder/Output/AudioOutput.java | 79 ++++ .../java/om/sstvencoder/Output/IOutput.java | 26 ++ .../om/sstvencoder/Output/OutputFactory.java | 34 ++ .../om/sstvencoder/Output/WaveFileOutput.java | 131 ++++++ .../main/java/om/sstvencoder/Settings.java | 147 ++++++ .../om/sstvencoder/TextOverlay/IReader.java | 40 ++ .../om/sstvencoder/TextOverlay/IWriter.java | 40 ++ .../om/sstvencoder/TextOverlay/Label.java | 96 ++++ .../TextOverlay/LabelCollection.java | 184 ++++++++ .../TextOverlay/LabelContainer.java | 117 +++++ .../sstvencoder/TextOverlay/LabelPainter.java | 303 +++++++++++++ .../om/sstvencoder/TextOverlayTemplate.java | 200 +++++++++ app/src/main/java/om/sstvencoder/Utility.java | 113 +++++ .../main/res/layout/activity_edit_text.xml | 53 +++ app/src/main/res/layout/activity_main.xml | 20 + app/src/main/res/menu/menu_edit_text.xml | 12 + app/src/main/res/menu/menu_main.xml | 43 ++ app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 8529 bytes .../sym_keyboard_done_lxx_dark.png | Bin 0 -> 14700 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 4183 bytes .../sym_keyboard_done_lxx_dark.png | Bin 0 -> 14643 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 9692 bytes .../sym_keyboard_done_lxx_dark.png | Bin 0 -> 14750 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 16762 bytes .../sym_keyboard_done_lxx_dark.png | Bin 0 -> 14819 bytes app/src/main/res/values/strings.xml | 36 ++ app/src/main/res/values/styles.xml | 3 + app/src/main/res/xml/paths.xml | 6 + build.gradle | 23 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 +++++++ gradlew.bat | 90 ++++ settings.gradle | 1 + 83 files changed, 5443 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java create mode 100644 app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java create mode 100644 app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java create mode 100644 app/src/main/java/om/sstvencoder/CropView.java create mode 100644 app/src/main/java/om/sstvencoder/EditTextActivity.java create mode 100644 app/src/main/java/om/sstvencoder/Encoder.java create mode 100644 app/src/main/java/om/sstvencoder/MainActivity.java create mode 100644 app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java create mode 100644 app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java create mode 100644 app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Martin.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Martin1.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Martin2.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Mode.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ModeDescription.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ModeFactory.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ModeInfo.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD120.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD160.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD180.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD240.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD290.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD50.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/PD90.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Robot36.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Robot72.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Scottie.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Scottie1.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Scottie2.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/ScottieDX.java create mode 100644 app/src/main/java/om/sstvencoder/Modes/Wraase.java create mode 100644 app/src/main/java/om/sstvencoder/Output/AudioOutput.java create mode 100644 app/src/main/java/om/sstvencoder/Output/IOutput.java create mode 100644 app/src/main/java/om/sstvencoder/Output/OutputFactory.java create mode 100644 app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java create mode 100644 app/src/main/java/om/sstvencoder/Settings.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/IReader.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/Label.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java create mode 100644 app/src/main/java/om/sstvencoder/TextOverlayTemplate.java create mode 100644 app/src/main/java/om/sstvencoder/Utility.java create mode 100644 app/src/main/res/layout/activity_edit_text.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/menu/menu_edit_text.xml create mode 100644 app/src/main/res/menu/menu_main.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/sym_keyboard_done_lxx_dark.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/sym_keyboard_done_lxx_dark.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/sym_keyboard_done_lxx_dark.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/sym_keyboard_done_lxx_dark.png create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/paths.xml create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09b993d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..78ad429 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +SSTV Encoder 2 +Copyright 2017 Olga Miller + +Mode specifications were taken from "Dayton Paper" of JL Barber: +http://www.barberdsp.com/files/Dayton%20Paper.pdf + +Icons were created using GIMP: +http://www.gimp.org/ diff --git a/README b/README new file mode 100644 index 0000000..1ae678a --- /dev/null +++ b/README @@ -0,0 +1,42 @@ +SSTV Encoder 2 +Copyright 2017 Olga Miller + +-------------- Description: + +This app sends images via Slow Scan Television (SSTV). + +Currently supported modes are: + Martin Modes: Martin 1, Martin 2 + PD Modes: PD 50, PD 90, PD 120, PD 160, PD 180, PD 240, PD 290 + Scottie Modes: Scottie 1, Scottie 2, Scottie DX + Robot Modes: Robot 36 Color, Robot 72 Color + Wraase Modes: Wraase SC2 180 + +-------------- Usage: + +Tap "Take Picture" or "Pick Picture" menu button or +use the Share option of any app like Gallery to load an image. + +Single tap to add text. +Single tap on text to edit. +Long press to move text. + +-------------- Remarks: + +After clicking on a mode the image will be scaled to that mode's native size. +To keep the aspect ratio, black borders will be added if necessary. +Original image can be resend using another mode without reloading. + +The mode specifications are taken from the Dayton Paper of JL Barber: +http://www.barberdsp.com/files/Dayton%20Paper.pdf + +Source code for decoding SSTV images can be found here: +https://github.com/xdsopl/robot36/tree/android + +On Google Play you can find the working app: +SSTV Encoder +https://play.google.com/store/apps/details?id=om.sstvencoder + +For decoding try: +Robot36 - SSTV Image Decoder +https://play.google.com/store/apps/details?id=xdsopl.robot36 \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..789e83f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25" + defaultConfig { + applicationId "om.sstvencoder" + minSdkVersion 15 + targetSdkVersion 25 + versionCode 21 + versionName "2.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:25.0.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..c076324 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/olga/Android/Sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3261a09 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java b/app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java new file mode 100644 index 0000000..6a5d55e --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ColorPalette/ColorPaletteView.java @@ -0,0 +1,96 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ColorPalette; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import java.util.ArrayList; + +public class ColorPaletteView extends View { + + public interface OnChangeListener { + void onChange(View v, int color); + } + + private final ArrayList mListeners; + private final IColorPalette mPalette; + + public ColorPaletteView(Context context, AttributeSet attrs) { + super(context, attrs); + mListeners = new ArrayList<>(); + mPalette = new GridColorPalette(GridColorPalette.getStandardColors(), + getResources().getDisplayMetrics().density); + } + + public int getColor() { + return mPalette.getSelectedColor(); + } + + public void setColor(int color) { + mPalette.selectColor(color); + } + + @Override + protected void onSizeChanged(int w, int h, int old_w, int old_h) { + super.onSizeChanged(w, h, old_w, old_h); + mPalette.updateSize(w, h); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + mPalette.draw(canvas); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent e) { + boolean consumed = false; + switch (e.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_MOVE: { + update(e.getX(), e.getY()); + consumed = true; + break; + } + } + return consumed || super.onTouchEvent(e); + } + + private void update(float x, float y) { + if (mPalette.selectColor(x, y)) { + invalidate(); + callback(); + } + } + + public void setOnChangeListener(OnChangeListener listener) { + mListeners.add(listener); + } + + private void callback() { + for (OnChangeListener listener : mListeners) { + listener.onChange(this, mPalette.getSelectedColor()); + } + } +} + diff --git a/app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java b/app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java new file mode 100644 index 0000000..40540f6 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ColorPalette/GridColorPalette.java @@ -0,0 +1,203 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ColorPalette; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; + +class GridColorPalette implements IColorPalette { + + static int[] getStandardColors() { + return new int[]{ + Color.BLACK, + Color.GRAY, + Color.LTGRAY, + Color.WHITE, + Color.YELLOW, + Color.CYAN, + Color.GREEN, + Color.MAGENTA, + Color.RED, + Color.BLUE + }; + } + + private final static float STROKE_WIDTH_FACTOR = 6f; + private final static float BOX_SIZE_DP = 96f; + private final static float SPACE_FACTOR = 6f; + private final static float CORNER_RADIUS = 6f; + private final int[] mColorList; + private final Paint mPaint; + private final RectF mSelectedBounds; + private final float mDisplayMetricsDensity; + private int mColumns, mRows; + private float mWidth, mHeight; + private float mBoxSize, mSpace, mStrokeWidth; + private int mSelectedColorIndex; + private boolean mValid; + + GridColorPalette(int[] colorList, float displayMetricsDensity) { + mColorList = colorList; + mDisplayMetricsDensity = displayMetricsDensity; + mPaint = new Paint(); + setPaintStyleForBox(); + mSelectedBounds = new RectF(); + mSelectedColorIndex = 0; + mValid = false; + } + + @Override + public void updateSize(float width, float height) { + mValid = width > 0 && height > 0; + + if (mValid && (mWidth != width || mHeight != height)) { + mWidth = width; + mHeight = height; + updateGrid(); + mStrokeWidth = mSpace / STROKE_WIDTH_FACTOR; + setSelectedColor(mSelectedColorIndex); + } + } + + // The approximately same box size independently on resolution has the higher priority. + // Thus the possible filling of the last row is not supported here. + private void updateGrid() { + int boxes = mColorList.length; + mBoxSize = BOX_SIZE_DP * mDisplayMetricsDensity; + mSpace = mBoxSize / SPACE_FACTOR; + + mColumns = min((int) ((mWidth - mSpace) / (mBoxSize + mSpace) + 0.5f), boxes); + mRows = (boxes + mColumns - 1) / mColumns; // ceil + updateBoxSizeAndSpace(); + + while (mRows * (mBoxSize + mSpace) + mSpace > mHeight) { + ++mColumns; + mRows = (boxes + mColumns - 1) / mColumns; + updateBoxSizeAndSpace(); + } + } + + private int min(int a, int b) { + return a <= b ? a : b; + } + + // Fill out the whole width of the View. + private void updateBoxSizeAndSpace() { + // Set 'space = boxSize / spaceFactor' into + // 'boxSize = (width - (columns + 1) * space ) / columns' + // and solve for boxSize: + mBoxSize = SPACE_FACTOR * mWidth / (1f + mColumns * (SPACE_FACTOR + 1f)); + mSpace = mBoxSize / SPACE_FACTOR; + } + + @Override + public void draw(Canvas canvas) { + if (!mValid) + return; + + float x = mSpace, y = mSpace; + float maxX = mColumns * (mBoxSize + mSpace); + for (int color : mColorList) { + RectF rect = new RectF(x, y, x + mBoxSize, y + mBoxSize); + mPaint.setColor(color); + canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, mPaint); + x += mBoxSize + mSpace; + if (x > maxX) { + x = mSpace; + y += mBoxSize + mSpace; + } + } + drawSelectedRect(canvas); + } + + private void drawSelectedRect(Canvas canvas) { + float padding = mSpace / 2f; + float l = mSelectedBounds.left; + float t = mSelectedBounds.top; + float r = mSelectedBounds.right; + float b = mSelectedBounds.bottom; + RectF rect = new RectF(l - padding, t - padding, r + padding, b + padding); + Paint.Style paintStyle = mPaint.getStyle(); + setPaintStyleForSelectedBox(); + canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, mPaint); + mPaint.setStyle(paintStyle); + } + + private void setPaintStyleForSelectedBox() { + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(mStrokeWidth); + mPaint.setColor(Color.WHITE); + } + + private void setPaintStyleForBox() { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setAntiAlias(true); + } + + @Override + public int getSelectedColor() { + return mColorList[mSelectedColorIndex]; + } + + @Override + public boolean selectColor(float x, float y) { + if (!mValid || mSelectedBounds.contains(x, y)) + return false; + + int column = (int) (x / (mBoxSize + mSpace)); + int row = (int) (y / (mBoxSize + mSpace)); + if (0 > row || row >= mRows || 0 > column && column >= mColumns) + return false; + + int i = row * mColumns + column; + if (i >= mColorList.length || i == mSelectedColorIndex) + return false; + + float left = mSpace + column * (mBoxSize + mSpace); + float top = mSpace + row * (mBoxSize + mSpace); + if (left > x || x > left + mBoxSize || top > y || y > top + mBoxSize) + return false; + + mSelectedBounds.set(left, top, left + mBoxSize, top + mBoxSize); + mSelectedColorIndex = i; + return true; + } + + @Override + public boolean selectColor(int color) { + for (int i = 0; i < mColorList.length; ++i) { + if (color == mColorList[i]) { + if (mValid) + setSelectedColor(i); + else + mSelectedColorIndex = i; + return true; + } + } + return false; + } + + private void setSelectedColor(int i) { + int row = i / mColumns; + int column = i - row * mColumns; + float x = mSpace + column * (mBoxSize + mSpace); + float y = mSpace + row * (mBoxSize + mSpace); + mSelectedBounds.set(x, y, x + mBoxSize, y + mBoxSize); + mSelectedColorIndex = i; + } +} diff --git a/app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java b/app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java new file mode 100644 index 0000000..c5f5bd1 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ColorPalette/IColorPalette.java @@ -0,0 +1,30 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ColorPalette; + +import android.graphics.Canvas; + +interface IColorPalette { + void updateSize(float width, float height); + + void draw(Canvas canvas); + + int getSelectedColor(); + + boolean selectColor(float x, float y); + + boolean selectColor(int color); +} diff --git a/app/src/main/java/om/sstvencoder/CropView.java b/app/src/main/java/om/sstvencoder/CropView.java new file mode 100644 index 0000000..8bd7bea --- /dev/null +++ b/app/src/main/java/om/sstvencoder/CropView.java @@ -0,0 +1,422 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.v4.view.GestureDetectorCompat; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.widget.ImageView; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.TextOverlay.Label; +import om.sstvencoder.TextOverlay.LabelCollection; + +public class CropView extends ImageView { + private class GestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + if (!mLongPress) { + moveImage(distanceX, distanceY); + return true; + } + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + mLongPress = false; + if (!mInScale && mLabelCollection.moveLabelBegin(e.getX(), e.getY())) { + invalidate(); + mLongPress = true; + } + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (!mLongPress) { + editLabelBegin(e.getX(), e.getY()); + return true; + } + return false; + } + } + + private class ScaleGestureListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + if (!mLongPress) { + mInScale = true; + return true; + } + return false; + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + scaleImage(detector.getScaleFactor()); + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mInScale = false; + } + } + + private GestureDetectorCompat mDetectorCompat; + private ScaleGestureDetector mScaleDetector; + private boolean mLongPress, mInScale; + private ModeSize mModeSize; + private final Paint mPaint, mRectPaint, mBorderPaint; + private RectF mInputRect; + private Rect mOutputRect; + private BitmapRegionDecoder mRegionDecoder; + private int mImageWidth, mImageHeight; + private Bitmap mCacheBitmap; + private boolean mSmallImage; + private boolean mImageOK; + private final Rect mCanvasDrawRect, mImageDrawRect; + private int mOrientation; + private Rect mCacheRect; + private int mCacheSampleSize; + private final BitmapFactory.Options mBitmapOptions; + private LabelCollection mLabelCollection; + + public CropView(Context context, AttributeSet attrs) { + super(context, attrs); + mDetectorCompat = new GestureDetectorCompat(getContext(), new GestureListener()); + mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleGestureListener()); + + mBitmapOptions = new BitmapFactory.Options(); + + mPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + mRectPaint = new Paint(); + mRectPaint.setStyle(Paint.Style.STROKE); + mRectPaint.setStrokeWidth(2); + mBorderPaint = new Paint(); + mBorderPaint.setColor(Color.BLACK); + + mCanvasDrawRect = new Rect(); + mImageDrawRect = new Rect(); + mCacheRect = new Rect(); + mOutputRect = new Rect(); + + mSmallImage = false; + mImageOK = false; + + mLabelCollection = new LabelCollection(); + } + + public void setModeSize(ModeSize size) { + mModeSize = size; + mOutputRect = Utility.getEmbeddedRect(getWidth(), getHeight(), mModeSize.width(), mModeSize.height()); + if (mImageOK) { + resetInputRect(); + invalidate(); + } + } + + private void resetInputRect() { + float iw = mModeSize.width(); + float ih = mModeSize.height(); + float ow = mImageWidth; + float oh = mImageHeight; + if (iw * oh > ow * ih) { + mInputRect = new RectF(0.0f, 0.0f, (iw * oh) / ih, oh); + mInputRect.offset((ow - (iw * oh) / ih) / 2.0f, 0.0f); + } else { + mInputRect = new RectF(0.0f, 0.0f, ow, (ih * ow) / iw); + mInputRect.offset(0.0f, (oh - (ih * ow) / iw) / 2.0f); + } + } + + public void rotateImage(int orientation) { + mOrientation += orientation; + mOrientation %= 360; + if (orientation == 90 || orientation == 270) { + int tmp = mImageWidth; + mImageWidth = mImageHeight; + mImageHeight = tmp; + } + if (mImageOK) { + resetInputRect(); + invalidate(); + } + } + + public void setNoBitmap() { + mImageOK = false; + mOrientation = 0; + recycle(); + + invalidate(); + } + + public void setBitmapStream(InputStream stream) throws IOException { + mImageOK = false; + mOrientation = 0; + recycle(); + + // app6 + exif + int bufferBytes = 1048576; + if (!stream.markSupported()) + stream = new BufferedInputStream(stream, bufferBytes); + stream.mark(bufferBytes); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(new BufferedInputStream(stream), null, options); + stream.reset(); + mImageWidth = options.outWidth; + mImageHeight = options.outHeight; + + if (mImageWidth * mImageHeight < 1024 * 1024) { + mCacheBitmap = BitmapFactory.decodeStream(stream); + mSmallImage = true; + } else { + mRegionDecoder = BitmapRegionDecoder.newInstance(stream, true); + mCacheRect.setEmpty(); + mSmallImage = false; + } + + if (mCacheBitmap == null && mRegionDecoder == null) { + String size = options.outWidth + "x" + options.outHeight; + throw new IOException("Stream could not be decoded. Image size: " + size); + } + + mImageOK = true; + resetInputRect(); + invalidate(); + } + + private void recycle() { + if (mRegionDecoder != null) { + mRegionDecoder.recycle(); + mRegionDecoder = null; + } + if (mCacheBitmap != null) { + mCacheBitmap.recycle(); + mCacheBitmap = null; + } + } + + public void scaleImage(float scaleFactor) { + if (!mImageOK) + return; + float newW = mInputRect.width() / scaleFactor; + float newH = mInputRect.height() / scaleFactor; + float dx = 0.5f * (mInputRect.width() - newW); + float dy = 0.5f * (mInputRect.height() - newH); + float max = 2.0f * Math.max(mImageWidth, mImageHeight); + if (Math.min(newW, newH) >= 4.0f && Math.max(newW, newH) <= max) { + mInputRect.inset(dx, dy); + invalidate(); + } + } + + public void moveImage(float distanceX, float distanceY) { + if (!mImageOK) + return; + float dx = (mInputRect.width() * distanceX) / mOutputRect.width(); + float dy = (mInputRect.height() * distanceY) / mOutputRect.height(); + dx = Math.max(mInputRect.width() * 0.1f, mInputRect.right + dx) - mInputRect.right; + dy = Math.max(mInputRect.height() * 0.1f, mInputRect.bottom + dy) - mInputRect.bottom; + dx = Math.min(mImageWidth - mInputRect.width() * 0.1f, mInputRect.left + dx) - mInputRect.left; + dy = Math.min(mImageHeight - mInputRect.height() * 0.1f, mInputRect.top + dy) - mInputRect.top; + mInputRect.offset(dx, dy); + invalidate(); + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent e) { + boolean consumed = false; + if (mLongPress) { + switch (e.getAction()) { + case MotionEvent.ACTION_MOVE: + mLabelCollection.moveLabel(e.getX(), e.getY()); + invalidate(); + consumed = true; + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mLabelCollection.moveLabelEnd(); + invalidate(); + mLongPress = false; + consumed = true; + break; + } + } + consumed = mScaleDetector.onTouchEvent(e) || consumed; + return mDetectorCompat.onTouchEvent(e) || consumed || super.onTouchEvent(e); + } + + @Override + protected void onSizeChanged(int w, int h, int old_w, int old_h) { + super.onSizeChanged(w, h, old_w, old_h); + if (mModeSize != null) + mOutputRect = Utility.getEmbeddedRect(w, h, mModeSize.width(), mModeSize.height()); + mLabelCollection.update(w, h); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + if (mImageOK) { + maximizeImageToCanvasRect(); + adjustCanvasAndImageRect(getWidth(), getHeight()); + canvas.drawRect(mOutputRect, mBorderPaint); + drawBitmap(canvas); + } + mLabelCollection.draw(canvas); + drawModeRect(canvas); + } + + private void maximizeImageToCanvasRect() { + mImageDrawRect.left = Math.round(mInputRect.left - mOutputRect.left * mInputRect.width() / mOutputRect.width()); + mImageDrawRect.top = Math.round(mInputRect.top - mOutputRect.top * mInputRect.height() / mOutputRect.height()); + mImageDrawRect.right = Math.round(mInputRect.right - (mOutputRect.right - getWidth()) * mInputRect.width() / mOutputRect.width()); + mImageDrawRect.bottom = Math.round(mInputRect.bottom - (mOutputRect.bottom - getHeight()) * mInputRect.height() / mOutputRect.height()); + } + + private void adjustCanvasAndImageRect(int width, int height) { + mCanvasDrawRect.set(0, 0, width, height); + if (mImageDrawRect.left < 0) { + mCanvasDrawRect.left -= (mImageDrawRect.left * mCanvasDrawRect.width()) / mImageDrawRect.width(); + mImageDrawRect.left = 0; + } + if (mImageDrawRect.top < 0) { + mCanvasDrawRect.top -= (mImageDrawRect.top * mCanvasDrawRect.height()) / mImageDrawRect.height(); + mImageDrawRect.top = 0; + } + if (mImageDrawRect.right > mImageWidth) { + mCanvasDrawRect.right -= ((mImageDrawRect.right - mImageWidth) * mCanvasDrawRect.width()) / mImageDrawRect.width(); + mImageDrawRect.right = mImageWidth; + } + if (mImageDrawRect.bottom > mImageHeight) { + mCanvasDrawRect.bottom -= ((mImageDrawRect.bottom - mImageHeight) * mCanvasDrawRect.height()) / mImageDrawRect.height(); + mImageDrawRect.bottom = mImageHeight; + } + } + + private void drawModeRect(Canvas canvas) { + mRectPaint.setColor(Color.BLUE); + canvas.drawRect(mOutputRect, mRectPaint); + mRectPaint.setColor(Color.GREEN); + drawRectInset(canvas, mOutputRect, -2); + mRectPaint.setColor(Color.RED); + drawRectInset(canvas, mOutputRect, -4); + } + + private void drawRectInset(Canvas canvas, Rect rect, int inset) { + canvas.drawRect(rect.left + inset, rect.top + inset, rect.right - inset, rect.bottom - inset, mRectPaint); + } + + private Rect getIntRect(RectF rect) { + return new Rect(Math.round(rect.left), Math.round(rect.top), Math.round(rect.right), Math.round(rect.bottom)); + } + + private int getSampleSize() { + int sx = Math.round(mInputRect.width() / mModeSize.width()); + int sy = Math.round(mInputRect.height() / mModeSize.height()); + int scale = Math.max(1, Math.max(sx, sy)); + return Integer.highestOneBit(scale); + } + + public Bitmap getBitmap() { + if (!mImageOK) + return null; + + Bitmap result = Bitmap.createBitmap(mModeSize.width(), mModeSize.height(), Bitmap.Config.ARGB_8888); + mImageDrawRect.set(getIntRect(mInputRect)); + adjustCanvasAndImageRect(mModeSize.width(), mModeSize.height()); + + Canvas canvas = new Canvas(result); + canvas.drawColor(Color.BLACK); + drawBitmap(canvas); + mLabelCollection.draw(canvas, mOutputRect, new Rect(0, 0, mModeSize.width(), mModeSize.height())); + + return result; + } + + private void drawBitmap(Canvas canvas) { + int w = mImageWidth; + int h = mImageHeight; + for (int i = 0; i < mOrientation / 90; ++i) { + int tmp = w; + w = h; + h = tmp; + mImageDrawRect.set(mImageDrawRect.top, h - mImageDrawRect.left, mImageDrawRect.bottom, h - mImageDrawRect.right); + mCanvasDrawRect.set(mCanvasDrawRect.top, -mCanvasDrawRect.right, mCanvasDrawRect.bottom, -mCanvasDrawRect.left); + } + mImageDrawRect.sort(); + canvas.save(); + canvas.rotate(mOrientation); + if (!mSmallImage) { + int sampleSize = getSampleSize(); + if (sampleSize < mCacheSampleSize || !mCacheRect.contains(mImageDrawRect)) { + if (mCacheBitmap != null) + mCacheBitmap.recycle(); + int cacheWidth = mImageDrawRect.width(); + int cacheHeight = mImageDrawRect.height(); + while (cacheWidth * cacheHeight < (sampleSize * 1024 * sampleSize * 1024)) { + cacheWidth += mImageDrawRect.width(); + cacheHeight += mImageDrawRect.height(); + } + mCacheRect.set( + Math.max(0, ~(sampleSize - 1) & (mImageDrawRect.centerX() - cacheWidth / 2)), + Math.max(0, ~(sampleSize - 1) & (mImageDrawRect.centerY() - cacheHeight / 2)), + Math.min(mRegionDecoder.getWidth(), ~(sampleSize - 1) & (mImageDrawRect.centerX() + cacheWidth / 2 + sampleSize - 1)), + Math.min(mRegionDecoder.getHeight(), ~(sampleSize - 1) & (mImageDrawRect.centerY() + cacheHeight / 2 + sampleSize - 1))); + mBitmapOptions.inSampleSize = mCacheSampleSize = sampleSize; + mCacheBitmap = mRegionDecoder.decodeRegion(mCacheRect, mBitmapOptions); + } + mImageDrawRect.offset(-mCacheRect.left, -mCacheRect.top); + mImageDrawRect.left /= mCacheSampleSize; + mImageDrawRect.top /= mCacheSampleSize; + mImageDrawRect.right /= mCacheSampleSize; + mImageDrawRect.bottom /= mCacheSampleSize; + } + canvas.drawBitmap(mCacheBitmap, mImageDrawRect, mCanvasDrawRect, mPaint); + canvas.restore(); + } + + private void editLabelBegin(float x, float y) { + Label label = mLabelCollection.editLabelBegin(x, y); + ((MainActivity) getContext()).startEditTextActivity(label); + } + + public void editLabelEnd(Label label) { + mLabelCollection.editLabelEnd(label); + } + + public LabelCollection getLabels() { + return mLabelCollection; + } +} diff --git a/app/src/main/java/om/sstvencoder/EditTextActivity.java b/app/src/main/java/om/sstvencoder/EditTextActivity.java new file mode 100644 index 0000000..e221e47 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/EditTextActivity.java @@ -0,0 +1,122 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.Spinner; + +import om.sstvencoder.ColorPalette.ColorPaletteView; +import om.sstvencoder.TextOverlay.Label; + +public class EditTextActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { + public static final int REQUEST_CODE = 101; + public static final String EXTRA = "EDIT_TEXT_EXTRA"; + private EditText mEditText; + private ColorPaletteView mColorPaletteView; + private float mTextSize; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_text); + mEditText = (EditText) findViewById(R.id.edit_text); + mColorPaletteView = (ColorPaletteView) findViewById(R.id.edit_color); + } + + @Override + protected void onStart() { + super.onStart(); + Label label = (Label) getIntent().getSerializableExtra(EXTRA); + mEditText.setText(label.getText()); + mTextSize = label.getTextSize(); + initTextSizeSpinner(textSizeToPosition(mTextSize)); + ((CheckBox) findViewById(R.id.edit_italic)).setChecked(label.getItalic()); + ((CheckBox) findViewById(R.id.edit_bold)).setChecked(label.getBold()); + mColorPaletteView.setColor(label.getForeColor()); + } + + private void initTextSizeSpinner(int position) { + Spinner editTextSize = (Spinner) findViewById(R.id.edit_text_size); + editTextSize.setOnItemSelectedListener(this); + String[] textSizeList = new String[]{"Small", "Normal", "Large", "Huge"}; + editTextSize.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, textSizeList)); + editTextSize.setSelection(position); + } + + private int textSizeToPosition(float textSize) { + int position = (int) (textSize - 1f); + if (0 <= position && position <= 3) + return position; + return 1; + } + + private float positionToTextSize(int position) { + return position + 1f; + } + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + mTextSize = positionToTextSize(position); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_edit_text, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_done: + done(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void done() { + Intent intent = new Intent(); + intent.putExtra(EXTRA, getLabel()); + setResult(RESULT_OK, intent); + finish(); + } + + @NonNull + private Label getLabel() { + Label label = new Label(); + label.setText(mEditText.getText().toString()); + label.setTextSize(mTextSize); + label.setItalic(((CheckBox) findViewById(R.id.edit_italic)).isChecked()); + label.setBold(((CheckBox) findViewById(R.id.edit_bold)).isChecked()); + label.setForeColor(mColorPaletteView.getColor()); + return label; + } +} diff --git a/app/src/main/java/om/sstvencoder/Encoder.java b/app/src/main/java/om/sstvencoder/Encoder.java new file mode 100644 index 0000000..64fda64 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Encoder.java @@ -0,0 +1,137 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.graphics.Bitmap; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +import om.sstvencoder.ModeInterfaces.IMode; +import om.sstvencoder.ModeInterfaces.IModeInfo; +import om.sstvencoder.Modes.ModeFactory; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.Output.OutputFactory; + +// Creates IMode instance +class Encoder { + private final Thread mThread; + private final List mQueue; + private boolean mQuit, mStop; + private Class mModeClass; + + Encoder() { + mQueue = new LinkedList<>(); + mQuit = false; + mStop = false; + mModeClass = ModeFactory.getDefaultMode(); + + mThread = new Thread() { + @Override + public void run() { + while (true) { + IMode mode; + synchronized (this) { + while (mQueue.isEmpty() && !mQuit) { + try { + wait(); + } catch (Exception ignore) { + } + } + if (mQuit) + return; + + mStop = false; + mode = mQueue.remove(0); + } + mode.init(); + + while (mode.process()) { + synchronized (this) { + if (mQuit || mStop) + break; + } + } + mode.finish(mStop); + } + } + }; + mThread.start(); + } + + boolean setMode(String className) { + try { + mModeClass = Class.forName(className); + } catch (Exception ignore) { + return false; + } + return true; + } + + IModeInfo getModeInfo() { + return ModeFactory.getModeInfo(mModeClass); + } + + IModeInfo[] getModeInfoList() { + return ModeFactory.getModeInfoList(); + } + + void play(Bitmap bitmap) { + IOutput output = OutputFactory.createOutputForSending(); + IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output); + if (mode != null) + enqueue(mode); + } + + boolean save(Bitmap bitmap, File file) { + IOutput output = OutputFactory.createOutputForSavingAsWave(file); + IMode mode = ModeFactory.CreateMode(mModeClass, bitmap, output); + if (mode != null) { + mode.init(); + + while (mode.process()) { + if (mQuit) + break; + } + mode.finish(mQuit); + } + return !mQuit; + } + + void stop() { + synchronized (mThread) { + mStop = true; + int size = mQueue.size(); + for (int i = 0; i < size; ++i) + mQueue.remove(0).finish(true); + } + } + + private void enqueue(IMode mode) { + synchronized (mThread) { + mQueue.add(mode); + mThread.notify(); + } + } + + void destroy() { + synchronized (mThread) { + mQuit = true; + mThread.notify(); + } + } +} diff --git a/app/src/main/java/om/sstvencoder/MainActivity.java b/app/src/main/java/om/sstvencoder/MainActivity.java new file mode 100644 index 0000000..d385a69 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/MainActivity.java @@ -0,0 +1,334 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.FileProvider; +import android.support.v7.app.AppCompatActivity; +import android.system.ErrnoException; +import android.system.OsConstants; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; +import android.widget.Toast; + +import java.io.File; +import java.io.FileNotFoundException; + +import om.sstvencoder.ModeInterfaces.IModeInfo; +import om.sstvencoder.TextOverlay.Label; + +public class MainActivity extends AppCompatActivity { + private static final String CLASS_NAME = "ClassName"; + private static final int REQUEST_IMAGE_CAPTURE = 1; + private static final int REQUEST_PICK_IMAGE = 2; + private Settings mSettings; + private TextOverlayTemplate mTextOverlayTemplate; + private CropView mCropView; + private Encoder mEncoder; + private File mFile; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + mCropView = (CropView) findViewById(R.id.cropView); + mEncoder = new Encoder(); + IModeInfo mode = mEncoder.getModeInfo(); + mCropView.setModeSize(mode.getModeSize()); + setTitle(mode.getModeName()); + mSettings = new Settings(this); + mSettings.load(); + mTextOverlayTemplate = new TextOverlayTemplate(); + mTextOverlayTemplate.load(mCropView.getLabels(), mSettings.getTextOverlayFile()); + loadImage(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + loadImage(intent); + super.onNewIntent(intent); + } + + private void loadImage(Intent intent) { + Uri uri = getImageUriFromIntent(intent); + boolean verbose = true; + if (uri == null) { + uri = mSettings.getImageUri(); + verbose = false; + } + if (loadImage(uri, verbose)) + mSettings.setImageUri(uri); + } + + private Uri getImageUriFromIntent(Intent intent) { + Uri uri = null; + if (isIntentTypeValid(intent.getType()) && isIntentActionValid(intent.getAction())) { + uri = intent.hasExtra(Intent.EXTRA_STREAM) ? + (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM) : intent.getData(); + if (uri == null) { + String s = getString(R.string.load_img_err_txt_unsupported); + showErrorMessage(getString(R.string.load_img_err_title), s, s + "\n\n" + intent); + } + } + return uri; + } + + // Set verbose to false for any Uri that might have expired (e.g. shared from browser). + private boolean loadImage(Uri uri, boolean verbose) { + if (uri != null) { + try { + ContentResolver resolver = getContentResolver(); + mCropView.setBitmapStream(resolver.openInputStream(uri)); + mCropView.rotateImage(getOrientation(resolver, uri)); + return true; + } catch (FileNotFoundException ex) { + if (ex.getCause() instanceof ErrnoException + && ((ErrnoException) ex.getCause()).errno == OsConstants.EACCES) { + requestPermissions(); + } else if (verbose) { + String s = getString(R.string.load_img_err_title) + ": \n" + ex.getMessage(); + Toast.makeText(this, s, Toast.LENGTH_LONG).show(); + } + } catch (Exception ex) { + if (verbose) { + String s = Utility.createMessage(ex) + "\n\n" + uri; + showErrorMessage(getString(R.string.load_img_err_title), ex.getMessage(), s); + } + } + } + mCropView.setNoBitmap(); + return false; + } + + private boolean isIntentActionValid(String action) { + return Intent.ACTION_SEND.equals(action); + } + + private boolean isIntentTypeValid(String type) { + return type != null && type.startsWith("image/"); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void requestPermissions() { + if (Build.VERSION_CODES.JELLY_BEAN > Build.VERSION.SDK_INT) + return; + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); + } + + private void showErrorMessage(final String title, final String shortText, final String longText) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(title); + builder.setMessage(shortText); + builder.setNeutralButton(getString(R.string.btn_ok), null); + builder.setPositiveButton(getString(R.string.btn_send_email), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String device = Build.MANUFACTURER + ", " + Build.BRAND + ", " + Build.MODEL + ", " + Build.VERSION.RELEASE; + String text = longText + "\n\n" + BuildConfig.VERSION_NAME + ", " + device; + Intent intent = Utility.createEmailIntent(getString(R.string.email_subject), text); + startActivity(Intent.createChooser(intent, getString(R.string.chooser_title))); + } + }); + builder.show(); + } + + private void showOrientationErrorMessage(Uri uri, Exception ex) { + String title = getString(R.string.load_img_orientation_err_title); + String longText = title + "\n\n" + Utility.createMessage(ex) + "\n\n" + uri; + showErrorMessage(title, ex.getMessage(), longText); + } + + public int getOrientation(ContentResolver resolver, Uri uri) { + int orientation = 0; + try { + Cursor cursor = resolver.query(uri, new String[]{MediaStore.Images.ImageColumns.ORIENTATION}, null, null, null); + if (cursor.moveToFirst()) + orientation = cursor.getInt(0); + cursor.close(); + } catch (Exception ignore) { + try { + ExifInterface exif = new ExifInterface(uri.getPath()); + orientation = Utility.convertToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)); + } catch (Exception ex) { + showOrientationErrorMessage(uri, ex); + } + } + return orientation; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + createModesMenu(menu); + return true; + } + + private void createModesMenu(Menu menu) { + SubMenu modesSubMenu = menu.findItem(R.id.action_modes).getSubMenu(); + IModeInfo[] modeInfoList = mEncoder.getModeInfoList(); + for (IModeInfo modeInfo : modeInfoList) { + MenuItem item = modesSubMenu.add(modeInfo.getModeName()); + Intent intent = new Intent(); + intent.putExtra(CLASS_NAME, modeInfo.getModeClassName()); + item.setIntent(intent); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_pick_picture: + dispatchPickPictureIntent(); + return true; + case R.id.action_take_picture: + dispatchTakePictureIntent(); + return true; + case R.id.action_save_wave: + save(); + return true; + case R.id.action_play: + play(); + return true; + case R.id.action_stop: + mEncoder.stop(); + return true; + case R.id.action_rotate: + mCropView.rotateImage(90); + return true; + case R.id.action_modes: + return true; + default: + String className = item.getIntent().getStringExtra(CLASS_NAME); + if (mEncoder.setMode(className)) { + IModeInfo modeInfo = mEncoder.getModeInfo(); + mCropView.setModeSize(modeInfo.getModeSize()); + setTitle(modeInfo.getModeName()); + } + return true; + } + } + + public void startEditTextActivity(@NonNull Label label) { + Intent intent = new Intent(this, EditTextActivity.class); + intent.putExtra(EditTextActivity.EXTRA, label); + startActivityForResult(intent, EditTextActivity.REQUEST_CODE); + } + + private void dispatchTakePictureIntent() { + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) { + Toast.makeText(this, "Device has no camera.", Toast.LENGTH_LONG).show(); + return; + } + Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (intent.resolveActivity(getPackageManager()) != null) { + mFile = Utility.createImageFilePath(); + if (mFile != null) { + Uri uri = FileProvider.getUriForFile(this, "om.sstvencoder", mFile); + intent.putExtra(MediaStore.EXTRA_OUTPUT, uri); + startActivityForResult(intent, REQUEST_IMAGE_CAPTURE); + } + } + } + + private void dispatchPickPictureIntent() { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + if (intent.resolveActivity(getPackageManager()) != null) + startActivityForResult(intent, REQUEST_PICK_IMAGE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case EditTextActivity.REQUEST_CODE: + Label label = null; + if (resultCode == RESULT_OK && data != null) + label = (Label) data.getSerializableExtra(EditTextActivity.EXTRA); + mCropView.editLabelEnd(label); + break; + case REQUEST_IMAGE_CAPTURE: + if (resultCode == RESULT_OK) { + Uri uri = Uri.fromFile(mFile); + if (loadImage(uri, true)) { + mSettings.setImageUri(uri); + addImageToGallery(uri); + } + } + break; + case REQUEST_PICK_IMAGE: + if (resultCode == RESULT_OK && data != null) { + Uri uri = data.getData(); + if (loadImage(uri, true)) + mSettings.setImageUri(uri); + } + break; + default: + super.onActivityResult(requestCode, resultCode, data); + break; + } + } + + private void addImageToGallery(Uri uri) { + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(uri); + sendBroadcast(intent); + } + + private void play() { + mEncoder.play(mCropView.getBitmap()); + } + + private void save() { + File file = Utility.createWaveFilePath(); + if (mEncoder.save(mCropView.getBitmap(), file)) { + ContentValues values = Utility.getWavContentValues(file); + Uri uri = MediaStore.Audio.Media.getContentUriForPath(file.getAbsolutePath()); + getContentResolver().insert(uri, values); + Toast.makeText(this, file.getName() + " saved.", Toast.LENGTH_SHORT).show(); + } + } + + @Override + protected void onPause() { + super.onPause(); + mSettings.save(); + mTextOverlayTemplate.save(mCropView.getLabels(), mSettings.getTextOverlayFile()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mEncoder.destroy(); + } +} \ No newline at end of file diff --git a/app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java b/app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java new file mode 100644 index 0000000..1a3d31d --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ModeInterfaces/IMode.java @@ -0,0 +1,24 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ModeInterfaces; + +public interface IMode { + void init(); + + boolean process(); + + void finish(boolean cancel); +} diff --git a/app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java b/app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java new file mode 100644 index 0000000..54363a8 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ModeInterfaces/IModeInfo.java @@ -0,0 +1,24 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ModeInterfaces; + +public interface IModeInfo { + int getModeName(); + + String getModeClassName(); + + ModeSize getModeSize(); +} diff --git a/app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java b/app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java new file mode 100644 index 0000000..fb71de5 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/ModeInterfaces/ModeSize.java @@ -0,0 +1,29 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.ModeInterfaces; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ModeSize { + int width(); + + int height(); +} \ No newline at end of file diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java new file mode 100644 index 0000000..2ef8780 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/NV21.java @@ -0,0 +1,60 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +class NV21 extends Yuv { + NV21(Bitmap bitmap) { + super(bitmap); + } + + protected void convertBitmapToYuv(Bitmap bitmap) { + mYuv = new byte[(3 * mWidth * mHeight) / 2]; + int pos = 0; + + for (int h = 0; h < mHeight; ++h) + for (int w = 0; w < mWidth; ++w) + mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h)); + + for (int h = 0; h < mHeight; h += 2) { + for (int w = 0; w < mWidth; w += 2) { + int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h)); + int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h)); + int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1)); + int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1)); + mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4); + int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h)); + int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h)); + int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1)); + int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1)); + mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4); + } + } + } + + public int getY(int x, int y) { + return 255 & mYuv[mWidth * y + x]; + } + + public int getU(int x, int y) { + return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x | 1)]; + } + + public int getV(int x, int y) { + return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + (x & ~1)]; + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java new file mode 100644 index 0000000..87019bc --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUV440P.java @@ -0,0 +1,61 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +class YUV440P extends Yuv { + YUV440P(Bitmap bitmap) { + super(bitmap); + } + + protected void convertBitmapToYuv(Bitmap bitmap) { + mYuv = new byte[2 * mWidth * mHeight]; + int pos = 0; + + for (int h = 0; h < mHeight; ++h) + for (int w = 0; w < mWidth; ++w) + mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h)); + + for (int h = 0; h < mHeight; h += 2) { + for (int w = 0; w < mWidth; ++w) { + int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h)); + int u1 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1)); + mYuv[pos++] = (byte) ((u0 + u1) / 2); + } + } + + for (int h = 0; h < mHeight; h += 2) { + for (int w = 0; w < mWidth; ++w) { + int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h)); + int v1 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1)); + mYuv[pos++] = (byte) ((v0 + v1) / 2); + } + } + } + + public int getY(int x, int y) { + return 255 & mYuv[mWidth * y + x]; + } + + public int getU(int x, int y) { + return 255 & mYuv[mWidth * mHeight + mWidth * (y >> 1) + x]; + } + + public int getV(int x, int y) { + return 255 & mYuv[((3 * mWidth * mHeight) >> 1) + mWidth * (y >> 1) + x]; + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java new file mode 100644 index 0000000..58971eb --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YUY2.java @@ -0,0 +1,53 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +class YUY2 extends Yuv { + YUY2(Bitmap bitmap) { + super(bitmap); + } + + protected void convertBitmapToYuv(Bitmap bitmap) { + mYuv = new byte[2 * mWidth * mHeight]; + + for (int pos = 0, h = 0; h < mHeight; ++h) { + for (int w = 0; w < mWidth; w += 2) { + mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h)); + int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h)); + int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h)); + mYuv[pos++] = (byte) ((u0 + u1) / 2); + mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w + 1, h)); + int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h)); + int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h)); + mYuv[pos++] = (byte) ((v0 + v1) / 2); + } + } + } + + public int getY(int x, int y) { + return 255 & mYuv[2 * mWidth * y + 2 * x]; + } + + public int getU(int x, int y) { + return 255 & mYuv[2 * mWidth * y + (((x & ~1) << 1) | 1)]; + } + + public int getV(int x, int y) { + return 255 & mYuv[2 * mWidth * y + ((x << 1) | 3)]; + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java new file mode 100644 index 0000000..b5b2a8e --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YV12.java @@ -0,0 +1,65 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +class YV12 extends Yuv { + YV12(Bitmap bitmap) { + super(bitmap); + } + + protected void convertBitmapToYuv(Bitmap bitmap) { + mYuv = new byte[(3 * mWidth * mHeight) / 2]; + int pos = 0; + + for (int h = 0; h < mHeight; ++h) + for (int w = 0; w < mWidth; ++w) + mYuv[pos++] = (byte) YuvConverter.convertToY(bitmap.getPixel(w, h)); + + for (int h = 0; h < mHeight; h += 2) { + for (int w = 0; w < mWidth; w += 2) { + int u0 = YuvConverter.convertToU(bitmap.getPixel(w, h)); + int u1 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h)); + int u2 = YuvConverter.convertToU(bitmap.getPixel(w, h + 1)); + int u3 = YuvConverter.convertToU(bitmap.getPixel(w + 1, h + 1)); + mYuv[pos++] = (byte) ((u0 + u1 + u2 + u3) / 4); + } + } + + for (int h = 0; h < mHeight; h += 2) { + for (int w = 0; w < mWidth; w += 2) { + int v0 = YuvConverter.convertToV(bitmap.getPixel(w, h)); + int v1 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h)); + int v2 = YuvConverter.convertToV(bitmap.getPixel(w, h + 1)); + int v3 = YuvConverter.convertToV(bitmap.getPixel(w + 1, h + 1)); + mYuv[pos++] = (byte) ((v0 + v1 + v2 + v3) / 4); + } + } + } + + public int getY(int x, int y) { + return 255 & mYuv[mWidth * y + x]; + } + + public int getU(int x, int y) { + return 255 & mYuv[mWidth * mHeight + (mWidth >> 1) * (y >> 1) + (x >> 1)]; + } + + public int getV(int x, int y) { + return 255 & mYuv[((5 * mWidth * mHeight) >> 2) + (mWidth >> 1) * (y >> 1) + (x >> 1)]; + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java new file mode 100644 index 0000000..16270ea --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/Yuv.java @@ -0,0 +1,46 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +public abstract class Yuv { + protected byte[] mYuv; + final int mWidth; + final int mHeight; + + Yuv(Bitmap bitmap) { + mWidth = bitmap.getWidth(); + mHeight = bitmap.getHeight(); + convertBitmapToYuv(bitmap); + } + + protected abstract void convertBitmapToYuv(Bitmap bitmap); + + public int getWidth() { + return mWidth; + } + + public int getHeight() { + return mHeight; + } + + public abstract int getY(int x, int y); + + public abstract int getU(int x, int y); + + public abstract int getV(int x, int y); +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java new file mode 100644 index 0000000..b6bc28b --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvConverter.java @@ -0,0 +1,45 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Color; + +final class YuvConverter { + static int convertToY(int color) { + double R = Color.red(color); + double G = Color.green(color); + double B = Color.blue(color); + return clamp(16.0 + (.003906 * ((65.738 * R) + (129.057 * G) + (25.064 * B)))); + } + + static int convertToU(int color) { + double R = Color.red(color); + double G = Color.green(color); + double B = Color.blue(color); + return clamp(128.0 + (.003906 * ((-37.945 * R) + (-74.494 * G) + (112.439 * B)))); + } + + static int convertToV(int color) { + double R = Color.red(color); + double G = Color.green(color); + double B = Color.blue(color); + return clamp(128.0 + (.003906 * ((112.439 * R) + (-94.154 * G) + (-18.285 * B)))); + } + + private static int clamp(double value) { + return value < 0.0 ? 0 : (value > 255.0 ? 255 : (int) value); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java new file mode 100644 index 0000000..27e9a5d --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvFactory.java @@ -0,0 +1,35 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.Bitmap; + +public final class YuvFactory { + public static Yuv createYuv(Bitmap bitmap, int format) { + switch (format) { + case YuvImageFormat.YV12: + return new YV12(bitmap); + case YuvImageFormat.NV21: + return new NV21(bitmap); + case YuvImageFormat.YUY2: + return new YUY2(bitmap); + case YuvImageFormat.YUV440P: + return new YUV440P(bitmap); + default: + throw new IllegalArgumentException("Only support YV12, NV21, YUY2 and YUV440P"); + } + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java new file mode 100644 index 0000000..8e8c615 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ImageFormats/YuvImageFormat.java @@ -0,0 +1,22 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes.ImageFormats; + +import android.graphics.ImageFormat; + +public class YuvImageFormat extends ImageFormat { + public static final int YUV440P = 0x50303434; +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Martin.java b/app/src/main/java/om/sstvencoder/Modes/Martin.java new file mode 100644 index 0000000..c606e15 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Martin.java @@ -0,0 +1,100 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import om.sstvencoder.Output.IOutput; + +abstract class Martin extends Mode { + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mSyncPorchSamples; + private final double mSyncPorchFrequency; + + private final int mSeparatorSamples; + private final double mSeparatorFrequency; + + protected double mColorScanDurationMs; + protected int mColorScanSamples; + + Martin(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mSyncPulseSamples = convertMsToSamples(4.862); + mSyncPulseFrequency = 1200.0; + + mSyncPorchSamples = convertMsToSamples(0.572); + mSyncPorchFrequency = 1500.0; + + mSeparatorSamples = convertMsToSamples(0.572); + mSeparatorFrequency = 1500.0; + } + + protected int getTransmissionSamples() { + int lineSamples = mSyncPulseSamples + mSyncPorchSamples + + 3 * (mSeparatorSamples + mColorScanSamples); + return mBitmap.getHeight() * lineSamples; + } + + protected void writeEncodedLine() { + addSyncPulse(); + addSyncPorch(); + addGreenScan(mLine); + addSeparator(); + addBlueScan(mLine); + addSeparator(); + addRedScan(mLine); + addSeparator(); + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addSyncPorch() { + for (int i = 0; i < mSyncPorchSamples; ++i) + setTone(mSyncPorchFrequency); + } + + private void addSeparator() { + for (int i = 0; i < mSeparatorSamples; ++i) + setTone(mSeparatorFrequency); + } + + private void addGreenScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.green(getColor(i, y))); + } + + private void addBlueScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.blue(getColor(i, y))); + } + + private void addRedScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.red(getColor(i, y))); + } + + private int getColor(int colorScanSample, int y) { + int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples; + return mBitmap.getPixel(x, y); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Martin1.java b/app/src/main/java/om/sstvencoder/Modes/Martin1.java new file mode 100644 index 0000000..652d5f1 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Martin1.java @@ -0,0 +1,33 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_martin1) +class Martin1 extends Martin { + Martin1(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 44; + mColorScanDurationMs = 146.432; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Martin2.java b/app/src/main/java/om/sstvencoder/Modes/Martin2.java new file mode 100644 index 0000000..0760baa --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Martin2.java @@ -0,0 +1,33 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_martin2) +class Martin2 extends Martin { + Martin2(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 40; + mColorScanDurationMs = 73.216; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Mode.java b/app/src/main/java/om/sstvencoder/Modes/Mode.java new file mode 100644 index 0000000..6fceef7 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Mode.java @@ -0,0 +1,133 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.IMode; +import om.sstvencoder.Output.IOutput; + +abstract class Mode implements IMode { + protected Bitmap mBitmap; + protected int mVISCode; + protected int mLine; + private IOutput mOutput; + private double mSampleRate; + private double mRunningIntegral; + + protected Mode(Bitmap bitmap, IOutput output) { + mOutput = output; + mSampleRate = mOutput.getSampleRate(); + mBitmap = bitmap; + } + + public void init() { + mRunningIntegral = 0.0; + mLine = 0; + mOutput.init(getTotalSamples()); + writeCalibrationHeader(); + } + + public boolean process() { + if (mLine >= mBitmap.getHeight()) + return false; + + writeEncodedLine(); + ++mLine; + return true; + } + + // Note that also Bitmap will be recycled here + public void finish(boolean cancel) { + mOutput.finish(cancel); + destroyBitmap(); + } + + private int getTotalSamples() { + return getHeaderSamples() + getTransmissionSamples(); + } + + private int getHeaderSamples() { + return 2 * convertMsToSamples(300.0) + + convertMsToSamples(10.0) + + 10 * convertMsToSamples(30.0); + } + + protected abstract int getTransmissionSamples(); + + private void writeCalibrationHeader() { + int leaderToneSamples = convertMsToSamples(300.0); + double leaderToneFrequency = 1900.0; + + int breakSamples = convertMsToSamples(10.0); + double breakFrequency = 1200.0; + + int visBitSamples = convertMsToSamples(30.0); + double visBitSSFrequency = 1200.0; + double[] visBitFrequency = new double[]{1300.0, 1100.0}; + + for (int i = 0; i < leaderToneSamples; ++i) + setTone(leaderToneFrequency); + + for (int i = 0; i < breakSamples; ++i) + setTone(breakFrequency); + + for (int i = 0; i < leaderToneSamples; ++i) + setTone(leaderToneFrequency); + + for (int i = 0; i < visBitSamples; ++i) + setTone(visBitSSFrequency); + + int parity = 0; + for (int pos = 0; pos < 7; ++pos) { + int bit = (mVISCode >> pos) & 1; + parity ^= bit; + for (int i = 0; i < visBitSamples; ++i) + setTone(visBitFrequency[bit]); + } + + for (int i = 0; i < visBitSamples; ++i) + setTone(visBitFrequency[parity]); + + for (int i = 0; i < visBitSamples; ++i) + setTone(visBitSSFrequency); + } + + protected abstract void writeEncodedLine(); + + protected int convertMsToSamples(double durationMs) { + return (int) Math.round(durationMs * mSampleRate / 1000.0); + } + + protected void setTone(double frequency) { + mRunningIntegral += 2.0 * frequency * Math.PI / mSampleRate; + mRunningIntegral %= 2.0 * Math.PI; + mOutput.write(Math.sin(mRunningIntegral)); + } + + protected void setColorTone(int color) { + double blackFrequency = 1500.0; + double whiteFrequency = 2300.0; + setTone(color * (whiteFrequency - blackFrequency) / 255.0 + blackFrequency); + } + + private void destroyBitmap() { + if (mBitmap != null && !mBitmap.isRecycled()) { + mBitmap.recycle(); + mBitmap = null; + } + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ModeDescription.java b/app/src/main/java/om/sstvencoder/Modes/ModeDescription.java new file mode 100644 index 0000000..b0d9e38 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ModeDescription.java @@ -0,0 +1,27 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@interface ModeDescription { + int name(); +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ModeFactory.java b/app/src/main/java/om/sstvencoder/Modes/ModeFactory.java new file mode 100644 index 0000000..fa1e489 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ModeFactory.java @@ -0,0 +1,74 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import java.lang.reflect.Constructor; + +import om.sstvencoder.ModeInterfaces.IMode; +import om.sstvencoder.ModeInterfaces.IModeInfo; +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; + +public final class ModeFactory { + public static Class getDefaultMode() { + return Robot36.class; + } + + public static IModeInfo[] getModeInfoList() { + return new IModeInfo[]{ + new ModeInfo(Martin1.class), new ModeInfo(Martin2.class), + new ModeInfo(PD50.class), new ModeInfo(PD90.class), new ModeInfo(PD120.class), + new ModeInfo(PD160.class), new ModeInfo(PD180.class), + new ModeInfo(PD240.class), new ModeInfo(PD290.class), + new ModeInfo(Scottie1.class), new ModeInfo(Scottie2.class), new ModeInfo(ScottieDX.class), + new ModeInfo(Robot36.class), new ModeInfo(Robot72.class), + new ModeInfo(Wraase.class) + }; + } + + public static IModeInfo getModeInfo(Class modeClass) { + if (!isModeClassValid(modeClass)) + return null; + + return new ModeInfo(modeClass); + } + + public static IMode CreateMode(Class modeClass, Bitmap bitmap, IOutput output) { + Mode mode = null; + + if (bitmap != null && output != null && isModeClassValid(modeClass)) { + ModeSize size = modeClass.getAnnotation(ModeSize.class); + + if (bitmap.getWidth() == size.width() && bitmap.getHeight() == size.height()) { + try { + Constructor constructor = modeClass.getConstructor(Bitmap.class, IOutput.class); + mode = (Mode) constructor.newInstance(bitmap, output); + } catch (Exception ignore) { + } + } + } + + return mode; + } + + private static boolean isModeClassValid(Class modeClass) { + return Mode.class.isAssignableFrom(modeClass) && + modeClass.isAnnotationPresent(ModeSize.class) && + modeClass.isAnnotationPresent(ModeDescription.class); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ModeInfo.java b/app/src/main/java/om/sstvencoder/Modes/ModeInfo.java new file mode 100644 index 0000000..0aeff01 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ModeInfo.java @@ -0,0 +1,39 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import om.sstvencoder.ModeInterfaces.IModeInfo; +import om.sstvencoder.ModeInterfaces.ModeSize; + +class ModeInfo implements IModeInfo { + private final Class mModeClass; + + ModeInfo(Class modeClass) { + mModeClass = modeClass; + } + + public int getModeName() { + return mModeClass.getAnnotation(ModeDescription.class).name(); + } + + public String getModeClassName() { + return mModeClass.getName(); + } + + public ModeSize getModeSize() { + return mModeClass.getAnnotation(ModeSize.class); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/PD.java b/app/src/main/java/om/sstvencoder/Modes/PD.java new file mode 100644 index 0000000..6a0b54e --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD.java @@ -0,0 +1,87 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.Modes.ImageFormats.Yuv; +import om.sstvencoder.Modes.ImageFormats.YuvFactory; +import om.sstvencoder.Modes.ImageFormats.YuvImageFormat; +import om.sstvencoder.Output.IOutput; + +abstract class PD extends Mode { + private final Yuv mYuv; + + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mPorchSamples; + private final double mPorchFrequency; + + protected double mColorScanDurationMs; + protected int mColorScanSamples; + + PD(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUV440P); + + mSyncPulseSamples = convertMsToSamples(20.0); + mSyncPulseFrequency = 1200.0; + + mPorchSamples = convertMsToSamples(2.08); + mPorchFrequency = 1500.0; + } + + protected int getTransmissionSamples() { + int lineSamples = mSyncPulseSamples + mPorchSamples + 4 * mColorScanSamples; + return mBitmap.getHeight() / 2 * lineSamples; + } + + protected void writeEncodedLine() { + addSyncPulse(); + addPorch(); + addYScan(mLine); + addVScan(mLine); + addUScan(mLine); + addYScan(++mLine); + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addPorch() { + for (int i = 0; i < mPorchSamples; ++i) + setTone(mPorchFrequency); + } + + private void addYScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(mYuv.getY((i * mYuv.getWidth()) / mColorScanSamples, y)); + } + + private void addUScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(mYuv.getU((i * mYuv.getWidth()) / mColorScanSamples, y)); + } + + private void addVScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(mYuv.getV((i * mYuv.getWidth()) / mColorScanSamples, y)); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/PD120.java b/app/src/main/java/om/sstvencoder/Modes/PD120.java new file mode 100644 index 0000000..982f605 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD120.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 640, height = 496) +@ModeDescription(name = R.string.action_pd120) +class PD120 extends PD { + PD120(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 95; + mColorScanDurationMs = 121.6; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD160.java b/app/src/main/java/om/sstvencoder/Modes/PD160.java new file mode 100644 index 0000000..aff7a0d --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD160.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 512, height = 400) +@ModeDescription(name = R.string.action_pd160) +class PD160 extends PD { + PD160(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 98; + mColorScanDurationMs = 195.584; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD180.java b/app/src/main/java/om/sstvencoder/Modes/PD180.java new file mode 100644 index 0000000..d03380c --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD180.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 640, height = 496) +@ModeDescription(name = R.string.action_pd180) +class PD180 extends PD { + PD180(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 96; + mColorScanDurationMs = 183.04; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD240.java b/app/src/main/java/om/sstvencoder/Modes/PD240.java new file mode 100644 index 0000000..a8ba5dd --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD240.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 640, height = 496) +@ModeDescription(name = R.string.action_pd240) +class PD240 extends PD { + PD240(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 97; + mColorScanDurationMs = 244.48; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD290.java b/app/src/main/java/om/sstvencoder/Modes/PD290.java new file mode 100644 index 0000000..b50ccee --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD290.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 800, height = 616) +@ModeDescription(name = R.string.action_pd290) +class PD290 extends PD { + PD290(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 94; + mColorScanDurationMs = 228.8; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD50.java b/app/src/main/java/om/sstvencoder/Modes/PD50.java new file mode 100644 index 0000000..f5f0f4e --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD50.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_pd50) +class PD50 extends PD { + PD50(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 93; + mColorScanDurationMs = 91.52; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/PD90.java b/app/src/main/java/om/sstvencoder/Modes/PD90.java new file mode 100644 index 0000000..5f746fe --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/PD90.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_pd90) +class PD90 extends PD { + PD90(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 99; + mColorScanDurationMs = 170.24; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/Robot36.java b/app/src/main/java/om/sstvencoder/Modes/Robot36.java new file mode 100644 index 0000000..9945e50 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Robot36.java @@ -0,0 +1,129 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.Modes.ImageFormats.Yuv; +import om.sstvencoder.Modes.ImageFormats.YuvFactory; +import om.sstvencoder.Modes.ImageFormats.YuvImageFormat; +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 240) +@ModeDescription(name = R.string.action_robot36) +class Robot36 extends Mode { + private final Yuv mYuv; + + private final int mLumaScanSamples; + private final int mChrominanceScanSamples; + + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mSyncPorchSamples; + private final double mSyncPorchFrequency; + + private final int mPorchSamples; + private final double mPorchFrequency; + + private final int mSeparatorSamples; + private final double mEvenSeparatorFrequency; + private final double mOddSeparatorFrequency; + + Robot36(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.NV21); + mVISCode = 8; + + mLumaScanSamples = convertMsToSamples(88.0); + mChrominanceScanSamples = convertMsToSamples(44.0); + + mSyncPulseSamples = convertMsToSamples(9.0); + mSyncPulseFrequency = 1200.0; + + mSyncPorchSamples = convertMsToSamples(3.0); + mSyncPorchFrequency = 1500.0; + + mPorchSamples = convertMsToSamples(1.5); + mPorchFrequency = 1900.0; + + mSeparatorSamples = convertMsToSamples(4.5); + mEvenSeparatorFrequency = 1500.0; + mOddSeparatorFrequency = 2300.0; + } + + protected int getTransmissionSamples() { + int lineSamples = mSyncPulseSamples + mSyncPorchSamples + + mLumaScanSamples + mSeparatorSamples + + mPorchSamples + mChrominanceScanSamples; + return mBitmap.getHeight() * lineSamples; + } + + protected void writeEncodedLine() { + addSyncPulse(); + addSyncPorch(); + addYScan(mLine); + + if (mLine % 2 == 0) { + addSeparator(mEvenSeparatorFrequency); + addPorch(); + addVScan(mLine); + } else { + addSeparator(mOddSeparatorFrequency); + addPorch(); + addUScan(mLine); + } + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addSyncPorch() { + for (int i = 0; i < mSyncPorchSamples; ++i) + setTone(mSyncPorchFrequency); + } + + private void addSeparator(double separatorFrequency) { + for (int i = 0; i < mSeparatorSamples; ++i) + setTone(separatorFrequency); + } + + private void addPorch() { + for (int i = 0; i < mPorchSamples; ++i) + setTone(mPorchFrequency); + } + + private void addYScan(int y) { + for (int i = 0; i < mLumaScanSamples; ++i) + setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y)); + } + + private void addUScan(int y) { + for (int i = 0; i < mChrominanceScanSamples; ++i) + setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y)); + } + + private void addVScan(int y) { + for (int i = 0; i < mChrominanceScanSamples; ++i) + setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y)); + } +} + diff --git a/app/src/main/java/om/sstvencoder/Modes/Robot72.java b/app/src/main/java/om/sstvencoder/Modes/Robot72.java new file mode 100644 index 0000000..8e02045 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Robot72.java @@ -0,0 +1,123 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.Modes.ImageFormats.Yuv; +import om.sstvencoder.Modes.ImageFormats.YuvFactory; +import om.sstvencoder.Modes.ImageFormats.YuvImageFormat; +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 240) +@ModeDescription(name = R.string.action_robot72) +class Robot72 extends Mode { + private final Yuv mYuv; + + private final int mLumaScanSamples; + private final int mChrominanceScanSamples; + + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mSyncPorchSamples; + private final double mSyncPorchFrequency; + + private final int mPorchSamples; + private final double mPorchFrequency; + + private final int mSeparatorSamples; + private final double mFirstSeparatorFrequency; + private final double mSecondSeparatorFrequency; + + Robot72(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mYuv = YuvFactory.createYuv(mBitmap, YuvImageFormat.YUY2); + mVISCode = 12; + + mLumaScanSamples = convertMsToSamples(138.0); + mChrominanceScanSamples = convertMsToSamples(69.0); + + mSyncPulseSamples = convertMsToSamples(9.0); + mSyncPulseFrequency = 1200.0; + + mSyncPorchSamples = convertMsToSamples(3.0); + mSyncPorchFrequency = 1500.0; + + mPorchSamples = convertMsToSamples(1.5); + mPorchFrequency = 1900.0; + + mSeparatorSamples = convertMsToSamples(4.5); + mFirstSeparatorFrequency = 1500.0; + mSecondSeparatorFrequency = 2300.0; + } + + protected int getTransmissionSamples() { + int lineSamples = mSyncPulseSamples + mSyncPorchSamples + mLumaScanSamples + + 2 * (mSeparatorSamples + mPorchSamples + mChrominanceScanSamples); + return mBitmap.getHeight() * lineSamples; + } + + protected void writeEncodedLine() { + addSyncPulse(); + addSyncPorch(); + addYScan(mLine); + addSeparator(mFirstSeparatorFrequency); + addPorch(); + addVScan(mLine); + addSeparator(mSecondSeparatorFrequency); + addPorch(); + addUScan(mLine); + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addSyncPorch() { + for (int i = 0; i < mSyncPorchSamples; ++i) + setTone(mSyncPorchFrequency); + } + + private void addSeparator(double separatorFrequency) { + for (int i = 0; i < mSeparatorSamples; ++i) + setTone(separatorFrequency); + } + + private void addPorch() { + for (int i = 0; i < mPorchSamples; ++i) + setTone(mPorchFrequency); + } + + private void addYScan(int y) { + for (int i = 0; i < mLumaScanSamples; ++i) + setColorTone(mYuv.getY((i * mYuv.getWidth()) / mLumaScanSamples, y)); + } + + private void addUScan(int y) { + for (int i = 0; i < mChrominanceScanSamples; ++i) + setColorTone(mYuv.getU((i * mYuv.getWidth()) / mChrominanceScanSamples, y)); + } + + private void addVScan(int y) { + for (int i = 0; i < mChrominanceScanSamples; ++i) + setColorTone(mYuv.getV((i * mYuv.getWidth()) / mChrominanceScanSamples, y)); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Scottie.java b/app/src/main/java/om/sstvencoder/Modes/Scottie.java new file mode 100644 index 0000000..25be473 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Scottie.java @@ -0,0 +1,102 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import om.sstvencoder.Output.IOutput; + +abstract class Scottie extends Mode { + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mSyncPorchSamples; + private final double mSyncPorchFrequency; + + private final int mSeparatorSamples; + private final double mSeparatorFrequency; + + protected double mColorScanDurationMs; + protected int mColorScanSamples; + + Scottie(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mSyncPulseSamples = convertMsToSamples(9.0); + mSyncPulseFrequency = 1200.0; + + mSyncPorchSamples = convertMsToSamples(1.5); + mSyncPorchFrequency = 1500.0; + + mSeparatorSamples = convertMsToSamples(1.5); + mSeparatorFrequency = 1500.0; + } + + protected int getTransmissionSamples() { + int lineSamples = 2 * mSeparatorSamples + 3 * mColorScanSamples + + mSyncPulseSamples + mSyncPorchSamples; + return mSyncPulseSamples + mBitmap.getHeight() * lineSamples; + } + + protected void writeEncodedLine() { + if (mLine == 0) + addSyncPulse(); + + addSeparator(); + addGreenScan(mLine); + addSeparator(); + addBlueScan(mLine); + addSyncPulse(); + addSyncPorch(); + addRedScan(mLine); + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addSyncPorch() { + for (int i = 0; i < mSyncPorchSamples; ++i) + setTone(mSyncPorchFrequency); + } + + private void addSeparator() { + for (int i = 0; i < mSeparatorSamples; ++i) + setTone(mSeparatorFrequency); + } + + private void addGreenScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.green(getColor(i, y))); + } + + private void addBlueScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.blue(getColor(i, y))); + } + + private void addRedScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.red(getColor(i, y))); + } + + private int getColor(int colorScanSample, int y) { + int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples; + return mBitmap.getPixel(x, y); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Scottie1.java b/app/src/main/java/om/sstvencoder/Modes/Scottie1.java new file mode 100644 index 0000000..086ce97 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Scottie1.java @@ -0,0 +1,33 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_scottie1) +class Scottie1 extends Scottie { + Scottie1(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 60; + mColorScanDurationMs = 138.24; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Scottie2.java b/app/src/main/java/om/sstvencoder/Modes/Scottie2.java new file mode 100644 index 0000000..4d05d19 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Scottie2.java @@ -0,0 +1,33 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_scottie2) +class Scottie2 extends Scottie { + Scottie2(Bitmap bitmap, IOutput output){ + super(bitmap, output); + mVISCode = 56; + mColorScanDurationMs = 88.064; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/ScottieDX.java b/app/src/main/java/om/sstvencoder/Modes/ScottieDX.java new file mode 100644 index 0000000..a2dd57f --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/ScottieDX.java @@ -0,0 +1,33 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_scottie_dx) +class ScottieDX extends Scottie { + ScottieDX(Bitmap bitmap, IOutput output) { + super(bitmap, output); + mVISCode = 76; + mColorScanDurationMs = 345.6; + mColorScanSamples = convertMsToSamples(mColorScanDurationMs); + } +} diff --git a/app/src/main/java/om/sstvencoder/Modes/Wraase.java b/app/src/main/java/om/sstvencoder/Modes/Wraase.java new file mode 100644 index 0000000..702ee65 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Modes/Wraase.java @@ -0,0 +1,91 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Modes; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import om.sstvencoder.ModeInterfaces.ModeSize; +import om.sstvencoder.Output.IOutput; +import om.sstvencoder.R; + +@ModeSize(width = 320, height = 256) +@ModeDescription(name = R.string.action_wraaseSC2_180) +class Wraase extends Mode { + private final int mSyncPulseSamples; + private final double mSyncPulseFrequency; + + private final int mPorchSamples; + private final double mPorchFrequency; + + private final int mColorScanSamples; + + Wraase(Bitmap bitmap, IOutput output) { + super(bitmap, output); + + mVISCode = 55; + mColorScanSamples = convertMsToSamples(235.0); + + mSyncPulseSamples = convertMsToSamples(5.5225); + mSyncPulseFrequency = 1200.0; + + mPorchSamples = convertMsToSamples(0.5); + mPorchFrequency = 1500.0; + } + + protected int getTransmissionSamples() { + int lineSamples = mSyncPulseSamples + mPorchSamples + 3 * mColorScanSamples; + return mBitmap.getHeight() * lineSamples; + } + + protected void writeEncodedLine() { + addSyncPulse(); + addPorch(); + addRedScan(mLine); + addGreenScan(mLine); + addBlueScan(mLine); + } + + private void addSyncPulse() { + for (int i = 0; i < mSyncPulseSamples; ++i) + setTone(mSyncPulseFrequency); + } + + private void addPorch() { + for (int i = 0; i < mPorchSamples; ++i) + setTone(mPorchFrequency); + } + + private void addRedScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.red(getColor(i, y))); + } + + private void addGreenScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.green(getColor(i, y))); + } + + private void addBlueScan(int y) { + for (int i = 0; i < mColorScanSamples; ++i) + setColorTone(Color.blue(getColor(i, y))); + } + + private int getColor(int colorScanSample, int y) { + int x = colorScanSample * mBitmap.getWidth() / mColorScanSamples; + return mBitmap.getPixel(x, y); + } +} diff --git a/app/src/main/java/om/sstvencoder/Output/AudioOutput.java b/app/src/main/java/om/sstvencoder/Output/AudioOutput.java new file mode 100644 index 0000000..68e41bc --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Output/AudioOutput.java @@ -0,0 +1,79 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Output; + +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioTrack; + +class AudioOutput implements IOutput { + private final double mSampleRate; + private short[] mAudioBuffer; + private AudioTrack mAudioTrack; + private int mBufferPos; + + AudioOutput(double sampleRate) { + mSampleRate = sampleRate; + mBufferPos = 0; + } + + @Override + public void init(int samples) { + mAudioBuffer = new short[(5 * (int) mSampleRate) / 2]; // 2.5 seconds of buffer + mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, + (int) mSampleRate, AudioFormat.CHANNEL_OUT_MONO, + AudioFormat.ENCODING_PCM_16BIT, mAudioBuffer.length * 2, + AudioTrack.MODE_STREAM); + mAudioTrack.play(); + } + + @Override + public double getSampleRate() { + return mSampleRate; + } + + @Override + public void write(double value) { + if (mBufferPos == mAudioBuffer.length) { + mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length); + mBufferPos = 0; + } + + mAudioBuffer[mBufferPos++] = (short) (value * Short.MAX_VALUE); + } + + @Override + public void finish(boolean cancel) { + if (mAudioTrack != null) { + if (!cancel) + drainBuffer(); + mAudioTrack.stop(); + mAudioTrack.release(); + mAudioTrack = null; + mAudioBuffer = null; + } + } + + private void drainBuffer() { + // The second run makes sure that the previous buffer indeed got played + for (int i = 0; i < 2; ++i) { + while (mBufferPos < mAudioBuffer.length) + mAudioBuffer[mBufferPos++] = 0; + mAudioTrack.write(mAudioBuffer, 0, mAudioBuffer.length); + mBufferPos = 0; + } + } +} diff --git a/app/src/main/java/om/sstvencoder/Output/IOutput.java b/app/src/main/java/om/sstvencoder/Output/IOutput.java new file mode 100644 index 0000000..5753ef4 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Output/IOutput.java @@ -0,0 +1,26 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Output; + +public interface IOutput { + double getSampleRate(); + + void init(int samples); + + void write(double value); + + void finish(boolean cancel); +} diff --git a/app/src/main/java/om/sstvencoder/Output/OutputFactory.java b/app/src/main/java/om/sstvencoder/Output/OutputFactory.java new file mode 100644 index 0000000..d5a25d9 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Output/OutputFactory.java @@ -0,0 +1,34 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Output; + +import java.io.File; + +public final class OutputFactory { + + public static IOutput createOutputForSending() { + double sampleRate = 44100.0; + return new AudioOutput(sampleRate); + } + + public static IOutput createOutputForSavingAsWave(File filePath) { + double sampleRate = 44100.0; + + if (filePath == null) + return null; + return new WaveFileOutput(filePath, sampleRate); + } +} diff --git a/app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java b/app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java new file mode 100644 index 0000000..9b4fdfd --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Output/WaveFileOutput.java @@ -0,0 +1,131 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.Output; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; + +class WaveFileOutput implements IOutput { + private final double mSampleRate; + private File mFile; + private BufferedOutputStream mOutputStream; + private int mSamples, mWrittenSamples; + + WaveFileOutput(File file, double sampleRate) { + mFile = file; + mSampleRate = sampleRate; + } + + public void init(int samples) { + int offset = (int) ((0.01 * mSampleRate) / 2.0); + mSamples = samples + 2 * offset; + mWrittenSamples = 0; + InitOutputStream(); + writeHeader(); + padWithZeros(offset); + } + + private void writeHeader() { + try { + int numChannels = 1; // mono + int bitsPerSample = Short.SIZE; + int blockAlign = numChannels * bitsPerSample / Byte.SIZE; + int subchunk2Size = mSamples * blockAlign; + + mOutputStream.write("RIFF".getBytes()); // ChunkID + mOutputStream.write(toLittleEndian(36 + subchunk2Size)); // ChunkSize + mOutputStream.write("WAVE".getBytes()); // Format + + mOutputStream.write("fmt ".getBytes()); // Subchunk1ID + mOutputStream.write(toLittleEndian(16)); // Subchunk1Size + mOutputStream.write(toLittleEndian((short) 1)); // AudioFormat + mOutputStream.write(toLittleEndian((short) numChannels)); // NumChannels + mOutputStream.write(toLittleEndian((int) mSampleRate)); // SampleRate + mOutputStream.write(toLittleEndian((int) mSampleRate * blockAlign)); // ByteRate + mOutputStream.write(toLittleEndian((short) blockAlign)); // BlockAlign + mOutputStream.write(toLittleEndian((short) bitsPerSample)); // BitsPerSample + + mOutputStream.write("data".getBytes()); // Subchunk2ID + mOutputStream.write(toLittleEndian(subchunk2Size)); // Subchunk2Size + } catch (Exception ignore) { + } + } + + private void InitOutputStream() { + try { + mOutputStream = new BufferedOutputStream(new FileOutputStream(mFile)); + } catch (Exception ignore) { + } + } + + @Override + public double getSampleRate() { + return mSampleRate; + } + + @Override + public void write(double value) { + short tmp = (short) (value * Short.MAX_VALUE); + ++mWrittenSamples; + try { + mOutputStream.write(toLittleEndian(tmp)); + } catch (Exception ignore) { + } + } + + @Override + public void finish(boolean cancel) { + if (!cancel) + padWithZeros(mSamples); + + try { + mOutputStream.close(); + mOutputStream = null; + } catch (Exception ignore) { + } + + if (mFile != null) { + if (cancel) + mFile.delete(); + mFile = null; + } + } + + private void padWithZeros(int count) { + try { + while (mWrittenSamples++ < count) + mOutputStream.write(toLittleEndian((short) 0)); + } catch (Exception ignore) { + } + } + + private byte[] toLittleEndian(int value) { + byte[] buffer = new byte[4]; + buffer[0] = (byte) (value & 255); + buffer[1] = (byte) ((value >> 8) & 255); + buffer[2] = (byte) ((value >> 16) & 255); + buffer[3] = (byte) ((value >> 24) & 255); + return buffer; + } + + private byte[] toLittleEndian(short value) { + byte[] buffer = new byte[2]; + buffer[0] = (byte) (value & 255); + buffer[1] = (byte) ((value >> 8) & 255); + return buffer; + } +} diff --git a/app/src/main/java/om/sstvencoder/Settings.java b/app/src/main/java/om/sstvencoder/Settings.java new file mode 100644 index 0000000..d5a7488 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Settings.java @@ -0,0 +1,147 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.content.Context; +import android.net.Uri; +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.JsonWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +class Settings { + private final static String IMAGE_URI = "image_uri"; + private final static String TEXT_OVERLAY_PATH = "text_overlay_path"; + private final String mFileName; + private Context mContext; + private String mImageUri; + private String mTextOverlayPath; + + private Settings() { + mFileName = "settings.json"; + } + + Settings(Context context) { + this(); + mContext = context; + mImageUri = ""; + } + + boolean load() { + boolean loaded = false; + JsonReader reader = null; + try { + InputStream in = new FileInputStream(getFile()); + reader = new JsonReader(new InputStreamReader(in, "UTF-8")); + readImageUri(reader); + readTextOverlayPath(reader); + loaded = true; + } catch (Exception ignore) { + } finally { + if (reader != null) { + try { + reader.close(); + } catch (Exception ignore) { + } + } + } + return loaded; + } + + boolean save() { + boolean saved = false; + JsonWriter writer = null; + try { + OutputStream out = new FileOutputStream(getFile()); + writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8")); + writer.setIndent(" "); + writeImageUri(writer); + writeTextOverlayPath(writer); + saved = true; + } catch (Exception ignore) { + } finally { + if (writer != null) { + try { + writer.close(); + } catch (Exception ignore) { + } + } + } + return saved; + } + + void setImageUri(Uri uri) { + mImageUri = uri == null ? "" : uri.toString(); + } + + Uri getImageUri() { + if ("".equals(mImageUri)) + return null; + return Uri.parse(mImageUri); + } + + File getTextOverlayFile() { + if (mTextOverlayPath == null) + mTextOverlayPath = new File(mContext.getFilesDir(), "text_overlay.json").getPath(); + return new File(mTextOverlayPath); + } + + private File getFile() { + return new File(mContext.getFilesDir(), mFileName); + } + + private void writeImageUri(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(IMAGE_URI).value(mImageUri); + writer.endObject(); + } + + private void writeTextOverlayPath(JsonWriter writer) throws IOException { + writer.beginObject(); + writer.name(TEXT_OVERLAY_PATH).value(mTextOverlayPath); + writer.endObject(); + } + + private void readImageUri(JsonReader reader) throws IOException { + reader.beginObject(); + { + reader.nextName(); + if (reader.peek() == JsonToken.NULL) { + reader.nextNull(); + mImageUri = null; + } else + mImageUri = reader.nextString(); + } + reader.endObject(); + } + + private void readTextOverlayPath(JsonReader reader) throws IOException { + reader.beginObject(); + { + reader.nextName(); + mTextOverlayPath = reader.nextString(); + } + reader.endObject(); + } +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/IReader.java b/app/src/main/java/om/sstvencoder/TextOverlay/IReader.java new file mode 100644 index 0000000..cc9736a --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/IReader.java @@ -0,0 +1,40 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import java.io.IOException; + +public interface IReader { + void beginRootObject() throws IOException; + + void beginObject() throws IOException; + + void endObject() throws IOException; + + void beginArray() throws IOException; + + void endArray() throws IOException; + + boolean hasNext() throws IOException; + + String readString() throws IOException; + + boolean readBoolean() throws IOException; + + float readFloat() throws IOException; + + int readInt() throws IOException; +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java b/app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java new file mode 100644 index 0000000..2fcebb8 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/IWriter.java @@ -0,0 +1,40 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import android.support.annotation.NonNull; + +import java.io.IOException; + +public interface IWriter { + void beginRootObject() throws IOException; + + void beginObject(@NonNull String name) throws IOException; + + void endObject() throws IOException; + + void beginArray(@NonNull String name) throws IOException; + + void endArray() throws IOException; + + void write(@NonNull String name, String value) throws IOException; + + void write(@NonNull String name, boolean value) throws IOException; + + void write(@NonNull String name, float value) throws IOException; + + void write(@NonNull String name, int value) throws IOException; +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/Label.java b/app/src/main/java/om/sstvencoder/TextOverlay/Label.java new file mode 100644 index 0000000..94410eb --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/Label.java @@ -0,0 +1,96 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import android.graphics.Color; + +import java.io.Serializable; + +public class Label implements Serializable { + private String mText; + private float mTextSize; + private String mFamilyName; + private boolean mBold, mItalic; + private int mForeColor, mBackColor; + + public Label() { + mText = ""; + mTextSize = 2.0f; + mFamilyName = null; + mBold = true; + mItalic = false; + mForeColor = Color.BLACK; + mBackColor = Color.TRANSPARENT; + } + + public String getText() { + return mText; + } + + public void setText(String text) { + if (text != null) + mText = text; + } + + public float getTextSize() { + return mTextSize; + } + + public void setTextSize(float textSize) { + if (textSize > 0f) + mTextSize = textSize; + } + + public String getFamilyName() { + return mFamilyName; + } + + public void setFamilyName(String familyName) { + mFamilyName = familyName; + } + + public boolean getBold() { + return mBold; + } + + public void setBold(boolean bold) { + mBold = bold; + } + + public boolean getItalic() { + return mItalic; + } + + public void setItalic(boolean italic) { + mItalic = italic; + } + + public int getForeColor() { + return mForeColor; + } + + public void setForeColor(int color) { + mForeColor = color; + } + + public int getBackColor() { + return mBackColor; + } + + public void setBackColor(int color) { + mBackColor = color; + } +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java b/app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java new file mode 100644 index 0000000..bd68859 --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/LabelCollection.java @@ -0,0 +1,184 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import om.sstvencoder.Utility; + +public class LabelCollection { + private class Size { + private float mW, mH; + + Size(float w, float h) { + mW = w; + mH = h; + } + + float width() { + return mW; + } + + float height() { + return mH; + } + } + + private final List mLabels; + private Size mScreenSize; + private float mTextSizeFactor; + private LabelContainer mActiveLabel, mEditLabel; + private float mPreviousX, mPreviousY; + + public LabelCollection() { + mLabels = new LinkedList<>(); + mPreviousX = 0f; + mPreviousY = 0f; + } + + public void update(float w, float h) { + if (mScreenSize != null) { + float x = (w - mScreenSize.width()) / 2f; + float y = (h - mScreenSize.height()) / 2f; + for (LabelContainer label : mLabels) + label.offset(x, y); + } + mScreenSize = new Size(w, h); + mTextSizeFactor = getTextSizeFactor(w, h); + for (LabelContainer label : mLabels) + label.update(mTextSizeFactor, w, h); + } + + private float getTextSizeFactor(float w, float h) { + Rect bounds = Utility.getEmbeddedRect((int) w, (int) h, 320, 240); + return 0.1f * bounds.height(); + } + + public void draw(Canvas canvas) { + for (LabelContainer label : mLabels) + label.draw(canvas); + if (mActiveLabel != null) + mActiveLabel.drawActive(canvas); + } + + public void draw(Canvas canvas, Rect src, Rect dst) { + for (LabelContainer label : mLabels) + label.draw(canvas, src, dst); + } + + public boolean moveLabelBegin(float x, float y) { + mActiveLabel = find(x, y); + if (mActiveLabel == null) + return false; + mLabels.remove(mActiveLabel); + mPreviousX = x; + mPreviousY = y; + return true; + } + + public void moveLabel(float x, float y) { + mActiveLabel.offset(x - mPreviousX, y - mPreviousY); + mActiveLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height()); + mPreviousX = x; + mPreviousY = y; + } + + public void moveLabelEnd() { + mLabels.add(mActiveLabel); + mActiveLabel = null; + mPreviousX = 0f; + mPreviousY = 0f; + } + + public Label editLabelBegin(float x, float y) { + mEditLabel = find(x, y); + if (mEditLabel == null) { + mEditLabel = new LabelContainer(new Label()); + mEditLabel.offset(x, y); + } + return mEditLabel.getContent(); + } + + public void editLabelEnd(Label label) { + if (label != null) { // not canceled + if ("".equals(label.getText().trim())) { + if (mLabels.contains(mEditLabel)) + mLabels.remove(mEditLabel); + } else { + if (!mLabels.contains(mEditLabel)) + mLabels.add(mEditLabel); + mEditLabel.setContent(label); + mEditLabel.update(mTextSizeFactor, mScreenSize.width(), mScreenSize.height()); + } + } + mEditLabel = null; + } + + private LabelContainer find(float x, float y) { + for (LabelContainer label : mLabels) { + if (label.contains(x, y)) + return label; + } + return null; + } + + private void add(LabelContainer label) { + if (mLabels.size() == 0) + mLabels.add(label); + else + mLabels.add(0, label); + } + + public void write(@NonNull IWriter writer) throws IOException { + writer.beginRootObject(); + { + writer.write("width", mScreenSize.width()); + writer.write("height", mScreenSize.height()); + writer.beginArray("labels"); + { + for (LabelContainer label : mLabels) + label.write(writer); + } + writer.endArray(); + } + writer.endObject(); + } + + public void read(@NonNull IReader reader) throws IOException { + reader.beginRootObject(); + { + float w = reader.readFloat(); + float h = reader.readFloat(); + reader.beginArray(); + { + while (reader.hasNext()) { + LabelContainer label = new LabelContainer(new Label()); + label.read(reader); + add(label); + } + } + reader.endArray(); + update(w, h); + } + reader.endObject(); + } +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java b/app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java new file mode 100644 index 0000000..7c7dcec --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/LabelContainer.java @@ -0,0 +1,117 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import android.graphics.Canvas; +import android.graphics.Rect; +import android.support.annotation.NonNull; + +import java.io.IOException; + +class LabelContainer { + private Label mLabel; + private LabelPainter mPainter; + private float mX, mY; // left-bottom corner + + LabelContainer(@NonNull Label label) { + mLabel = label; + mPainter = new LabelPainter(label); + mX = mY = 0f; + } + + boolean contains(float x, float y) { + return mPainter.getBounds().contains(x, y); + } + + void draw(Canvas canvas) { + mPainter.draw(canvas); + } + + void drawActive(Canvas canvas) { + mPainter.drawActive(canvas); + } + + void draw(Canvas canvas, Rect src, Rect dst) { + mPainter.draw(canvas, src, dst); + } + + void offset(float x, float y) { + mX += x; + mY += y; + } + + void update(float textSizeFactor, float screenW, float screenH) { + mPainter.update(textSizeFactor, screenW, screenH, mX, mY); + } + + Label getContent() { + return mLabel; + } + + void setContent(@NonNull Label label) { + mLabel = label; + mPainter.setLabel(label); + } + + void write(IWriter writer) throws IOException { + writer.beginRootObject(); + { + writer.write("position_x", mX); + writer.write("position_y", mY); + writer.beginObject("label"); + { + writeLabel(writer, mLabel); + } + writer.endObject(); + } + writer.endObject(); + } + + void read(IReader reader) throws IOException { + reader.beginRootObject(); + { + mX = reader.readFloat(); + mY = reader.readFloat(); + reader.beginObject(); + { + readLabel(reader, mLabel); + } + reader.endObject(); + } + reader.endObject(); + } + + private void writeLabel(IWriter writer, Label label) throws IOException { + writer.write("text", label.getText()); + writer.write("text_size", label.getTextSize()); + writer.write("family_name", label.getFamilyName()); + writer.write("bold", label.getBold()); + writer.write("italic", label.getItalic()); + writer.write("fore_color", label.getForeColor()); + writer.write("back_color", label.getBackColor()); + } + + private void readLabel(IReader reader, Label label) throws IOException { + label.setText(reader.readString()); + label.setTextSize(reader.readFloat()); + label.setFamilyName(reader.readString()); + label.setBold(reader.readBoolean()); + label.setItalic(reader.readBoolean()); + label.setForeColor(reader.readInt()); + label.setBackColor(reader.readInt()); + } +} + diff --git a/app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java b/app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java new file mode 100644 index 0000000..a941bbb --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlay/LabelPainter.java @@ -0,0 +1,303 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder.TextOverlay; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.support.annotation.NonNull; + +class LabelPainter { + private interface IDrawer { + void draw(Canvas canvas); + + void drawShadow(Canvas canvas); + + void draw(Canvas canvas, Rect src, Rect dst); + + RectF getBounds(); + } + + private class InDrawer implements IDrawer { + private float mSizeFactor; + private float mX, mY; + + private InDrawer(float sizeFactor, float x, float y) { + mSizeFactor = sizeFactor; + mX = x; + mY = y; + setPaintSettings(mSizeFactor); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawText(mLabel.getText(), mX, mY, mPaint); + } + + @Override + public void drawShadow(Canvas canvas) { + RectF bounds = new RectF(getBounds()); + float rx = 10f; + float ry = 10f; + + mPaint.setColor(Color.LTGRAY); + mPaint.setAlpha(100); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawRoundRect(bounds, rx, ry, mPaint); + + mPaint.setAlpha(255); + mPaint.setStyle(Paint.Style.STROKE); + + mPaint.setColor(Color.RED); + bounds.inset(-5.0f, -5.0f); + canvas.drawRoundRect(bounds, rx, ry, mPaint); + + mPaint.setColor(Color.GREEN); + bounds.inset(1.0f, 1.0f); + canvas.drawRoundRect(bounds, rx, ry, mPaint); + + mPaint.setColor(Color.BLUE); + bounds.inset(1.0f, 1.0f); + canvas.drawRoundRect(bounds, rx, ry, mPaint); + + setPaintSettings(mSizeFactor); + } + + @Override + public void draw(Canvas canvas, Rect src, Rect dst) { + float factor = (dst.height() / (float) src.height()); + float x = (mX - src.left) * factor; + float y = (mY - src.top) * factor; + setTextSize(factor * mSizeFactor); + canvas.drawText(mLabel.getText(), x, y, mPaint); + setTextSize(mSizeFactor); + } + + @Override + public RectF getBounds() { + RectF bounds = new RectF(getTextBounds()); + bounds.offset(mX, mY); + return bounds; + } + + private Rect getTextBounds() { + Rect bounds = new Rect(); + String text = mLabel.getText(); + mPaint.getTextBounds(text, 0, text.length(), bounds); + return bounds; + } + + private void setPaintSettings(float sizeFactor) { + mPaint.setAlpha(255); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(mLabel.getForeColor()); + mPaint.setTypeface(Typeface.create(mLabel.getFamilyName(), getTypeface())); + setTextSize(sizeFactor); + } + + private void setTextSize(float sizeFactor) { + mPaint.setTextSize(mLabel.getTextSize() * sizeFactor); + } + + private int getTypeface() { + int typeface = Typeface.NORMAL; + + if (mLabel.getBold() && mLabel.getItalic()) + typeface = Typeface.BOLD_ITALIC; + else { + if (mLabel.getBold()) + typeface = Typeface.BOLD; + else if (mLabel.getItalic()) + typeface = Typeface.ITALIC; + } + return typeface; + } + } + + private class OutDrawer implements IDrawer { + private Path mPath; + private RectF mBoundsOutside; + private float mMinSize, mX, mY; + + private OutDrawer(float min) { + mMinSize = min * 0.5f; + mPaint.setAlpha(255); + } + + private void leftOut(RectF rect, float screenH) { + mX = 0f; + mY = Math.min(Math.max(mMinSize, rect.top + rect.height() * 0.5f), screenH - mMinSize); + mPath = getLeftAlignedTriangle(mX, mY, mMinSize); + mBoundsOutside = new RectF(mX, mY - mMinSize, mX + mMinSize, mY + mMinSize); + } + + private void topOut(RectF rect, float screenW) { + mX = Math.min(Math.max(mMinSize, rect.left + rect.width() * 0.5f), screenW - mMinSize); + mY = 0f; + mPath = getTopAlignedTriangle(mX, mY, mMinSize); + mBoundsOutside = new RectF(mX - mMinSize, mY, mX + mMinSize * 0.5f, mY + mMinSize); + } + + private void rightOut(RectF rect, float screenW, float screenH) { + mX = screenW; + mY = Math.min(Math.max(mMinSize, rect.top + rect.height() * 0.5f), screenH - mMinSize); + mPath = getRightAlignedTriangle(mX, mY, mMinSize); + mBoundsOutside = new RectF(mX - mMinSize, mY - mMinSize, mX, mY + mMinSize); + } + + private void bottomOut(RectF rect, float screenW, float screenH) { + mX = Math.min(Math.max(mMinSize, rect.left + rect.width() * 0.5f), screenW - mMinSize); + mY = screenH; + mPath = getBottomAlignedTriangle(mX, mY, mMinSize); + mBoundsOutside = new RectF(mX - mMinSize, mY - mMinSize, mX + mMinSize, mY); + } + + private Path getLeftAlignedTriangle(float x, float y, float r) { + Path path = new Path(); + path.moveTo(x, y - r); + path.lineTo(x, y + r); + path.lineTo(x + r * 0.6f, y); + path.lineTo(x, y - r); + return path; + } + + private Path getTopAlignedTriangle(float x, float y, float r) { + Path path = new Path(); + path.moveTo(x - r, y); + path.lineTo(x, y + r * 0.6f); + path.lineTo(x + r, y); + path.lineTo(x - r, y); + return path; + } + + private Path getRightAlignedTriangle(float x, float y, float r) { + Path path = new Path(); + path.moveTo(x, y - r); + path.lineTo(x - r * 0.6f, y); + path.lineTo(x, y + r); + path.lineTo(x, y - r); + return path; + } + + private Path getBottomAlignedTriangle(float x, float y, float r) { + Path path = new Path(); + path.moveTo(x - r, y); + path.lineTo(x, y - r * 0.6f); + path.lineTo(x + r, y); + path.lineTo(x - r, y); + return path; + } + + @Override + public void draw(Canvas canvas) { + mPaint.setColor(mLabel.getForeColor()); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawPath(mPath, mPaint); + + mPaint.setColor(Color.WHITE); + mPaint.setStyle(Paint.Style.STROKE); + canvas.drawPath(mPath, mPaint); + } + + @Override + public void draw(Canvas canvas, Rect src, Rect dst) { + } + + @Override + public void drawShadow(Canvas canvas) { + float r = 2f * mMinSize; + + mPaint.setColor(Color.LTGRAY); + mPaint.setAlpha(100); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(mX, mY, r, mPaint); + + mPaint.setAlpha(255); + mPaint.setStyle(Paint.Style.STROKE); + + mPaint.setColor(Color.RED); + canvas.drawCircle(mX, mY, r + 1f, mPaint); + mPaint.setColor(Color.GREEN); + canvas.drawCircle(mX, mY, r, mPaint); + mPaint.setColor(Color.BLUE); + canvas.drawCircle(mX, mY, r - 1f, mPaint); + } + + @Override + public RectF getBounds() { + return mBoundsOutside; + } + } + + private final Paint mPaint; + private Label mLabel; + private IDrawer mDrawer; + + LabelPainter(@NonNull Label label) { + mLabel = label; + mPaint = new Paint(); + mPaint.setAntiAlias(true); + } + + void draw(Canvas canvas) { + mDrawer.draw(canvas); + } + + void drawActive(Canvas canvas) { + mDrawer.drawShadow(canvas); + mDrawer.draw(canvas); + } + + void draw(Canvas canvas, Rect src, Rect dst) { + mDrawer.draw(canvas, src, dst); + } + + RectF getBounds() { + return mDrawer.getBounds(); + } + + void setLabel(@NonNull Label label) { + mLabel = label; + } + + void update(float sizeFactor, float screenW, float screenH, float x, float y) { + InDrawer inDrawer = new InDrawer(sizeFactor, x, y); + + RectF rect = inDrawer.getBounds(); + float minSize = 1.5f * sizeFactor; + + OutDrawer outDrawer = null; + if (rect.right < minSize) { // left out + outDrawer = new OutDrawer(minSize); + outDrawer.leftOut(rect, screenH); + } else if (rect.bottom < minSize) {// top out + outDrawer = new OutDrawer(minSize); + outDrawer.topOut(rect, screenW); + } else if (rect.left > (screenW - minSize)) { // right out + outDrawer = new OutDrawer(minSize); + outDrawer.rightOut(rect, screenW, screenH); + } else if (rect.top > (screenH - minSize)) { // bottom out + outDrawer = new OutDrawer(minSize); + outDrawer.bottomOut(rect, screenW, screenH); + } + + mDrawer = outDrawer == null ? inDrawer : outDrawer; + } +} diff --git a/app/src/main/java/om/sstvencoder/TextOverlayTemplate.java b/app/src/main/java/om/sstvencoder/TextOverlayTemplate.java new file mode 100644 index 0000000..060c40b --- /dev/null +++ b/app/src/main/java/om/sstvencoder/TextOverlayTemplate.java @@ -0,0 +1,200 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.support.annotation.NonNull; +import android.util.JsonReader; +import android.util.JsonToken; +import android.util.JsonWriter; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +import om.sstvencoder.TextOverlay.IReader; +import om.sstvencoder.TextOverlay.IWriter; +import om.sstvencoder.TextOverlay.LabelCollection; + +class TextOverlayTemplate { + private class LabelCollectionWriter implements IWriter { + private JsonWriter mWriter; + + private LabelCollectionWriter(@NonNull JsonWriter writer) { + mWriter = writer; + } + + @Override + public void beginRootObject() throws IOException { + mWriter.beginObject(); + } + + @Override + public void beginObject(@NonNull String name) throws IOException { + mWriter.name(name); + mWriter.beginObject(); + } + + @Override + public void endObject() throws IOException { + mWriter.endObject(); + } + + @Override + public void beginArray(@NonNull String name) throws IOException { + mWriter.name(name); + mWriter.beginArray(); + } + + @Override + public void endArray() throws IOException { + mWriter.endArray(); + } + + @Override + public void write(@NonNull String name, String value) throws IOException { + mWriter.name(name).value(value); + } + + @Override + public void write(@NonNull String name, boolean value) throws IOException { + mWriter.name(name).value(value); + } + + @Override + public void write(@NonNull String name, float value) throws IOException { + mWriter.name(name).value(value); + } + + @Override + public void write(@NonNull String name, int value) throws IOException { + mWriter.name(name).value(value); + } + } + + private class LabelCollectionReader implements IReader { + private JsonReader mReader; + + private LabelCollectionReader(@NonNull JsonReader reader) { + mReader = reader; + } + + @Override + public void beginRootObject() throws IOException { + mReader.beginObject(); + } + + @Override + public void beginObject() throws IOException { + mReader.nextName(); + mReader.beginObject(); + } + + @Override + public void endObject() throws IOException { + mReader.endObject(); + } + + @Override + public void beginArray() throws IOException { + mReader.nextName(); + mReader.beginArray(); + } + + @Override + public void endArray() throws IOException { + mReader.endArray(); + } + + @Override + public boolean hasNext() throws IOException { + return mReader.hasNext(); + } + + @Override + public String readString() throws IOException { + mReader.nextName(); + if (mReader.peek() == JsonToken.NULL) { + mReader.nextNull(); + return null; + } + return mReader.nextString(); + } + + @Override + public boolean readBoolean() throws IOException { + mReader.nextName(); + return mReader.nextBoolean(); + } + + @Override + public float readFloat() throws IOException { + mReader.nextName(); + return Float.valueOf(mReader.nextString()); + } + + @Override + public int readInt() throws IOException { + mReader.nextName(); + return mReader.nextInt(); + } + } + + boolean load(@NonNull LabelCollection labels, File file) { + boolean loaded = false; + JsonReader jsonReader = null; + try { + InputStream in = new FileInputStream(file); + jsonReader = new JsonReader(new InputStreamReader(in, "UTF-8")); + labels.read(new LabelCollectionReader(jsonReader)); + loaded = true; + } catch (Exception ignore) { + } finally { + if (jsonReader != null) { + try { + jsonReader.close(); + } catch (Exception ignore) { + } + } + } + return loaded; + } + + boolean save(@NonNull LabelCollection labels, File file) { + boolean saved = false; + JsonWriter jsonWriter = null; + try { + OutputStream out = new FileOutputStream(file); + jsonWriter = new JsonWriter(new OutputStreamWriter(out, "UTF-8")); + jsonWriter.setIndent(" "); + labels.write(new LabelCollectionWriter(jsonWriter)); + saved = true; + } catch (Exception ignore) { + } finally { + if (jsonWriter != null) { + try { + jsonWriter.close(); + } catch (Exception ignore) { + } + } + } + return saved; + } +} diff --git a/app/src/main/java/om/sstvencoder/Utility.java b/app/src/main/java/om/sstvencoder/Utility.java new file mode 100644 index 0000000..0d4dc2c --- /dev/null +++ b/app/src/main/java/om/sstvencoder/Utility.java @@ -0,0 +1,113 @@ +/* +Copyright 2017 Olga Miller + +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 om.sstvencoder; + +import android.content.ContentValues; +import android.content.Intent; +import android.graphics.Rect; +import android.media.ExifInterface; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.annotation.NonNull; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public final class Utility { + @NonNull + public static Rect getEmbeddedRect(int w, int h, int iw, int ih) { + Rect rect; + + int ow = (9 * w) / 10; + int oh = (9 * h) / 10; + + if (iw * oh < ow * ih) { + rect = new android.graphics.Rect(0, 0, (iw * oh) / ih, oh); + rect.offset((w - (iw * oh) / ih) / 2, (h - oh) / 2); + } else { + rect = new android.graphics.Rect(0, 0, ow, (ih * ow) / iw); + rect.offset((w - ow) / 2, (h - (ih * ow) / iw) / 2); + } + return rect; + } + + static String createMessage(Exception ex) { + String message = ex.getMessage() + "\n"; + for (StackTraceElement el : ex.getStackTrace()) + message += "\n" + el.toString(); + return message; + } + + @NonNull + static Intent createEmailIntent(final String subject, final String text) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/email"); + intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"olga.rgb@gmail.com"}); + intent.putExtra(Intent.EXTRA_SUBJECT, subject); + intent.putExtra(Intent.EXTRA_TEXT, text); + return intent; + } + + static int convertToDegrees(int exifOrientation) { + switch (exifOrientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + } + return 0; + } + + @NonNull + static ContentValues getWavContentValues(File file) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Audio.Media.ALBUM, "SSTV Encoder"); + values.put(MediaStore.Audio.Media.ARTIST, ""); + values.put(MediaStore.Audio.Media.DATA, file.toString()); + values.put(MediaStore.Audio.Media.IS_MUSIC, true); + values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/wav"); + values.put(MediaStore.Audio.Media.TITLE, file.getName()); + return values; + } + + static File createImageFilePath() { + File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); + if (!isExternalStorageWritable()) + return null; + return new File(dir, createFileName() + ".jpg"); + } + + static File createWaveFilePath() { + // sdcard/Music + File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); + if (!isExternalStorageWritable()) + return null; + return new File(dir, createFileName() + ".wav"); + } + + private static String createFileName() { + return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); + } + + private static boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } +} diff --git a/app/src/main/res/layout/activity_edit_text.xml b/app/src/main/res/layout/activity_edit_text.xml new file mode 100644 index 0000000..2d77cee --- /dev/null +++ b/app/src/main/res/layout/activity_edit_text.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..a5335da --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/menu/menu_edit_text.xml b/app/src/main/res/menu/menu_edit_text.xml new file mode 100644 index 0000000..c10caff --- /dev/null +++ b/app/src/main/res/menu/menu_edit_text.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml new file mode 100644 index 0000000..b0d3a4e --- /dev/null +++ b/app/src/main/res/menu/menu_main.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..56d39a081614b11a18dcf265c09b63cbbffeae0f GIT binary patch literal 8529 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/sym_keyboard_done_lxx_dark.png b/app/src/main/res/mipmap-mdpi/sym_keyboard_done_lxx_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..8a63c116a94c083c4e916c694d2fc9ecd5fed713 GIT binary patch literal 14643 zcmeI3Pi)&%9LJ5iNoz_^95!~SEKd`KEwP`x%XoW>*G5?~Z7?qH$8p|kDub5cPFo`2|Q&v`y!!Dy0O;2+7zr4w@nwsQJ z#PUMkOrw&PZCGfbFIJ*e9U{3Kg>GRRge z&2%(FXWhK=vQc%C9Oo8leVX@GF z-LbJx?CCR0hGWI}Y>1HJCv(TeTK>B!{v^c_2@Ykh2Pk8ksqf!4Spe!RHs4kBS zvJj2R;ds%5n9oD|w$vFzsk&RGnOc3s)WE(i^>ItX$DCXS8h7-MTB1jqZKblBjTh${ zShRTQdI#A8I^Eg2)lt^G1Id&v8@byp$!+bo)(O@+pj?l)Z6#T8kFu2F?gpeL_>R{0 zuv*ThP5ZwS=;sIa{xxbPr0=w${8XN*e-hg$IyKoslPh>l5I4G?veVO@JHfl>ep&0B z1;4d(a<-Q1_Q_1R$C-o|0v?p?O#t3G+uGWdef^?4q2;Ql9}f%o!;9{3(+a!0txG#w zVRyH+?R-hsoK(13t*aTh+%wld6Bh8}0ZNUn3(_FQ6c-T@6i{4{1~I0%h=`zo;(|1YF~vnh1O*fqq(O`+E+Qf* zptv9nVoY%n5kUdP1!)jtii?N{3Mei}gBVj>L_|1;_Dg?A$8Y-8@GE`4 zT{-juejSiivh#U{X&hvj*UvJ{Uk~y1cZR7746|{HVWjsNX0LJKwC$-S@X&1$lAMir_IlHfBx$Exxk%`dy|b^?2k)Ve%Soy;z8x^(9K=kw>K^f4Sr!C zeRFDf| zF!K(p{yBE;=U)a!pT1UIzv1k5K6(D!-!t(~kJmoD{^PA{4^At?1I*PEtLq!zebB%| OV{%ionX5-nz40%Au;Ld0 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..e72aa2ba0d9c53592a8fc26af1f3ed960eb8e6d0 GIT binary patch literal 9692 zcmV<2B_rC2P)WFU8GbZ8()Nlj2>E@cM*03ZNKL_t(|+U=crkX=W8 z??2sVz1z&4O&aa8EXkG^NtR_>-oO~gfFVUpFiRi_iHyNG6-*vbmF2;s0tE>K2oG!m zkKphgNhL9XfS2$njKNsmcVkPIELoDRZT7pKt-IeJeeRu+SIP2tsi&?wb!XR%DQi^(`L8}!LMM?MXzVH9nn$;)0<+=V(KHDE3+q&)Q z&gA>P&-{5q3=UK&7DF7zIXd~zGF;cikx&R7dMW{~>j01>$(y%s-FEdM128c$amG|` zc6{Gs-tYinc+~8_pe+}IEXx3J97(C@;W&1BC#Py3o}8RK4}wsV!@lSuqQw8tju}(AL{kSc>v{d@rYOccfUKH(^C)xf>KG+)8jBS zi)kqE@e;cSryvX?hGmxl}mf+q(Yx`avtgalrEg zo+t2qK^R(4Diw$Reuo7MT+TS7NME0W5U(9Tgn%STp7nkM-z7;DX__5#NC0%={2=g; z_{O$uiMuyA69mT*xUPK-0t-f=ASo0iKlw?W-d@Sjkjuh_9_O7`W@zZu4T#nnrLt#z zuj2=vD}QpI(>qIf#5XoNst#HuokL1HnNnKwC67EJx$CYL z^X7RhU+(k1_f=mtKqN_wG3HtC<%RCgoy{Bj+Sle|jKLUC3bM?QB!(!`v|5H*O))m6 z*u6XDo_pFIBiJ=l9Y8ujluCk~J5x4qj=B5pHj5WqR-Ace5h-68AcPP^QTs(NgjogP zlvBdo{(;t@wIRzuDMOkX;@BRomZs6rOin6BM>C#$GUdh_>$KaJDeX2mj%I2~5Qd78 z5y`%NDLZ#2Y~CF6i(fQZzrMhQ7nWaER&*x+xmN!x4#52RUKapsE2r6@ltC#ANRrHC zhNfQETyQ~!kr4~V0}r&h_uhykv0yZt7L4(6#eoAEd-h~(-=1*ST`e|jDDuWPmR}Y| z7-NoT_0Kv0<+7Y{vpfR~%Zd&&3~5Tv3{4annoUiura1ld0tXJ*N&eZ-8tmDV5k-b} z+t6reYBkNoM8^L889R5T+FvcE9wsLm%p&6|*FSpw(&|@pWevfci~0fe`j4r9@b-YJms>WU+`Wm2j%PI0FOt zix*S6;J5I7f!1_QRBx}LuTL{Hm_Aard1k*@w(2O!zL!!Bz%od5_S5v~iagYbNW?;*nivRFWt%hayF z9(Q06Z{Y&M)hAO}w;ruKS+k+1N7CPK7#wt%Ki}n;V|>=G4Y}u@HrHKOpW#mot(IYG z$~yM#+Y_$1V&ElVgVvgMyY-R)I;#L=5kIzd+9^K^Aj0#&ai&dF zsfc`{OuYFq+V|a$w{QW4lTW7n*0*&NHLE2_s#S-Bp^8UjD!5B@m*?37E`m6#Fo^;Yo!--J}qcTV{bedq| z1l?+(TP^zDeF@pvIO(1}L=Qbo>+ZWT5#Y4Z%_gE&LrqSQjgFG;-AlT27wv~1rm%W7 zJ@34vD`5nIq*Rjh_F6_PS>p4JZ;a7w>Vxu*IJVCElb;+qBoKrU)az4rF};LiRsrzl z4R-^7qw_cugR!~~W(IbyiI_GRk=f4rplLUcPS382<(2&D**VVb_9viu2ndzFQ&wSSdmKy;E=6V+;?mo^DjuAsPX9qC1HA$oK( zlmC9LHDghXZZ*-3CTen$Y;=rt?_Q#ZA7=2QA4N)Wkh)qdT8F)8k;e~zSi=~dhq?Cn zo4*-hB1%*@B5pUkY_%;8O`;!FSwkAr|YL*py_fa}?1Lfa- zJB}l}K!l;7P!RO>IsD^4PN20yYb%FnZTQx=#@V>BkLAmQXJosLMvX&gp?dWJ(A?Cr z_bot30j@xJAbg1o1yd!7tVrAe2XCH7cw$KL)B+~ZpbN+}#Z)RdeSPSmA>83%{Kbn2 zPgq6t@NXFZ-v73KB~3By2-9kz>UGrE80qfa^k4BO%&fi+EE|GM=HGfDZ~?@n;V z6$4!jOQ~#DCx7-6v7KordIOmkCtLv%q%RP@Km-z55Xh3GaZ3|-(8XKiQCJ;PI=hGn z0z{#JES7L86`Z~*?$8k4f&~OCR?xibE^0si2`bAlX^M^`OskDNkIzOPNNGGO4+eq_C zt&UWw;8d%)bLZ0BbQ@DY{1IxVFNqU$6rrZ3=zY(7^GS8{vM7A~(umfUkaaEC<0LrNs9I-~WD__rJd|tCIGt0uUS@<{l2vU^0VI77P;` zbfnQuP5JyXYAhq&lM($UqH%N6zTXDX)Tj;^V~T8VM)G(}@w6hnzuSZ3x>iT>gRY?G zRC}5D-v1%lwQD*H&tp!b6nz^nrCgR66)UjE~IinnfUR9$!n+3b&X0Cx^2Ny zQ;KX-k&P(g$7A~5UBz2mM06P82guNxr`ioS(!Ax@D6R8FU@Ra)Q2nEgl*xyhbksVN!&MMOTjvm{!?YaopDy%XLamr_DD>h zK@U2(b3MF;9{%!xcuUOaH^wlTw#14TfF(V?5gTwwd_nDU`~Y(*dJxP1xK&pMxwFyG0-&rde3~$#tk& z9Rn!9a~+(40fN)dq*8&QA<4334!{5VMZWg6CQ)RVnlkL&t9amnlnonvmM(p1jm9&U z@@1EO=zjq8_4PdC;xb@ZsgM%)M7e6}?f4%Q>)ODSjs!`K* zrc{lZt}&q|8CMgGW~1!S_VZ-=B-@h5+0?v^ZOP+Qyb6QiAkvq}JW+E>5~tU}o#PU& z3dr_lWc!shU5UX&2Hns^4@G#31H3tbHAkL@3<8{<3Mz?7w{8QKcLQV@rqLvP{keG# zAi4=#r6PIgp{%QZ3IS77h7B9M!(xFDf_7vR(r3lO2y?n z-x=fUUmwA73@cWIc;58QeRlunh)qmllJ0AyBO(*g)@_=)NkccOsTvb%f-yD5-gGa& zi5}sR=usR;vaqwm-v} z;}BP3;xx6DHrGY=Rf&J~Ys-M?Rt%`u309xf?OPSXig%kfrMaRma2$cwhS$B$KV%ln z8UR{rZn~+?r$4=i|M-td{^U=F_}jl-i04iB0(rvY*kBpQ2FJ1@Sk7_bahy_E%lVb_ z*-$!zRpBanoF2BuTS-hxViFRQ&^B!vx9pIG8+FZJ@VxBBWoL-54 zj8E&QnPsv!-~^8L+SJAalJr412>Y@)xlpSi#w)vsf9dL}S>Q?(6~)nTV!tQP*`wvQZw19%0xY zX0R}Ta0SvA7Kn-@SRT;4rDd6rS_e%6YOf+VC$udwLLeO1GNGr3^nnMbS%82jB0T%d zoB@_3LI{5O%Y;0A$h9QFdFLG(3tmzHF1u_epZ)A!mM#tW!WS0sfe#F_WXW?!LV*k@ zhyq1fq$rD&WQme2QE@7ioie?$mp-?jA$N#*-Z009$8l14629=+nrtP_S<%)JEz@F3 z)!30dL1ZG1FCCYIATf?25i!m(hj>$BL!8c$f_h33ZU_<5nU0Qp4^wZD?b;19dplcA z{F6@Xc5Z78{r!^r?z5BF+1oHVsad<$e|Z5I8%z1hR~B*Mg}wClzOW;DC`y=6fRI#O zA5VCA!owFnp$rN0aI`E-RAhy!Q)S2-VyVBBRpBZcy3X$ONm5c`oe-OZrfxE-#u&{; zIXOI$X1hVGB5*;30&l59e0w@Ae`pJWu4()=LGDd$`B8)+@vm>qv%)+{0}7{|f(WM@ zi}u~~O}HB2 zs;SOANpe@3OCR|JB_N9ecd2hB@mwF5)n3y39+;NR9S2jZ;h(y$%K)Vy2n6@s zljaeU!1JKlG@N|0cSHajR#q`Q{2=@Gtv=-QA8bdRiX;vKUkZF7C`n0AQ*wQ+&LYoa zO(EnBl`_8Z2}MXr7OBVzy-qLvZXa{qx%|t-zwku56J->c&PbgUje>^Zk1HQQ8bC{2 zWiW|H>D>jI|JKGdHF^xPP!atqAv`C5FhutB;>?|cf8q(mE0&XO-+^hi(4(WITOTKQ z^f!1bPoz|`op{TZISdU+CMIm7*Z8<#>(=auTGJ1k0blykem?&3xrco5zuv!}hSoH+ zp`kT3t*9x@q*hEQ&1k0Bo26_^67FlYscXYYg%Y0h@bC}`$OJ+avPdq%2*dVly9I%a z1RF7p=`q$6awf>U&=Uw7jy|ElfXJg7HKy^`<)#R`aX|zD$(?uQOEzzvRGQ$l_1(P< zX$rm%_uZFui~d0%s8rDfD>r%e7NaKICFfD_g(5R=ZDGE+4Ll`0|6`Z+q2u@#5eB131$F}ci{{iA#H&Hll zRld>%bLL3i{`Qa`|2QIw47HkO?_R~oi0w~(SpYZ`AJAD1=ax%oV@S0o(T29lnDSHJ z8hY$WQ?{fTH$@4lF~r6Y89~z+8pcr5hB2kNv{IxbLMsy>A@_MPF+XkpjD~42s$Kld z=cedp&I-@8VqTBLnPc#dcL~n+iEm5L4TBofWKU$Ye-;s*?;*=2OkWje$r6GyP9whi zUQE@JO=FnQnte*KBg?oy zN%{L)lZR7HC_;K=4|C)kj&YZAZg39k{ZoleY!fv#!uE7K|4{ojxHjUERf#*_Avn{w zA!%&Tb%WZcN$yW=cI^9zas_8-2>;YmZPhG_(e*m&zzET`*Wx%fBQBQ(!^4sjPjtyL zx-9588BJ#L^Nk@ZhOG4idSnHSA|>U*!Uf!&+(V=zYNF0Q zwVy}RE&M$C1#k4@0lp9fLJ$f;NeaqBQk9Z9}h za$lN}0U3~DNDPc?%>vhBj^iPP%nyh8&OGi+?y})v4!%ZlZtnx|4pP_4>mRY_lTB33AP z2TkhJk3$(#Q(q=cHx}48 z9!VZS$h^;}EN})T-YT!_3rs_!Mm6yrNxms0KvpU^bB6FwKP_iOgs#<4Pwhbna9s!j z=38~B;x6WWrT3DN(Kc-+^%PI3J^V-Whv0!I2xO1MoiFj%d5~dqgDH*LrSjc5 zE}{@3`}*)#9dGwDBq^rVLXVB1AK!-Simo%BKi|n2VRC0|*t%8CE&#XQ8nb@=v;z36 zUqzgEUPz(f@V2)Vx%S%D)BYm{VvK2HObcV`Xgz^a2T*DcS+;{Dd60JdMw-okA;{U# z??~o54kvgXAFdS1fR;8)YQs}Xac`U=g(MUK6;Wo;8RR%`B?d!kQkuHSxSrs#bgN~7 zClDop9FX{HJy2j`Fb#tm)kL@DEC>RdoXwwS+vn31({7_Dr$`=p7%8pIFBWY`Y8RQ^ z@X=@(wr$I17l5fLMSs6_8wU<#OidYZhqz|2w_CpP*8?j6&f} z)|#6(TGvr(6s7i%rrU_)+o{*TgC_(b+gPa8m9NQF`=5^#rL%KQb?oAWLBC@|9XW@b_ z3v{!I+B0p2!cfrDBUroELu*J9Xtxbpx6U#EbLTp=+t$wS*pabhi9?~_(}4`B`N5Wg88n)I^UyD z3KC4OlJd!?Yoo;|-!Js7A;O8tbn5IVU z*LK&Y>sl7{_TZng*7|`Y!9)>ie4<+s^L-nV9&?P7*Y>R$nwU6zKX6z8y!EXmZn&XM ztEGAH!G!bA4`;NjM9+uoi03WE^H$*dCldtc(A)bLgyHKkCPM2vT92XB zZj$5yTCE@910^Bpmy$)U!zHCcH-DJYn*B<1eJjSnArK)IQDLq#m)D1{L!pRFL_;@t zD%&${hC+et6}T&1>x>g@Z_!v@c$d};iDC(7INx5SGE5X<>U9`7kQZdE85$gvUAemB zjAxYrmtI=t7r%%&>#RN8d1uUrKU|s7wAJFY)BLA}U?qg8AY}z9t4KM78vdVKwjdjZ7TC*d|kV4`KkAf&r zl~s=OR-#ZOCZTDXjO%gki0{l-$p|{Jsm$_x=GoSPwLszf=VUHH@`XftndBkM{}O>w?-rCN)^Ro2zUtLVV~X= zAcTt$4#uRk+c#iL1Fb!>Y&Y%pPpMQsLV%zr&zR2+d^V>Uv4Ms$9MGEo+m1LtbO}U2 zMV1+MtX!TMMU=}F+tSB5y?C14hfokWRh!2c1tbR3G^jn=?pJml2m(aeMteppNHa{- zMvaW%<`psDx6x3iXwym3Mn@Hk7CE!SfWsY86w_|Uv|174<24$M7D9LkQ9#Nb9A^%$ zyM!=21J64FV-mD(qSP2^x`QOS3r|2n2zsStvFl)f)EL^@P}7?2sX`+0g-<~g=$C!0 z@mJ4aL5l-wB(HYmEGSF-^8_7tWdqI{ZXTk!2Zanvtd{QIs$-Q6r8sga{F$jFba7&SDCMHv%dTS!-xLN*v#Z z0|dh62!oE~x55AgBp4dna6l=fkhsFh!_o@Jy35dLGLsRTm?=GFGx&}KQ53if9a~fA zbZIwo`vda;($p|9F^f+4a1$~6G(nBlD&L>0 zNYjjVJ0eXLLWD?J#&HI6CY%RkXdUIz(H?{lxVcnO5t5~@W2FgW9dgasuv8*(g-0ku zhMXZZ8b#Li1)YkJaBV~+`=qVKXE_VnR!p?fP@WU?_1SztY0H9E>!AK1--rHw(S7ET zQnp*-Rg;KKKeDm&fF_*-VLGx><3fF^dd1+(fLmw@hDO50y%(+o5b%iYChx?RE^f2oWGe87Ft3#-!cID9fHg z0G@yV8x@i7^GGp7#)@>=PFwN=hvYy?U z*+J46t9_e>?)m{-*LqqT3fX4fEJF%&5DR>Nnj{@r;#HG~*`4YRj^p5Y4#i@K$eCc- zpmTZKK?pwwVNPy_6vkv2V;>D85jcR4U`Wc2%1mY;$pH|#Q0;Z2YlB8vQ)H&i2iVSu z&dR0JryH4ro-ixi=>=u-M=Nxc(GCDlf4;3cOg(Ov6R}Ro(&hpkpTGm9sGag8I zc*WhPjl1r?*8ucYkNn}?M~7X6;}bSGIc@zq@vY1i-Dg#*B>E_Im|C|dhD6o39&s*PGdTpAZXS85A9aYu727!G-?$^IQMWG=0gFmR` z4Nubvj_pM+$Ul|jmwK?LQ;lKWM#MZ4DcitJjK!WFT{q-$=^(%YcC6^TW+~=kk z8ghst_}~XiTy<50rAzHYG(Pf?X%Mcxw#6NH*l+4syY}Eb08DsL5FLQ;~KH|7ol=-Sxc%FT(_-ZXo3RY-vN-M>gBXf^!7SkCnqI5_+ZS~nBpTJ>FLJPFJ@AN z5Z%eQ@`&h8K0u-1bSM8vKIb_@~Usg8(K(*@d z-uG5s^GR5iX_CY)3*B4?>hv( zi{r@G)R(NioGV@_O`K$1vye-S+s^lao_NzfbtZ`m)}hZ?0JJ zBEL_#GbSe|&KRGV`ar#Y^c%GgCFt>eZ%0q1a&uo_^_u50J4eUSadaFVN5|1|bQ~Q= e$II)z-z5%8pL}Q|fM&ld6$P1GRS!6Rvf@1KwwrhXwS{^aHe44cP z+w*+Czu$A8=g;oVz1XpN<2=i~7KUNwwYNn&@s%^bv!~(Lpe=}z-I zjd|tD@A83~Mh+%9()e^2yI_RyiK9 zZSzFAs2WCna@(MWwhnHNNrU~8FK$~KwD|J^PLM{1Xw9cn8C}Q+Y=yi6j?HG)W-UmJ z{(voH7PNLpJFHVMfUVCkRDor4xtt^CawuAo<$XRM%Q;!6(~cE(eIR3qd3#1*R!lOP zCxUcIlT|}jGFCIM*sElXfX!wWDt}7%l~&7zGJ1&}rpV?+mE|2AThA1i${sbVr3yof zODsyEG|Ct{_VIOnYM){#dY{suWVyRxVLT_%Xt{RX@ut&tOY27KZj7LWw60T+4XB9i zM7omIB-FYa-^#LjcW=mv%H&jKGJ6`19p&W)HnXQ_D=dZL4EgcB3SkY2hN8t3B^4~L zvW`iF)^OPRU{sXkjEVFiKU=G>Om-y|iiidZ;>GB;a~?bAjX7Nc=M~&eJJ%v`97s}B z*5rsPaXB$im4kQ3c#q(63r;W00hF~l@aBn&hWMYdKrP4ZNqO6o^786R_E3TVvH-)f0E zX{MBFlXbi}2f(7mOSf>4ZAH7PTPHhGa^XNyMNLQMb_>`h_gnb{D<4p%r?71WQ8JIR zpk(d_6!)`Lt(9Swoh6(6eOVswcU;3vP2zxue^h z7^b5|APr(naS;(g0mTJr5MzpqhzJTOE=YqIQ(QzuP(X1(8pN35A|iqUiVM;p#uOJ3 z5fo5dkOncPxQK|LfZ~EQh%v=QL<9vC7on3(_FQ6c-T@6i{4{1~I0%h=`zo;(|1YF~vnh z1O*fqq(O`+E+Qf*ptv9nVoY%n5kUdP1!)jtii?N{3Mei}gBVj>L_|{!9Z9jSzzYb`X+B%~QGq{{#_CCun*KXkJMTW`Z_Y5!XWEkO1hPg|5 z?TIfpVC`S+kx(pucI@Dq`KP(LLu<}_-L><=($JDa!^?Xwo$m3@T9^y1JNIYu@F)9M ze01N+Chg;|ei`+rmalK#y}`ENnc0zX*Qoco=F}F(yMN(W$A!SlOQ$!TAHV1Lp3vKY z!`GL*I^%M9!MX9LLkE5s8nw)v@K2le-5n1!kDT25LT=G36T%nG$yG-}{r)RmkKFOC zI-|qJ550e7?C*VZ9NFI%Kb$_5*mU*8;tyAhM9x0`?n~e7*fT59I`rhK9XEzg3?F#D zCGg{8TYj=^963J!vqevB8#(g*A2Y9wxqm%BdhETQ_e)pL99h3>;>@RQ6XUNZKJezZ ow{-oUzjl3i-Sitv^YfSsE04W%@zlXLFa@T)b#vtC`knj!1x_y)(*OVf literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4738a67f25c315aef05778baa8f24a4b8b9e5945 GIT binary patch literal 16762 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 L>gTe~DWM4f8ewTc literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/sym_keyboard_done_lxx_dark.png b/app/src/main/res/mipmap-xxhdpi/sym_keyboard_done_lxx_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..ccd270e78c42d70a8fe5d548287511e61048e30b GIT binary patch literal 14819 zcmeI3Z)_7~9LI0LP2GSuDi}m^6hRhf@2-Eg_SP0g3$%gGu@%_n3%T7r+b*v+WQ@@dxg zx99o(zQ5-_&!66#8)$A?IjwqDHAPX=!l7Ub{0^D#J8pxY*UvqA8h+iG2({}JRdbj5 zuAmMayqBWfbL427(H2?Fi)!2^NNOjrrQ->xrYP@{bV3j}0)y@Z-Lm4d{C@O93oT1N z%X(LYjU)n~M-KICptZj#D)w&_J(6XKzuKGTp+OuN0-cV>6rE4|EE!)O#%42Pp)(R= zqtDW47NpxE&2&K30PV85tRl-f>3WZib8${Lx0trGoP%K<4CkBO4(8lo2qp)oX6u~SUY34TcN_L_bG;uwkrC(tjV}f z5a^;NCk$CtXwz5dRFj6!VlfNlU%BgwC-Q|9J;x4GWYR){;cP5ZE=dyeIfg$=5DB-gvxhY28@92_ncLEt{!F`x1a@ z0lJ#hM6i4lyp(z6uHKNl3X@ahWcExtc951QNi%!0X_=*vogpv0Rz9Es!BDlRs>b}; zRn|O?kPZasg%LrN6%%Q#mnqd3Cc9t-f`S43a4|Zqtjo%}qjm?+x_PJF%GUENiGm>( zMnsukQhY#-sTv#>@N-_KTr-lDS!R4#(G5WnK{)7#4K`Vpcuzeih(Khm^^(9^os#6T zdg|e3bae_sos*S3oC9PaMj~kX1nZzGCe5vqvzBhs8cCmE{g5WZV@`-68gukFTcV83 zgi;|{hl{fhS+sEJW)8Ae&|BO(-Vu{C2U0@NbYN~bpJjZ%66z;^#J8Y8!Bs+QB7{5t6fG)&_H81Toc%hp0zB_ zDfRX;=D8n}i)SI*T0A-9%XQ*pdd=gEhYJA?O2|wA+&L543YD|^HgiH_$&9|<&cYYN znV-2-w7WF5{l=+icWG)NbFCs9etWK3Ni&jVp1IjGp%va90Dqm!<#2hK(&o~PW;qRF zZ9y4sUfo=h^{`SVr?jJNV*6x?NJlO)J5|UJd1fZFt?)Jh-k~u0Ta@BS&hLU-92BnT z78|2>qzFm_8xveuM5ut^LTO-Qf(wfX6%bq~4QxzsVG*GMf(xaAjR`I+B2++dp){~D z!G%SH3J5Ng1~w+Ru!v9r!G+Sm#sn7@5h@_KP#V~n;KCw81q2sL0~-@uSVX9R;6iC& zV}c8d2o(@qC=F~(aA6Ul0)h*rfsF|+EFx4uaG^A?F~NmJgbD~Qlm<2?xUh&&0l|gR zz{Ug@77;2SxKJ9{nBc-9LIngDN&_1cTv$Y?fZ#%DU}J&{iwG4ETqq4}OmJZlp#p*n zrGbrah^xBr&>v9XlYS}qNMFr`2WG*?0ckPR5}~O6`4si|c8a=o9e)2xQAw7f&TpV7 zeji26REHkeXUi`9IvhTZ0vI5{@Ha4Ul^&Zb*~=W zIWnczvv}~pNX3;=cImMD>Ya=SsXPmlzt|5QTV}E+6%N-+E`m0`CXg-D+UfvXZXW52r F{{l}!H%kBj literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..9d6ffd1 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,36 @@ + + + SSTV Encoder + Pick Picture + Take Picture + Save Wave File + Stop + Play + Rotate Image + Done + Modes + Martin 1 + Martin 2 + PD 50 + PD 90 + PD 120 + PD 160 + PD 180 + PD 240 + PD 290 + Scottie 1 + Scottie 2 + Scottie DX + Robot 36 + Robot 72 + Wraase SC2 180 + Image loading error + Image orientation error + Unsupported content. + Send Email + OK + SSTV Encoder - Bug Report + Send Bug Report: + Bold + Italic + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..0bcdc53 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,3 @@ + +