Initial commit

This commit is contained in:
Olga Miller 2017-01-03 18:32:45 +01:00
commit 6a5cf6b221
83 changed files with 5443 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild

202
LICENSE Normal file
View file

@ -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.

8
NOTICE Normal file
View file

@ -0,0 +1,8 @@
SSTV Encoder 2
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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/

42
README Normal file
View file

@ -0,0 +1,42 @@
SSTV Encoder 2
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
-------------- 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

1
app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

24
app/build.gradle Normal file
View file

@ -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'
}

17
app/proguard-rules.pro vendored Normal file
View file

@ -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 *;
#}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="om.sstvencoder">
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="18"/>
<uses-feature
android:name="android.hardware.camera"
android:required="false"/>
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="om.sstvencoder"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths"/>
</provider>
<activity
android:name=".MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
<activity android:name=".EditTextActivity"/>
</application>
</manifest>

View file

@ -0,0 +1,96 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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<OnChangeListener> 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());
}
}
}

View file

@ -0,0 +1,203 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,30 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}

View file

@ -0,0 +1,422 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,122 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,137 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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<IMode> 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();
}
}
}

View file

@ -0,0 +1,334 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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();
}
}

View file

@ -0,0 +1,24 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}

View file

@ -0,0 +1,24 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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();
}

View file

@ -0,0 +1,29 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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();
}

View file

@ -0,0 +1,60 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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)];
}
}

View file

@ -0,0 +1,61 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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];
}
}

View file

@ -0,0 +1,53 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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)];
}
}

View file

@ -0,0 +1,65 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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)];
}
}

View file

@ -0,0 +1,46 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}

View file

@ -0,0 +1,45 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,35 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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");
}
}
}

View file

@ -0,0 +1,22 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}

View file

@ -0,0 +1,100 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,133 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}
}

View file

@ -0,0 +1,27 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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();
}

View file

@ -0,0 +1,74 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,39 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,87 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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));
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,129 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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));
}
}

View file

@ -0,0 +1,123 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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));
}
}

View file

@ -0,0 +1,102 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,33 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,91 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,79 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}
}

View file

@ -0,0 +1,26 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}

View file

@ -0,0 +1,34 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,131 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,147 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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();
}
}

View file

@ -0,0 +1,40 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}

View file

@ -0,0 +1,40 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}

View file

@ -0,0 +1,96 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,184 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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<LabelContainer> 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();
}
}

View file

@ -0,0 +1,117 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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());
}
}

View file

@ -0,0 +1,303 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,200 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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;
}
}

View file

@ -0,0 +1,113 @@
/*
Copyright 2017 Olga Miller <olga.rgb@gmail.com>
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);
}
}

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_edit_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".EditTextActivity">
<EditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone|flagNoExtractUi"
android:inputType="text"
android:textSize="32sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/edit_italic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:text="@string/italic"/>
<CheckBox
android:id="@+id/edit_bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.3"
android:checked="true"
android:text="@string/bold"/>
<Spinner
android:id="@+id/edit_text_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0.4"
android:minHeight="32sp"/>
</LinearLayout>
<om.sstvencoder.ColorPalette.ColorPaletteView
android:id="@+id/edit_color"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<om.sstvencoder.CropView
android:id="@+id/cropView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"/>
</RelativeLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".EditTextActivity">
<item
android:id="@+id/action_done"
android:icon="@mipmap/sym_keyboard_done_lxx_dark"
android:title="@string/action_done"
app:showAsAction="always"/>
</menu>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity">
<item
android:id="@+id/action_pick_picture"
android:icon="@android:drawable/ic_menu_gallery"
android:title="@string/action_pick_picture"
app:showAsAction="always"/>
<item
android:id="@+id/action_take_picture"
android:icon="@android:drawable/ic_menu_camera"
android:title="@string/action_take_picture"
app:showAsAction="always"/>
<item
android:id="@+id/action_save_wave"
android:icon="@android:drawable/ic_menu_save"
android:title="@string/action_save_wave"
app:showAsAction="always"/>
<item
android:id="@+id/action_play"
android:icon="@android:drawable/ic_media_play"
android:title="@string/action_play"
app:showAsAction="always"/>
<item
android:id="@+id/action_stop"
android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:title="@string/action_stop"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_rotate"
android:icon="@android:drawable/ic_menu_rotate"
android:title="@string/action_rotate"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/action_modes"
android:title="@string/action_modes"
app:showAsAction="ifRoom">
<menu/>
</item>
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">SSTV Encoder</string>
<string name="action_pick_picture">Pick Picture</string>
<string name="action_take_picture">Take Picture</string>
<string name="action_save_wave">Save Wave File</string>
<string name="action_stop">Stop</string>
<string name="action_play">Play</string>
<string name="action_rotate">Rotate Image</string>
<string name="action_done">Done</string>
<string name="action_modes">Modes</string>
<string name="action_martin1">Martin 1</string>
<string name="action_martin2">Martin 2</string>
<string name="action_pd50">PD 50</string>
<string name="action_pd90">PD 90</string>
<string name="action_pd120">PD 120</string>
<string name="action_pd160">PD 160</string>
<string name="action_pd180">PD 180</string>
<string name="action_pd240">PD 240</string>
<string name="action_pd290">PD 290</string>
<string name="action_scottie1">Scottie 1</string>
<string name="action_scottie2">Scottie 2</string>
<string name="action_scottie_dx">Scottie DX</string>
<string name="action_robot36">Robot 36</string>
<string name="action_robot72">Robot 72</string>
<string name="action_wraaseSC2_180">Wraase SC2 180</string>
<string name="load_img_err_title">Image loading error</string>
<string name="load_img_orientation_err_title">Image orientation error</string>
<string name="load_img_err_txt_unsupported">Unsupported content.</string>
<string name="btn_send_email">Send Email</string>
<string name="btn_ok">OK</string>
<string name="email_subject">SSTV Encoder - Bug Report</string>
<string name="chooser_title">Send Bug Report:</string>
<string name="bold">Bold</string>
<string name="italic">Italic</string>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<style name="AppTheme" parent="Base.Theme.AppCompat"/>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="pictures"
path=""/>
</paths>

23
build.gradle Normal file
View file

@ -0,0 +1,23 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}

17
gradle.properties Normal file
View file

@ -0,0 +1,17 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
#Mon Dec 28 10:00:20 PST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip

160
gradlew vendored Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

1
settings.gradle Normal file
View file

@ -0,0 +1 @@
include ':app'