3 Commits

23 changed files with 1137 additions and 5 deletions

View File

@@ -1,9 +1,10 @@
WEBP Samples
=============
Webp is an Android sample including a small app to demo usage of webp in [Native Activity](http://developer.android.com/reference/android/app/NativeActivity.html)
view:
- rotate decoding 3 webp images and load them into on-screen buffer. Decoding is in its own thread
Sample in this directory is to demonstrate image decoding
- view: demonstrate how to use webp in [Native Activity](http://developer.android.com/reference/android/app/NativeActivity.html)
- image-decoder: demonstrate AImageDecoder usage integrated in Android 12
Note that the directory names will be changed in DP2
This sample uses the new [Android Studio CMake plugin](https://developer.android.com/ndk/guides/cmake.html).

View File

@@ -1,5 +1,6 @@
#Thu Feb 18 09:46:00 PST 2021
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip

View File

@@ -0,0 +1,40 @@
# Image-Decoder
The sample demonstrates how to use the new AImageDecoder animated decoding API,
added in Android 12. The animated images such as webp, avif and other formats, are
decoded and copied into the native window to display them on the screen.
The goal of the sample is to collect feedback for new AImageDecoder API added in Android 12
and Android 11, please try it out and let us know your thoughts!
## Pre-requistites
- Android Gradle Plugin 4.1.2
- Platform SDK and NDK for Android 12
- An Android device running Android 12(API level 31)
## Getting Start
- build the sample and run it on an Android 12 device
- swap in new image files: drop them into src/main/assets/images, and add to imageFiles[] in image_viewer.cpp
## Support
Android 12, AImageDecoder API and this sample are at developer preview stage, you
may expect changes in the future Android 12 release cycles. If you find any error
or have any feedback, please [file an issue](https://github.com/googlesamples/android-ndk/issues/new).
Comments and patches are highly very welcome too!
## Copyright
Copyright 2021 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you 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.

View File

@@ -0,0 +1,30 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 'android-S'
ndkVersion '23.0.7123448'
defaultConfig {
applicationId 'com.android.example.webp_image_decoder'
minSdkVersion 'S'
targetSdkVersion 'S'
externalNativeBuild {
cmake {
arguments '-DANDROID_STL=c++_static'
}
}
}
buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
cmake {
version '3.10.2'
path 'src/main/cpp/CMakeLists.txt'
}
}
}

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.example.webp_accelerated_view"
android:versionCode="1"
android:versionName="1.0">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-feature android:glEsVersion="0x00030002"></uses-feature>
<application
android:allowBackup="false"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:hasCode="false">
<activity android:name="android.app.NativeActivity"
android:label="@string/app_name"
android:exported="true">
<meta-data android:name="android.app.lib_name"
android:value="image_decoder" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

View File

@@ -0,0 +1,42 @@
#[[
# Copyright (C) 2021 The Android Open Source Project
#
# 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.
#]]
project("image_decoder")
cmake_minimum_required(VERSION 3.10.2)
set(CMAKE_VERBOSE_MAKEFILE on)
# Build native_app_glue as a static lib
include_directories(${ANDROID_NDK}/sources/android/native_app_glue)
add_library(native_app_glue STATIC
${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c)
# Create app's shared lib
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -Wall")
# Export ANativeActivity_onCreate(),
# Refer to: https://github.com/android-ndk/ndk/issues/381.
set(CMAKE_SHARED_LINKER_FLAGS
"${CMAKE_SHARED_LINKER_FLAGS} -u ANativeActivity_onCreate")
add_library(image_decoder SHARED
android_main.cpp
image_viewer.cpp
image_decoder.cpp)
target_link_libraries(image_decoder native_app_glue jnigraphics android log m)

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*
*/
#pragma once
#include <android/log.h>
#if 1
#ifndef MODULE_NAME
#define MODULE_NAME "ImageDecoder"
#endif
#define LOGV(...) \
__android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__)
#define LOGD(...) \
__android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__)
#define LOGI(...) \
__android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__)
#define LOGW(...) \
__android_log_print(ANDROID_LOG_WARN, MODULE_NAME, __VA_ARGS__)
#define LOGE(...) \
__android_log_print(ANDROID_LOG_ERROR, MODULE_NAME, __VA_ARGS__)
#define LOGF(...) \
__android_log_print(ANDROID_LOG_FATAL, MODULE_NAME, __VA_ARGS__)
#define ASSERT(cond, ...) \
if (!(cond)) { \
__android_log_assert(#cond, MODULE_NAME, __VA_ARGS__); \
}
#else
#define LOGV(...)
#define LOGD(...)
#define LOGI(...)
#define LOGW(...)
#define LOGE(...)
#define LOGF(...)
#define ASSERT(cond, ...)
#endif

View File

@@ -0,0 +1,107 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#include "image_viewer.h"
#include "android_debug.h"
static int32_t ProcessAndroidInput(struct android_app *app, AInputEvent *event) {
ImageViewer* imageViewer = reinterpret_cast<ImageViewer*>(app->userData);
if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_MOTION) {
imageViewer->StartAnimation(true);
return 1;
} else if (AInputEvent_getType(event) == AINPUT_EVENT_TYPE_KEY) {
LOGI("Key event: action=%d keyCode=%d metaState=0x%x",
AKeyEvent_getAction(event),
AKeyEvent_getKeyCode(event),
AKeyEvent_getMetaState(event));
}
return 0;
}
static void ProcessAndroidCmd(struct android_app *app, int32_t cmd) {
static int32_t format = WINDOW_FORMAT_RGBA_8888;
ImageViewer* imageViewer = reinterpret_cast<ImageViewer*>(app->userData);
switch (cmd) {
case APP_CMD_INIT_WINDOW:
if (imageViewer->GetAndroidApp()->window) {
// save current format to format variable, and set
// display format to 8888
format = ANativeWindow_getFormat(app->window);
ANativeWindow_setBuffersGeometry(app->window,
ANativeWindow_getWidth(app->window),
ANativeWindow_getHeight(app->window),
WINDOW_FORMAT_RGBA_8888);
imageViewer->PrepareDrawing();
imageViewer->UpdateDisplay();
imageViewer->StartAnimation(true);
}
break;
case APP_CMD_TERM_WINDOW:
imageViewer->StartAnimation(false);
imageViewer->TerminateDisplay();
ANativeWindow_setBuffersGeometry(app->window,
ANativeWindow_getWidth(app->window),
ANativeWindow_getHeight(app->window),
format);
break;
case APP_CMD_LOST_FOCUS:
imageViewer->StartAnimation(false);
imageViewer->UpdateDisplay();
break;
}
}
// Android application glue entry function for us
extern "C" void android_main(struct android_app* state) {
ImageViewer imageViewer(state);
state->userData = reinterpret_cast<void*>(&imageViewer);
state->onAppCmd = ProcessAndroidCmd;
state->onInputEvent = ProcessAndroidInput;
// loop waiting for stuff to do.
while (1) {
// Read all pending events.
int ident;
int events;
struct android_poll_source* source;
// If not animating, we will block forever waiting for events.
// If animating, we loop until all events are read, then continue
// to draw the next frame of animation.
while ((ident = ALooper_pollAll(imageViewer.IsAnimating() ? 0 : -1, NULL, &events,
(void**)&source)) >= 0) {
// Process this event.
if (source != NULL) {
source->process(state, source);
}
// Check if we are exiting.
if (state->destroyRequested != 0) {
LOGI("Engine thread destroy requested!");
imageViewer.TerminateDisplay();
return;
}
}
if (imageViewer.IsAnimating()) {
imageViewer.UpdateDisplay();
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#pragma once
#include <ctime>
/**
* DisplayTimer: a utility class to track the display time in nano seconds
*/
class DisplayTimer {
public:
explicit DisplayTimer(void) {
Reset(0);
}
void Reset(uint64_t durationInNanoSecond) {
durationInNano = durationInNanoSecond;
struct timespec cur;
clock_gettime(CLOCKS_MONO, &cur);
startTime = cur.tv_sec * 1000000000UL + cur.tv_nsec;
}
bool IsExpired(void) {
uint64_t timePassed = GetTimePassed();
return timePassed >= durationInNano ? true : false;
}
uint64_t timeLeft(void) {
uint64_t timePassed = GetTimePassed();
return (timePassed >= durationInNano) ? 0 : (timePassed - durationInNano);
}
private:
uint64_t GetTimePassed(void) {
struct timespec cur;
clock_gettime(CLOCKS_MONO, &cur);
uint64_t curNS = cur.tv_sec * 1000000000UL + cur.tv_nsec;
return (curNS - startTime);
}
uint64_t durationInNano;
uint64_t startTime;
};

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#include <android/imagedecoder.h>
#include <android/data_space.h>
#include "image_decoder.h"
#include "image_scale_descriptor.h"
#include "android_debug.h"
#include <cassert>
#include <thread>
// Default displaying time image for each frame, used when switching from one image file to
// the next image file at run time:
// - frames inside the animated files would provide the display time, so kDEFAULT_DURATION
// is not needed
// - non-animated image files will need this.
const uint64_t kDEFAULT_DURATION = (2 * 1000000000UL); // 2 second
/*
* ImageDecoder constructor:
*/
ImageDecoder::ImageDecoder(const char** files, uint32_t count,
DecodeSurfaceDescriptor *frameBuf,
AAssetManager* mgr)
: assetMgr(mgr), curImgFile(nullptr), imgAsset(nullptr),
decoder(nullptr),
headerInfo(nullptr), frameInfo(nullptr), frameDuration(0UL),
decodeState(DecodeState::IDLE),
decodedImageStride(0), rotationAngle(0) {
for (auto i = 0; i < count; i++) {
fileNames.push(files[i]);
}
bufInfo = *frameBuf;
if (count && frameBuf) {
switch (bufInfo.format) {
case SurfaceFormat::SURFACE_FORMAT_RGB_565:
bytePerPixel = 2;
break;
case SurfaceFormat::SURFACE_FORMAT_RGBA_8888:
case SurfaceFormat::SURFACE_FORMAT_RGBX_8888:
bytePerPixel = 4;
break;
default:
ASSERT(false, "Unsupported display buffer format(%d) to %s",
bufInfo.format, __FUNCTION__);
return;
}
}
memset(&targetRect, 0, sizeof(targetRect));
}
/**
* Report decoder is currently decoding frames.
* User should call this function before sending command to decode the
* next frame.
*/
bool ImageDecoder::IsBusy(void) {
return (decodeState == DecodeState::BUSY ? true : false);
}
/*
* DecodeNextImage(): start decode the next image or frame.
*/
bool ImageDecoder::DecodeNextImage(void) {
std::unique_lock<std::mutex> decodeLock(decodeMutex);
if(IsBusy())
return false;
int status;
if(!curImgFile) {
curImgFile = fileNames.front();
fileNames.pop();
// Open the asset with the give name from the APK's assets folder.
imgAsset =
AAssetManager_open(assetMgr, curImgFile, AASSET_MODE_BUFFER);
ASSERT(imgAsset, "%s does not exist in %s", curImgFile,
__FUNCTION__);
// Create an AImageDecoder from the given AAsset
decoder = nullptr;
status = AImageDecoder_createFromAAsset(imgAsset, &decoder);
ASSERT(ANDROID_IMAGE_DECODER_SUCCESS == status,
"Failed to create ImageDecoder for %s with the error of %s",
curImgFile, AImageDecoder_resultToString(status));
status = AImageDecoder_setAndroidBitmapFormat(
decoder, GetDecodingBitmapFormat());
ASSERT(ANDROID_IMAGE_DECODER_SUCCESS == status,
"Failed to request decoding output format %s(format: %d)",
curImgFile, bufInfo.format);
status = AImageDecoder_setUnpremultipliedRequired(decoder, false);
ASSERT(ANDROID_IMAGE_DECODER_SUCCESS == status,
"Failed to use pre-multiply alpha for %s", curImgFile);
// scale the image to the max possible size to fit into display window
// but keep the image's aspect ratio.
headerInfo =AImageDecoder_getHeaderInfo(decoder);
ASSERT(headerInfo != nullptr, "Failed to get ImageHeaderInfo for %s",
curImgFile);
ARect imgRect = {
.left = 0, .top = 0,
.right = AImageDecoderHeaderInfo_getWidth(headerInfo),
.bottom = AImageDecoderHeaderInfo_getHeight(headerInfo)
};
ARect dstRect = {
.left =0, .top = 0,
.right = bufInfo.width,
.bottom =bufInfo.height
};
ImageScaleDescriptor scalerInfo(dstRect, imgRect);
rotationAngle = scalerInfo.getScaleRectInfo(&targetRect);
status = AImageDecoder_setTargetSize(decoder, targetRect.right - targetRect.left,
targetRect.bottom - targetRect.top);
ASSERT(ANDROID_IMAGE_DECODER_SUCCESS == status,
"Failed to set target rect (%d, %d), error:%s", bufInfo.width, bufInfo.height,
AImageDecoder_resultToString(status));
// scRGB/sRGB/SCRGB_LINEAR are okay for this sample
ADataSpace dataSpace =
static_cast<ADataSpace>(AImageDecoderHeaderInfo_getDataSpace(headerInfo));
if (dataSpace != ADATASPACE_SCRGB && dataSpace != ADATASPACE_SRGB &&
dataSpace != ADATASPACE_SCRGB_LINEAR) {
status = AImageDecoder_setDataSpace(decoder, ADATASPACE_SRGB);
ASSERT(ANDROID_IMAGE_DECODER_SUCCESS == status,
"Failed to set SRGB color space for %s", curImgFile);
}
decodedImageStride = AImageDecoder_getMinimumStride(decoder);
decodedImageBits.resize(bufInfo.height * decodedImageStride);
}
frameDuration = kDEFAULT_DURATION;
if (!frameInfo) {
// create frameInfo for each image file, delete it when that file decoding is completed.
// for animated image files, frameInfo will be created once and reused for the whole file.
frameInfo = AImageDecoderFrameInfo_create();
ASSERT(frameInfo, "Failed to create FrameInfo object for %s", curImgFile);
}
if (AImageDecoder_isAnimated(decoder) &&
ANDROID_IMAGE_DECODER_SUCCESS == AImageDecoder_getFrameInfo(decoder, frameInfo)) {
frameDuration = AImageDecoderFrameInfo_getDuration(frameInfo);
}
// Create and start decoding thread: the thread is detached right after creation,
// hence it will run to completion.
decodeState = DecodeState::BUSY;
std::thread decodingThread([&] () {
std::unique_lock<std::mutex> threadLock(decodeMutex);
int status = AImageDecoder_decodeImage(decoder,
decodedImageBits.data(),
decodedImageStride,
decodedImageBits.size());
ASSERT(status == ANDROID_IMAGE_DECODER_SUCCESS, "Failed to decode image %s",
curImgFile);
if(!AImageDecoder_isAnimated(decoder) ||
ANDROID_IMAGE_DECODER_SUCCESS != AImageDecoder_advanceFrame(decoder)) {
if (frameInfo) {
AImageDecoderFrameInfo_delete(frameInfo);
frameInfo = nullptr;
}
AImageDecoder_delete(decoder);
decoder = nullptr;
AAsset_close(imgAsset);
imgAsset = nullptr;
fileNames.push(curImgFile);
curImgFile = nullptr;
}
decodeState = DecodeState::COMPLETED;
});
decodingThread.detach();
return true;
}
/**
* Report the decoded frame info.
* param frame - pointer to data structure for decoded frame info
* return bool:
* true - image successfully copied into frame
* false - image is not ready (not decoded yet)
*/
bool ImageDecoder::GetDecodedFrame(DecodeFrameDescriptor* _Nonnull frame) {
if(decodeState == DecodeState::COMPLETED) {
frame->bits = decodedImageBits.data();
frame->width = targetRect.right - targetRect.left;
frame->height = targetRect.bottom - targetRect.top;
frame->stride = decodedImageStride;
frame->rotation = rotationAngle;
frame->displayDuration = frameDuration;
return true;
}
return false;
}
/** GetDecodingBitmapFormat():
* translate the required image format to image decoder's format
*/
AndroidBitmapFormat ImageDecoder::GetDecodingBitmapFormat() {
switch (bufInfo.format) {
case SurfaceFormat::SURFACE_FORMAT_RGBA_8888:
return ANDROID_BITMAP_FORMAT_RGBA_8888;
case SurfaceFormat::SURFACE_FORMAT_RGB_565:
return ANDROID_BITMAP_FORMAT_RGB_565;
default:
LOGE("Unsupported display buffer format (%d) in %s", bufInfo.format, __FUNCTION__);
}
return ANDROID_BITMAP_FORMAT_NONE;
}
/**
* Destructor to wait for outstanding decoding finish before
* tearing down.
*/
ImageDecoder::~ImageDecoder() {
// make sure the decoding thread is finished
std::lock_guard<std::mutex> lock(decodeMutex);
}

View File

@@ -0,0 +1,114 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#pragma once
#include <queue>
#include <mutex>
#include <atomic>
#include <android/asset_manager.h>
#include <android/imagedecoder.h>
enum class DecodeState { IDLE, BUSY, COMPLETED };
enum class SurfaceFormat : unsigned int {
SURFACE_FORMAT_RGBA_8888,
SURFACE_FORMAT_RGBX_8888,
SURFACE_FORMAT_RGB_565,
SURFACE_FORMAT_YUV_420 // Not implemented yet
};
struct DecodeSurfaceDescriptor {
// surface size in pixels
int32_t width, height, stride;
SurfaceFormat format;
};
struct DecodeFrameDescriptor {
int32_t width;
int32_t height;
int32_t stride;
uint8_t* _Nullable bits;
int32_t rotation;
uint64_t displayDuration;
};
/*
* ImageDecoder:
* Select a picture to decode in its own thread. The thread is deleted
* once a picture is decoded into its internal memory. The next picture
* will be decoded when:
* - the decoded frame is retrieved
* - Another DecodeFrame() is called [this is trigger]
* when display format changes, call DestroyDecoder() to release this decoder
* and allocate a new decoder object.
*/
class ImageDecoder {
public:
explicit ImageDecoder(const char* _Nonnull * _Nonnull files, uint32_t count,
DecodeSurfaceDescriptor * _Nullable frameBuf,
AAssetManager* _Nonnull assetMgr);
/**
* Report whether the decoder is currently decoding frames.
* Caller should:
* - call isBusy() to make sure decoder is not busy
* - call DecoderNextImage() to kick start decoding
* - [Optional] call isBusy() to make sure frame is decoded
* - call getDecodedFrame() to retrieve the decoded bits
* - call DecodeNextImage() again to kick start the next frame decoding
* ...
*/
bool IsBusy(void);
/**
* Kick start to decode the next image(or frame). Refer to isBusy() for usage.
* This is non-re-entrant function: do not call if previous frame is not completed
* yet.
* return bool: true - decoding started,
* false - error happened, decoding could not proceed
*/
bool DecodeNextImage(void);
/**
* Report the decoded frame info.
* param frame - pointer to data structure for decoded frame info
* return bool:
* true - image successfully copied into the given frame.
* false - image is not ready (not decoded yet).
*/
bool GetDecodedFrame(DecodeFrameDescriptor* _Nonnull frame);
~ImageDecoder();
private:
DecodeSurfaceDescriptor bufInfo;
AAssetManager * _Nonnull assetMgr;
std::queue<const char* _Nonnull> fileNames;
const char* _Nullable curImgFile;
AAsset* _Nullable imgAsset;
AImageDecoder* _Nullable decoder;
const AImageDecoderHeaderInfo* _Nullable headerInfo;
AImageDecoderFrameInfo* _Nullable frameInfo;
uint64_t frameDuration;
std::atomic<DecodeState> decodeState;
std::vector<uint8_t> decodedImageBits;
int32_t decodedImageStride;
ARect targetRect;
int32_t rotationAngle;
uint32_t bytePerPixel;
std::mutex decodeMutex;
AndroidBitmapFormat GetDecodingBitmapFormat();
};

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#include <android/rect.h>
#include <cstring>
class ImageScaleDescriptor {
public:
explicit ImageScaleDescriptor(ARect dRect, ARect sRect) {
requestRect = dRect;
srcRect = sRect;
memset(&resultRect, 0, sizeof(requestRect));
rotationAngle = 0;
};
/**
* Calculate the min scaled rectangle size.
* @param rect: resulting scaled rectangle
* @return counter clockwise rotation angle
*/
int32_t getScaleRectInfo(ARect* _Nonnull rect) {
bool srcPortrait = true;
int32_t srcH = srcRect.bottom - srcRect.top;
int32_t srcW = srcRect.right - srcRect.left;
if(srcW > srcH) srcPortrait = false;
bool dstPortrait = true;
int32_t dstH = requestRect.bottom - requestRect.top;
int32_t dstW = requestRect.right - requestRect.left;
if(dstW > dstH) dstPortrait = false;
int32_t height =0, width = 0;
if(dstPortrait != srcPortrait) {
rotationAngle = 270;
float hRatio = static_cast<float>(dstH) / srcW;
float wRatio = static_cast<float>(dstW) / srcH;
float ratio = (hRatio < wRatio)? hRatio : wRatio;
width = static_cast<int32_t>(ratio * srcW);
height = static_cast<int32_t>(ratio * srcH);
} else {
rotationAngle = 0;
float hRatio = static_cast<float>(dstH) / srcH;
float wRatio = static_cast<float>(dstW) / srcW;
float ratio = (hRatio < wRatio) ? hRatio : wRatio;
height = static_cast<int32_t>(ratio * srcH);
width = static_cast<int32_t>(ratio * srcW);
}
rect->left = 0;
rect->right = width;
rect->top = 0;
rect->bottom = height;
return rotationAngle;
}
~ImageScaleDescriptor() {};
private:
ARect requestRect, srcRect, resultRect;
int32_t rotationAngle; // Counter Clock rotation Angle
};

View File

@@ -0,0 +1,278 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#include <cstdio>
#include <cassert>
#include <android/native_window.h>
#include <android_native_app_glue.h>
#include "image_viewer.h"
#include "image_decoder.h"
#include "display_timer.h"
#include "android_debug.h"
/*
* Compressed files that are inside assets folder:
* they will be decoded and displayed as slide-show
* decoding will happen in its own thread
*/
const char * imageFiles[] = {
"images/android.webp",
"images/android.avif",
"images/android.heic",
};
const int kFRAME_COUNT = sizeof(imageFiles) / sizeof(imageFiles[0]);
ImageViewer::ImageViewer(android_app* _Nullable app) :
androidApp(app),
imgDecoder(nullptr),
animatingInProgress(false) {
}
ImageViewer::~ImageViewer() {
delete imgDecoder;
}
struct android_app* _Nullable ImageViewer::GetAndroidApp(void) const{
return androidApp;
}
void ImageViewer::StartAnimation(bool start) {
animatingInProgress = start;
}
bool ImageViewer::IsAnimating(void) const {
return animatingInProgress;
}
void ImageViewer::TerminateDisplay(void) {
StartAnimation(false);
}
// Engine class implementations
bool ImageViewer::PrepareDrawing(void) {
// create decoder
if (imgDecoder) {
delete imgDecoder;
imgDecoder = nullptr;
}
ANativeWindow_Buffer buf;
if (ANativeWindow_lock(androidApp->window, &buf, NULL) < 0) {
LOGW("Unable to lock window buffer to create decoder");
return false;
}
ClearFrameBuffer(&buf);
ANativeWindow_unlockAndPost(androidApp->window);
DecodeSurfaceDescriptor descriptor;
switch (buf.format) {
case WINDOW_FORMAT_RGB_565:
descriptor.format = SurfaceFormat::SURFACE_FORMAT_RGB_565;
break;
case WINDOW_FORMAT_RGBX_8888:
descriptor.format = SurfaceFormat::SURFACE_FORMAT_RGBX_8888;
break;
case WINDOW_FORMAT_RGBA_8888:
descriptor.format = SurfaceFormat::SURFACE_FORMAT_RGBA_8888;
break;
default:
return false;
}
descriptor.width = buf.width;
descriptor.height = buf.height;
descriptor.stride = buf.stride;
imgDecoder = new ImageDecoder(imageFiles, kFRAME_COUNT, &descriptor, androidApp->activity->assetManager);
if (!imgDecoder) {
LOGE("Unable to create ImageDecoder instance in %s", __FUNCTION__);
return false;
}
// Kick start the 1st image decoding
imgDecoder->DecodeNextImage();
return true;
}
/**
* Only copy decoded webp picture when:
* - current frame has been on for kFrame_DISPLAY_TIME seconds
* - a new picture is decoded
* After copying, start decoding the next frame
*/
bool ImageViewer::UpdateDisplay(void) {
if (!androidApp->window || !imgDecoder) {
LOGE("Decoder(%p) or NativeWindow(%p) is not ready in %s",
androidApp->window, imgDecoder, __FUNCTION__);
return false;
}
if(!displayTimer.IsExpired() || imgDecoder->IsBusy()) {
// current frame is displayed less than required duration or image is not ready
return false;
}
DecodeFrameDescriptor rawImg;
if(!imgDecoder->GetDecodedFrame(&rawImg))
return false;
ANativeWindow_Buffer buffer;
if (ANativeWindow_lock(androidApp->window, &buffer, nullptr) < 0) {
LOGW("Unable to lock window buffer");
return false;
}
ClearFrameBuffer(&buffer); // clear the screen
UpdateFrameBuffer(&buffer, &rawImg);
ANativeWindow_unlockAndPost(androidApp->window);
displayTimer.Reset(rawImg.displayDuration);
imgDecoder->DecodeNextImage();
return true;
}
/**
* Internal function to translate ANativeWindow format to byte per pixel.
* @param buf: points to the ANativeWindow format.
* @return int32_t: byte per pixel for the give ANativeWindow
*/
int32_t ImageViewer::getNativeWindowBpp(ANativeWindow_Buffer* _Nonnull buf) {
uint32_t bpp = 0;
switch (buf->format) {
case WINDOW_FORMAT_RGB_565:
bpp = 2;
break;
case WINDOW_FORMAT_RGBA_8888:
case WINDOW_FORMAT_RGBX_8888:
bpp = 4;
break;
default:
LOGE("Could not recognize the NativeWindow format: %d in %s",
buf->format, __FUNCTION__);
assert(0);;
}
return bpp;
}
/**
* ClearFrameBuffer: zero out the framebuffer
* @param buf: the display buffer to clear
*/
void ImageViewer::ClearFrameBuffer(ANativeWindow_Buffer * _Nonnull buf) {
uint8_t *dst = reinterpret_cast<uint8_t *> (buf->bits);
int32_t bpp = getNativeWindowBpp(buf);
uint32_t dstStride, width;
dstStride = buf->stride * bpp;
width = buf->width * bpp;
// blank the screen
for (auto height = 0; height < buf->height; ++height) {
memset(dst, 0, width);
dst += dstStride;
}
}
/**
* UpdateFrameBuffer():
* Internal function to perform bits copying onto current frame buffer
* @param buf: already locked-down target NativeWindow to update,
* @param frame: already decoded raw image to display.
* - if nullptr, clear screen
* - otherwise, pixel by pixel copy with letterbox enabled
*/
void ImageViewer::UpdateFrameBuffer(ANativeWindow_Buffer * _Nonnull buf,
DecodeFrameDescriptor* _Nonnull frame) {
uint8_t *dst = reinterpret_cast<uint8_t *> (buf->bits);
int32_t bpp = getNativeWindowBpp(buf);
uint32_t dstStride, width;
dstStride = buf->stride * bpp;
width = buf->width * bpp;
// perform pixel by pixel copying.
uint8_t *src = frame->bits;
int32_t start_x;
int32_t start_y;
if(frame->rotation == 0 || frame->rotation == 180) {
start_x = (buf->width - frame->width) / 2;
start_y = (buf->height - frame->height) / 2;
} else if (frame->rotation == 90 || frame->rotation == 270){
// need to rotate 90 or 270 degree
start_x = (buf->width - frame->height) / 2;
start_y = (buf->height - frame->width) / 2;
} else {
LOGE("Wrong frame->rotation(%d) to %s", frame->rotation, __FUNCTION__ );
return;
}
assert(start_x >= 0 && start_y >= 0);
dst += (start_y * dstStride) + (start_x * bpp);
// line by line copying for 0 degree rotation
if(frame->rotation == 0) {
for (auto height = 0; height < frame->height; ++height) {
memcpy(dst, src, width);
dst += dstStride, src += frame->stride;
}
return;
}
if(frame->rotation == 180) {
src += frame->stride * (frame->height - 1);
for (auto height = 0; height < frame->height; ++height) {
memcpy(dst, src, width);
dst += dstStride, src -= frame->stride;
}
return;
}
// one pixel at a time, rotate on the fly
if (frame->rotation == 90) {
// one pixel at a time, rotate on the fly
src += (frame->width - 1) * bpp;
for (auto height = 0; height < frame->width; height++) {
uint8_t *srcLine = src;
uint8_t *dstLine = dst;
for (auto w = 0; w < frame->height; w++) {
memcpy(dstLine, srcLine, bpp);
dstLine += bpp; // advance one pixel
srcLine += frame->stride; // move down a line
}
dst += dstStride; // advance 1 line
src -= bpp; // move 1 pixel backward for the next source column
}
return;
}
if(frame->rotation == 270) {
src += (frame->height - 1) * frame->stride;
for (auto height = 0; height < frame->width; height++) {
uint8_t *srcLine = src;
uint8_t *dstLine = dst;
for (auto w = 0; w < frame->height; w++) {
memcpy(dstLine, srcLine, bpp);
dstLine += bpp; // advance one pixel
srcLine -= frame->stride; // move up a line
}
dst += dstStride; // advance 1 line
src += bpp; // move 1 pixel forward for the next source column
}
return;
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* 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.
*/
#pragma once
#include <android/native_window.h>
#include <android_native_app_glue.h>
#include "image_decoder.h"
#include "display_timer.h"
/*
* Main object handles Android window frame update, and use webp to decode
* pictures
*/
class ImageViewer {
public:
explicit ImageViewer(android_app* _Nullable app);
~ImageViewer();
// Functions to connect to native_activity
struct android_app* _Nullable GetAndroidApp(void) const;
void StartAnimation(bool start);
bool IsAnimating(void) const;
void TerminateDisplay(void);
// PrepareDrawing(): Initialize the Engine with current native window geometry
// and blank current screen to avoid garbage displaying on device
bool PrepareDrawing(void);
// Update the next image into display window when
// - decoding is completed, and
// - the current image has been displayed longer than the requested time
bool UpdateDisplay(void);
private:
void ClearFrameBuffer(ANativeWindow_Buffer* _Nonnull buf);
void UpdateFrameBuffer(ANativeWindow_Buffer * _Nonnull buf,
DecodeFrameDescriptor* _Nonnull frame);
int32_t getNativeWindowBpp(ANativeWindow_Buffer* _Nonnull buf);
struct android_app* _Nullable androidApp;
ImageDecoder* _Nullable imgDecoder;
bool animatingInProgress;
DisplayTimer displayTimer;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">NdkImageDecoder</string>
</resources>

View File

@@ -1 +1 @@
include ':view'
include ':image-decoder'