Cloud save: working implementation, no dialog to select your games yet

This commit is contained in:
Sergii Pylypenko
2014-07-09 20:50:16 +03:00
parent ed57fac3c9
commit 8e2ad620d8
18 changed files with 663 additions and 53 deletions

View File

@@ -39,4 +39,14 @@ class CloudSave
public void onActivityResult(int request, int response, Intent data) {
}
public boolean save(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs)
{
return false;
}
public boolean load(String filename, String saveId, String dialogTitle)
{
return false;
}
}

View File

@@ -80,6 +80,8 @@ import android.view.Display;
import android.text.InputType;
import android.util.Log;
import android.view.Surface;
import android.app.ProgressDialog;
public class MainActivity extends Activity
@@ -106,6 +108,8 @@ public class MainActivity extends Activity
_layout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT));
_layout2 = new LinearLayout(this);
_layout2.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
loadingDialog = new ProgressDialog(this);
loadingDialog.setMessage(getString(R.string.accessing_network));
final Semaphore loadedLibraries = new Semaphore(0);
@@ -220,7 +224,7 @@ public class MainActivity extends Activity
Intent intent = new Intent(this, DummyService.class);
startService(intent);
}
_cloudSave = new CloudSave(this);
cloudSave = new CloudSave(this);
}
public void setUpStatusLabel()
@@ -429,19 +433,19 @@ public class MainActivity extends Activity
@Override
protected void onStart() {
super.onStart();
_cloudSave.onStart();
cloudSave.onStart();
}
@Override
protected void onStop() {
super.onStart();
_cloudSave.onStop();
cloudSave.onStop();
}
@Override
public void onActivityResult(int request, int response, Intent data) {
super.onActivityResult(request, response, data);
_cloudSave.onActivityResult(request, response, data);
cloudSave.onActivityResult(request, response, data);
}
public void showScreenKeyboardWithoutTextInputField()
@@ -1235,7 +1239,8 @@ public class MainActivity extends Activity
private LinearLayout _layout = null;
private LinearLayout _layout2 = null;
private Advertisement _ad = null;
public CloudSave _cloudSave = null;
public CloudSave cloudSave = null;
public ProgressDialog loadingDialog = null;
private FrameLayout _videoLayout = null;
private EditText _screenKeyboard = null;

View File

@@ -834,6 +834,52 @@ class DemoRenderer extends GLSurfaceView_SDL.Renderer
context.requestNewAdvertisement();
}
public boolean cloudSave(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs)
{
context.runOnUiThread(new Runnable()
{
public void run()
{
context.loadingDialog.show();
}
});
boolean ret = context.cloudSave.save(filename, saveId, dialogTitle, description, imageFile, playedTimeMs);
context.runOnUiThread(new Runnable()
{
public void run()
{
context.loadingDialog.dismiss();
}
});
return ret;
}
public boolean cloudLoad(String filename, String saveId, String dialogTitle)
{
context.runOnUiThread(new Runnable()
{
public void run()
{
context.loadingDialog.show();
}
});
boolean ret = context.cloudSave.load(filename, saveId, dialogTitle);
context.runOnUiThread(new Runnable()
{
public void run()
{
context.loadingDialog.dismiss();
}
});
return ret;
}
private int PowerOf2(int i)
{
int value = 1;

View File

@@ -19,8 +19,24 @@ package com.google.example.games.basegameutils;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.graphics.BitmapFactory;
import android.graphics.Bitmap;
import java.io.*;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.gms.common.Scopes;
import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesStatusCodes;
import com.google.android.gms.games.snapshot.Snapshot;
import com.google.android.gms.games.snapshot.SnapshotMetadata;
import com.google.android.gms.games.snapshot.SnapshotMetadataBuffer;
import com.google.android.gms.games.snapshot.SnapshotMetadataChange;
import com.google.android.gms.games.snapshot.Snapshots;
public class CloudSave implements GameHelper.GameHelperListener {
@@ -40,54 +56,225 @@ public class CloudSave implements GameHelper.GameHelperListener {
mHelper.setup(this);
}
public GameHelper getGameHelper() {
return mHelper;
}
public void onStart() {
public void onStart()
{
mHelper.onStart(parent);
}
public void onStop() {
public void onStop()
{
mHelper.onStop();
}
public boolean save(String filename, String description, String imageFile)
public void onActivityResult(int request, int response, Intent data)
{
mHelper.onActivityResult(request, response, data);
}
public synchronized boolean save(String filename, String saveId, String dialogTitle, String description, String imageFile, long playedTimeMs)
{
Log.d("SDL", "CloudSave: save: file " + filename + " saveId " + saveId + " dialogTitle " + dialogTitle + " desc " + description + " imageFile " + imageFile + " playedTime " + playedTimeMs);
if( !signIn() )
return false;
if( !filename.startsWith("/") )
filename = Globals.DataDir + "/" + filename;
if( imageFile.length() > 0 && !imageFile.startsWith("/") )
imageFile = Globals.DataDir + "/" + imageFile;
try
{
if( saveId == null || saveId.length() == 0 )
{
Log.i("SDL", "CloudSave: save: user dialog is not supported yet");
return false;
}
Snapshots.OpenSnapshotResult result = Games.Snapshots.open(getApiClient(), saveId, true).await();
Snapshot crapshot = processSnapshotOpenResult(result, 0);
if( crapshot == null )
return false;
crapshot.writeBytes(readFile(filename));
Bitmap bmp = BitmapFactory.decodeFile(imageFile);
while( bmp != null && bmp.getByteCount() > Games.Snapshots.getMaxCoverImageSize(getApiClient()) )
bmp = Bitmap.createScaledBitmap(bmp, bmp.getWidth() * 3 / 4, bmp.getHeight() * 3 / 4, true);
SnapshotMetadataChange.Builder metadataChange = new SnapshotMetadataChange.Builder()
.setDescription(description)
.setPlayedTimeMillis(playedTimeMs);
if( bmp != null )
metadataChange.setCoverImage(bmp);
Games.Snapshots.commitAndClose(getApiClient(), crapshot, metadataChange.build())
.setResultCallback(new ResultCallback<Snapshots.CommitSnapshotResult>()
{
public void onResult(Snapshots.CommitSnapshotResult r)
{
Log.i("SDL", "CloudSave: save final net sync result: " + r.getStatus().toString());
}
});
Log.i("SDL", "CloudSave: save succeeded");
return true;
}
catch(Exception e)
{
Log.i("SDL", "CloudSave: save failed: " + e.toString());
}
return false;
}
public boolean load (String filename)
public synchronized boolean load(String filename, String saveId, String dialogTitle)
{
Log.d("SDL", "CloudSave: load: file " + filename + " saveId " + saveId + " dialogTitle " + dialogTitle);
if( !signIn() )
return false;
if( !filename.startsWith("/") )
filename = Globals.DataDir + "/" + filename;
try
{
if( saveId == null || saveId.length() == 0 )
{
Log.i("SDL", "CloudSave: load: user dialog is not supported yet");
return false;
}
Snapshots.OpenSnapshotResult result = Games.Snapshots.open(getApiClient(), saveId, false).await();
if (result.getStatus().getStatusCode() != GamesStatusCodes.STATUS_OK)
{
Log.i("SDL", "CloudSave: load: failed to load game " + saveId + ": " + result.getStatus());
return false;
}
boolean written = writeFile(filename, result.getSnapshot().readFully());
Log.i("SDL", "CloudSave: load: status: " + written);
return written;
}
catch(Exception e)
{
Log.i("SDL", "CloudSave: load failed: " + e.toString());
}
return false;
}
public class loadDialogResult
{
public boolean status = false;
public String filename = "";
}
public loadDialogResult loadDialog(String filename, String dialogTitle)
{
loadDialogResult res = new loadDialogResult();
res.status = false;
res.filename = "";
return res;
}
// ===== Private API =====
public void onActivityResult(int request, int response, Intent data) {
mHelper.onActivityResult(request, response, data);
boolean signInSucceeded = false;
boolean signInFailed = false;
public boolean signIn()
{
//Log.i("SDL", "CloudSave: signIn()");
if( !isSignedIn() )
{
signInSucceeded = false;
signInFailed = false;
Log.i("SDL", "CloudSave: beginUserInitiatedSignIn()");
beginUserInitiatedSignIn();
Log.i("SDL", "CloudSave: beginUserInitiatedSignIn() exit");
while (!signInSucceeded && !signInFailed)
{
try { Thread.sleep(300); } catch( Exception e ) {}
}
return signInSucceeded;
}
return true;
}
public void onSignInSucceeded() {
Log.i("SDL", "CloudSave: onSignInSucceeded()");
signInSucceeded = true;
}
public void onSignInFailed() {
Log.i("SDL", "CloudSave: onSignInFailed()");
signInFailed = true;
}
public Snapshot processSnapshotOpenResult(Snapshots.OpenSnapshotResult result, int retryCount)
{
Snapshot mResolvedSnapshot = null;
retryCount++;
int status = result.getStatus().getStatusCode();
Log.i("SDL", "CloudSave: processSnapshotOpenResult status: " + result.getStatus());
if (status == GamesStatusCodes.STATUS_OK) {
return result.getSnapshot();
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONTENTS_UNAVAILABLE) {
return result.getSnapshot();
} else if (status == GamesStatusCodes.STATUS_SNAPSHOT_CONFLICT) {
Snapshot snapshot = result.getSnapshot();
Snapshot conflictSnapshot = result.getConflictingSnapshot();
// Resolve between conflicts by selecting the newest of the conflicting snapshots.
mResolvedSnapshot = snapshot;
if (snapshot.getMetadata().getPlayedTime() == conflictSnapshot.getMetadata().getPlayedTime()) {
if (snapshot.getMetadata().getLastModifiedTimestamp() < conflictSnapshot.getMetadata().getLastModifiedTimestamp()) {
mResolvedSnapshot = conflictSnapshot;
}
} else if (snapshot.getMetadata().getPlayedTime() < conflictSnapshot.getMetadata().getPlayedTime()) {
mResolvedSnapshot = conflictSnapshot;
}
Snapshots.OpenSnapshotResult resolveResult = Games.Snapshots.resolveConflict(
getApiClient(), result.getConflictId(), mResolvedSnapshot)
.await();
if (retryCount < 3) {
return processSnapshotOpenResult(resolveResult, retryCount);
} else {
Log.i("SDL", "CloudSave: could not resolve snapshot conflict");
}
}
Log.i("SDL", "CloudSave: could not get savegame snapshot");
return null;
}
static public byte[] readFile(String filename)
{
int len = (int)(new File(filename).length());
if( len == 0 )
return new byte[0];
try
{
byte buf[] = new byte[len];
if( new FileInputStream(filename).read(buf, 0, len) != len )
return new byte[0];
return buf;
}
catch( Exception e )
{
Log.i("SDL", "CloudSave: readFile() error: " + e.toString());
}
return new byte[0];
}
static public boolean writeFile(String filename, byte[] data)
{
try
{
BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(filename));
out.write(data, 0, data.length);
out.close();
return true;
}
catch( Exception e )
{
Log.i("SDL", "CloudSave: writeFile() error: " + e.toString() + " file " + filename);
}
return false;
}
public GameHelper getGameHelper() {
return mHelper;
}
public GoogleApiClient getApiClient() {

View File

@@ -2,6 +2,8 @@
grep '<string name=' values/strings.xml | while read str; do
if echo "$str" | grep 'translatable="false"' ; then continue; fi
var=`echo $str | sed 's/<string name=["]\([^"]*\).*/\1/'`
text=`echo $str | sed 's/<string name=["][^"]*["]>\([^<]*\).*/\1/'`
if [ "$var" = "app_name" ]; then

View File

@@ -188,4 +188,5 @@
<string name="gamehelper_app_misconfigured">The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information.</string>
<string name="gamehelper_license_failed">License check failed.</string>
<string name="gamehelper_unknown_error">Unknown error.</string>
<string name="accessing_network">Accessing network, please wait</string>
</resources>

View File

@@ -161,4 +161,5 @@
<string name="gamehelper_app_misconfigured">The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information.</string>
<string name="gamehelper_license_failed">License check failed.</string>
<string name="gamehelper_unknown_error">Unknown error.</string>
<string name="accessing_network">Accessing network, please wait</string>
</resources>

View File

@@ -160,4 +160,5 @@
<string name="gamehelper_app_misconfigured">The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information.</string>
<string name="gamehelper_license_failed">License check failed.</string>
<string name="gamehelper_unknown_error">Unknown error.</string>
<string name="accessing_network">Accessing network, please wait</string>
</resources>

View File

@@ -192,6 +192,8 @@
<string name="gamehelper_app_misconfigured">The application is incorrectly configured. Check that the package name and signing certificate match the client ID created in Developer Console. Also, if the application is not yet published, check that the account you are trying to sign in with is listed as a tester account. See logs for more information.</string>
<string name="gamehelper_license_failed">License check failed.</string>
<string name="gamehelper_unknown_error">Unknown error.</string>
<string name="accessing_network">Accessing network, please wait</string>
<string name="google_play_game_services_app_id" translatable="false">==GOOGLEPLAYGAMESERVICES_APP_ID==</string>
</resources>

View File

@@ -3,7 +3,7 @@
APP_MODULES := application sdl-1.2 sdl_main sdl_native_helpers jpeg png ogg flac vorbis freetype tremor ogg
# To filter out static libs from all libs in makefile
APP_AVAILABLE_STATIC_LIBS := jpeg png tremor freetype xerces ogg tremor vorbis flac boost_date_time boost_filesystem boost_iostreams boost_program_options boost_regex boost_signals boost_system boost_thread boost_locale glu icudata icutest icui18n icuio icule iculx icutu icuuc
APP_AVAILABLE_STATIC_LIBS := jpeg png tremor freetype xerces ogg tremor vorbis flac boost_date_time boost_filesystem boost_iostreams boost_program_options boost_regex boost_signals boost_system boost_thread boost_locale glu icudata icutest icui18n icuio icule iculx icutu icuuc sdl_savepng
APP_ABI := armeabi

View File

@@ -1251,7 +1251,8 @@ BNX_BOOL saveGame( BNX_GAME *game )
sysFPut16( game->level_count, file );
fclose( file );
SDL_ANDROID_CloudSave( sysGetFullFileName( csSaveGameName ) );
SDL_SaveBMP(SDL_GetVideoSurface(), "screenshot.bmp");
SDL_ANDROID_CloudSave( sysGetFullFileName( csSaveGameName ), "save", "Biniax2", "savegame", "screenshot.bmp", game->moves );
return BNX_TRUE;
}
@@ -1262,7 +1263,7 @@ BNX_BOOL loadGame( BNX_GAME *game )
BNX_INT32 i;
BNX_INT32 j;
SDL_ANDROID_CloudLoad( sysGetFullFileName( csSaveGameName ) );
SDL_ANDROID_CloudLoad( sysGetFullFileName( csSaveGameName ), "save", "Biniax2" );
if ( sysGetFileLen( sysGetFullFileName( csSaveGameName ) ) != cSaveFileSize )
return BNX_FALSE;

View File

@@ -96,26 +96,27 @@ extern DECLSPEC void SDLCALL SDL_ANDROID_CloseAudioRecording(void);
/*
Save the file to the cloud, filename must be already present on disk.
This function will block, until user signs in to the cloud account, and presses Save button.
Description and imageFile may be NULL.
If saveId is NULL or empty, user will select the savegame from the dialog, if it's not empty -
do not show any dialog to the user, except foir sign-in, and write the savegame with specified ID.
dialogTitle may be NULL.
description and screenshotFile may be NULL. playedTimeMs is used for conflict resolution -
savegame with longer play time will get priority.
Use SDL_SaveBMP(SDL_GetVideoSurface(), "screenshot.bmp") to create the screenshot.
Returns 1 if save succeeded, 0 if user aborted sign-in, or there was no network available.
*/
extern DECLSPEC int SDLCALL SDL_ANDROID_CloudSave(const char * filename, const char * description, const char * imageFile);
extern DECLSPEC int SDLCALL SDL_ANDROID_CloudSave(const char *filename, const char *saveId, const char *dialogTitle,
const char *description, const char *screenshotFile, long long playedTimeMs);
/*
Load the specified file from the cloud.
This function will block, until user signs in to the cloud account.
Returns 1 if load succeeded, 0 if user aborted sign-in, or there was no network available.
*/
extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoad(const char *filename);
/*
Show the file loading dialog, to allow user to pick up a file from the cloud.
This function will block, until user signs in to the cloud account, and selects a file.
The resulting filename is written to the filename buffer, which must be 512 bytes or more.
This function will block, until user signs in to the cloud account, and selects a savegame.
The resulting savegame is written to the passed filename.
If saveId is NULL or empty, user will select the savegame from the dialog, if it's not empty -
do not show any dialog to the user, except foir sign-in, and read the savegame with specified ID.
dialogTitle may be NULL.
Returns 1 if load succeeded, 0 if user aborted sign-in, or there was no network available.
*/
extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoadDialog(char *filename, int len, const char *dialogTitle);
extern DECLSPEC int SDLCALL SDL_ANDROID_CloudLoad(const char *filename, const char *saveId, const char *dialogTitle);
#ifdef __cplusplus
}

View File

@@ -80,6 +80,8 @@ static jmethodID JavaGetAdvertisementParams = NULL;
static jmethodID JavaSetAdvertisementVisible = NULL;
static jmethodID JavaSetAdvertisementPosition = NULL;
static jmethodID JavaRequestNewAdvertisement = NULL;
static jmethodID JavaRequestCloudSave = NULL;
static jmethodID JavaRequestCloudLoad = NULL;
static int glContextLost = 0;
static int showScreenKeyboardDeferred = 0;
static const char * showScreenKeyboardOldText = "";
@@ -347,6 +349,11 @@ JAVA_EXPORT_NAME(DemoRenderer_nativeInitJavaCallbacks) ( JNIEnv* env, jobject t
JavaSetAdvertisementVisible = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "setAdvertisementVisible", "(I)V");
JavaSetAdvertisementPosition = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "setAdvertisementPosition", "(II)V");
JavaRequestNewAdvertisement = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "requestNewAdvertisement", "()V");
JavaRequestCloudSave = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "cloudSave",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;J)Z");
JavaRequestCloudLoad = (*JavaEnv)->GetMethodID(JavaEnv, JavaRendererClass, "cloudLoad",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z");
ANDROID_InitOSKeymap();
}
@@ -467,20 +474,55 @@ int SDLCALL SDL_ANDROID_RequestNewAdvertisement(void)
return 1;
}
int SDLCALL SDL_ANDROID_CloudSave(const char * filename, const char * description, const char * imageFile)
int SDLCALL SDL_ANDROID_CloudSave(const char *filename, const char *saveId, const char *dialogTitle,
const char *description, const char *screenshotFile, long long playedTimeMs)
{
return 0;
if( !filename )
return 0;
if( !saveId )
saveId = "";
if( !dialogTitle )
dialogTitle = "";
if( !description )
description = "";
if( !screenshotFile )
screenshotFile = "";
(*JavaEnv)->PushLocalFrame(JavaEnv, 5);
jstring s1 = (*JavaEnv)->NewStringUTF(JavaEnv, filename);
jstring s2 = (*JavaEnv)->NewStringUTF(JavaEnv, saveId);
jstring s3 = (*JavaEnv)->NewStringUTF(JavaEnv, dialogTitle);
jstring s4 = (*JavaEnv)->NewStringUTF(JavaEnv, description);
jstring s5 = (*JavaEnv)->NewStringUTF(JavaEnv, screenshotFile);
int result = (*JavaEnv)->CallBooleanMethod( JavaEnv, JavaRenderer, JavaRequestCloudSave, s1, s2, s3, s4, s5, playedTimeMs );
(*JavaEnv)->DeleteLocalRef(JavaEnv, s5);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s4);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s3);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s2);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s1);
(*JavaEnv)->PopLocalFrame(JavaEnv, NULL);
return result;
}
int SDLCALL SDL_ANDROID_CloudLoad(const char *filename)
int SDLCALL SDL_ANDROID_CloudLoad(const char *filename, const char *saveId, const char *dialogTitle)
{
return 0;
if( !filename )
return 0;
if( !saveId )
saveId = "";
if( !dialogTitle )
dialogTitle = "";
(*JavaEnv)->PushLocalFrame(JavaEnv, 3);
jstring s1 = (*JavaEnv)->NewStringUTF(JavaEnv, filename);
jstring s2 = (*JavaEnv)->NewStringUTF(JavaEnv, saveId);
jstring s3 = (*JavaEnv)->NewStringUTF(JavaEnv, dialogTitle);
int result = (*JavaEnv)->CallBooleanMethod( JavaEnv, JavaRenderer, JavaRequestCloudLoad, s1, s2, s3 );
(*JavaEnv)->DeleteLocalRef(JavaEnv, s3);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s2);
(*JavaEnv)->DeleteLocalRef(JavaEnv, s1);
(*JavaEnv)->PopLocalFrame(JavaEnv, NULL);
return result;
}
int SDLCALL SDL_ANDROID_CloudLoadDialog(char *filename, int len, const char *dialogTitle)
{
return 0;
}
// Dummy callback for SDL2 to satisfy linker
extern void SDL_Android_Init(JNIEnv* env, jclass cls);

View File

@@ -0,0 +1,19 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := sdl_savepng
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(LOCAL_PATH)/../png/include $(LOCAL_PATH)/../sdl-$(SDL_VERSION)/include $(LOCAL_PATH)/include
LOCAL_CPP_EXTENSION := .cpp
LOCAL_SRC_FILES := $(notdir $(wildcard $(LOCAL_PATH)/*.c))
LOCAL_STATIC_LIBRARIES := png
LOCAL_SHARED_LIBRARIES := sdl-$(SDL_VERSION)
LOCAL_LDLIBS := -lz
include $(BUILD_STATIC_LIBRARY)

View File

@@ -0,0 +1,101 @@
# SDL_SavePNG
Minimal libpng interface to save SDL_Surfaces as PNG files.
You might want to take a look in "savepng.h" - it is much shorter and simpler
than this README.
## Install
Add "savepng.c" and "savepng.h" to your project.
Link the libpng library, i.e. add the `-lpng` LDFLAG (even if you already have
`-lSDL_image`).
## Use
```
#include "savepng.h"
SDL_Surface *bmp = ... //your surface
if (SDL_SavePNG(bmp, "image.png")) { //boring way with error checking
printf("Unable to save png -- %s\n", SDL_GetError());
}
```
As you can see, `SDL_SavePNG` accepts an SDL_Surface and a filename for it's
input. Similar to SDL_SaveBMP, it is a wrapper around the actual RWops-based
`SDL_SavePNG_RW` function, so you could use that, if needed.
Lastly, there is `SDL_PNGFormatAlpha`, modeled after SDL_DisplayFormatAlpha,
that would convert *any SDL_Surface* to an *SDL_Surface suitable for PNG
output*. Each call to `SDL_PNGFormatAlpha` produces a **new** SDL_Surface that
**must** be freed using `SDL_FreeSurface`.
```
//safest way, usefull for 'screen' surface
SDL_Surface *tmp = SDL_PNGFormatAlpha(screen);
SDL_SavePNG(tmp, "screenshot.png");
SDL_FreeSurface(tmp)
```
Such conversion is actually only required for *one* surface format (see below),
and would do **nothing** for all other formats, making it **very fast**. The
format in question is:
### 32-bpp surfaces without alpha
There is a interesting caveat of combining naive libpng and cunning SDL in a
32-bpp video mode.
The *screen* surface (obtained by `SDL_SetVideoMode` or similarly) might (and
will!) ignore it's alpha-component even in the 32bpp mode. Meaning that an
0xAARRGGBB color would be blitted as 0xFFrrggbb irregardless, as if it was a
24bpp color.
Since screen itself is never blitted onto anything else, ignoring the alpha
makes perfect sense. However, unlike 24bpp images, the alpha component *does*
exist. Thus, when such surface is saved, it appears to be completely
transparent, as the alpha values for each pixel are set to 0.
Depending on your video mode, you might or might not need to first convert your
surface using `SDL_PNGFormatAlpha`. If you have absolute control over the video
surface, you can force it to 24bpp (or less) mode, which would avoid the
problem.
If the surface passed to `SDL_PNGFormatAlpha` is already suitable, a no-op is
performed. It is very fast, so you should probably always convert your surfaces
before saving.
### No text chunks
Unfortunately, a simplistic interface such as SDL_SavePNG provides no means to
write PNG meta-data. If you need to add iTXT chunks to your PNGs, you would
have to modify this code or write your own version.
If you have some kind of simple API, that would be thematically consistent with
SDL, in mind -- please share.
## Demo
See `main.c` and `Makefile` for an example program. It too is shorter than this
README.
# About
The problem in question is very simple, and this little piece of functionality
was implemented and re-implemented multiple times by multiple authors (notably,
Angelo "Encelo" Theodorou and Darren Grant, among others). I decided to write
my own version to ensure it's correctness, learn more about libpng, and to
provide a copy-pastable, maintained, libpng15-aware, palette-supporting
variation that I could link to. You can view it as a continuation of their
efforts.
SDL_Image would've been perfect place for this, but that library has different
purposes.
*Next up: code to load SDL_Surfaces as OpenGL 1.1 textures. J/K ;)*
# Copying
SDL_SavePNG is available under the zlib/libpng license.

View File

@@ -0,0 +1,36 @@
#ifndef _SDL_SAVEPNG
#define _SDL_SAVEPNG
/*
* SDL_SavePNG -- libpng-based SDL_Surface writer.
*
* This code is free software, available under zlib/libpng license.
* http://www.libpng.org/pub/png/src/libpng-LICENSE.txt
*/
#include <SDL_video.h>
/*
* Save an SDL_Surface as a PNG file.
*
* Returns 0 success or -1 on failure, the error message is then retrievable
* via SDL_GetError().
*/
#define SDL_SavePNG(surface, file) \
SDL_SavePNG_RW(surface, SDL_RWFromFile(file, "wb"), 1)
/*
* Save an SDL_Surface as a PNG file, using writable RWops.
*
* surface - the SDL_Surface structure containing the image to be saved
* dst - a data stream to save to
* freedst - non-zero to close the stream after being written
*
* Returns 0 success or -1 on failure, the error message is then retrievable
* via SDL_GetError().
*/
extern int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *rw, int freedst);
/*
* Return new SDL_Surface with a format suitable for PNG output.
*/
extern SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src);
#endif

View File

@@ -0,0 +1,155 @@
/*
* SDL_SavePNG -- libpng-based SDL_Surface writer.
*
* This code is free software, available under zlib/libpng license.
* http://www.libpng.org/pub/png/src/libpng-LICENSE.txt
*/
#include <SDL.h>
#include <png.h>
#define SUCCESS 0
#define ERROR -1
#define USE_ROW_POINTERS
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
#define rmask 0xFF000000
#define gmask 0x00FF0000
#define bmask 0x0000FF00
#define amask 0x000000FF
#else
#define rmask 0x000000FF
#define gmask 0x0000FF00
#define bmask 0x00FF0000
#define amask 0xFF000000
#endif
/* libpng callbacks */
static void png_error_SDL(png_structp ctx, png_const_charp str)
{
SDL_SetError("libpng: %s\n", str);
}
static void png_write_SDL(png_structp png_ptr, png_bytep data, png_size_t length)
{
SDL_RWops *rw = (SDL_RWops*)png_get_io_ptr(png_ptr);
SDL_RWwrite(rw, data, sizeof(png_byte), length);
}
SDL_Surface *SDL_PNGFormatAlpha(SDL_Surface *src)
{
SDL_Surface *surf;
SDL_Rect rect = { 0 };
/* NO-OP for images < 32bpp and 32bpp images that already have Alpha channel */
if (src->format->BitsPerPixel <= 24 || src->format->Amask) {
src->refcount++;
return src;
}
/* Convert 32bpp alpha-less image to 24bpp alpha-less image */
rect.w = src->w;
rect.h = src->h;
surf = SDL_CreateRGBSurface(src->flags, src->w, src->h, 24,
src->format->Rmask, src->format->Gmask, src->format->Bmask, 0);
SDL_LowerBlit(src, &rect, surf, &rect);
return surf;
}
int SDL_SavePNG_RW(SDL_Surface *surface, SDL_RWops *dst, int freedst)
{
png_structp png_ptr;
png_infop info_ptr;
png_colorp pal_ptr;
SDL_Palette *pal;
int i, colortype;
#ifdef USE_ROW_POINTERS
png_bytep *row_pointers;
#endif
/* Initialize and do basic error checking */
if (!dst)
{
SDL_SetError("Argument 2 to SDL_SavePNG_RW can't be NULL, expecting SDL_RWops*\n");
if (freedst) SDL_RWclose(dst);
return (ERROR);
}
if (!surface)
{
SDL_SetError("Argument 1 to SDL_SavePNG_RW can't be NULL, expecting SDL_Surface*\n");
if (freedst) SDL_RWclose(dst);
return (ERROR);
}
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, png_error_SDL, NULL); /* err_ptr, err_fn, warn_fn */
if (!png_ptr)
{
SDL_SetError("Unable to png_create_write_struct on %s\n", PNG_LIBPNG_VER_STRING);
if (freedst) SDL_RWclose(dst);
return (ERROR);
}
info_ptr = png_create_info_struct(png_ptr);
if (!info_ptr)
{
SDL_SetError("Unable to png_create_info_struct\n");
png_destroy_write_struct(&png_ptr, NULL);
if (freedst) SDL_RWclose(dst);
return (ERROR);
}
if (setjmp(png_jmpbuf(png_ptr))) /* All other errors, see also "png_error_SDL" */
{
png_destroy_write_struct(&png_ptr, &info_ptr);
if (freedst) SDL_RWclose(dst);
return (ERROR);
}
/* Setup our RWops writer */
png_set_write_fn(png_ptr, dst, png_write_SDL, NULL); /* w_ptr, write_fn, flush_fn */
/* Prepare chunks */
colortype = PNG_COLOR_MASK_COLOR;
if (surface->format->BytesPerPixel > 0
&& surface->format->BytesPerPixel <= 8
&& (pal = surface->format->palette))
{
colortype |= PNG_COLOR_MASK_PALETTE;
pal_ptr = (png_colorp)malloc(pal->ncolors * sizeof(png_color));
for (i = 0; i < pal->ncolors; i++) {
pal_ptr[i].red = pal->colors[i].r;
pal_ptr[i].green = pal->colors[i].g;
pal_ptr[i].blue = pal->colors[i].b;
}
png_set_PLTE(png_ptr, info_ptr, pal_ptr, pal->ncolors);
free(pal_ptr);
}
else if (surface->format->BytesPerPixel > 3 || surface->format->Amask)
colortype |= PNG_COLOR_MASK_ALPHA;
png_set_IHDR(png_ptr, info_ptr, surface->w, surface->h, 8, colortype,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
// png_set_packing(png_ptr);
/* Allow BGR surfaces */
if (surface->format->Rmask == bmask
&& surface->format->Gmask == gmask
&& surface->format->Bmask == rmask)
png_set_bgr(png_ptr);
/* Write everything */
png_write_info(png_ptr, info_ptr);
#ifdef USE_ROW_POINTERS
row_pointers = (png_bytep*) malloc(sizeof(png_bytep)*surface->h);
for (i = 0; i < surface->h; i++)
row_pointers[i] = (png_bytep)(Uint8*)surface->pixels + i * surface->pitch;
png_write_image(png_ptr, row_pointers);
free(row_pointers);
#else
for (i = 0; i < surface->h; i++)
png_write_row(png_ptr, (png_bytep)(Uint8*)surface->pixels + i * surface->pitch);
#endif
png_write_end(png_ptr, info_ptr);
/* Done */
png_destroy_write_struct(&png_ptr, &info_ptr);
if (freedst) SDL_RWclose(dst);
return (SUCCESS);
}

View File

@@ -1,4 +1,4 @@
-optimizationpasses 1
-optimizationpasses 0
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify