* Now also saves the EXIF data in the supplied message in Translate() (decompress
only, you cannot add EXIF data yet). * Preparations to honour the orientation of JPEG images stored in the EXIF tags. git-svn-id: file:///srv/svn/repos/haiku/haiku/trunk@20633 a95241bf-73f2-0310-859d-f6bbb57e9c96
This commit is contained in:
parent
8560c78809
commit
52e8f46af7
@ -32,9 +32,13 @@ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#include "JPEGTranslator.h"
|
||||
|
||||
#include "exif_parser.h"
|
||||
|
||||
#include <TabView.h>
|
||||
|
||||
|
||||
#define MARKER_EXIF 0xe1
|
||||
|
||||
// Set these accordingly
|
||||
#define JPEG_ACRONYM "JPEG"
|
||||
#define JPEG_FORMAT 'JPEG'
|
||||
@ -79,6 +83,12 @@ translation_format outputFormats[] = {
|
||||
{}
|
||||
};
|
||||
|
||||
// Main functions of translator :)
|
||||
static status_t Copy(BPositionIO *in, BPositionIO *out);
|
||||
static status_t Compress(BPositionIO *in, BPositionIO *out);
|
||||
static status_t Decompress(BPositionIO *in, BPositionIO *out, BMessage* ioExtension);
|
||||
static status_t Error(j_common_ptr cinfo, status_t error = B_ERROR);
|
||||
|
||||
|
||||
bool gAreSettingsRunning = false;
|
||||
|
||||
@ -1054,14 +1064,14 @@ Translate(BPositionIO *inSource, const translator_info *inInfo,
|
||||
} else if (inInfo->type == B_TRANSLATOR_BITMAP && outType == JPEG_FORMAT) {
|
||||
return Compress(inSource, outDestination);
|
||||
} else if (inInfo->type == JPEG_FORMAT && outType == B_TRANSLATOR_BITMAP) {
|
||||
return Decompress(inSource, outDestination);
|
||||
return Decompress(inSource, outDestination, ioExtension);
|
||||
}
|
||||
|
||||
return B_NO_TRANSLATOR;
|
||||
}
|
||||
|
||||
/*! The user has requested the same format for input and output, so just copy */
|
||||
status_t
|
||||
static status_t
|
||||
Copy(BPositionIO *in, BPositionIO *out)
|
||||
{
|
||||
int block_size = 65536;
|
||||
@ -1095,7 +1105,7 @@ Copy(BPositionIO *in, BPositionIO *out)
|
||||
|
||||
|
||||
/*! Encode into the native format */
|
||||
status_t
|
||||
static status_t
|
||||
Compress(BPositionIO *in, BPositionIO *out)
|
||||
{
|
||||
// Load Settings
|
||||
@ -1307,8 +1317,8 @@ Compress(BPositionIO *in, BPositionIO *out)
|
||||
|
||||
|
||||
/*! Decode the native format */
|
||||
status_t
|
||||
Decompress(BPositionIO *in, BPositionIO *out)
|
||||
static status_t
|
||||
Decompress(BPositionIO *in, BPositionIO *out, BMessage* ioExtension)
|
||||
{
|
||||
// Load Settings
|
||||
jpeg_settings settings;
|
||||
@ -1321,16 +1331,39 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
jpeg_create_decompress(&cinfo);
|
||||
be_jpeg_stdio_src(&cinfo, in);
|
||||
|
||||
jpeg_save_markers(&cinfo, MARKER_EXIF, 131072);
|
||||
// make sure the EXIF tag is stored
|
||||
|
||||
// Read info about image
|
||||
jpeg_read_header(&cinfo, TRUE);
|
||||
|
||||
BMessage exif;
|
||||
|
||||
if (ioExtension != NULL) {
|
||||
// add EXIF data to message, if any
|
||||
jpeg_marker_struct* marker = cinfo.marker_list;
|
||||
while (marker != NULL) {
|
||||
if (marker->marker == MARKER_EXIF
|
||||
&& !strncmp((char*)marker->data, "Exif", 4)) {
|
||||
// Strip EXIF header from TIFF data
|
||||
ioExtension->AddData("exif", B_RAW_TYPE,
|
||||
(uint8 *)marker->data + 6, marker->data_length - 6);
|
||||
|
||||
BMemoryIO io(marker->data + 6, marker->data_length - 6);
|
||||
convert_exif_to_message(io, exif);
|
||||
}
|
||||
marker = marker->next;
|
||||
}
|
||||
}
|
||||
|
||||
// Default color info
|
||||
color_space out_color_space = B_RGB32;
|
||||
int out_color_components = 4;
|
||||
color_space outColorSpace = B_RGB32;
|
||||
int outColorComponents = 4;
|
||||
|
||||
// Function pointer to convert function
|
||||
// It will point to proper function if needed
|
||||
void (*converter)(uchar *inscanline, uchar *outscanline, int inrow_bytes) = convert_from_24_to_32;
|
||||
void (*converter)(uchar *inScanLine, uchar *outScanLine,
|
||||
int inRowBytes) = convert_from_24_to_32;
|
||||
|
||||
// If color space isn't rgb
|
||||
if (cinfo.out_color_space != JCS_RGB) {
|
||||
@ -1342,8 +1375,8 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
// Check if user wants to read only as RGB32 or not
|
||||
if (!settings.Always_B_RGB32) {
|
||||
// Grayscale
|
||||
out_color_space = B_GRAY8;
|
||||
out_color_components = 1;
|
||||
outColorSpace = B_GRAY8;
|
||||
outColorComponents = 1;
|
||||
converter = NULL;
|
||||
} else {
|
||||
// RGB
|
||||
@ -1376,12 +1409,21 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
// Initialize decompression
|
||||
jpeg_start_decompress(&cinfo);
|
||||
|
||||
// !!! Initialize this bounds rect to the size of your image
|
||||
BRect bounds(0, 0, cinfo.output_width-1, cinfo.output_height-1);
|
||||
int32 orientation;
|
||||
if (exif.FindInt32("Orientation", &orientation) != B_OK)
|
||||
orientation = 1;
|
||||
|
||||
// Initialize this bounds rect to the size of your image
|
||||
BRect bounds(0, 0, cinfo.output_width - 1, cinfo.output_height - 1);
|
||||
if (orientation & 4) {
|
||||
// image is rotated
|
||||
bounds.right = cinfo.output_height - 1;
|
||||
bounds.bottom = cinfo.output_width - 1;
|
||||
}
|
||||
|
||||
// Bytes count in one line of image (scanline)
|
||||
int64 row_bytes = cinfo.output_width * out_color_components;
|
||||
|
||||
int64 rowBytes = cinfo.output_width * outColorComponents;
|
||||
|
||||
// Fill out the B_TRANSLATOR_BITMAP's header
|
||||
TranslatorBitmap header;
|
||||
header.magic = B_HOST_TO_BENDIAN_INT32(B_TRANSLATOR_BITMAP);
|
||||
@ -1389,9 +1431,9 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
header.bounds.top = B_HOST_TO_BENDIAN_FLOAT(bounds.top);
|
||||
header.bounds.right = B_HOST_TO_BENDIAN_FLOAT(bounds.right);
|
||||
header.bounds.bottom = B_HOST_TO_BENDIAN_FLOAT(bounds.bottom);
|
||||
header.colors = (color_space)B_HOST_TO_BENDIAN_INT32(out_color_space);
|
||||
header.rowBytes = B_HOST_TO_BENDIAN_INT32(row_bytes);
|
||||
header.dataSize = B_HOST_TO_BENDIAN_INT32(row_bytes * cinfo.output_height);
|
||||
header.colors = (color_space)B_HOST_TO_BENDIAN_INT32(outColorSpace);
|
||||
header.rowBytes = B_HOST_TO_BENDIAN_INT32(rowBytes);
|
||||
header.dataSize = B_HOST_TO_BENDIAN_INT32(rowBytes * cinfo.output_height);
|
||||
|
||||
// Write out the header
|
||||
status_t err = out->Write(&header, sizeof(TranslatorBitmap));
|
||||
@ -1408,14 +1450,14 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
// Allocate scanline
|
||||
// Use libjpeg memory allocation functions, so in case of error it will free them itself
|
||||
in_scanline = (unsigned char *)(cinfo.mem->alloc_large)((j_common_ptr)&cinfo,
|
||||
JPOOL_PERMANENT, row_bytes);
|
||||
JPOOL_PERMANENT, rowBytes);
|
||||
|
||||
// We need 2nd scanline storage only for conversion
|
||||
if (converter != NULL) {
|
||||
// There will be conversion, allocate second scanline...
|
||||
// Use libjpeg memory allocation functions, so in case of error it will free them itself
|
||||
out_scanline = (unsigned char *)(cinfo.mem->alloc_large)((j_common_ptr)&cinfo,
|
||||
JPOOL_PERMANENT, row_bytes);
|
||||
JPOOL_PERMANENT, rowBytes);
|
||||
// ... and make it the one to write to file
|
||||
writeline = out_scanline;
|
||||
} else
|
||||
@ -1427,11 +1469,11 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
|
||||
// Convert if needed
|
||||
if (converter != NULL)
|
||||
converter(in_scanline, out_scanline, row_bytes);
|
||||
converter(in_scanline, out_scanline, rowBytes);
|
||||
|
||||
// Write the scanline buffer to the output stream
|
||||
err = out->Write(writeline, row_bytes);
|
||||
if (err < row_bytes)
|
||||
err = out->Write(writeline, rowBytes);
|
||||
if (err < rowBytes)
|
||||
return err < B_OK ? Error((j_common_ptr)&cinfo, err)
|
||||
: Error((j_common_ptr)&cinfo, B_ERROR);
|
||||
}
|
||||
@ -1445,7 +1487,7 @@ Decompress(BPositionIO *in, BPositionIO *out)
|
||||
Frees jpeg alocated memory
|
||||
Returns given error (B_ERROR by default)
|
||||
*/
|
||||
status_t
|
||||
static status_t
|
||||
Error(j_common_ptr cinfo, status_t error)
|
||||
{
|
||||
jpeg_destroy(cinfo);
|
||||
|
@ -215,10 +215,4 @@ EXTERN(void) be_jpeg_stdio_dest(j_compress_ptr cinfo, BPositionIO *outfile); //
|
||||
EXTERN(struct jpeg_error_mgr *) be_jpeg_std_error (struct jpeg_error_mgr * err, jpeg_settings * settings); // from "be_jerror.cpp"
|
||||
|
||||
|
||||
// Main functions of translator :)
|
||||
status_t Copy(BPositionIO *in, BPositionIO *out);
|
||||
status_t Compress(BPositionIO *in, BPositionIO *out);
|
||||
status_t Decompress(BPositionIO *in, BPositionIO *out);
|
||||
status_t Error(j_common_ptr cinfo, status_t error = B_ERROR);
|
||||
|
||||
#endif // _JPEGTRANSLATOR_H_
|
||||
|
@ -9,6 +9,9 @@ SubDirC++Flags [ FDefines BEOS_R5_COMPATIBLE ] ;
|
||||
|
||||
SubDirSysHdrs [ FDirName $(SUBDIR) libjpeg ] ;
|
||||
|
||||
SubDirSysHdrs [ FDirName $(SUBDIR) $(DOTDOT) raw ] ;
|
||||
# for TIFF.h and ReadHelper.h
|
||||
|
||||
local jpeg_files =
|
||||
# libjpeg
|
||||
jcapimin.c
|
||||
@ -82,6 +85,7 @@ Translator JPEGTranslator :
|
||||
be_jdatadst.cpp
|
||||
be_jdatasrc.cpp
|
||||
be_jerror.cpp
|
||||
exif_parser.cpp
|
||||
JPEGTranslator.cpp
|
||||
|
||||
$(jpeg_files)
|
||||
|
327
src/add-ons/translators/jpeg/exif_parser.cpp
Normal file
327
src/add-ons/translators/jpeg/exif_parser.cpp
Normal file
@ -0,0 +1,327 @@
|
||||
/*
|
||||
* Copyright 2007, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
|
||||
|
||||
#include "exif_parser.h"
|
||||
|
||||
#include <ReadHelper.h>
|
||||
|
||||
#include <Message.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
enum {
|
||||
TAG_EXIF_OFFSET = 0x8769,
|
||||
TAG_SUB_DIR_OFFSET = 0xa005,
|
||||
|
||||
TAG_MAKER = 0x10f,
|
||||
TAG_MODEL = 0x110,
|
||||
TAG_ORIENTATION = 0x112,
|
||||
TAG_EXPOSURE_TIME = 0x829a,
|
||||
TAG_ISO = 0x8827,
|
||||
};
|
||||
|
||||
static const convert_tag kDefaultTags[] = {
|
||||
{TAG_MAKER, B_ANY_TYPE, "Maker"},
|
||||
{TAG_MODEL, B_ANY_TYPE, "Model"},
|
||||
{TAG_ORIENTATION, B_INT32_TYPE, "Orientation"},
|
||||
{TAG_EXPOSURE_TIME, B_DOUBLE_TYPE, "ExposureTime"},
|
||||
{TAG_ISO, B_INT32_TYPE, "ISO"},
|
||||
};
|
||||
static const size_t kNumDefaultTags = sizeof(kDefaultTags)
|
||||
/ sizeof(kDefaultTags[0]);
|
||||
|
||||
|
||||
static status_t parse_tiff_directory(TReadHelper& read, BMessage& target,
|
||||
const convert_tag* tags, size_t tagCount);
|
||||
|
||||
|
||||
static status_t
|
||||
add_to_message(TReadHelper& source, BMessage& target, tiff_tag& tag,
|
||||
const char* name, type_code type)
|
||||
{
|
||||
type_code defaultType = B_INT32_TYPE;
|
||||
double doubleValue = 0.0;
|
||||
int32 intValue = 0;
|
||||
|
||||
switch (tag.type) {
|
||||
case TIFF_STRING_TYPE:
|
||||
{
|
||||
if (type != B_ANY_TYPE && type != B_STRING_TYPE)
|
||||
return B_BAD_VALUE;
|
||||
|
||||
char* buffer = (char*)malloc(tag.length);
|
||||
if (buffer == NULL)
|
||||
return B_NO_MEMORY;
|
||||
|
||||
source(buffer, tag.length);
|
||||
|
||||
// remove trailing spaces
|
||||
int32 i = tag.length;
|
||||
while (--i > 0 && isspace(buffer[i]) || !buffer[i]) {
|
||||
buffer[i] = '\0';
|
||||
}
|
||||
|
||||
status_t status = target.AddString(name, buffer);
|
||||
free(buffer);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
case TIFF_UNDEFINED_TYPE:
|
||||
{
|
||||
if (type != B_ANY_TYPE && type != B_STRING_TYPE && type != B_RAW_TYPE)
|
||||
return B_BAD_VALUE;
|
||||
|
||||
char* buffer = (char*)malloc(tag.length);
|
||||
if (buffer == NULL)
|
||||
return B_NO_MEMORY;
|
||||
|
||||
source(buffer, tag.length);
|
||||
|
||||
status_t status;
|
||||
if (type == B_STRING_TYPE)
|
||||
status = target.AddString(name, buffer);
|
||||
else
|
||||
status = target.AddData(name, B_RAW_TYPE, buffer, tag.length);
|
||||
|
||||
free(buffer);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// unsigned
|
||||
case TIFF_UINT8_TYPE:
|
||||
intValue = source.Next<uint8>();
|
||||
break;
|
||||
case TIFF_UINT16_TYPE:
|
||||
defaultType = B_INT32_TYPE;
|
||||
intValue = source.Next<uint16>();
|
||||
break;
|
||||
case TIFF_UINT32_TYPE:
|
||||
defaultType = B_INT32_TYPE;
|
||||
intValue = source.Next<uint32>();
|
||||
break;
|
||||
case TIFF_UFRACTION_TYPE:
|
||||
{
|
||||
defaultType = B_DOUBLE_TYPE;
|
||||
double value = source.Next<uint32>();
|
||||
doubleValue = value / source.Next<uint32>();
|
||||
break;
|
||||
}
|
||||
|
||||
// signed
|
||||
case TIFF_INT8_TYPE:
|
||||
intValue = source.Next<int8>();
|
||||
break;
|
||||
case TIFF_INT16_TYPE:
|
||||
intValue = source.Next<int16>();
|
||||
break;
|
||||
case TIFF_INT32_TYPE:
|
||||
intValue = source.Next<int32>();
|
||||
break;
|
||||
case TIFF_FRACTION_TYPE:
|
||||
{
|
||||
defaultType = B_DOUBLE_TYPE;
|
||||
double value = source.Next<int32>();
|
||||
doubleValue = value / source.Next<int32>();
|
||||
}
|
||||
|
||||
// floating point
|
||||
case TIFF_FLOAT_TYPE:
|
||||
defaultType = B_FLOAT_TYPE;
|
||||
doubleValue = source.Next<float>();
|
||||
break;
|
||||
case TIFF_DOUBLE_TYPE:
|
||||
defaultType = B_DOUBLE_TYPE;
|
||||
doubleValue = source.Next<double>();
|
||||
break;
|
||||
|
||||
default:
|
||||
return B_BAD_VALUE;
|
||||
}
|
||||
|
||||
if (defaultType == B_INT32_TYPE)
|
||||
doubleValue = intValue;
|
||||
else
|
||||
intValue = int32(doubleValue + 0.5);
|
||||
|
||||
if (type == B_ANY_TYPE)
|
||||
type = defaultType;
|
||||
|
||||
switch (type) {
|
||||
case B_INT32_TYPE:
|
||||
return target.AddInt32(name, intValue);
|
||||
case B_FLOAT_TYPE:
|
||||
return target.AddFloat(name, doubleValue);
|
||||
case B_DOUBLE_TYPE:
|
||||
return target.AddDouble(name, doubleValue);
|
||||
|
||||
default:
|
||||
return B_BAD_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static const convert_tag*
|
||||
find_convert_tag(uint16 id, const convert_tag* tags, size_t count)
|
||||
{
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (tags[i].tag == id)
|
||||
return &tags[i];
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
Reads a TIFF tag and positions the file stream to its data section
|
||||
*/
|
||||
void
|
||||
parse_tiff_tag(TReadHelper& read, tiff_tag& tag, off_t& offset)
|
||||
{
|
||||
read(tag.tag);
|
||||
read(tag.type);
|
||||
read(tag.length);
|
||||
|
||||
offset = read.Position() + 4;
|
||||
|
||||
uint32 length = tag.length;
|
||||
|
||||
switch (tag.type) {
|
||||
case TIFF_UINT16_TYPE:
|
||||
case TIFF_INT16_TYPE:
|
||||
length *= 2;
|
||||
break;
|
||||
|
||||
case TIFF_UINT32_TYPE:
|
||||
case TIFF_INT32_TYPE:
|
||||
case TIFF_FLOAT_TYPE:
|
||||
length *= 4;
|
||||
break;
|
||||
|
||||
case TIFF_UFRACTION_TYPE:
|
||||
case TIFF_FRACTION_TYPE:
|
||||
case TIFF_DOUBLE_TYPE:
|
||||
length *= 8;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (length > 4) {
|
||||
uint32 position;
|
||||
read(position);
|
||||
|
||||
read.Seek(position, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static status_t
|
||||
parse_tiff_directory(TReadHelper& read, off_t offset, BMessage& target,
|
||||
const convert_tag* convertTags, size_t convertTagCount)
|
||||
{
|
||||
read.Seek(offset, SEEK_SET);
|
||||
|
||||
uint16 tags;
|
||||
read(tags);
|
||||
if (tags > 512)
|
||||
return B_BAD_DATA;
|
||||
|
||||
while (tags--) {
|
||||
off_t nextOffset;
|
||||
tiff_tag tag;
|
||||
parse_tiff_tag(read, tag, nextOffset);
|
||||
|
||||
//printf("TAG %u\n", tag.tag);
|
||||
|
||||
switch (tag.tag) {
|
||||
case TAG_EXIF_OFFSET:
|
||||
case TAG_SUB_DIR_OFFSET:
|
||||
{
|
||||
status_t status = parse_tiff_directory(read, target,
|
||||
convertTags, convertTagCount);
|
||||
if (status < B_OK)
|
||||
return status;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
const convert_tag* convertTag = find_convert_tag(tag.tag,
|
||||
convertTags, convertTagCount);
|
||||
if (convertTag != NULL) {
|
||||
add_to_message(read, target, tag, convertTag->name,
|
||||
convertTag->type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
read.Seek(nextOffset, SEEK_SET);
|
||||
}
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
|
||||
static status_t
|
||||
parse_tiff_directory(TReadHelper& read, BMessage& target,
|
||||
const convert_tag* tags, size_t tagCount)
|
||||
{
|
||||
while (true) {
|
||||
int32 offset;
|
||||
read(offset);
|
||||
if (offset == 0)
|
||||
break;
|
||||
|
||||
status_t status = parse_tiff_directory(read, offset, target,
|
||||
tags, tagCount);
|
||||
if (status < B_OK)
|
||||
return status;
|
||||
}
|
||||
|
||||
return B_OK;
|
||||
}
|
||||
|
||||
|
||||
// #pragma mark -
|
||||
|
||||
|
||||
status_t
|
||||
convert_exif_to_message(BPositionIO& source, BMessage& target,
|
||||
const convert_tag* tags, size_t tagCount)
|
||||
{
|
||||
TReadHelper read(source);
|
||||
|
||||
uint16 endian;
|
||||
read(endian);
|
||||
if (endian != 'MM' && endian != 'II')
|
||||
return B_BAD_TYPE;
|
||||
|
||||
#if B_HOST_IS_LENDIAN
|
||||
read.SetSwap(endian == 'MM');
|
||||
#else
|
||||
read.SetSwap(endian == 'II');
|
||||
#endif
|
||||
|
||||
int16 magic;
|
||||
read(magic);
|
||||
if (magic != 42)
|
||||
return B_BAD_TYPE;
|
||||
|
||||
return parse_tiff_directory(read, target, tags, tagCount);
|
||||
}
|
||||
|
||||
|
||||
status_t
|
||||
convert_exif_to_message(BPositionIO& source, BMessage& target)
|
||||
{
|
||||
return convert_exif_to_message(source, target, kDefaultTags,
|
||||
kNumDefaultTags);
|
||||
}
|
25
src/add-ons/translators/jpeg/exif_parser.h
Normal file
25
src/add-ons/translators/jpeg/exif_parser.h
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2007, Axel Dörfler, axeld@pinc-software.de. All rights reserved.
|
||||
* Distributed under the terms of the MIT License.
|
||||
*/
|
||||
#ifndef EXIF_PARSER_H
|
||||
#define EXIF_PARSER_H
|
||||
|
||||
|
||||
#include <DataIO.h>
|
||||
#include <TypeConstants.h>
|
||||
|
||||
class BMessage;
|
||||
|
||||
|
||||
struct convert_tag {
|
||||
uint16 tag;
|
||||
type_code type;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
status_t convert_exif_to_message(BPositionIO& source, BMessage& target);
|
||||
status_t convert_exif_to_message_etc(BPositionIO& source, BMessage& target,
|
||||
const convert_tag* tags, size_t tagCount);
|
||||
|
||||
#endif // EXIF_PARSER_H
|
Loading…
x
Reference in New Issue
Block a user