Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
10db8b5
Initial commit
dozingcat Oct 10, 2017
47a2e07
Write full-size and thumbnail PNGs.
dozingcat Oct 10, 2017
018bd61
Ascii converter
dozingcat Oct 15, 2017
c8519a4
Renderscript working
dozingcat Oct 18, 2017
2569af8
Second Renderscript renderer.
dozingcat Oct 18, 2017
23459c5
Combine ImageReader and Allocation processors into a single list.
dozingcat Oct 20, 2017
e2b5224
More RenderScript
dozingcat Oct 21, 2017
822b854
Refactor CameraImage, save bitmaps generated by RenderScript.
dozingcat Oct 23, 2017
5336c6b
Python script to convert YUV, fix buffer bug.
dozingcat Oct 23, 2017
9a70402
Save images as JPEG rather than PNG.
dozingcat Oct 25, 2017
3043c7f
Edge and solid color RS scripts, sync for Android Studio 3.0 final.
dozingcat Oct 29, 2017
2c5ab07
Remove non-Renderscript processing
dozingcat Oct 29, 2017
fae144a
SolidColorAllocationProcessor
dozingcat Oct 29, 2017
0b4a7e1
RenderScript implementation of ASCII processor.
dozingcat Oct 30, 2017
fc9cfc1
Set ASCII bitmap pixels in RenderScript.
dozingcat Nov 5, 2017
9e46ae5
Library view with image grid sort of working.
dozingcat Dec 3, 2017
7982b8e
Activity to view images from library. Currently using a single fixed …
dozingcat Dec 10, 2017
f6cba86
Image processor to permute RGB components.
dozingcat Dec 11, 2017
f41d0d0
Simplify class hierarchy, add Effect interface.
dozingcat Dec 16, 2017
b11f304
cleanup
dozingcat Dec 16, 2017
fe40e53
Add EffectRegistry, store and load effect params when saving pictures.
dozingcat Dec 17, 2017
ed6cb78
"effect" package
dozingcat Dec 17, 2017
52c0f63
fix ascii bug
dozingcat Dec 17, 2017
32109ad
CombinationEffect to show multiple effects and let the user pick one.
dozingcat Dec 17, 2017
2929dc9
Don't go backwards in time.
dozingcat Dec 17, 2017
a441ae6
Change effect (in-memory only) for saved pictures.
dozingcat Dec 18, 2017
abfce9a
Share pictures
dozingcat Dec 19, 2017
edc4ea3
Fix gzip decompression.
dozingcat Dec 23, 2017
f89bf7d
start of video recording
dozingcat Dec 24, 2017
4f936fa
MediaMetadata class to replace untyped maps.
dozingcat Jan 1, 2018
ea66189
Metadata refactoring
dozingcat Jan 1, 2018
5344516
Maybe fix crashes by not calling RS from multiple threads.
dozingcat Jan 2, 2018
174e7dc
Video playback sort of working
dozingcat Jan 2, 2018
6fb5ecd
Switch effects when viewing video.
dozingcat Jan 2, 2018
37df98a
Always render to screen resolution in ascii mode.
dozingcat Jan 7, 2018
507b17e
reduce resolution when showing effect grid
dozingcat Jan 7, 2018
d3b7eed
Effect with 3x3 convolution with configurable coefficients.
dozingcat Jan 7, 2018
1ba3450
Consolidate color schemes.
dozingcat Jan 7, 2018
585bc55
Fix egregious threading bugs, video actually works now. Add nonfuncti…
dozingcat Jan 14, 2018
c86b9ea
Text/background for ascii effect, 16 effects for 4x4 grid.
dozingcat Jan 14, 2018
1e8b322
fix audio recording
dozingcat Jan 14, 2018
22dbc7e
Basic video playback, timestamps not used yet.
dozingcat Jan 14, 2018
1ded540
Audio playback, better video frame timings.
dozingcat Jan 16, 2018
f4aff10
Fix render size bug in effect grid and make it load faster.
dozingcat Jan 17, 2018
e6e16d4
UI updates with material icons.
dozingcat Jan 20, 2018
eb65c99
Save and restore selected effect in preferences.
dozingcat Jan 21, 2018
64fb640
WebM encoding, no progress UI yet.
dozingcat Jan 22, 2018
e8969e1
Update library when changing effect of saved picture.
dozingcat Jan 22, 2018
906cef1
Store "exported effect" so we know if we need to re-encode to WebM be…
dozingcat Jan 23, 2018
30d1d87
Delete pictures and videos.
dozingcat Jan 24, 2018
bc530a2
Show date and file size in grid list.
dozingcat Jan 24, 2018
d78d6bf
Encode webm before sharing video.
dozingcat Jan 28, 2018
b516f41
Ascii color modes.
dozingcat Jan 28, 2018
a7f74f2
start of preferences and new picture handler
dozingcat Jan 28, 2018
dea0d7a
fix primary ascii color mode
dozingcat Jan 28, 2018
116edb1
Make back button cancel effect selection.
dozingcat Jan 29, 2018
0ef5353
prefs for ascii characters, sort of
dozingcat Jan 29, 2018
1e7fd80
Update current ascii effect when setting characters in preferences.
dozingcat Jan 30, 2018
7c76fb2
Implement ProcessPictureOperation, job for saved camera pictures work…
dozingcat Jan 31, 2018
eb444b0
Button to import existing picture.
dozingcat Jan 31, 2018
ca10125
refactor
dozingcat Jan 31, 2018
889f353
Refactor ascii RS in preparation for HTML/text export.
dozingcat Feb 2, 2018
eeb9ab8
ASCII text/html and aspect ratio fixes.
dozingcat Feb 3, 2018
e9909dd
Refactoring
dozingcat Feb 5, 2018
52fef4b
Improve image export, show modal dialog during PNG compression.
dozingcat Feb 5, 2018
e097217
Show dialog for video export, fix frame archive bugs.
dozingcat Feb 6, 2018
872f643
Share current frame of video, use FileProvider to share.
dozingcat Feb 8, 2018
9d83977
help/about activity and placeholder html
dozingcat Feb 9, 2018
39c5af6
Add animated gradient color schemes, make background drawing more gen…
dozingcat Feb 11, 2018
22de29e
Fix rendering bugs by premultiplying alpha.
dozingcat Feb 11, 2018
ebf584d
Add setting for number of ASCII columns.
dozingcat Feb 12, 2018
ba32680
Fix orientation bug with ASCII text/html export.
dozingcat Feb 12, 2018
aa05f5c
close to final color schemes
dozingcat Feb 14, 2018
1074045
Fix bug creating temp file when importing picture.
dozingcat Feb 14, 2018
20c8ebe
UI rearrangement, no text buttons and icons on both sides.
dozingcat Feb 16, 2018
1d53e28
persist resolution setting
dozingcat Feb 16, 2018
e1266ca
Add HTML files, prepare for rename to Vector Camera.
dozingcat Feb 18, 2018
8969266
Rename to Vector Camera
dozingcat Feb 18, 2018
30f2c64
cleanup and refactoring
dozingcat Feb 18, 2018
fadaf00
cleanup
dozingcat Feb 19, 2018
95bc41c
Cleanup, permissions improvements.
dozingcat Feb 19, 2018
c34a6d4
cleanup
dozingcat Feb 19, 2018
246eff1
more cleanup
dozingcat Feb 19, 2018
220fb52
Attempt at an icon
dozingcat Feb 19, 2018
a5fe0e4
Hopefully final cleanup for 1.0.
dozingcat Feb 19, 2018
82c9c32
Put version in help, add README.md and LICENSE.
dozingcat Feb 19, 2018
0cfeef7
add privacy notice
dozingcat Feb 19, 2018
5f93a55
Merge branch 'master' of https://github.com/dozingcat/VectorCamera
dozingcat Feb 20, 2018
b654aaa
in progress
dozingcat Feb 23, 2018
c1cb78e
Support portrait mode for preview and taking pictures and videos.
dozingcat Feb 24, 2018
9571c51
Make effect selection grids display reasonably in portrait mode.
dozingcat Feb 25, 2018
bfe16e4
Adjust main activity UI layout for portrait.
dozingcat Feb 25, 2018
1ea9991
Improve timing of switching cameras, hopefully fixing crashes.
dozingcat Feb 25, 2018
013a641
cleanup
dozingcat Feb 25, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,8 +20,7 @@
android:theme="@android:style/Theme.Material.NoActionBar.Fullscreen">

<activity android:name="com.dozingcatsoftware.vectorcamera.MainActivity"
android:screenOrientation="landscape"
android:configChanges="keyboard|keyboardHidden">
android:configChanges="orientation|screenSize|keyboard|keyboardHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Expand All@@ -35,12 +34,12 @@

<activity
android:name="com.dozingcatsoftware.vectorcamera.ViewImageActivity"
android:screenOrientation="landscape">
android:configChanges="orientation|screenSize">
</activity>

<activity
android:name="com.dozingcatsoftware.vectorcamera.ViewVideoActivity"
android:screenOrientation="landscape">
android:configChanges="orientation|screenSize">
</activity>

<activity android:name="com.dozingcatsoftware.vectorcamera.AboutActivity">
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/dozingcatsoftware/util/images.kt
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,11 @@ fun getDisplaySize(context: Context): Size {
return Size(metrics.widthPixels, metrics.heightPixels)
}

fun getLandscapeDisplaySize(context: Context): Size {
val ds = getDisplaySize(context)
return if (ds.width >= ds.height) ds else Size(ds.height, ds.width)
}

/**
* Notifies the OS to index an image so it appears in apps that show media files. Allows optional
* callback to notify client when the scan is completed, e.g. so it can access the "content" URI
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,6 +19,8 @@ data class CameraImage(val rs: RenderScript,
val orientation: ImageOrientation, val status: CameraStatus,
val timestamp: Long, val displaySize: Size = zeroSize) {

// width() and height() return the dimensions of the actual camera input, which is always
// landscape. In portrait orientation the rendered image will have the dimensioned swapped.
fun width(): Int {
return singleYuvAllocation?.type?.x ?: planarYuvAllocations!!.y.type.x
}
Expand All@@ -34,6 +36,11 @@ data class CameraImage(val rs: RenderScript,
rs, singleYuvAllocation, planarYuvAllocations, orientation, status, timestamp, ds)
}

fun withDisplaySizeAndOrientation(ds: Size, o: ImageOrientation): CameraImage {
return CameraImage(
rs, singleYuvAllocation, planarYuvAllocations, o, status, timestamp, ds)
}

fun resizedTo(size: Size): CameraImage {
val resizeScript = ScriptIntrinsicResize.create(rs)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
var status = CameraStatus.CLOSED
private var targetStatus = CameraStatus.CLOSED
private var imageAllocationCallback: ((CameraImage) -> Unit)? = null
private var cameraClosedCallback: (() -> Unit)? = null
private var handler = Handler()

private val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
Expand All@@ -34,12 +35,11 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
val facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING)
val isFrontFacing = (facing == CameraMetadata.LENS_FACING_FRONT)
val orientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)
if (isFrontFacing && orientation == 90 || !isFrontFacing && orientation == 270) {
ImageOrientation.ROTATED_180
}
else {
ImageOrientation.NORMAL
}
val upsideDown =
(isFrontFacing && orientation == 90) || (!isFrontFacing && orientation == 270)
// X flipped if the camera front facing or the orientation is backwards, but not both.
Log.i(TAG, "isFrontFacing: ${isFrontFacing} upsideDown: ${upsideDown}")
ImageOrientation(xFlipped = (isFrontFacing != upsideDown), yFlipped = upsideDown)
}()

fun start(targetStatus: CameraStatus, targetSize: Size,
Expand All@@ -61,19 +61,24 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
}
}

fun stop() {
/**
* Stops the camera and invokes the callback after the camera is completely closed. This avoids
* errors that can occur when trying to open the camera while it's in the process of closing.
*/
fun stop(closedCallback: (() -> Unit)? = null) {
this.targetStatus = CameraStatus.CLOSED
this.cameraClosedCallback = closedCallback
updateStatus(this.status)
}

private fun updateStatus(status: CameraStatus) {
Log.i(TAG, "CameraStatus change: " + status)
Log.i(TAG, "CameraStatus change: ${status}, target=${targetStatus}")
this.status = status

if (this.targetStatus == CameraStatus.CLOSED) {
if (this.status.isCapturing()) {
this.status = CameraStatus.CLOSING
stopCaptureSession()
stopCamera()
}
}
else if (this.targetStatus.isCapturing()) {
Expand All@@ -100,7 +105,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
}
}

fun openCamera() {
private fun openCamera() {
val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
stopCamera()
try {
Expand All@@ -111,8 +116,15 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
updateStatus(CameraStatus.OPENED)
}

override fun onDisconnected(cam: CameraDevice) {
override fun onClosed(cam: CameraDevice) {
Log.i(TAG, "camera onClosed")
updateStatus(CameraStatus.CLOSED)
cameraClosedCallback?.invoke()
}

override fun onDisconnected(cam: CameraDevice) {
Log.i(TAG, "onDisconnected")
cam.close()
}

override fun onError(camera: CameraDevice, error: Int) {
Expand DownExpand Up@@ -157,6 +169,7 @@ class CameraImageGenerator(val context: Context, val rs: RenderScript,
}

override fun onClosed(session: CameraCaptureSession?) {
Log.i(TAG, "capture session onClosed, status=${status}")
super.onClosed(session)
updateStatus(CameraStatus.OPENED)
}
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,8 +31,8 @@ class CreateVideoZipFileAsyncTask(
val videoReader = params[0]!!.videoReader
val mediaLibrary = params[0]!!.mediaLibrary
val videoId = params[0]!!.videoId
val videoWidth = videoReader.videoWidth()
val videoHeight = videoReader.videoHeight()
val videoWidth = videoReader.landscapeVideoWidth()
val videoHeight = videoReader.landscapeVideoHeight()

// Frames are JPEGs because PNG encoding is very slow.
zipFile = mediaLibrary.videoFramesArchiveForItemId(videoId)
Expand Down
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
package com.dozingcatsoftware.vectorcamera

enum class ImageOrientation {
NORMAL,
ROTATED_180,
;
class ImageOrientation(
val xFlipped: Boolean, val yFlipped: Boolean, val portrait: Boolean = false) {

fun isXFlipped(): Boolean {
return this == ROTATED_180
}

fun isYFlipped(): Boolean {
return this == ROTATED_180
}
fun withPortrait(newPortrait: Boolean) = ImageOrientation(xFlipped, yFlipped, newPortrait)

companion object {
fun withXYFlipped(xFlipped: Boolean, yFlipped: Boolean): ImageOrientation {
if (xFlipped != yFlipped) {
throw IllegalArgumentException("Only 0 and 180 degree rotations supported")
}
return if (xFlipped) ImageOrientation.ROTATED_180 else ImageOrientation.NORMAL
}
val NORMAL = ImageOrientation(false, false)
}
}
}
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,22 +2,25 @@ package com.dozingcatsoftware.vectorcamera

import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.preference.PreferenceManager
import android.provider.MediaStore
import android.renderscript.RenderScript
import android.util.Log
import android.util.Size
import android.view.MotionEvent
import android.view.View
import android.view.Window
import com.dozingcatsoftware.vectorcamera.effect.CombinationEffect
import com.dozingcatsoftware.vectorcamera.effect.Effect
import com.dozingcatsoftware.vectorcamera.effect.EffectRegistry
import com.dozingcatsoftware.util.getDisplaySize
import com.dozingcatsoftware.util.getLandscapeDisplaySize
import kotlinx.android.synthetic.main.activity_main.*
import java.io.FileOutputStream
import android.view.*
import android.widget.FrameLayout
import android.widget.LinearLayout



enum class ShutterMode {IMAGE, VIDEO}

Expand All@@ -34,7 +37,6 @@ class MainActivity : Activity() {
private val photoLibrary = PhotoLibrary.defaultLibrary()

private lateinit var rs: RenderScript
private lateinit var displaySize: Size
private val allEffectFactories = EffectRegistry.defaultEffectFactories()
private var currentEffect: Effect? = null
private var previousEffect: Effect? = null
Expand All@@ -48,13 +50,14 @@ class MainActivity : Activity() {
private lateinit var previousImageSize: ImageSize
private var shutterMode = ShutterMode.IMAGE

private var layoutIsPortrait = false

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setContentView(R.layout.activity_main)
PreferenceManager.setDefaultValues(this.baseContext, R.xml.preferences, false)

displaySize = getDisplaySize(this)
// Use PROFILE type only on first run?
rs = RenderScript.create(this, RenderScript.ContextType.NORMAL)
imageProcessor = CameraImageProcessor(rs)
Expand DownExpand Up@@ -92,9 +95,16 @@ class MainActivity : Activity() {

override fun onResume() {
super.onResume()
// TODO: Read high quality flag from preferences.
checkPermissionAndStartCamera()
overlayView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE

// View size is zero in onResume, have to wait for layout notification.
var listener: ViewTreeObserver.OnGlobalLayoutListener? = null
listener = ViewTreeObserver.OnGlobalLayoutListener {
updateLayout(isPortraitOrientation())
overlayView.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
overlayView.viewTreeObserver.addOnGlobalLayoutListener(listener)
}

override fun onPause() {
Expand All@@ -111,6 +121,15 @@ class MainActivity : Activity() {
super.onPause()
}

override fun onConfigurationChanged(newConfig: Configuration) {
Log.i(TAG, "configurationChanged: ${newConfig.orientation}")
super.onConfigurationChanged(newConfig)
val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
if (isPortrait != layoutIsPortrait) {
updateLayout(isPortrait)
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)

Expand DownExpand Up@@ -156,12 +175,47 @@ class MainActivity : Activity() {
if (preferredImageSize == ImageSize.FULL_SCREEN) 1.0f else 0.5f
}

fun updateLayout(isPortrait: Boolean) {
Log.i(TAG, "updateLayout: ${isPortrait}")
layoutIsPortrait = isPortrait
val layoutWidth =
if (isPortrait) FrameLayout.LayoutParams.MATCH_PARENT
else FrameLayout.LayoutParams.WRAP_CONTENT
val layoutHeight =
if (isPortrait) FrameLayout.LayoutParams.WRAP_CONTENT
else FrameLayout.LayoutParams.MATCH_PARENT
val orientation = if (isPortrait) LinearLayout.HORIZONTAL else LinearLayout.VERTICAL
val direction =
if (isPortrait) LinearLayout.LAYOUT_DIRECTION_RTL
else LinearLayout.LAYOUT_DIRECTION_LTR

run {
val params = FrameLayout.LayoutParams(layoutWidth, layoutHeight)
params.gravity = if (isPortrait) Gravity.TOP else Gravity.LEFT
leftTopControlBar.layoutParams = params
leftTopControlBar.orientation = orientation
leftTopControlBar.layoutDirection = direction
}
run {
val params = FrameLayout.LayoutParams(layoutWidth, layoutHeight)
params.gravity = if (isPortrait) Gravity.BOTTOM else Gravity.RIGHT
rightBottomControlBar.layoutParams = params
rightBottomControlBar.orientation = orientation
rightBottomControlBar.layoutDirection = direction
}
}

private fun isPortraitOrientation(): Boolean {
return overlayView.height > overlayView.width
}

private fun targetCameraImageSize(): Size {
val ds = getLandscapeDisplaySize(this)
return when (preferredImageSize) {
ImageSize.FULL_SCREEN -> displaySize
ImageSize.HALF_SCREEN -> Size(displaySize.width / 2, displaySize.height / 2)
ImageSize.FULL_SCREEN -> ds
ImageSize.HALF_SCREEN -> Size(ds.width / 2, ds.height / 2)
ImageSize.VIDEO_RECORDING -> Size(640, 360)
ImageSize.EFFECT_GRID -> Size(displaySize.width / 4, displaySize.height / 4)
ImageSize.EFFECT_GRID -> Size(ds.width / 4, ds.height / 4)
}
}

Expand DownExpand Up@@ -247,8 +301,11 @@ class MainActivity : Activity() {

private fun handleAllocationFromCamera(imageFromCamera: CameraImage) {
handler.post(fun() {
// Slightly ugly but some effects need the display size.
val cameraImage = imageFromCamera.withDisplaySize(displaySize)
// Add fields that the image generator doesn't have. Might be better to have a separate
// class that holds a CameraImage, display size, and portrait flag.
val ds = getLandscapeDisplaySize(this)
val orientation = imageFromCamera.orientation.withPortrait(isPortraitOrientation())
val cameraImage = imageFromCamera.withDisplaySizeAndOrientation(ds, orientation)
if (cameraImage.status == CameraStatus.CAPTURING_PHOTO) {
Log.i(TAG, "Restarting preview capture")
restartCameraImageGenerator()
Expand DownExpand Up@@ -281,10 +338,13 @@ class MainActivity : Activity() {
}
imageProcessor.pause()
cameraSelector.selectNextCamera()
cameraImageGenerator.stop()
cameraImageGenerator = cameraSelector.createImageGenerator(rs)
restartCameraImageGenerator()
updateControls()
cameraImageGenerator.stop({
handler.post({
cameraImageGenerator = cameraSelector.createImageGenerator(rs)
restartCameraImageGenerator()
updateControls()
})
})
}

private fun switchResolution(view: View) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,9 @@ data class MediaMetadata(val mediaType: MediaType, val effectMetadata: EffectMet
"type" to mediaType.name.toLowerCase(),
"width" to width,
"height" to height,
"xFlipped" to orientation.isXFlipped(),
"yFlipped" to orientation.isYFlipped(),
"xFlipped" to orientation.xFlipped,
"yFlipped" to orientation.yFlipped,
"portrait" to orientation.portrait,
"timestamp" to timestamp,
"frameTimestamps" to frameTimestamps,
"audioStartTimestamp" to audioStartTimestamp,
Expand DownExpand Up@@ -54,8 +55,9 @@ data class MediaMetadata(val mediaType: MediaType, val effectMetadata: EffectMet
EffectMetadata.fromJson(effectDict),
(json["width"] as Number).toInt(),
(json["height"] as Number).toInt(),
ImageOrientation.withXYFlipped(
json["xFlipped"] as Boolean, json["yFlipped"] as Boolean),
ImageOrientation(
json["xFlipped"] as Boolean, json["yFlipped"] as Boolean,
json.getOrElse("portrait", {false}) as Boolean),
json["timestamp"] as Long,
frameTimestamps as List<Long>,
audioStartTimestamp.toLong(),
Expand Down
Loading