Completed single image export function

From "Open" dialog, select an image (single click, or use
arrow keys / etc., to highlight the image), then select the
"Export" button at the lower right.

The image will be saved in the export directory.  By default
this will be based on the config found via the XDG_CONFIG_HOME
environment variable, which is scanned for a "XDG_PICTURES_DIR"
setting.  If none is found, "Pictures" in the directory specified
by the HOME env. var. will be used.  In both cases, a new
"TuxPaint" subdirectory will be created, and exports will be placed
there.

The export location may be overridden using the "--exportdir"
command-line option or "exportdir" config file option
(e.g., "--exportdir /path/to/dir" or "exportdir=/path/to/dir",
respectively).  In this case, the directory is assumed to preexist,
and no "TuxPaint" subdirectory will be made.

There's currently no way to disable the export feature altogether.
If there's demand, we can add it as a simplification option.

Finally, this feature simply copies the PNG file (but no extra
data files) from Tux Paint's "saved" directory to the export dir.

Closes https://sourceforge.net/p/tuxpaint/feature-requests/192/
This commit is contained in:
Bill Kendrick 2020-07-26 14:10:03 -07:00
parent 28b28d583e
commit b0a2b4dacc
5 changed files with 205 additions and 50 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -11,15 +11,22 @@ $Id$
2020.July.25 (0.9.25) 2020.July.25 (0.9.25)
* New Features * New Features
------------ ------------
* [WIP] GIF export option from Open -> Slideshow dialog. * Export drawings:
* Utilizes "gifenc", public domain by Marcel Rodrigues: * GIF export option from the Open -> Slideshow dialog.
https://github.com/lecram/gifenc * Utilizes "gifenc", public domain by Marcel Rodrigues:
* Depends on "libimagequant", from the "pngquant2" project: https://github.com/lecram/gifenc
https://github.com/ImageOptim/libimagequant * Depends on "libimagequant", from the "pngquant2" project:
(GPL v3 or later, for Free/Libre Open Source Software) https://github.com/ImageOptim/libimagequant
* Uses XDG user dirs settings, if available, to determine (GPL v3 or later, for Free/Libre Open Source Software)
where to store them, or "~/Pictures", otherwise. * WORK IN PROGRESS
"--exportdir" option can be used to override this. (Closes https://sourceforge.net/p/tuxpaint/feature-requests/191/)
* Single image (PNG) export from the Open dialog.
(Closes https://sourceforge.net/p/tuxpaint/feature-requests/192/)
* Both utilize XDG user dirs settings, if available, to determine
where to store them, or "~/Pictures", otherwise. In those cases,
files are stored in a new "TuxPaint" subdirectory.
* The "--exportdir" option can be used to override the default
location (but a "TuxPaint" subdir. will NOT be placed there).
* Ports & Building * Ports & Building
---------------- ----------------

View file

@ -6,7 +6,7 @@ Options Documentation
Copyright (c) 2002-2020 by various contributors; see AUTHORS.txt Copyright (c) 2002-2020 by various contributors; see AUTHORS.txt
http://www.tuxpaint.org/ http://www.tuxpaint.org/
July 25, 2020 July 26, 2020
---------------------------------------------------------------------- ----------------------------------------------------------------------
@ -581,7 +581,8 @@ Windows Users
Use this option to change where Tux Paint exports files — Use this option to change where Tux Paint exports files —
single images, or animated GIF slideshows — for external use. single images, or animated GIF slideshows — for external use.
If you do not override it, the default location is: If you do not override it, the default location is determined
as follows:
* Linux & Unix — If available, wherever your desktop * Linux & Unix — If available, wherever your desktop
environment is configured for pictures to be stored, environment is configured for pictures to be stored,
based on your XDG (X Desktop Group) configuration. (Try based on your XDG (X Desktop Group) configuration. (Try
@ -599,6 +600,14 @@ Windows Users
* Windows — TBD! * Windows — TBD!
* macOS — TBD! * macOS — TBD!
Note: When the defaults are used, a new "TuxPaint"
subdirectory will be created and used. (e.g.,
"~/Pictures/TuxPaint") When the "--exportdir" option is used,
the exact path specified will be used (no "TuxPaint"
subdirectory is created). It is expected that the parent
directory exists. (The directory itself will be created, if it
doesn't.)
Example: exportdir=/home/penguin/TuxPaintExports Example: exportdir=/home/penguin/TuxPaintExports
datadir=DIRECTORY datadir=DIRECTORY
@ -1050,6 +1059,7 @@ Windows Users
--keyboard --keyboard
--savedir DIRECTORY --savedir DIRECTORY
--datadir DIRECTORY --datadir DIRECTORY
--exportdir DIRECTORY
--saveover --saveover
--saveovernew --saveovernew
--nosave --nosave

View file

@ -19,7 +19,7 @@ version
<p>Copyright (c) 2002-2020 by various contributors; see AUTHORS.txt<br/> <p>Copyright (c) 2002-2020 by various contributors; see AUTHORS.txt<br/>
<a href="http://www.tuxpaint.org/">http://www.tuxpaint.org/</a></p> <a href="http://www.tuxpaint.org/">http://www.tuxpaint.org/</a></p>
<p>July 25, 2020</p> <p>July 26, 2020</p>
</center> </center>
@ -715,7 +715,8 @@ version
<p>Use this option to change where Tux&nbsp;Paint exports files &mdash; single <p>Use this option to change where Tux&nbsp;Paint exports files &mdash; single
images, or animated GIF slideshows &mdash; for external use. images, or animated GIF slideshows &mdash; for external use.
<p>If you do not override it, the <b><i>default</i></b> location is: <p>If you do not override it, the <b><i>default</i></b> location is
determined as follows:
<ul> <ul>
<li>Linux &amp; Unix &mdash; If available, wherever your desktop environment <li>Linux &amp; Unix &mdash; If available, wherever your desktop environment
is configured for pictures to be stored, based on your XDG is configured for pictures to be stored, based on your XDG
@ -737,6 +738,14 @@ version
</ul> </ul>
</p> </p>
<p><b>Note:</b> When the defaults are used, a new "<code>TuxPaint</code>"
subdirectory will be created and used. (e.g., "<code>~/Pictures/TuxPaint</code>")
When the "<code>--exportdir</code>" option is used, the exact path specified
will be used (no "<code>TuxPaint</code>" subdirectory is created).
It is expected that the parent directory exists. (The directory itself
will be created, if it doesn't.)
</p>
<p><b>Example:</b> <code>exportdir=/home/penguin/TuxPaintExports</code></p> <p><b>Example:</b> <code>exportdir=/home/penguin/TuxPaintExports</code></p>
</dd> </dd>
@ -1606,6 +1615,7 @@ version
--keyboard<br> --keyboard<br>
--savedir&nbsp;<i>DIRECTORY</i><br> --savedir&nbsp;<i>DIRECTORY</i><br>
--datadir&nbsp;<i>DIRECTORY</i><br> --datadir&nbsp;<i>DIRECTORY</i><br>
--exportdir&nbsp;<i>DIRECTORY</i><br>
--saveover<br> --saveover<br>
--saveovernew<br> --saveovernew<br>
--nosave<br> --nosave<br>

View file

@ -1985,6 +1985,7 @@ static void play_slideshow(int *selected, int num_selected, char *dirname, char
static void draw_selection_digits(int right, int bottom, int n); static void draw_selection_digits(int right, int bottom, int n);
static int export_gif(int *selected, int num_selected, char *dirname, char **d_names, char **d_exts, int speed); static int export_gif(int *selected, int num_selected, char *dirname, char **d_names, char **d_exts, int speed);
static int export_pict(char * fname);
static char * get_export_filepath(const char * ext); static char * get_export_filepath(const char * ext);
static void wait_for_sfx(void); static void wait_for_sfx(void);
@ -13989,7 +13990,8 @@ static int do_open(void)
int *d_places; int *d_places;
FILE *fi; FILE *fi;
char fname[1024]; char fname[1024];
int num_files, i, done, slideshow, update_list, want_erase, cur, which, num_files_in_dirs, j, any_saved_files; int num_files, i, done, slideshow, update_list, want_erase, want_export;
int cur, which, num_files_in_dirs, j, any_saved_files;
SDL_Rect dest; SDL_Rect dest;
SDL_Event event; SDL_Event event;
SDLKey key; SDLKey key;
@ -14323,16 +14325,15 @@ static int do_open(void)
/* Let user choose an image: */ /* Let user choose an image: */
/* Instructions for 'Open' file dialog */ /* Instructions for 'Open' file dialog */
char *freeme = textdir(gettext_noop("Choose the picture you want, " "then click “Open”.")); char *instructions = textdir(gettext_noop("Choose the picture you want, then click “Open”."));
draw_tux_text(TUX_BORED, instructions, 1);
draw_tux_text(TUX_BORED, freeme, 1);
free(freeme);
/* NOTE: cur is now set above; if file_id'th file is found, it's /* NOTE: cur is now set above; if file_id'th file is found, it's
set to that file's index; otherwise, we default to '0' */ set to that file's index; otherwise, we default to '0' */
update_list = 1; update_list = 1;
want_erase = 0; want_erase = 0;
want_export = 0;
done = 0; done = 0;
slideshow = 0; slideshow = 0;
@ -14447,6 +14448,25 @@ static int do_open(void)
SDL_BlitSurface(img_openlabels_back, NULL, screen, &dest); SDL_BlitSurface(img_openlabels_back, NULL, screen, &dest);
/* "Export" button: */
dest.x = WINDOW_WIDTH - 96 - 48 - 48 - 48;
dest.y = (48 * 7 + 40 + HEIGHTOFFSET) - 48;
if (d_places[which] != PLACE_STARTERS_DIR && d_places[which] != PLACE_PERSONAL_STARTERS_DIR)
SDL_BlitSurface(img_btn_up, NULL, screen, &dest);
else
SDL_BlitSurface(img_btn_off, NULL, screen, &dest);
dest.x = WINDOW_WIDTH - 96 - 48 - 48 - 48 + (48 - img_pict_export->w) / 2;
dest.y = (48 * 7 + 40 + HEIGHTOFFSET) - 48;
SDL_BlitSurface(img_pict_export, NULL, screen, &dest);
dest.x = WINDOW_WIDTH - 96 - 48 - 48 - 48 + (48 - img_openlabels_pict_export->w) / 2;
dest.y = (48 * 7 + 40 + HEIGHTOFFSET) - img_openlabels_pict_export->h;
SDL_BlitSurface(img_openlabels_pict_export, NULL, screen, &dest);
/* "Erase" button: */ /* "Erase" button: */
dest.x = WINDOW_WIDTH - 96 - 48 - 48; dest.x = WINDOW_WIDTH - 96 - 48 - 48;
@ -14683,6 +14703,16 @@ static int do_open(void)
want_erase = 1; want_erase = 1;
} }
else if (event.button.x >= (WINDOW_WIDTH - 96 - 48 - 48 - 48) &&
event.button.x < (WINDOW_WIDTH - 48 - 48 - 96) &&
event.button.y >= (48 * 7 + 40 + HEIGHTOFFSET) - 48 &&
event.button.y < (48 * 7 + 40 + HEIGHTOFFSET) &&
d_places[which] != PLACE_STARTERS_DIR && d_places[which] != PLACE_PERSONAL_STARTERS_DIR)
{
/* Export */
want_export = 1;
}
} }
else if (event.type == SDL_MOUSEBUTTONDOWN && else if (event.type == SDL_MOUSEBUTTONDOWN &&
event.button.button >= 4 && event.button.button <= 5 && wheely) event.button.button >= 4 && event.button.button <= 5 && wheely)
@ -14738,8 +14768,9 @@ static int do_open(void)
else if (((event.button.x >= 96 && event.button.x < 96 + 48 + 48) || else if (((event.button.x >= 96 && event.button.x < 96 + 48 + 48) ||
(event.button.x >= (WINDOW_WIDTH - 96 - 48) && (event.button.x >= (WINDOW_WIDTH - 96 - 48) &&
event.button.x < (WINDOW_WIDTH - 96)) || event.button.x < (WINDOW_WIDTH - 96)) ||
(event.button.x >= (WINDOW_WIDTH - 96 - 48 - 48) && (event.button.x >= (WINDOW_WIDTH - 96 - 48 - 48 - 48) &&
event.button.x < (WINDOW_WIDTH - 48 - 96) && event.button.x < (WINDOW_WIDTH - 48 - 96) &&
/* Both "Erase" and "Export" only work on our own files... */
d_places[which] != PLACE_STARTERS_DIR && d_places[which] != PLACE_STARTERS_DIR &&
d_places[which] != PLACE_PERSONAL_STARTERS_DIR)) && d_places[which] != PLACE_PERSONAL_STARTERS_DIR)) &&
event.button.y >= (48 * 7 + 40 + HEIGHTOFFSET) - 48 && event.button.y >= (48 * 7 + 40 + HEIGHTOFFSET) - 48 &&
@ -14897,6 +14928,21 @@ static int do_open(void)
update_list = 1; update_list = 1;
} }
} }
if (want_export)
{
want_export = 0;
snprintf(fname, sizeof(fname), "saved/%s%s", d_names[which], d_exts[which]);
rfname = get_fname(fname, DIR_SAVE);
if (export_pict(rfname))
do_prompt_snd(PROMPT_PICT_EXPORT_TXT, PROMPT_EXPORT_YES, "", SND_TUXOK, screen->w / 2, screen->h / 2);
else
do_prompt_snd(PROMPT_PICT_EXPORT_FAILED_TXT, PROMPT_EXPORT_YES, "", SND_YOUCANNOT, screen->w / 2, screen->h / 2);
draw_tux_text(TUX_BORED, instructions, 1);
update_list = 1;
}
} }
while (!done); while (!done);
@ -15011,6 +15057,8 @@ static int do_open(void)
update_canvas(0, 0, WINDOW_WIDTH - 96 - 96, 48 * 7 + 40 + HEIGHTOFFSET); update_canvas(0, 0, WINDOW_WIDTH - 96 - 96, 48 * 7 + 40 + HEIGHTOFFSET);
free(instructions);
} }
@ -15711,27 +15759,27 @@ static int do_slideshow(void)
} }
else else
{ {
export_successful = export_gif(selected, num_selected, dirname, d_names, d_exts, speed); export_successful = export_gif(selected, num_selected, dirname, d_names, d_exts, speed);
/* Redraw entire screen, after export: */ /* Redraw entire screen, after export: */
SDL_FillRect(screen, NULL, SDL_MapRGB(canvas->format, 255, 255, 255)); SDL_FillRect(screen, NULL, SDL_MapRGB(canvas->format, 255, 255, 255));
draw_toolbar(); draw_toolbar();
draw_colors(COLORSEL_CLOBBER_WIPE); draw_colors(COLORSEL_CLOBBER_WIPE);
draw_none(); draw_none();
/* Show a message depending on success */ /* Show a message depending on success */
if (export_successful) if (export_successful)
do_prompt_snd(PROMPT_GIF_EXPORT_TXT, PROMPT_EXPORT_YES, "", SND_TUXOK, screen->w / 2, screen->h / 2); do_prompt_snd(PROMPT_GIF_EXPORT_TXT, PROMPT_EXPORT_YES, "", SND_TUXOK, screen->w / 2, screen->h / 2);
else else
do_prompt_snd(PROMPT_GIF_EXPORT_FAILED_TXT, PROMPT_EXPORT_YES, "", SND_YOUCANNOT, screen->w / 2, screen->h / 2); do_prompt_snd(PROMPT_GIF_EXPORT_FAILED_TXT, PROMPT_EXPORT_YES, "", SND_YOUCANNOT, screen->w / 2, screen->h / 2);
freeme = textdir(TUX_TIP_SLIDESHOW); freeme = textdir(TUX_TIP_SLIDESHOW);
draw_tux_text(TUX_BORED, freeme, 1); draw_tux_text(TUX_BORED, freeme, 1);
free(freeme); free(freeme);
SDL_Flip(screen); SDL_Flip(screen);
update_list = 1; update_list = 1;
} }
} }
else if (event.button.x >= (WINDOW_WIDTH - 96 - 48) && else if (event.button.x >= (WINDOW_WIDTH - 96 - 48) &&
@ -19054,7 +19102,7 @@ static int do_new_dialog(void)
if (thumbs[num_files] == NULL) if (thumbs[num_files] == NULL)
{ {
fprintf(stderr, fprintf(stderr,
"\nError: Couldn't create a thumbnail of " "saved image!\n" "%s\n", fname); "\nError: Couldn't create a thumbnail of saved image!\n" "%s\n", fname);
} }
num_files++; num_files++;
@ -19149,7 +19197,7 @@ static int do_new_dialog(void)
if (thumbs[num_files] == NULL) if (thumbs[num_files] == NULL)
{ {
fprintf(stderr, fprintf(stderr,
"\nError: Couldn't create a thumbnail of " "saved image!\n" "%s\n", fname); "\nError: Couldn't create a thumbnail of saved image!\n" "%s\n", fname);
} }
SDL_FreeSurface(img); SDL_FreeSurface(img);
@ -19682,7 +19730,7 @@ static int do_new_dialog(void)
fprintf(stderr, fprintf(stderr,
"\nWarning: Couldn't load the saved image! (3)\n" "\nWarning: Couldn't load the saved image! (3)\n"
"%s\n" "%s\n"
"The Simple DirectMedia Layer error that occurred " "was:\n" "%s\n\n", fname, SDL_GetError()); "The Simple DirectMedia Layer error that occurred was:\n" "%s\n\n", fname, SDL_GetError());
do_prompt(PROMPT_OPEN_UNOPENABLE_TXT, PROMPT_OPEN_UNOPENABLE_YES, "", 0, 0); do_prompt(PROMPT_OPEN_UNOPENABLE_TXT, PROMPT_OPEN_UNOPENABLE_YES, "", 0, 0);
} }
@ -19740,7 +19788,7 @@ static int do_new_dialog(void)
fprintf(stderr, fprintf(stderr,
"\nWarning: Couldn't load the saved image! (4)\n" "\nWarning: Couldn't load the saved image! (4)\n"
"%s\n" "%s\n"
"The Simple DirectMedia Layer error that occurred " "was:\n" "%s\n\n", fname, SDL_GetError()); "The Simple DirectMedia Layer error that occurred was:\n" "%s\n\n", fname, SDL_GetError());
do_prompt(PROMPT_OPEN_UNOPENABLE_TXT, PROMPT_OPEN_UNOPENABLE_YES, "", 0, 0); do_prompt(PROMPT_OPEN_UNOPENABLE_TXT, PROMPT_OPEN_UNOPENABLE_YES, "", 0, 0);
} }
@ -22594,6 +22642,7 @@ static void tmpcfg_merge(struct cfginfo *loser, const struct cfginfo *winner)
static void setup_config(char *argv[]) static void setup_config(char *argv[])
{ {
char str[128]; char str[128];
char * picturesdir;
#ifndef _WIN32 #ifndef _WIN32
const char *home = getenv("HOME"); const char *home = getenv("HOME");
@ -22673,7 +22722,10 @@ static void setup_config(char *argv[])
* __HAIKU__ * __HAIKU__
* __APPLE__ * __APPLE__
*/ */
exportdir = get_xdg_user_dir("PICTURES", "Pictures"); picturesdir = get_xdg_user_dir("PICTURES", "Pictures");
snprintf(str, sizeof(str), "%s/TuxPaint", picturesdir);
free(picturesdir);
exportdir = strdup(str);
} }
/* Load options from user's own configuration (".rc" / ".cfg") file: */ /* Load options from user's own configuration (".rc" / ".cfg") file: */
@ -24235,10 +24287,11 @@ static void setup(void)
small_font = TuxPaint_Font_OpenFont(PANGO_DEFAULT_FONT, DATA_PREFIX "fonts/default_font.ttf", small_font = TuxPaint_Font_OpenFont(PANGO_DEFAULT_FONT, DATA_PREFIX "fonts/default_font.ttf",
#ifdef __APPLE__ #ifdef __APPLE__
12 - (only_uppercase * 2)); 12 - (only_uppercase * 2)
#else #else
13 - (only_uppercase * 2)); 13 - (only_uppercase * 2)
#endif #endif
);
if (small_font == NULL) if (small_font == NULL)
{ {
@ -24793,10 +24846,10 @@ static int trash(char *path)
} }
while (!feof(fi)) while (!feof(fi))
{ {
len = fread(buf, sizeof(buf), 1, fi); len = fread(buf, sizeof(unsigned char), sizeof(buf), fi);
if (len > 0) if (len > 0)
{ {
fwrite(buf, sizeof(buf), 1, fo); fwrite(buf, sizeof(unsigned char), sizeof(buf), fo);
} }
} }
fclose(fi); fclose(fi);
@ -25336,6 +25389,12 @@ static int export_gif(int *selected, int num_selected, char *dirname, char **d_n
show_progress_bar(screen); show_progress_bar(screen);
gif_fname = get_export_filepath("gif"); gif_fname = get_export_filepath("gif");
if (gif_fname == NULL)
{
/* Can't create export dir! Abort! */
return FALSE;
}
/* FIXME: Open GIF */ /* FIXME: Open GIF */
@ -25426,9 +25485,69 @@ static int export_gif(int *selected, int num_selected, char *dirname, char **d_n
return(!done); return(!done);
} }
/**
* Copy an image (just the main PNG) from Tux Paint's "saved"
* directory to the user's chosen export directory
* (e.g., ~/Pictures, or whatever "--exportdir" says).
*
* Used when exporting a single image from the Open dialog.
*
* @param char * fname -- full path to the image to export
* @return int 1 = success, 0 = failed
*/
static int export_pict(char * fname) {
FILE * fi, * fo;
size_t len;
unsigned char buf[1024];
char * pict_fname;
fi = fopen(fname, "rb");
if (fi == NULL)
{
fprintf(stderr, "Cannot export from saved Tux Paint file '%s'\nThe error that occurred was:\n%s\n\n", fname, strerror(errno));
return FALSE;
}
pict_fname = get_export_filepath("png");
if (pict_fname == NULL)
{
fclose(fi);
return FALSE;
}
fo = fopen(pict_fname, "wb");
if (fo == NULL)
{
fprintf(stderr, "Cannot export to new file '%s'\nThe error that occurred was:\n%s\n\n", pict_fname, strerror(errno));
free(pict_fname);
fclose(fi);
return FALSE;
}
while (!feof(fi))
{
len = fread(buf, sizeof(unsigned char), sizeof(buf), fi);
if (len > 0)
{
fwrite(buf, sizeof(unsigned char), sizeof(buf), fo);
}
}
/* FIXME: Probably good to check for errors here and respond accordingly -bjk 2020.07.26 */
fclose(fi);
fclose(fo);
free(pict_fname);
return TRUE;
}
/** /**
* Returns the name of a new file, located in the user's chosen * Returns the name of a new file, located in the user's chosen
* export directory (e.g., ~/Pictures, or whatever "--exportdir" says). * export directory (e.g., ~/Pictures/TuxPaint, or whatever "--exportdir" says).
*
* Also ensures that the directory exists, in the first place.
* *
* Used when exporting animated GIFs (via "Export GIF" in the * Used when exporting animated GIFs (via "Export GIF" in the
* Open->Slideshow dialog) and static PNGs (via "Export" in the * Open->Slideshow dialog) and static PNGs (via "Export" in the
@ -25436,7 +25555,8 @@ static int export_gif(int *selected, int num_selected, char *dirname, char **d_n
* *
* @param const char * ext -- extnesion of the file (e.g., "png" or "gif") * @param const char * ext -- extnesion of the file (e.g., "png" or "gif")
* @return char * -- filepath for the new file to be created * @return char * -- filepath for the new file to be created
* (e.g., /home/username/Pictures/2020072620110100.gif") * (e.g., /home/username/Pictures/TuxPaint/2020072620110100.gif")
* Or NULL if the directory cannot be created.
*/ */
static char * get_export_filepath(const char * ext) { static char * get_export_filepath(const char * ext) {
char *rname; char *rname;
@ -25444,6 +25564,14 @@ static char * get_export_filepath(const char * ext) {
char timestamp[16]; char timestamp[16];
time_t t; time_t t;
/* Make sure the export dir exists */
if (!make_directory(DIR_EXPORT, "", "Can't create export directory"))
{
return NULL;
}
/* Create a unique filename, within that dir */
t = time(NULL); t = time(NULL);
strftime(timestamp, sizeof(timestamp), "%Y%m%d%H%M%S", localtime(&t)); strftime(timestamp, sizeof(timestamp), "%Y%m%d%H%M%S", localtime(&t));
snprintf(fname, sizeof(fname), "%s.%s", timestamp, ext); snprintf(fname, sizeof(fname), "%s.%s", timestamp, ext);