diff options
Diffstat (limited to '')
29 files changed, 7606 insertions, 0 deletions
diff --git a/xbmc/pictures/CMakeLists.txt b/xbmc/pictures/CMakeLists.txt new file mode 100644 index 0000000..9bbe6cd --- /dev/null +++ b/xbmc/pictures/CMakeLists.txt @@ -0,0 +1,27 @@ +set(SOURCES ExifParse.cpp + GUIDialogPictureInfo.cpp + GUIViewStatePictures.cpp + GUIWindowPictures.cpp + GUIWindowSlideShow.cpp + IptcParse.cpp + JpegParse.cpp + libexif.cpp + Picture.cpp + PictureInfoLoader.cpp + PictureInfoTag.cpp + PictureScalingAlgorithm.cpp + PictureThumbLoader.cpp + SlideShowPicture.cpp) + +set(HEADERS GUIDialogPictureInfo.h + GUIViewStatePictures.h + GUIWindowPictures.h + GUIWindowSlideShow.h + Picture.h + PictureInfoLoader.h + PictureInfoTag.h + PictureScalingAlgorithm.h + PictureThumbLoader.h + SlideShowPicture.h) + +core_add_library(pictures) diff --git a/xbmc/pictures/ExifParse.cpp b/xbmc/pictures/ExifParse.cpp new file mode 100644 index 0000000..21ebb60 --- /dev/null +++ b/xbmc/pictures/ExifParse.cpp @@ -0,0 +1,958 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +//-------------------------------------------------------------------------- +// Program to pull the EXIF information out of various types of digital +// images and present it in a reasonably consistent way +// +// Original code pulled from 'jhead' by Matthias Wandel (http://www.sentex.net/~mwandel/) - jhead +// Adapted for XBMC by DD. +//-------------------------------------------------------------------------- + +// Note: Jhead supports TAG_MAKER_NOTE exif field, +// but that is omitted for now - to make porting easier and addition smaller + +#include "ExifParse.h" + +#ifdef TARGET_WINDOWS +#include <windows.h> +#else +#include <memory.h> +#include <cstring> +#endif + +#include <math.h> +#include <stdint.h> +#include <stdio.h> + +#ifndef min +#define min(a,b) (a)>(b)?(b):(a) +#endif +#ifndef max +#define max(a,b) (a)<(b)?(b):(a) +#endif + + +// Prototypes for exif utility functions. +static void ErrNonfatal(const char* const msg, int a1, int a2); + +#define DIR_ENTRY_ADDR(Start, Entry) ((Start)+2+12*(Entry)) + + +//-------------------------------------------------------------------------- +// Describes tag values +#define TAG_DESCRIPTION 0x010E +#define TAG_MAKE 0x010F +#define TAG_MODEL 0x0110 +#define TAG_ORIENTATION 0x0112 +#define TAG_X_RESOLUTION 0x011A // Not processed. Format rational64u (see http://search.cpan.org/src/EXIFTOOL/Image-ExifTool-6.76/html/TagNames/EXIF.html) +#define TAG_Y_RESOLUTION 0x011B // Not processed. Format rational64u +#define TAG_RESOLUTION_UNIT 0x0128 // Not processed. Format int16u. Values: 1-none; 2-inches; 3-cm +#define TAG_SOFTWARE 0x0131 +#define TAG_DATETIME 0x0132 +#define TAG_THUMBNAIL_OFFSET 0x0201 +#define TAG_THUMBNAIL_LENGTH 0x0202 +#define TAG_Y_CB_CR_POS 0x0213 // Not processed. Format int16u. Values: 1-Centered; 2-Co-sited +#define TAG_EXPOSURETIME 0x829A +#define TAG_FNUMBER 0x829D +#define TAG_EXIF_OFFSET 0x8769 +#define TAG_EXPOSURE_PROGRAM 0x8822 +#define TAG_GPSINFO 0x8825 +#define TAG_ISO_EQUIVALENT 0x8827 +#define TAG_EXIF_VERSION 0x9000 // Not processed. +#define TAG_COMPONENT_CFG 0x9101 // Not processed. +#define TAG_DATETIME_ORIGINAL 0x9003 +#define TAG_DATETIME_DIGITIZED 0x9004 +#define TAG_SHUTTERSPEED 0x9201 +#define TAG_APERTURE 0x9202 +#define TAG_EXPOSURE_BIAS 0x9204 +#define TAG_MAXAPERTURE 0x9205 +#define TAG_SUBJECT_DISTANCE 0x9206 +#define TAG_METERING_MODE 0x9207 +#define TAG_LIGHT_SOURCE 0x9208 +#define TAG_FLASH 0x9209 +#define TAG_FOCALLENGTH 0x920A +#define TAG_MAKER_NOTE 0x927C // Not processed yet. Maybe in the future. +#define TAG_USERCOMMENT 0x9286 +#define TAG_XP_COMMENT 0x9c9c +#define TAG_FLASHPIX_VERSION 0xA000 // Not processed. +#define TAG_COLOUR_SPACE 0xA001 // Not processed. Format int16u. Values: 1-RGB; 2-Adobe RGB 65535-Uncalibrated +#define TAG_EXIF_IMAGEWIDTH 0xa002 +#define TAG_EXIF_IMAGELENGTH 0xa003 +#define TAG_INTEROP_OFFSET 0xa005 +#define TAG_FOCALPLANEXRES 0xa20E +#define TAG_FOCALPLANEUNITS 0xa210 +#define TAG_EXPOSURE_INDEX 0xa215 +#define TAG_EXPOSURE_MODE 0xa402 +#define TAG_WHITEBALANCE 0xa403 +#define TAG_DIGITALZOOMRATIO 0xA404 +#define TAG_FOCALLENGTH_35MM 0xa405 + +#define TAG_GPS_LAT_REF 1 +#define TAG_GPS_LAT 2 +#define TAG_GPS_LONG_REF 3 +#define TAG_GPS_LONG 4 +#define TAG_GPS_ALT_REF 5 +#define TAG_GPS_ALT 6 + +//-------------------------------------------------------------------------- +// Exif format descriptor stuff +namespace +{ +constexpr auto FMT_BYTE = 1; +[[maybe_unused]] constexpr auto FMT_STRING = 2; +constexpr auto FMT_USHORT = 3; +constexpr auto FMT_ULONG = 4; +constexpr auto FMT_URATIONAL = 5; +constexpr auto FMT_SBYTE = 6; +[[maybe_unused]] constexpr auto FMT_UNDEFINED = 7; +constexpr auto FMT_SSHORT = 8; +constexpr auto FMT_SLONG = 9; +constexpr auto FMT_SRATIONAL = 10; +constexpr auto FMT_SINGLE = 11; +constexpr auto FMT_DOUBLE = 12; +// NOTE: Remember to change NUM_FORMATS if you define a new format +constexpr auto NUM_FORMATS = 12; + +const unsigned int BytesPerFormat[NUM_FORMATS] = {1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8}; +} // namespace + +//-------------------------------------------------------------------------- +// Internationalisation string IDs. The enum order must match that in the +// language file (e.g. 'language/resource.language.en_gb/strings.po', and EXIF_PARSE_STRING_ID_BASE +// must match the ID of the first Exif string in that file. +#define EXIF_PARSE_STRING_ID_BASE 21800 +enum { +// Distance + ExifStrDistanceInfinite = EXIF_PARSE_STRING_ID_BASE, +// Whitebalance et.al. + ExifStrManual, + ExifStrAuto, +// Flash modes + ExifStrYes, + ExifStrNo, + ExifStrFlashNoStrobe, + ExifStrFlashStrobe, + ExifStrFlashManual, + ExifStrFlashManualNoReturn, + ExifStrFlashManualReturn, + ExifStrFlashAuto, + ExifStrFlashAutoNoReturn, + ExifStrFlashAutoReturn, + ExifStrFlashRedEye, + ExifStrFlashRedEyeNoReturn, + ExifStrFlashRedEyeReturn, + ExifStrFlashManualRedEye, + ExifStrFlashManualRedEyeNoReturn, + ExifStrFlashManualRedEyeReturn, + ExifStrFlashAutoRedEye, + ExifStrFlashAutoRedEyeNoReturn, + ExifStrFlashAutoRedEyeReturn, +// Light sources + ExifStrDaylight, + ExifStrFluorescent, + ExifStrIncandescent, + ExifStrFlash, + ExifStrFineWeather, + ExifStrShade, +// Metering Mode + ExifStrMeteringCenter, + ExifStrMeteringSpot, + ExifStrMeteringMatrix, +// Exposure Program + ExifStrExposureProgram, + ExifStrExposureAperture, + ExifStrExposureShutter, + ExifStrExposureCreative, + ExifStrExposureAction, + ExifStrExposurePortrait, + ExifStrExposureLandscape, +// Exposure mode + ExifStrExposureModeAuto, +// ISO equivalent + ExifStrIsoEquivalent, +// GPS latitude, longitude, altitude + ExifStrGpsLatitude, + ExifStrGpsLongitude, + ExifStrGpsAltitude, +}; + + + + +//-------------------------------------------------------------------------- +// Report non fatal errors. Now that microsoft.net modifies exif headers, +// there's corrupted ones, and there could be more in the future. +//-------------------------------------------------------------------------- +static void ErrNonfatal(const char* const msg, int a1, int a2) +{ + printf("ExifParse - Nonfatal Error : %s %d %d", msg, a1, a2); +} + +//-------------------------------------------------------------------------- +// Convert a 16 bit unsigned value from file's native byte order +//-------------------------------------------------------------------------- +int CExifParse::Get16(const void* const Short, const bool motorolaOrder) +{ + if (motorolaOrder) { + return (((const unsigned char *)Short)[0] << 8) | ((const unsigned char *)Short)[1]; + } else { + return (((const unsigned char *)Short)[1] << 8) | ((const unsigned char *)Short)[0]; + } +} + +//-------------------------------------------------------------------------- +// Convert a 32 bit signed value from file's native byte order +//-------------------------------------------------------------------------- +int CExifParse::Get32(const void* const Long, const bool motorolaOrder) +{ + if (motorolaOrder) { + return (((const char *)Long)[0] << 24) | (((const unsigned char *)Long)[1] << 16) + | (((const unsigned char *)Long)[2] << 8 ) | (((const unsigned char *)Long)[3] << 0 ); + } else { + return (((const char *)Long)[3] << 24) | (((const unsigned char *)Long)[2] << 16) + | (((const unsigned char *)Long)[1] << 8 ) | (((const unsigned char *)Long)[0] << 0 ); + } +} + +//-------------------------------------------------------------------------- +// It appears that CStdString constructor replaces "\n" with "\r\n" which results +// in "\r\r\n" if there already is "\r\n", which in turn results in corrupted +// display. So this is an attempt to undo effects of a smart constructor. Also, +// replaces all nonprintable characters with "." +//-------------------------------------------------------------------------- +/*void CExifParse::FixComment(CStdString& comment) +{ + comment.Replace("\r\r\n", "\r\n"); + for (unsigned int i=0; i<comment.length(); i++) + { + if ((comment[i] < 32) && (comment[i] != '\n') && (comment[i] != '\t') && (comment[i] != '\r')) + { + comment[i] = '.'; + } + } +}*/ + +//-------------------------------------------------------------------------- +// Evaluate number, be it int, rational, or float from directory. +//-------------------------------------------------------------------------- +double CExifParse::ConvertAnyFormat(const void* const ValuePtr, int Format) +{ + double Value; + Value = 0; + + switch(Format) + { + case FMT_SBYTE: Value = *(const signed char*)ValuePtr; break; + case FMT_BYTE: Value = *(const unsigned char*)ValuePtr; break; + + case FMT_USHORT: Value = Get16(ValuePtr, m_MotorolaOrder); break; + case FMT_ULONG: Value = (unsigned)Get32(ValuePtr, m_MotorolaOrder); break; + + case FMT_URATIONAL: + case FMT_SRATIONAL: + { + int Num,Den; + Num = Get32(ValuePtr, m_MotorolaOrder); + Den = Get32(4+(const char *)ValuePtr, m_MotorolaOrder); + + if (Den == 0) Value = 0; + else Value = (double)Num/Den; + } + break; + + case FMT_SSHORT: Value = (signed short)Get16(ValuePtr, m_MotorolaOrder); break; + case FMT_SLONG: Value = Get32(ValuePtr, m_MotorolaOrder); break; + + // Not sure if this is correct (never seen float used in Exif format) + case FMT_SINGLE: Value = (double)*(const float*)ValuePtr; break; + case FMT_DOUBLE: Value = *(const double*)ValuePtr; break; + + default: + ErrNonfatal("Illegal format code %d",Format,0); + } + return Value; +} + +//-------------------------------------------------------------------------- +// Exif date tag is stored as a fixed format string "YYYY:MM:DD HH:MM:SS". +// If date is not set, then the string is filled with blanks and colons: +// " : : : : ". We want this string localised. +//-------------------------------------------------------------------------- +/*void CExifParse::LocaliseDate (void) +{ + if (m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME][0] != ' ') + { + int year = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(0, 4).c_str()); + int month = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(5, 2).c_str()); + int day = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(8, 2).c_str()); + int hour = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(11,2).c_str()); + int min = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(14,2).c_str()); + int sec = atoi(m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME].substr(17,2).c_str()); + CDateTime date(year, month, day, hour, min, sec); + m_ExifInfo[SLIDESHOW_EXIF_DATE_TIME] = date.GetAsLocalizedDateTime(); + } +}*/ + + +//-------------------------------------------------------------------------- +// Convert exposure time into a human readable format +//-------------------------------------------------------------------------- +/*void CExifParse::GetExposureTime(const float exposureTime, CStdString& outStr) +{ + if (exposureTime) + { + if (exposureTime < 0.010) outStr.Format("%6.4fs ", exposureTime); + else outStr.Format("%5.3fs ", exposureTime); + if (exposureTime <= 0.5) outStr.Format("%s (1/%d)", outStr, (int)(0.5 + 1/exposureTime)); + } +}*/ + +//-------------------------------------------------------------------------- +// Process one of the nested EXIF directories. +//-------------------------------------------------------------------------- +void CExifParse::ProcessDir(const unsigned char* const DirStart, + const unsigned char* const OffsetBase, + const unsigned ExifLength, + int NestingLevel) +{ + if (NestingLevel > 4) + { + ErrNonfatal("Maximum directory nesting exceeded (corrupt exif header)", 0,0); + return; + } + + char IndentString[25]; + memset(IndentString, ' ', 25); + IndentString[NestingLevel * 4] = '\0'; + + + int NumDirEntries = Get16((const void*)DirStart, m_MotorolaOrder); + + const unsigned char* const DirEnd = DIR_ENTRY_ADDR(DirStart, NumDirEntries); + if (DirEnd+4 > (OffsetBase+ExifLength)) + { + if (DirEnd+2 == OffsetBase+ExifLength || DirEnd == OffsetBase+ExifLength) + { + // Version 1.3 of jhead would truncate a bit too much. + // This also caught later on as well. + } + else + { + ErrNonfatal("Illegally sized directory", 0,0); + return; + } + } + + for (int de=0;de<NumDirEntries;de++) + { + int Tag, Format, Components; + unsigned char* ValuePtr; + int ByteCount; + const unsigned char* const DirEntry = DIR_ENTRY_ADDR(DirStart, de); + + Tag = Get16(DirEntry, m_MotorolaOrder); + Format = Get16(DirEntry+2, m_MotorolaOrder); + Components = Get32(DirEntry+4, m_MotorolaOrder); + + if (Format <= 0 || Format > NUM_FORMATS) + { + ErrNonfatal("Illegal number format %d for tag %04x", Format, Tag); + continue; + } + + if ((unsigned)Components > 0x10000) + { + ErrNonfatal("Illegal number of components %d for tag %04x", Components, Tag); + continue; + } + + ByteCount = Components * BytesPerFormat[Format - 1]; + + if (ByteCount > 4) + { + unsigned OffsetVal; + OffsetVal = (unsigned)Get32(DirEntry+8, m_MotorolaOrder); + // If its bigger than 4 bytes, the dir entry contains an offset. + if (OffsetVal > UINT32_MAX - ByteCount || OffsetVal + ByteCount > ExifLength) + { + // Bogus pointer offset and / or bytecount value + ErrNonfatal("Illegal value pointer for tag %04x", Tag,0); + continue; + } + ValuePtr = (unsigned char*)(const_cast<unsigned char*>(OffsetBase)+OffsetVal); + + if (OffsetVal > m_LargestExifOffset) + { + m_LargestExifOffset = OffsetVal; + } + + } + else { + // 4 bytes or less and value is in the dir entry itself + ValuePtr = (unsigned char*)(const_cast<unsigned char*>(DirEntry)+8); + } + + + // Extract useful components of tag + switch(Tag) + { + case TAG_DESCRIPTION: + { + int length = max(ByteCount, 0); + length = min(length, MAX_COMMENT); + strncpy(m_ExifInfo->Description, (char *)ValuePtr, length); + m_ExifInfo->Description[length] = '\0'; + break; + } + case TAG_MAKE: + { + int space = sizeof(m_ExifInfo->CameraMake); + if (space > 0) + { + strncpy(m_ExifInfo->CameraMake, (char *)ValuePtr, space - 1); + m_ExifInfo->CameraMake[space - 1] = '\0'; + } + break; + } + case TAG_MODEL: + { + int space = sizeof(m_ExifInfo->CameraModel); + if (space > 0) + { + strncpy(m_ExifInfo->CameraModel, (char *)ValuePtr, space - 1); + m_ExifInfo->CameraModel[space - 1] = '\0'; + } + break; + } +// case TAG_SOFTWARE: strncpy(m_ExifInfo->Software, ValuePtr, 5); break; + case TAG_FOCALPLANEXRES: m_FocalPlaneXRes = ConvertAnyFormat(ValuePtr, Format); break; + case TAG_THUMBNAIL_OFFSET: m_ExifInfo->ThumbnailOffset = (unsigned)ConvertAnyFormat(ValuePtr, Format); break; + case TAG_THUMBNAIL_LENGTH: m_ExifInfo->ThumbnailSize = (unsigned)ConvertAnyFormat(ValuePtr, Format); break; + + case TAG_MAKER_NOTE: + continue; + break; + + case TAG_DATETIME_ORIGINAL: + { + + int space = sizeof(m_ExifInfo->DateTime); + if (space > 0) + { + strncpy(m_ExifInfo->DateTime, (char *)ValuePtr, space - 1); + m_ExifInfo->DateTime[space - 1] = '\0'; + // If we get a DATETIME_ORIGINAL, we use that one. + m_DateFound = true; + } + break; + } + case TAG_DATETIME_DIGITIZED: + case TAG_DATETIME: + { + if (!m_DateFound) + { + // If we don't already have a DATETIME_ORIGINAL, use whatever + // time fields we may have. + int space = sizeof(m_ExifInfo->DateTime); + if (space > 0) + { + strncpy(m_ExifInfo->DateTime, (char *)ValuePtr, space - 1); + m_ExifInfo->DateTime[space - 1] = '\0'; + } + } + break; + } + case TAG_USERCOMMENT: + { + // The UserComment allows comments without the charset limitations of ImageDescription. + // Therefore the UserComment field is prefixed by a CharacterCode field (8 Byte): + // - ASCII: 'ASCII\0\0\0' + // - Unicode: 'UNICODE\0' + // - JIS X208-1990: 'JIS\0\0\0\0\0' + // - Unknown: '\0\0\0\0\0\0\0\0' (application specific) + + m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_UNKNOWN; + + const int EXIF_COMMENT_CHARSET_LENGTH = 8; + if (ByteCount >= EXIF_COMMENT_CHARSET_LENGTH) + { + // As some implementations use spaces instead of \0 for the padding, + // we're not so strict and check only the prefix. + if (memcmp(ValuePtr, "ASCII", 5) == 0) + m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_ASCII; + else if (memcmp(ValuePtr, "UNICODE", 7) == 0) + m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_UNICODE; + else if (memcmp(ValuePtr, "JIS", 3) == 0) + m_ExifInfo->CommentsCharset = EXIF_COMMENT_CHARSET_JIS; + + int length = ByteCount - EXIF_COMMENT_CHARSET_LENGTH; + length = min(length, MAX_COMMENT); + memcpy(m_ExifInfo->Comments, ValuePtr + EXIF_COMMENT_CHARSET_LENGTH, length); + m_ExifInfo->Comments[length] = '\0'; +// FixComment(comment); // Ensure comment is printable + } + } + break; + + case TAG_XP_COMMENT: + { + // The XP user comment field is always unicode (UCS-2) encoded + m_ExifInfo->XPCommentsCharset = EXIF_COMMENT_CHARSET_UNICODE; + size_t length = min(ByteCount, MAX_COMMENT); + memcpy(m_ExifInfo->XPComment, ValuePtr, length); + m_ExifInfo->XPComment[length] = '\0'; + } + break; + + case TAG_FNUMBER: + // Simplest way of expressing aperture, so I trust it the most. + // (overwrite previously computd value if there is one) + m_ExifInfo->ApertureFNumber = (float)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_APERTURE: + case TAG_MAXAPERTURE: + // More relevant info always comes earlier, so only use this field if we don't + // have appropriate aperture information yet. + if (m_ExifInfo->ApertureFNumber == 0) + { + m_ExifInfo->ApertureFNumber = (float)exp(ConvertAnyFormat(ValuePtr, Format)*log(2.0)*0.5); + } + break; + + case TAG_FOCALLENGTH: + // Nice digital cameras actually save the focal length as a function + // of how far they are zoomed in. + m_ExifInfo->FocalLength = (float)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_SUBJECT_DISTANCE: + // Inidcates the distacne the autofocus camera is focused to. + // Tends to be less accurate as distance increases. + { + float distance = (float)ConvertAnyFormat(ValuePtr, Format); + m_ExifInfo->Distance = distance; + } + break; + + case TAG_EXPOSURETIME: + { + // Simplest way of expressing exposure time, so I trust it most. + // (overwrite previously computd value if there is one) + float expTime = (float)ConvertAnyFormat(ValuePtr, Format); + if (expTime) + m_ExifInfo->ExposureTime = expTime; + } + break; + + case TAG_SHUTTERSPEED: + // More complicated way of expressing exposure time, so only use + // this value if we don't already have it from somewhere else. + if (m_ExifInfo->ExposureTime == 0) + { + m_ExifInfo->ExposureTime = (float)(1/exp(ConvertAnyFormat(ValuePtr, Format)*log(2.0))); + } + break; + + case TAG_FLASH: + m_ExifInfo->FlashUsed = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_ORIENTATION: + m_ExifInfo->Orientation = (int)ConvertAnyFormat(ValuePtr, Format); + if (m_ExifInfo->Orientation < 0 || m_ExifInfo->Orientation > 8) + { + ErrNonfatal("Undefined rotation value %d", m_ExifInfo->Orientation, 0); + m_ExifInfo->Orientation = 0; + } + break; + + case TAG_EXIF_IMAGELENGTH: + case TAG_EXIF_IMAGEWIDTH: + // Use largest of height and width to deal with images that have been + // rotated to portrait format. + { + int a = (int)ConvertAnyFormat(ValuePtr, Format); + if (m_ExifImageWidth < a) m_ExifImageWidth = a; + } + break; + + case TAG_FOCALPLANEUNITS: + switch((int)ConvertAnyFormat(ValuePtr, Format)) + { + // According to the information I was using, 2 means meters. + // But looking at the Cannon powershot's files, inches is the only + // sensible value. + case 1: m_FocalPlaneUnits = 25.4; break; // inch + case 2: m_FocalPlaneUnits = 25.4; break; + case 3: m_FocalPlaneUnits = 10; break; // centimeter + case 4: m_FocalPlaneUnits = 1; break; // millimeter + case 5: m_FocalPlaneUnits = .001; break; // micrometer + } + break; + + case TAG_EXPOSURE_BIAS: + m_ExifInfo->ExposureBias = (float)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_WHITEBALANCE: + m_ExifInfo->Whitebalance = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_LIGHT_SOURCE: + //Quercus: 17-1-2004 Added LightSource, some cams return this, whitebalance or both + m_ExifInfo->LightSource = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_METERING_MODE: + m_ExifInfo->MeteringMode = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_EXPOSURE_PROGRAM: + m_ExifInfo->ExposureProgram = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_EXPOSURE_INDEX: + if (m_ExifInfo->ISOequivalent == 0) + { + // Exposure index and ISO equivalent are often used interchangeably, + // so we will do the same. + // http://photography.about.com/library/glossary/bldef_ei.htm + m_ExifInfo->ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format); + } + break; + + case TAG_ISO_EQUIVALENT: + m_ExifInfo->ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format); + if (m_ExifInfo->ISOequivalent < 50) + m_ExifInfo->ISOequivalent *= 200; // Fixes strange encoding on some older digicams. + break; + + case TAG_EXPOSURE_MODE: + m_ExifInfo->ExposureMode = (int)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_DIGITALZOOMRATIO: + m_ExifInfo->DigitalZoomRatio = (float)ConvertAnyFormat(ValuePtr, Format); + break; + + case TAG_EXIF_OFFSET: + case TAG_INTEROP_OFFSET: + { + const unsigned char* const SubdirStart = OffsetBase + (unsigned)Get32(ValuePtr, m_MotorolaOrder); + if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength) + { + ErrNonfatal("Illegal exif or interop ofset directory link",0,0); + } + else + { + ProcessDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1); + } + continue; + } + break; + + case TAG_GPSINFO: + { + const unsigned char* const SubdirStart = OffsetBase + (unsigned)Get32(ValuePtr, m_MotorolaOrder); + if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength) + { + ErrNonfatal("Illegal GPS directory link",0,0); + } + else + { + ProcessGpsInfo(SubdirStart, ByteCount, OffsetBase, ExifLength); + } + continue; + } + break; + + case TAG_FOCALLENGTH_35MM: + // The focal length equivalent 35 mm is a 2.2 tag (defined as of April 2002) + // if its present, use it to compute equivalent focal length instead of + // computing it from sensor geometry and actual focal length. + m_ExifInfo->FocalLength35mmEquiv = (unsigned)ConvertAnyFormat(ValuePtr, Format); + break; + } + } + + + // In addition to linking to subdirectories via exif tags, + // there's also a potential link to another directory at the end of each + // directory. this has got to be the result of a committee! + unsigned Offset; + + if (DIR_ENTRY_ADDR(DirStart, NumDirEntries) + 4 <= OffsetBase+ExifLength) + { + Offset = (unsigned)Get32(DirStart+2+12*NumDirEntries, m_MotorolaOrder); + if (Offset) + { + const unsigned char* const SubdirStart = OffsetBase + Offset; + if (SubdirStart > OffsetBase+ExifLength || SubdirStart < OffsetBase) + { + if (SubdirStart > OffsetBase && SubdirStart < OffsetBase+ExifLength+20) + { + // Jhead 1.3 or earlier would crop the whole directory! + // As Jhead produces this form of format incorrectness, + // I'll just let it pass silently + } + else + { + ErrNonfatal("Illegal subdirectory link",0,0); + } + } + else + { + if (SubdirStart <= OffsetBase+ExifLength) + { + ProcessDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1); + } + } + if (Offset > m_LargestExifOffset) + { + m_LargestExifOffset = Offset; + } + } + } + else + { + // The exif header ends before the last next directory pointer. + } + + if (m_ExifInfo->ThumbnailOffset) + { + m_ExifInfo->ThumbnailAtEnd = false; + + if (m_ExifInfo->ThumbnailOffset <= ExifLength) + { + if (m_ExifInfo->ThumbnailSize > ExifLength - m_ExifInfo->ThumbnailOffset) + { + // If thumbnail extends past exif header, only save the part that + // actually exists. Canon's EOS viewer utility will do this - the + // thumbnail extracts ok with this hack. + m_ExifInfo->ThumbnailSize = ExifLength - m_ExifInfo->ThumbnailOffset; + } + } + } +} + + +//-------------------------------------------------------------------------- +// Process a EXIF marker +// Describes all the drivel that most digital cameras include... +//-------------------------------------------------------------------------- +bool CExifParse::Process (const unsigned char* const ExifSection, const unsigned short length, ExifInfo_t *info) +{ + m_ExifInfo = info; + // EXIF signature: "Exif\0\0" + // Check EXIF signatures + const char ExifHeader[] = "Exif\0\0"; + const char ExifAlignment0[] = "II"; + const char ExifAlignment1[] = "MM"; + const char ExifExtra = 0x2a; + + const char* pos = (const char*)(ExifSection + sizeof(short)); // position data pointer after length field + + if (memcmp(pos, ExifHeader,6)) + { + printf("ExifParse: incorrect Exif header"); + return false; + } + pos += 6; + + if (memcmp(pos, ExifAlignment0, strlen(ExifAlignment0)) == 0) + { + m_MotorolaOrder = false; + } + else if (memcmp(pos, ExifAlignment1, strlen(ExifAlignment1)) == 0) + { + m_MotorolaOrder = true; + } + else + { + printf("ExifParse: invalid Exif alignment marker"); + return false; + } + pos += strlen(ExifAlignment0); + + // Check the next value for correctness. + if (Get16((const void*)(pos), m_MotorolaOrder) != ExifExtra) + { + printf("ExifParse: invalid Exif start (1)"); + return false; + } + pos += sizeof(short); + + unsigned long FirstOffset = (unsigned)Get32((const void*)pos, m_MotorolaOrder); + if (FirstOffset < 8 || FirstOffset + 8 >= length) + { + ErrNonfatal("Invalid offset of first IFD value: %u", FirstOffset, 0); + return false; + } + + + + // First directory starts 16 bytes in. All offset are relative to 8 bytes in. + ProcessDir(ExifSection+8+FirstOffset, ExifSection+8, length-8, 0); + + m_ExifInfo->ThumbnailAtEnd = m_ExifInfo->ThumbnailOffset >= m_LargestExifOffset; + + // Compute the CCD width, in millimeters. + if (m_FocalPlaneXRes != 0) + { + // Note: With some cameras, its not possible to compute this correctly because + // they don't adjust the indicated focal plane resolution units when using less + // than maximum resolution, so the CCDWidth value comes out too small. Nothing + // that Jhead can do about it - its a camera problem. + m_ExifInfo->CCDWidth = (float)(m_ExifImageWidth * m_FocalPlaneUnits / m_FocalPlaneXRes); + } + + if (m_ExifInfo->FocalLength) + { + if (m_ExifInfo->FocalLength35mmEquiv == 0) + { + // Compute 35 mm equivalent focal length based on sensor geometry if we haven't + // already got it explicitly from a tag. + if (m_ExifInfo->CCDWidth != 0.0f) + { + m_ExifInfo->FocalLength35mmEquiv = + (int)(m_ExifInfo->FocalLength / m_ExifInfo->CCDWidth * 36 + 0.5f); + } + } + } + return true; +} + + + +//-------------------------------------------------------------------------- +// GPS Lat/Long extraction helper function +//-------------------------------------------------------------------------- +void CExifParse::GetLatLong( + const unsigned int Format, + const unsigned char* ValuePtr, + const int ComponentSize, + char *latLongString) +{ + if (Format != FMT_URATIONAL) + { + ErrNonfatal("Illegal number format %d for GPS Lat/Long", Format, 0); + } + else + { + double Values[3]; + for (unsigned a=0; a<3 ;a++) + { + Values[a] = ConvertAnyFormat(ValuePtr+a*ComponentSize, Format); + } + if (Values[0] < 0 || Values[0] > 180 || Values[1] < 0 || Values[1] >= 60 || Values[2] < 0 || Values[2] >= 60) + { + // Ignore invalid values (DMS format expected) + ErrNonfatal("Invalid Lat/Long value", 0, 0); + latLongString[0] = 0; + } + else + { + char latLong[30]; + sprintf(latLong, "%3.0fd %2.0f' %5.2f\"", Values[0], Values[1], Values[2]); + strcat(latLongString, latLong); + } + } +} + +//-------------------------------------------------------------------------- +// Process GPS info directory +//-------------------------------------------------------------------------- +void CExifParse::ProcessGpsInfo( + const unsigned char* const DirStart, + int ByteCountUnused, + const unsigned char* const OffsetBase, + unsigned ExifLength) +{ + int NumDirEntries = Get16(DirStart, m_MotorolaOrder); + + for (int de=0;de<NumDirEntries;de++) + { + const unsigned char* DirEntry = DIR_ENTRY_ADDR(DirStart, de); + + // Fix from aosp 34a2564d3268a5ca1472c5076675782fbaf724d6 + if (DirEntry + 12 > OffsetBase + ExifLength) + { + ErrNonfatal("GPS info directory goes past end of exif", 0, 0); + return; + } + + unsigned Tag = Get16(DirEntry, m_MotorolaOrder); + unsigned Format = Get16(DirEntry+2, m_MotorolaOrder); + unsigned Components = (unsigned)Get32(DirEntry+4, m_MotorolaOrder); + if (Format == 0 || Format > NUM_FORMATS) + { + ErrNonfatal("Illegal number format %d for tag %04x", Format, Tag); + continue; + } + + unsigned ComponentSize = BytesPerFormat[Format - 1]; + unsigned ByteCount = Components * ComponentSize; + + const unsigned char* ValuePtr; + + if (ByteCount > 4) + { + unsigned OffsetVal = (unsigned)Get32(DirEntry+8, m_MotorolaOrder); + // If its bigger than 4 bytes, the dir entry contains an offset. + if (OffsetVal > UINT32_MAX - ByteCount || OffsetVal + ByteCount > ExifLength) + { + // Bogus pointer offset and / or bytecount value + ErrNonfatal("Illegal value pointer for tag %04x", Tag,0); + continue; + } + ValuePtr = OffsetBase+OffsetVal; + } + else + { + // 4 bytes or less and value is in the dir entry itself + ValuePtr = DirEntry+8; + } + + switch(Tag) + { + case TAG_GPS_LAT_REF: + m_ExifInfo->GpsLat[0] = ValuePtr[0]; + m_ExifInfo->GpsLat[1] = 0; + break; + + case TAG_GPS_LONG_REF: + m_ExifInfo->GpsLong[0] = ValuePtr[0]; + m_ExifInfo->GpsLong[1] = 0; + break; + + case TAG_GPS_LAT: + GetLatLong(Format, ValuePtr, ComponentSize, m_ExifInfo->GpsLat); + break; + case TAG_GPS_LONG: + GetLatLong(Format, ValuePtr, ComponentSize, m_ExifInfo->GpsLong); + break; + + case TAG_GPS_ALT_REF: + if (ValuePtr[0] != 0) + m_ExifInfo->GpsAlt[0] = '-'; + m_ExifInfo->GpsAlt[1] = 0; + break; + + case TAG_GPS_ALT: + { + char temp[18]; + sprintf(temp, "%.2fm", static_cast<double>(ConvertAnyFormat(ValuePtr, Format))); + strcat(m_ExifInfo->GpsAlt, temp); + } + break; + } + } +} + diff --git a/xbmc/pictures/ExifParse.h b/xbmc/pictures/ExifParse.h new file mode 100644 index 0000000..6da3bf2 --- /dev/null +++ b/xbmc/pictures/ExifParse.h @@ -0,0 +1,37 @@ +#pragma once + +#include "libexif.h" + +class CExifParse +{ + public: + ~CExifParse(void) = default; + bool Process(const unsigned char* const Data, const unsigned short length, ExifInfo_t *info); + static int Get16(const void* const Short, const bool motorolaOrder=true); + static int Get32(const void* const Long, const bool motorolaOrder=true); + + private: + ExifInfo_t *m_ExifInfo = nullptr; + double m_FocalPlaneXRes = 0.0; + double m_FocalPlaneUnits = 0.0; + unsigned m_LargestExifOffset = 0; // Last exif data referenced (to check if thumbnail is at end) + int m_ExifImageWidth = 0; + bool m_MotorolaOrder = false; + bool m_DateFound = false; + +// void LocaliseDate (void); +// void GetExposureTime (const float exposureTime); + double ConvertAnyFormat(const void* const ValuePtr, int Format); + void ProcessDir(const unsigned char* const DirStart, + const unsigned char* const OffsetBase, + const unsigned ExifLength, int NestingLevel); + void ProcessGpsInfo(const unsigned char* const DirStart, + int ByteCountUnused, + const unsigned char* const OffsetBase, + unsigned ExifLength); + void GetLatLong(const unsigned int Format, + const unsigned char* ValuePtr, + const int ComponentSize, + char *latlongString); +}; + diff --git a/xbmc/pictures/GUIDialogPictureInfo.cpp b/xbmc/pictures/GUIDialogPictureInfo.cpp new file mode 100644 index 0000000..90764e0 --- /dev/null +++ b/xbmc/pictures/GUIDialogPictureInfo.cpp @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIDialogPictureInfo.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "input/Key.h" + +#define CONTROL_PICTURE_INFO 5 + +#define SLIDESHOW_STRING_BASE 21800 - SLIDESHOW_LABELS_START + +CGUIDialogPictureInfo::CGUIDialogPictureInfo(void) + : CGUIDialog(WINDOW_DIALOG_PICTURE_INFO, "DialogPictureInfo.xml") +{ + m_pictureInfo = new CFileItemList; + m_loadType = KEEP_IN_MEMORY; +} + +CGUIDialogPictureInfo::~CGUIDialogPictureInfo(void) +{ + delete m_pictureInfo; +} + +void CGUIDialogPictureInfo::SetPicture(CFileItem *item) +{ + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(item); +} + +void CGUIDialogPictureInfo::OnInitWindow() +{ + UpdatePictureInfo(); + CGUIDialog::OnInitWindow(); +} + +bool CGUIDialogPictureInfo::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + // if we're running from slideshow mode, drop the "next picture" and "previous picture" actions through. + case ACTION_NEXT_PICTURE: + case ACTION_PREV_PICTURE: + case ACTION_PLAYER_PLAY: + case ACTION_PAUSE: + if (CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == WINDOW_SLIDESHOW) + { + CGUIWindow* pWindow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(WINDOW_SLIDESHOW); + return pWindow->OnAction(action); + } + break; + + case ACTION_SHOW_INFO: + Close(); + return true; + }; + return CGUIDialog::OnAction(action); +} + +void CGUIDialogPictureInfo::FrameMove() +{ + const CFileItem* item = CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().GetCurrentSlide(); + if (item && item->GetPath() != m_currentPicture) + { + UpdatePictureInfo(); + m_currentPicture = item->GetPath(); + } + CGUIDialog::FrameMove(); +} + +void CGUIDialogPictureInfo::UpdatePictureInfo() +{ + // add stuff from the current slide to the list + CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PICTURE_INFO); + OnMessage(msgReset); + m_pictureInfo->Clear(); + for (int info = SLIDESHOW_LABELS_START; info <= SLIDESHOW_LABELS_END; ++info) + { + // we only want to add SLIDESHOW_EXIF_DATE_TIME + // so we skip the other date formats + if (info == SLIDESHOW_EXIF_DATE || info == SLIDESHOW_EXIF_LONG_DATE || info == SLIDESHOW_EXIF_LONG_DATE_TIME ) + continue; + + std::string picInfo = + CServiceBroker::GetGUI()->GetInfoManager().GetLabel(info, INFO::DEFAULT_CONTEXT); + if (!picInfo.empty()) + { + CFileItemPtr item(new CFileItem(g_localizeStrings.Get(SLIDESHOW_STRING_BASE + info))); + item->SetLabel2(picInfo); + m_pictureInfo->Add(item); + } + } + CGUIMessage msg(GUI_MSG_LABEL_BIND, GetID(), CONTROL_PICTURE_INFO, 0, 0, m_pictureInfo); + OnMessage(msg); +} + +void CGUIDialogPictureInfo::OnDeinitWindow(int nextWindowID) +{ + CGUIDialog::OnDeinitWindow(nextWindowID); + CGUIMessage msgReset(GUI_MSG_LABEL_RESET, GetID(), CONTROL_PICTURE_INFO); + OnMessage(msgReset); + m_pictureInfo->Clear(); + m_currentPicture.clear(); +} diff --git a/xbmc/pictures/GUIDialogPictureInfo.h b/xbmc/pictures/GUIDialogPictureInfo.h new file mode 100644 index 0000000..20e5b3b --- /dev/null +++ b/xbmc/pictures/GUIDialogPictureInfo.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/GUIDialog.h" + +class CFileItemList; + +class CGUIDialogPictureInfo : + public CGUIDialog +{ +public: + CGUIDialogPictureInfo(void); + ~CGUIDialogPictureInfo(void) override; + void SetPicture(CFileItem *item); + void FrameMove() override; + +protected: + void OnInitWindow() override; + void OnDeinitWindow(int nextWindowID) override; + bool OnAction(const CAction& action) override; + void UpdatePictureInfo(); + + CFileItemList* m_pictureInfo; + std::string m_currentPicture; +}; diff --git a/xbmc/pictures/GUIViewStatePictures.cpp b/xbmc/pictures/GUIViewStatePictures.cpp new file mode 100644 index 0000000..47d0cdf --- /dev/null +++ b/xbmc/pictures/GUIViewStatePictures.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIViewStatePictures.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "filesystem/Directory.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileExtensionProvider.h" +#include "view/ViewState.h" +#include "view/ViewStateSettings.h" + +using namespace XFILE; +using namespace ADDON; + +CGUIViewStateWindowPictures::CGUIViewStateWindowPictures(const CFileItemList& items) : CGUIViewState(items) +{ + if (items.IsVirtualDirectoryRoot()) + { + AddSortMethod(SortByLabel, 551, LABEL_MASKS()); + AddSortMethod(SortByDriveType, 564, LABEL_MASKS()); + SetSortMethod(SortByLabel); + + SetViewAsControl(DEFAULT_VIEW_LIST); + + SetSortOrder(SortOrderAscending); + } + else + { + AddSortMethod(SortByLabel, 551, LABEL_MASKS("%L", "%I", "%L", "")); // Filename, Size | Foldername, empty + AddSortMethod(SortBySize, 553, LABEL_MASKS("%L", "%I", "%L", "%I")); // Filename, Size | Foldername, Size + AddSortMethod(SortByDate, 552, LABEL_MASKS("%L", "%J", "%L", "%J")); // Filename, Date | Foldername, Date + AddSortMethod(SortByDateTaken, 577, LABEL_MASKS("%L", "%t", "%L", "%J")); // Filename, DateTaken | Foldername, Date + AddSortMethod(SortByFile, 561, LABEL_MASKS("%L", "%I", "%L", "")); // Filename, Size | FolderName, empty + + const CViewState *viewState = CViewStateSettings::GetInstance().Get("pictures"); + SetSortMethod(viewState->m_sortDescription); + SetViewAsControl(viewState->m_viewMode); + SetSortOrder(viewState->m_sortDescription.sortOrder); + } + LoadViewState(items.GetPath(), WINDOW_PICTURES); +} + +void CGUIViewStateWindowPictures::SaveViewState() +{ + SaveViewToDb(m_items.GetPath(), WINDOW_PICTURES, CViewStateSettings::GetInstance().Get("pictures")); +} + +std::string CGUIViewStateWindowPictures::GetLockType() +{ + return "pictures"; +} + +std::string CGUIViewStateWindowPictures::GetExtensions() +{ + std::string extensions = CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_SHOWVIDEOS)) + extensions += "|" + CServiceBroker::GetFileExtensionProvider().GetVideoExtensions(); + + return extensions; +} + +VECSOURCES& CGUIViewStateWindowPictures::GetSources() +{ + VECSOURCES *pictureSources = CMediaSourceSettings::GetInstance().GetSources("pictures"); + + // Guard against source type not existing + if (pictureSources == nullptr) + { + static VECSOURCES empty; + return empty; + } + + return *pictureSources; +} + diff --git a/xbmc/pictures/GUIViewStatePictures.h b/xbmc/pictures/GUIViewStatePictures.h new file mode 100644 index 0000000..343089a --- /dev/null +++ b/xbmc/pictures/GUIViewStatePictures.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "view/GUIViewState.h" + +class CGUIViewStateWindowPictures : public CGUIViewState +{ +public: + explicit CGUIViewStateWindowPictures(const CFileItemList& items); + + std::string GetLockType() override; + std::string GetExtensions() override; + VECSOURCES& GetSources() override; + +protected: + void SaveViewState() override; +}; + diff --git a/xbmc/pictures/GUIWindowPictures.cpp b/xbmc/pictures/GUIWindowPictures.cpp new file mode 100644 index 0000000..ebc8fe9 --- /dev/null +++ b/xbmc/pictures/GUIWindowPictures.cpp @@ -0,0 +1,625 @@ +/* + * Copyright (C) 2005-2020 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowPictures.h" + +#include "Autorun.h" +#include "FileItem.h" +#include "GUIDialogPictureInfo.h" +#include "GUIPassword.h" +#include "GUIWindowSlideShow.h" +#include "PictureInfoLoader.h" +#include "ServiceBroker.h" +#include "URL.h" +#include "Util.h" +#include "addons/gui/GUIDialogAddonInfo.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "dialogs/GUIDialogMediaSource.h" +#include "dialogs/GUIDialogProgress.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "input/actions/ActionIDs.h" +#include "interfaces/AnnouncementManager.h" +#include "media/MediaLockState.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "playlists/PlayList.h" +#include "playlists/PlayListFactory.h" +#include "settings/MediaSourceSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/SortUtils.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" +#include "view/GUIViewState.h" + +#define CONTROL_BTNSORTASC 4 +#define CONTROL_LABELFILES 12 + +using namespace XFILE; +using namespace KODI::MESSAGING; + +using namespace std::chrono_literals; + +#define CONTROL_BTNSLIDESHOW 6 +#define CONTROL_BTNSLIDESHOW_RECURSIVE 7 +#define CONTROL_SHUFFLE 9 + +CGUIWindowPictures::CGUIWindowPictures(void) + : CGUIMediaWindow(WINDOW_PICTURES, "MyPics.xml") +{ + m_thumbLoader.SetObserver(this); + m_slideShowStarted = false; + m_dlgProgress = NULL; +} + +void CGUIWindowPictures::OnInitWindow() +{ + CGUIMediaWindow::OnInitWindow(); + if (m_slideShowStarted) + { + CGUIWindowSlideShow* wndw = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + std::string path; + if (wndw && wndw->GetCurrentSlide()) + path = URIUtils::GetDirectory(wndw->GetCurrentSlide()->GetPath()); + if (m_vecItems->IsPath(path)) + { + if (wndw && wndw->GetCurrentSlide()) + m_viewControl.SetSelectedItem(wndw->GetCurrentSlide()->GetPath()); + SaveSelectedItemInHistory(); + } + m_slideShowStarted = false; + } +} + +CGUIWindowPictures::~CGUIWindowPictures(void) = default; + +bool CGUIWindowPictures::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_DEINIT: + { + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + } + break; + + case GUI_MSG_WINDOW_INIT: + { + // is this the first time accessing this window? + if (m_vecItems->GetPath() == "?" && message.GetStringParam().empty()) + message.SetStringParam(CMediaSourceSettings::GetInstance().GetDefaultSource("pictures")); + + m_dlgProgress = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>(WINDOW_DIALOG_PROGRESS); + } + break; + + case GUI_MSG_CLICKED: + { + int iControl = message.GetSenderId(); + if (iControl == CONTROL_BTNSLIDESHOW) // Slide Show + { + OnSlideShow(); + } + else if (iControl == CONTROL_BTNSLIDESHOW_RECURSIVE) // Recursive Slide Show + { + OnSlideShowRecursive(); + } + else if (iControl == CONTROL_SHUFFLE) + { + const std::shared_ptr<CSettings> settings = CServiceBroker::GetSettingsComponent()->GetSettings(); + settings->ToggleBool(CSettings::SETTING_SLIDESHOW_SHUFFLE); + settings->Save(); + } + else if (m_viewControl.HasControl(iControl)) // list/thumb control + { + int iItem = m_viewControl.GetSelectedItem(); + int iAction = message.GetParam1(); + + // iItem is checked for validity inside these routines + if (iAction == ACTION_DELETE_ITEM) + { + // is delete allowed? + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION)) + OnDeleteItem(iItem); + else + return false; + } + else if (iAction == ACTION_PLAYER_PLAY) + { + ShowPicture(iItem, true); + return true; + } + else if (iAction == ACTION_SHOW_INFO) + { + OnItemInfo(iItem); + return true; + } + } + } + break; + } + return CGUIMediaWindow::OnMessage(message); +} + +void CGUIWindowPictures::UpdateButtons() +{ + CGUIMediaWindow::UpdateButtons(); + + // Update the shuffle button + SET_CONTROL_SELECTED(GetID(), CONTROL_SHUFFLE, CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE)); + + // check we can slideshow or recursive slideshow + int nFolders = m_vecItems->GetFolderCount(); + if (nFolders == m_vecItems->Size() || + m_vecItems->GetPath() == "addons://sources/image/") + { + CONTROL_DISABLE(CONTROL_BTNSLIDESHOW); + } + else + { + CONTROL_ENABLE(CONTROL_BTNSLIDESHOW); + } + if (m_guiState.get() && !m_guiState->HideParentDirItems()) + nFolders--; + if (m_vecItems->Size() == 0 || nFolders == 0 || + m_vecItems->GetPath() == "addons://sources/image/") + { + CONTROL_DISABLE(CONTROL_BTNSLIDESHOW_RECURSIVE); + } + else + { + CONTROL_ENABLE(CONTROL_BTNSLIDESHOW_RECURSIVE); + } +} + +void CGUIWindowPictures::OnPrepareFileItems(CFileItemList& items) +{ + CGUIMediaWindow::OnPrepareFileItems(items); + + for (int i=0;i<items.Size();++i ) + if (StringUtils::EqualsNoCase(items[i]->GetLabel(), "folder.jpg")) + items.Remove(i); + + if (items.GetFolderCount() == items.Size() || !CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_USETAGS)) + return; + + // Start the music info loader thread + CPictureInfoLoader loader; + loader.SetProgressCallback(m_dlgProgress); + loader.Load(items); + + bool bShowProgress = !CServiceBroker::GetGUI()->GetWindowManager().HasModalDialog(true); + bool bProgressVisible = false; + + auto start = std::chrono::steady_clock::now(); + + while (loader.IsLoading() && m_dlgProgress && !m_dlgProgress->IsCanceled()) + { + if (bShowProgress) + { // Do we have to init a progress dialog? + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + if (!bProgressVisible && duration.count() > 1500 && m_dlgProgress) + { // tag loading takes more then 1.5 secs, show a progress dialog + CURL url(items.GetPath()); + + m_dlgProgress->SetHeading(CVariant{189}); + m_dlgProgress->SetLine(0, CVariant{505}); + m_dlgProgress->SetLine(1, CVariant{""}); + m_dlgProgress->SetLine(2, CVariant{url.GetWithoutUserDetails()}); + m_dlgProgress->Open(); + m_dlgProgress->ShowProgressBar(true); + bProgressVisible = true; + } + + if (bProgressVisible && m_dlgProgress) + { // keep GUI alive + m_dlgProgress->Progress(); + } + } // if (bShowProgress) + KODI::TIME::Sleep(1ms); + } // while (loader.IsLoading()) + + if (bProgressVisible && m_dlgProgress) + m_dlgProgress->Close(); +} + +bool CGUIWindowPictures::Update(const std::string &strDirectory, bool updateFilterPath /* = true */) +{ + if (m_thumbLoader.IsLoading()) + m_thumbLoader.StopThread(); + + if (!CGUIMediaWindow::Update(strDirectory, updateFilterPath)) + return false; + + m_vecItems->SetArt("thumb", ""); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_GENERATETHUMBS)) + m_thumbLoader.Load(*m_vecItems); + + CPictureThumbLoader thumbLoader; + std::string thumb = thumbLoader.GetCachedImage(*m_vecItems, "thumb"); + m_vecItems->SetArt("thumb", thumb); + + return true; +} + +bool CGUIWindowPictures::OnClick(int iItem, const std::string &player) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size() ) return true; + CFileItemPtr pItem = m_vecItems->Get(iItem); + + if (pItem->IsCBZ() || pItem->IsCBR()) + { + CURL pathToUrl; + if (pItem->IsCBZ()) + pathToUrl = URIUtils::CreateArchivePath("zip", pItem->GetURL(), ""); + else + pathToUrl = URIUtils::CreateArchivePath("rar", pItem->GetURL(), ""); + + OnShowPictureRecursive(pathToUrl.Get()); + return true; + } + else if (CGUIMediaWindow::OnClick(iItem, player)) + return true; + + return false; +} + +bool CGUIWindowPictures::GetDirectory(const std::string &strDirectory, CFileItemList& items) +{ + if (!CGUIMediaWindow::GetDirectory(strDirectory, items)) + return false; + + std::string label; + if (items.GetLabel().empty() && m_rootDir.IsSource(items.GetPath(), CMediaSourceSettings::GetInstance().GetSources("pictures"), &label)) + items.SetLabel(label); + + if (items.GetContent().empty() && !items.IsVirtualDirectoryRoot() && !items.IsPlugin()) + items.SetContent("images"); + return true; +} + +bool CGUIWindowPictures::OnPlayMedia(int iItem, const std::string &player) +{ + if (m_vecItems->Get(iItem)->IsVideo()) + return CGUIMediaWindow::OnPlayMedia(iItem); + + return ShowPicture(iItem, false); +} + +bool CGUIWindowPictures::ShowPicture(int iItem, bool startSlideShow) +{ + if ( iItem < 0 || iItem >= m_vecItems->Size() ) return false; + CFileItemPtr pItem = m_vecItems->Get(iItem); + std::string strPicture = pItem->GetPath(); + +#ifdef HAS_DVD_DRIVE + if (pItem->IsDVD()) + return MEDIA_DETECT::CAutorun::PlayDiscAskResume(m_vecItems->Get(iItem)->GetPath()); +#endif + + if (pItem->m_bIsShareOrDrive) + return false; + + CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (!pSlideShow) + return false; + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo()) + g_application.StopPlaying(); + + pSlideShow->Reset(); + bool bShowVideos = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_SHOWVIDEOS); + for (const auto& pItem : *m_vecItems) + { + if (!pItem->m_bIsFolder && + !(URIUtils::IsRAR(pItem->GetPath()) || URIUtils::IsZIP(pItem->GetPath())) && + (pItem->IsPicture() || (bShowVideos && pItem->IsVideo()))) + { + pSlideShow->Add(pItem.get()); + } + } + + if (pSlideShow->NumSlides() == 0) + return false; + + pSlideShow->Select(strPicture); + + if (startSlideShow) + pSlideShow->StartSlideShow(); + else + { + CVariant param; + param["player"]["speed"] = 1; + param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay", + pSlideShow->GetCurrentSlide(), param); + } + + m_slideShowStarted = true; + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW); + + return true; +} + +void CGUIWindowPictures::OnShowPictureRecursive(const std::string& strPath) +{ + CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (pSlideShow) + { + // stop any video + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo()) + g_application.StopPlaying(); + + SortDescription sorting = m_guiState->GetSortMethod(); + pSlideShow->AddFromPath(strPath, true, + sorting.sortBy, sorting.sortOrder, sorting.sortAttributes); + if (pSlideShow->NumSlides()) + { + m_slideShowStarted = true; + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW); + } + } +} + +void CGUIWindowPictures::OnSlideShowRecursive(const std::string &strPicture) +{ + CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (pSlideShow) + { + std::string strExtensions; + CFileItemList items; + CGUIViewState* viewState=CGUIViewState::GetViewState(GetID(), items); + if (viewState) + { + strExtensions = viewState->GetExtensions(); + delete viewState; + } + m_slideShowStarted = true; + + SortDescription sorting = m_guiState->GetSortMethod(); + pSlideShow->RunSlideShow(strPicture, true, + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE),false, + "", true, + sorting.sortBy, sorting.sortOrder, sorting.sortAttributes, + strExtensions); + } +} + +void CGUIWindowPictures::OnSlideShowRecursive() +{ + OnSlideShowRecursive(m_vecItems->GetPath()); +} + +void CGUIWindowPictures::OnSlideShow() +{ + OnSlideShow(m_vecItems->GetPath()); +} + +void CGUIWindowPictures::OnSlideShow(const std::string &strPicture) +{ + CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (pSlideShow) + { + std::string strExtensions; + CFileItemList items; + CGUIViewState* viewState=CGUIViewState::GetViewState(GetID(), items); + if (viewState) + { + strExtensions = viewState->GetExtensions(); + delete viewState; + } + m_slideShowStarted = true; + + SortDescription sorting = m_guiState->GetSortMethod(); + pSlideShow->RunSlideShow(strPicture, false ,false, false, + "", true, + sorting.sortBy, sorting.sortOrder, sorting.sortAttributes, + strExtensions); + } +} + +void CGUIWindowPictures::OnRegenerateThumbs() +{ + if (m_thumbLoader.IsLoading()) return; + m_thumbLoader.SetRegenerateThumbs(true); + m_thumbLoader.Load(*m_vecItems); +} + +void CGUIWindowPictures::GetContextButtons(int itemNumber, CContextButtons &buttons) +{ + CFileItemPtr item; + if (itemNumber >= 0 && itemNumber < m_vecItems->Size()) + item = m_vecItems->Get(itemNumber); + + if (item) + { + if ( m_vecItems->IsVirtualDirectoryRoot() || m_vecItems->GetPath() == "sources://pictures/" ) + { + CGUIDialogContextMenu::GetContextButtons("pictures", item, buttons); + } + else + { + if (item) + { + if (!(item->m_bIsFolder || item->IsZIP() || item->IsRAR() || item->IsCBZ() || item->IsCBR() || item->IsScript())) + { + if (item->IsPicture()) + buttons.Add(CONTEXT_BUTTON_INFO, 13406); // picture info + buttons.Add(CONTEXT_BUTTON_VIEW_SLIDESHOW, item->m_bIsFolder ? 13317 : 13422); // View Slideshow + } + if (item->m_bIsFolder) + buttons.Add(CONTEXT_BUTTON_RECURSIVE_SLIDESHOW, 13318); // Recursive Slideshow + + if (!m_thumbLoader.IsLoading()) + buttons.Add(CONTEXT_BUTTON_REFRESH_THUMBS, 13315); // Create Thumbnails + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_ALLOWFILEDELETION) && !item->IsReadOnly()) + { + buttons.Add(CONTEXT_BUTTON_DELETE, 117); + buttons.Add(CONTEXT_BUTTON_RENAME, 118); + } + } + + if (!item->IsPlugin() && !item->IsScript() && !m_vecItems->IsPlugin()) + buttons.Add(CONTEXT_BUTTON_SWITCH_MEDIA, 523); + } + } + CGUIMediaWindow::GetContextButtons(itemNumber, buttons); +} + +bool CGUIWindowPictures::OnContextButton(int itemNumber, CONTEXT_BUTTON button) +{ + CFileItemPtr item = (itemNumber >= 0 && itemNumber < m_vecItems->Size()) ? m_vecItems->Get(itemNumber) : CFileItemPtr(); + if (CGUIDialogContextMenu::OnContextButton("pictures", item, button)) + { + Update(""); + return true; + } + switch (button) + { + case CONTEXT_BUTTON_VIEW_SLIDESHOW: + if (item && item->m_bIsFolder) + OnSlideShow(item->GetPath()); + else + ShowPicture(itemNumber, true); + return true; + case CONTEXT_BUTTON_RECURSIVE_SLIDESHOW: + if (item) + OnSlideShowRecursive(item->GetPath()); + return true; + case CONTEXT_BUTTON_INFO: + OnItemInfo(itemNumber); + return true; + case CONTEXT_BUTTON_REFRESH_THUMBS: + OnRegenerateThumbs(); + return true; + case CONTEXT_BUTTON_DELETE: + OnDeleteItem(itemNumber); + return true; + case CONTEXT_BUTTON_RENAME: + OnRenameItem(itemNumber); + return true; + case CONTEXT_BUTTON_SWITCH_MEDIA: + CGUIDialogContextMenu::SwitchMedia("pictures", m_vecItems->GetPath()); + return true; + default: + break; + } + return CGUIMediaWindow::OnContextButton(itemNumber, button); +} + +bool CGUIWindowPictures::OnAddMediaSource() +{ + return CGUIDialogMediaSource::ShowAndAddMediaSource("pictures"); +} + +void CGUIWindowPictures::OnItemLoaded(CFileItem *pItem) +{ + CPictureThumbLoader::ProcessFoldersAndArchives(pItem); +} + +void CGUIWindowPictures::LoadPlayList(const std::string& strPlayList) +{ + CLog::Log(LOGDEBUG, + "CGUIWindowPictures::LoadPlayList()... converting playlist into slideshow: {}", + strPlayList); + std::unique_ptr<PLAYLIST::CPlayList> pPlayList(PLAYLIST::CPlayListFactory::Create(strPlayList)); + if (nullptr != pPlayList) + { + if (!pPlayList->Load(strPlayList)) + { + HELPERS::ShowOKDialogText(CVariant{6}, CVariant{477}); + return ; //hmmm unable to load playlist? + } + } + + PLAYLIST::CPlayList playlist = *pPlayList; + if (playlist.size() > 0) + { + // set up slideshow + CGUIWindowSlideShow *pSlideShow = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (!pSlideShow) + return; + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo()) + g_application.StopPlaying(); + + // convert playlist items into slideshow items + pSlideShow->Reset(); + for (int i = 0; i < playlist.size(); ++i) + { + CFileItemPtr pItem = playlist[i]; + //CLog::Log(LOGDEBUG,"-- playlist item: {}", pItem->GetPath()); + if (pItem->IsPicture() && !(pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBZ() || pItem->IsCBR())) + pSlideShow->Add(pItem.get()); + } + + // start slideshow if there are items + pSlideShow->StartSlideShow(); + if (pSlideShow->NumSlides()) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW); + } +} + +void CGUIWindowPictures::OnItemInfo(int itemNumber) +{ + CFileItemPtr item = m_vecItems->Get(itemNumber); + if (!item) + return; + if (!m_vecItems->IsPlugin() && (item->IsPlugin() || item->IsScript())) + { + CGUIDialogAddonInfo::ShowForItem(item); + return; + } + if (item->m_bIsFolder || item->IsZIP() || item->IsRAR() || item->IsCBZ() || item->IsCBR() || !item->IsPicture()) + return; + CGUIDialogPictureInfo *pictureInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPictureInfo>(WINDOW_DIALOG_PICTURE_INFO); + if (pictureInfo) + { + pictureInfo->SetPicture(item.get()); + pictureInfo->Open(); + } +} + +std::string CGUIWindowPictures::GetStartFolder(const std::string &dir) +{ + if (StringUtils::EqualsNoCase(dir, "plugins") || + StringUtils::EqualsNoCase(dir, "addons")) + return "addons://sources/image/"; + + SetupShares(); + VECSOURCES shares; + m_rootDir.GetSources(shares); + bool bIsSourceName = false; + int iIndex = CUtil::GetMatchingSource(dir, shares, bIsSourceName); + if (iIndex > -1) + { + if (iIndex < static_cast<int>(shares.size()) && shares[iIndex].m_iHasLock == LOCK_STATE_LOCKED) + { + CFileItem item(shares[iIndex]); + if (!g_passwordManager.IsItemUnlocked(&item,"pictures")) + return ""; + } + if (bIsSourceName) + return shares[iIndex].strPath; + return dir; + } + return CGUIMediaWindow::GetStartFolder(dir); +} diff --git a/xbmc/pictures/GUIWindowPictures.h b/xbmc/pictures/GUIWindowPictures.h new file mode 100644 index 0000000..115e562 --- /dev/null +++ b/xbmc/pictures/GUIWindowPictures.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "PictureThumbLoader.h" +#include "windows/GUIMediaWindow.h" + +class CGUIDialogProgress; + +class CGUIWindowPictures : public CGUIMediaWindow, public IBackgroundLoaderObserver +{ +public: + CGUIWindowPictures(void); + ~CGUIWindowPictures(void) override; + bool OnMessage(CGUIMessage& message) override; + void OnInitWindow() override; + +protected: + bool GetDirectory(const std::string &strDirectory, CFileItemList& items) override; + void OnItemInfo(int item); + bool OnClick(int iItem, const std::string &player = "") override; + void UpdateButtons() override; + void OnPrepareFileItems(CFileItemList& items) override; + bool Update(const std::string &strDirectory, bool updateFilterPath = true) override; + void GetContextButtons(int itemNumber, CContextButtons &buttons) override; + bool OnContextButton(int itemNumber, CONTEXT_BUTTON button) override; + bool OnAddMediaSource() override; + std::string GetStartFolder(const std::string &dir) override; + + void OnRegenerateThumbs(); + bool OnPlayMedia(int iItem, const std::string &player = "") override; + bool ShowPicture(int iItem, bool startSlideShow); + void OnShowPictureRecursive(const std::string& strPath); + void OnSlideShow(const std::string& strPicture); + void OnSlideShow(); + void OnSlideShowRecursive(const std::string& strPicture); + void OnSlideShowRecursive(); + void OnItemLoaded(CFileItem* pItem) override; + void LoadPlayList(const std::string& strPlayList) override; + + CGUIDialogProgress* m_dlgProgress; + + CPictureThumbLoader m_thumbLoader; + bool m_slideShowStarted; +}; diff --git a/xbmc/pictures/GUIWindowSlideShow.cpp b/xbmc/pictures/GUIWindowSlideShow.cpp new file mode 100644 index 0000000..a1a898b --- /dev/null +++ b/xbmc/pictures/GUIWindowSlideShow.cpp @@ -0,0 +1,1382 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIWindowSlideShow.h" + +#include "FileItem.h" +#include "GUIDialogPictureInfo.h" +#include "GUIInfoManager.h" +#include "GUIUserMessages.h" +#include "ServiceBroker.h" +#include "TextureDatabase.h" +#include "URL.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "application/ApplicationPowerHandling.h" +#include "filesystem/Directory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUILabelControl.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/Texture.h" +#include "input/Key.h" +#include "interfaces/AnnouncementManager.h" +#include "pictures/GUIViewStatePictures.h" +#include "pictures/PictureThumbLoader.h" +#include "playlists/PlayListTypes.h" +#include "rendering/RenderSystem.h" +#include "settings/DisplaySettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/Random.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/XTimeUtils.h" +#include "utils/log.h" + +#include <random> + +using namespace XFILE; +using namespace KODI::MESSAGING; +using namespace std::chrono_literals; + +#define MAX_ZOOM_FACTOR 10 +#define MAX_PICTURE_SIZE 2048*2048 + +#define IMMEDIATE_TRANSITION_TIME 1 + +#define PICTURE_MOVE_AMOUNT 0.02f +#define PICTURE_MOVE_AMOUNT_ANALOG 0.01f +#define PICTURE_MOVE_AMOUNT_TOUCH 0.002f +#define PICTURE_VIEW_BOX_COLOR 0xffffff00 // YELLOW +#define PICTURE_VIEW_BOX_BACKGROUND 0xff000000 // BLACK + +#define ROTATION_SNAP_RANGE 10.0f + +#define LABEL_ROW1 10 +#define CONTROL_PAUSE 13 + +static float zoomamount[10] = { 1.0f, 1.2f, 1.5f, 2.0f, 2.8f, 4.0f, 6.0f, 9.0f, 13.5f, 20.0f }; + +CBackgroundPicLoader::CBackgroundPicLoader() + : CThread("BgPicLoader") + , m_iPic{0} + , m_iSlideNumber{0} + , m_maxWidth{0} + , m_maxHeight{0} + , m_isLoading{false} + , m_pCallback{nullptr} +{ +} + +CBackgroundPicLoader::~CBackgroundPicLoader() +{ + StopThread(); +} + +void CBackgroundPicLoader::Create(CGUIWindowSlideShow *pCallback) +{ + m_pCallback = pCallback; + m_isLoading = false; + CThread::Create(false); +} + +void CBackgroundPicLoader::Process() +{ + auto totalTime = std::chrono::milliseconds(0); + unsigned int count = 0; + while (!m_bStop) + { // loop around forever, waiting for the app to call LoadPic + if (AbortableWait(m_loadPic, 10ms) == WAIT_SIGNALED) + { + if (m_pCallback) + { + auto start = std::chrono::steady_clock::now(); + std::unique_ptr<CTexture> texture = + CTexture::LoadFromFile(m_strFileName, m_maxWidth, m_maxHeight); + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + totalTime += duration; + count++; + // tell our parent + bool bFullSize = false; + if (texture) + { + bFullSize = ((int)texture->GetWidth() < m_maxWidth) && ((int)texture->GetHeight() < m_maxHeight); + if (!bFullSize) + { + int iSize = texture->GetWidth() * texture->GetHeight() - MAX_PICTURE_SIZE; + if ((iSize + (int)texture->GetWidth() > 0) || (iSize + (int)texture->GetHeight() > 0)) + bFullSize = true; + if (!bFullSize && texture->GetWidth() == CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) + bFullSize = true; + if (!bFullSize && texture->GetHeight() == CServiceBroker::GetRenderSystem()->GetMaxTextureSize()) + bFullSize = true; + } + } + m_pCallback->OnLoadPic(m_iPic, m_iSlideNumber, m_strFileName, std::move(texture), + bFullSize); + m_isLoading = false; + } + } + } + if (count > 0) + CLog::Log(LOGDEBUG, "Time for loading {} images: {} ms, average {} ms", count, + totalTime.count(), totalTime.count() / count); +} + +void CBackgroundPicLoader::LoadPic(int iPic, int iSlideNumber, const std::string &strFileName, const int maxWidth, const int maxHeight) +{ + m_iPic = iPic; + m_iSlideNumber = iSlideNumber; + m_strFileName = strFileName; + m_maxWidth = maxWidth; + m_maxHeight = maxHeight; + m_isLoading = true; + m_loadPic.Set(); +} + +CGUIWindowSlideShow::CGUIWindowSlideShow(void) + : CGUIDialog(WINDOW_SLIDESHOW, "SlideShow.xml") +{ + m_Resolution = RES_INVALID; + m_loadType = KEEP_IN_MEMORY; + m_bLoadNextPic = false; + Reset(); +} + +void CGUIWindowSlideShow::AnnouncePlayerPlay(const CFileItemPtr& item) +{ + CVariant param; + param["player"]["speed"] = m_bSlideShow && !m_bPause ? 1 : 0; + param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay", item, param); +} + +void CGUIWindowSlideShow::AnnouncePlayerPause(const CFileItemPtr& item) +{ + CVariant param; + param["player"]["speed"] = 0; + param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPause", item, param); +} + +void CGUIWindowSlideShow::AnnouncePlayerStop(const CFileItemPtr& item) +{ + CVariant param; + param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + param["end"] = true; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnStop", item, param); +} + +void CGUIWindowSlideShow::AnnouncePlaylistClear() +{ + CVariant data; + data["playlistid"] = PLAYLIST::TYPE_PICTURE; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnClear", data); +} + +void CGUIWindowSlideShow::AnnouncePlaylistAdd(const CFileItemPtr& item, int pos) +{ + CVariant data; + data["playlistid"] = PLAYLIST::TYPE_PICTURE; + data["position"] = pos; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Playlist, "OnAdd", item, data); +} + +void CGUIWindowSlideShow::AnnouncePropertyChanged(const std::string &strProperty, const CVariant &value) +{ + if (strProperty.empty() || value.isNull()) + return; + + CVariant data; + data["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + data["property"][strProperty] = value; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPropertyChanged", + data); +} + +bool CGUIWindowSlideShow::IsPlaying() const +{ + return m_Image[m_iCurrentPic].IsLoaded(); +} + +void CGUIWindowSlideShow::Reset() +{ + m_bSlideShow = false; + m_bShuffled = false; + m_bPause = false; + m_bPlayingVideo = false; + m_bErrorMessage = false; + m_Image[0].UnLoad(); + m_Image[0].Close(); + m_Image[1].UnLoad(); + m_Image[1].Close(); + + m_fRotate = 0.0f; + m_fInitialRotate = 0.0f; + m_iZoomFactor = 1; + m_fZoom = 1.0f; + m_fInitialZoom = 0.0f; + m_iCurrentSlide = 0; + m_iNextSlide = 1; + m_iCurrentPic = 0; + m_iDirection = 1; + m_iLastFailedNextSlide = -1; + m_slides.clear(); + AnnouncePlaylistClear(); + m_Resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); +} + +void CGUIWindowSlideShow::OnDeinitWindow(int nextWindowID) +{ + if (m_Resolution != CDisplaySettings::GetInstance().GetCurrentResolution()) + { + //FIXME: Use GUI resolution for now + //CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(CDisplaySettings::GetInstance().GetCurrentResolution(), true); + } + + if (nextWindowID != WINDOW_FULLSCREEN_VIDEO && + nextWindowID != WINDOW_FULLSCREEN_GAME) + { + // wait for any outstanding picture loads + if (m_pBackgroundLoader) + { + // sleep until the loader finishes loading the current pic + CLog::Log(LOGDEBUG,"Waiting for BackgroundLoader thread to close"); + while (m_pBackgroundLoader->IsLoading()) + KODI::TIME::Sleep(10ms); + // stop the thread + CLog::Log(LOGDEBUG,"Stopping BackgroundLoader thread"); + m_pBackgroundLoader->StopThread(); + m_pBackgroundLoader.reset(); + } + // and close the images. + m_Image[0].Close(); + m_Image[1].Close(); + } + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(nullptr); + m_bSlideShow = false; + + CGUIDialog::OnDeinitWindow(nextWindowID); +} + +void CGUIWindowSlideShow::Add(const CFileItem *picture) +{ + CFileItemPtr item(new CFileItem(*picture)); + if (!item->HasVideoInfoTag() && !item->HasPictureInfoTag()) + { + // item without tag; get mimetype then we can tell whether it's video item + item->FillInMimeType(); + + if (!item->IsVideo()) + // then it is a picture and force tag generation + item->GetPictureInfoTag(); + } + AnnouncePlaylistAdd(item, m_slides.size()); + + m_slides.emplace_back(std::move(item)); +} + +void CGUIWindowSlideShow::ShowNext() +{ + if (m_slides.size() == 1) + return; + + m_iDirection = 1; + m_iNextSlide = GetNextSlide(); + m_iZoomFactor = 1; + m_fZoom = 1.0f; + m_fRotate = 0.0f; + m_bLoadNextPic = true; +} + +void CGUIWindowSlideShow::ShowPrevious() +{ + if (m_slides.size() == 1) + return; + + m_iDirection = -1; + m_iNextSlide = GetNextSlide(); + m_iZoomFactor = 1; + m_fZoom = 1.0f; + m_fRotate = 0.0f; + m_bLoadNextPic = true; +} + +void CGUIWindowSlideShow::Select(const std::string& strPicture) +{ + for (size_t i = 0; i < m_slides.size(); ++i) + { + const CFileItemPtr item = m_slides.at(i); + if (item->GetPath() == strPicture) + { + m_iDirection = 1; + if (!m_Image[m_iCurrentPic].IsLoaded() && (!m_pBackgroundLoader || !m_pBackgroundLoader->IsLoading())) + { + // will trigger loading current slide when next Process call. + m_iCurrentSlide = i; + m_iNextSlide = GetNextSlide(); + } + else + { + m_iNextSlide = i; + m_bLoadNextPic = true; + } + return ; + } + } +} + +void CGUIWindowSlideShow::GetSlideShowContents(CFileItemList &list) +{ + for (size_t index = 0; index < m_slides.size(); index++) + list.Add(CFileItemPtr(new CFileItem(*m_slides.at(index)))); +} + +std::shared_ptr<const CFileItem> CGUIWindowSlideShow::GetCurrentSlide() +{ + if (m_iCurrentSlide >= 0 && m_iCurrentSlide < static_cast<int>(m_slides.size())) + return m_slides.at(m_iCurrentSlide); + return CFileItemPtr(); +} + +bool CGUIWindowSlideShow::InSlideShow() const +{ + return m_bSlideShow; +} + +void CGUIWindowSlideShow::StartSlideShow() +{ + m_bSlideShow = true; + m_iDirection = 1; + if (m_slides.size()) + AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide)); +} + +void CGUIWindowSlideShow::SetDirection(int direction) +{ + direction = direction >= 0 ? 1 : -1; + if (m_iDirection != direction) + { + m_iDirection = direction; + m_iNextSlide = GetNextSlide(); + } +} + +void CGUIWindowSlideShow::Process(unsigned int currentTime, CDirtyRegionList ®ions) +{ + const RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + + // reset the screensaver if we're in a slideshow + // (unless we are the screensaver!) + auto& components = CServiceBroker::GetAppComponents(); + const auto appPower = components.GetComponent<CApplicationPowerHandling>(); + if (m_bSlideShow && !m_bPause && !appPower->IsInScreenSaver()) + appPower->ResetScreenSaver(); + int iSlides = m_slides.size(); + if (!iSlides) + return; + + // if we haven't processed yet, we should mark the whole screen + if (!HasProcessed()) + regions.push_back(CDirtyRegion(CRect(0.0f, 0.0f, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()))); + + if (m_iCurrentSlide < 0 || m_iCurrentSlide >= static_cast<int>(m_slides.size())) + m_iCurrentSlide = 0; + if (m_iNextSlide < 0 || m_iNextSlide >= static_cast<int>(m_slides.size())) + m_iNextSlide = GetNextSlide(); + + // Create our background loader if necessary + if (!m_pBackgroundLoader) + { + m_pBackgroundLoader.reset(new CBackgroundPicLoader()); + m_pBackgroundLoader->Create(this); + } + + bool bSlideShow = m_bSlideShow && !m_bPause && !m_bPlayingVideo; + if (bSlideShow && m_slides.at(m_iCurrentSlide)->HasProperty("unplayable")) + { + m_iNextSlide = GetNextSlide(); + if (m_iCurrentSlide == m_iNextSlide) + return; + m_iCurrentSlide = m_iNextSlide; + m_iNextSlide = GetNextSlide(); + } + + if (m_bErrorMessage) + { // we have an error when loading either the current or next picture + // check to see if we have a picture loaded + CLog::Log(LOGDEBUG, "We have an error loading picture {}!", m_pBackgroundLoader->SlideNumber()); + if (m_iCurrentSlide == m_pBackgroundLoader->SlideNumber()) + { + if (m_Image[m_iCurrentPic].IsLoaded()) + { + // current image was already loaded, so we can ignore this error. + m_bErrorMessage = false; + } + else + { + CLog::Log(LOGERROR, "Error loading the current image {}: {}", m_iCurrentSlide, + m_slides.at(m_iCurrentSlide)->GetPath()); + if (!m_slides.at(m_iCurrentPic)->IsVideo()) + { + // try next if we are in slideshow + CLog::Log(LOGINFO, "set image {} unplayable", m_slides.at(m_iCurrentSlide)->GetPath()); + m_slides.at(m_iCurrentSlide)->SetProperty("unplayable", true); + } + if (m_bLoadNextPic || (bSlideShow && !m_bPause && !m_slides.at(m_iCurrentPic)->IsVideo())) + { + // change to next item, wait loading. + m_iCurrentSlide = m_iNextSlide; + m_iNextSlide = GetNextSlide(); + m_bErrorMessage = false; + } + // else just drop through - there's nothing we can do (error message will be displayed) + } + } + else if (m_iNextSlide == m_pBackgroundLoader->SlideNumber()) + { + CLog::Log(LOGERROR, "Error loading the next image {}: {}", m_iNextSlide, + m_slides.at(m_iNextSlide)->GetPath()); + // load next image failed, then skip to load next of next if next is not video. + if (!m_slides.at(m_iNextSlide)->IsVideo()) + { + CLog::Log(LOGINFO, "set image {} unplayable", m_slides.at(m_iNextSlide)->GetPath()); + m_slides.at(m_iNextSlide)->SetProperty("unplayable", true); + // change to next item, wait loading. + m_iNextSlide = GetNextSlide(); + } + else + { // prevent reload the next pic and repeat fail. + m_iLastFailedNextSlide = m_iNextSlide; + } + m_bErrorMessage = false; + } + else + { // Non-current and non-next slide, just ignore error. + CLog::Log(LOGERROR, "Error loading the non-current non-next image {}/{}: {}", m_iNextSlide, + m_pBackgroundLoader->SlideNumber(), m_slides.at(m_iNextSlide)->GetPath()); + m_bErrorMessage = false; + } + } + + if (m_bErrorMessage) + { // hack, just mark it all + regions.push_back(CDirtyRegion(CRect(0.0f, 0.0f, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()))); + return; + } + + if (!m_Image[m_iCurrentPic].IsLoaded() && !m_pBackgroundLoader->IsLoading()) + { // load first image + CFileItemPtr item = m_slides.at(m_iCurrentSlide); + std::string picturePath = GetPicturePath(item.get()); + if (!picturePath.empty()) + { + if (item->IsVideo()) + CLog::Log(LOGDEBUG, "Loading the thumb {} for current video {}: {}", picturePath, + m_iCurrentSlide, item->GetPath()); + else + CLog::Log(LOGDEBUG, "Loading the current image {}: {}", m_iCurrentSlide, item->GetPath()); + + // load using the background loader + int maxWidth, maxHeight; + + GetCheckedSize((float)res.iWidth * m_fZoom, + (float)res.iHeight * m_fZoom, + maxWidth, maxHeight); + m_pBackgroundLoader->LoadPic(m_iCurrentPic, m_iCurrentSlide, picturePath, maxWidth, maxHeight); + m_iLastFailedNextSlide = -1; + m_bLoadNextPic = false; + } + } + + // check if we should discard an already loaded next slide + if (m_Image[1 - m_iCurrentPic].IsLoaded() && m_Image[1 - m_iCurrentPic].SlideNumber() != m_iNextSlide) + m_Image[1 - m_iCurrentPic].Close(); + + if (m_iNextSlide != m_iCurrentSlide && m_Image[m_iCurrentPic].IsLoaded() && !m_Image[1 - m_iCurrentPic].IsLoaded() && !m_pBackgroundLoader->IsLoading() && m_iLastFailedNextSlide != m_iNextSlide) + { // load the next image + m_iLastFailedNextSlide = -1; + CFileItemPtr item = m_slides.at(m_iNextSlide); + std::string picturePath = GetPicturePath(item.get()); + if (!picturePath.empty() && (!item->IsVideo() || !m_bSlideShow || m_bPause)) + { + if (item->IsVideo()) + CLog::Log(LOGDEBUG, "Loading the thumb {} for next video {}: {}", picturePath, m_iNextSlide, + item->GetPath()); + else + CLog::Log(LOGDEBUG, "Loading the next image {}: {}", m_iNextSlide, item->GetPath()); + + int maxWidth, maxHeight; + GetCheckedSize((float)res.iWidth * m_fZoom, + (float)res.iHeight * m_fZoom, + maxWidth, maxHeight); + m_pBackgroundLoader->LoadPic(1 - m_iCurrentPic, m_iNextSlide, picturePath, maxWidth, maxHeight); + } + } + + bool bPlayVideo = m_slides.at(m_iCurrentSlide)->IsVideo() && m_iVideoSlide != m_iCurrentSlide; + if (bPlayVideo) + bSlideShow = false; + + // render the current image + if (m_Image[m_iCurrentPic].IsLoaded()) + { + m_Image[m_iCurrentPic].SetInSlideshow(bSlideShow); + m_Image[m_iCurrentPic].Pause(!bSlideShow); + m_Image[m_iCurrentPic].Process(currentTime, regions); + } + + // Check if we should be transitioning immediately + if (m_bLoadNextPic && m_Image[m_iCurrentPic].IsLoaded()) + { + CLog::Log(LOGDEBUG, "Starting immediate transition due to user wanting slide {}", + m_slides.at(m_iNextSlide)->GetPath()); + if (m_Image[m_iCurrentPic].StartTransition()) + { + m_Image[m_iCurrentPic].SetTransitionTime(1, IMMEDIATE_TRANSITION_TIME); + m_bLoadNextPic = false; + } + } + + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + // render the next image + if (m_Image[m_iCurrentPic].DrawNextImage()) + { + if (m_bSlideShow && !m_bPause && m_slides.at(m_iNextSlide)->IsVideo()) + { + // do not show thumb of video when playing slideshow + } + else if (m_Image[1 - m_iCurrentPic].IsLoaded()) + { + if (appPlayer->IsPlayingVideo()) + appPlayer->ClosePlayer(); + m_bPlayingVideo = false; + m_iVideoSlide = -1; + + // first time render the next image, make sure using current display effect. + if (!m_Image[1 - m_iCurrentPic].IsStarted()) + { + CSlideShowPic::DISPLAY_EFFECT effect = GetDisplayEffect(m_iNextSlide); + if (m_Image[1 - m_iCurrentPic].DisplayEffectNeedChange(effect)) + m_Image[1 - m_iCurrentPic].Reset(effect); + } + // set the appropriate transition time + m_Image[1 - m_iCurrentPic].SetTransitionTime(0, m_Image[m_iCurrentPic].GetTransitionTime(1)); + m_Image[1 - m_iCurrentPic].Pause(!m_bSlideShow || m_bPause || m_slides.at(m_iNextSlide)->IsVideo()); + m_Image[1 - m_iCurrentPic].Process(currentTime, regions); + } + else // next pic isn't loaded. We should hang around if it is in progress + { + if (m_pBackgroundLoader->IsLoading()) + { + // CLog::Log(LOGDEBUG, "Having to hold the current image ({}) while we load {}", m_vecSlides[m_iCurrentSlide], m_vecSlides[m_iNextSlide]); + m_Image[m_iCurrentPic].Keep(); + } + } + } + + // check if we should swap images now + if (m_Image[m_iCurrentPic].IsFinished() || (m_bLoadNextPic && !m_Image[m_iCurrentPic].IsLoaded())) + { + m_bLoadNextPic = false; + if (m_Image[m_iCurrentPic].IsFinished()) + CLog::Log(LOGDEBUG, "Image {} is finished rendering, switching to {}", + m_slides.at(m_iCurrentSlide)->GetPath(), m_slides.at(m_iNextSlide)->GetPath()); + else + // what if it's bg loading? + CLog::Log(LOGDEBUG, "Image {} is not loaded, switching to {}", + m_slides.at(m_iCurrentSlide)->GetPath(), m_slides.at(m_iNextSlide)->GetPath()); + + if (m_Image[m_iCurrentPic].IsFinished() && m_iCurrentSlide == m_iNextSlide && m_Image[m_iCurrentPic].SlideNumber() == m_iNextSlide) + m_Image[m_iCurrentPic].Reset(GetDisplayEffect(m_iCurrentSlide)); + else + { + if (m_Image[m_iCurrentPic].IsLoaded()) + m_Image[m_iCurrentPic].Reset(GetDisplayEffect(m_iCurrentSlide)); + else + m_Image[m_iCurrentPic].Close(); + + if ((m_Image[1 - m_iCurrentPic].IsLoaded() && m_Image[1 - m_iCurrentPic].SlideNumber() == m_iNextSlide) || + (m_pBackgroundLoader->IsLoading() && m_pBackgroundLoader->SlideNumber() == m_iNextSlide && m_pBackgroundLoader->Pic() == 1 - m_iCurrentPic)) + { + m_iCurrentPic = 1 - m_iCurrentPic; + } + else + { + m_Image[1 - m_iCurrentPic].Close(); + m_iCurrentPic = 1 - m_iCurrentPic; + } + m_iCurrentSlide = m_iNextSlide; + m_iNextSlide = GetNextSlide(); + + bPlayVideo = m_slides.at(m_iCurrentSlide)->IsVideo() && m_iVideoSlide != m_iCurrentSlide; + } + AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide)); + + m_iZoomFactor = 1; + m_fZoom = 1.0f; + m_fRotate = 0.0f; + } + + if (bPlayVideo && !PlayVideo()) + return; + + if (m_Image[m_iCurrentPic].IsLoaded()) + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPicturesInfoProvider().SetCurrentSlide(m_slides.at(m_iCurrentSlide).get()); + + RenderPause(); + if (m_slides.at(m_iCurrentSlide)->IsVideo() && appPlayer && appPlayer->IsRenderingGuiLayer()) + { + MarkDirtyRegion(); + } + CGUIWindow::Process(currentTime, regions); + m_renderRegion.SetRect(0, 0, (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), (float)CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight()); +} + +void CGUIWindowSlideShow::Render() +{ + if (m_slides.empty()) + return; + + CGraphicContext& gfxCtx = CServiceBroker::GetWinSystem()->GetGfxContext(); + gfxCtx.Clear(0xff000000); + + if (m_slides.at(m_iCurrentSlide)->IsVideo()) + { + gfxCtx.SetViewWindow(0, 0, m_coordsRes.iWidth, m_coordsRes.iHeight); + gfxCtx.SetRenderingResolution(gfxCtx.GetVideoResolution(), false); + + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + + if (appPlayer->IsRenderingVideoLayer()) + { + const CRect old = gfxCtx.GetScissors(); + CRect region = GetRenderRegion(); + region.Intersect(old); + gfxCtx.SetScissors(region); + gfxCtx.Clear(0); + gfxCtx.SetScissors(old); + } + else if (appPlayer) + { + const UTILS::COLOR::Color alpha = gfxCtx.MergeAlpha(0xff000000) >> 24; + appPlayer->Render(false, alpha); + } + + gfxCtx.SetRenderingResolution(m_coordsRes, m_needsScaling); + } + else + { + if (m_Image[m_iCurrentPic].IsLoaded()) + m_Image[m_iCurrentPic].Render(); + + if (m_Image[m_iCurrentPic].DrawNextImage() && m_Image[1 - m_iCurrentPic].IsLoaded()) + m_Image[1 - m_iCurrentPic].Render(); + } + + RenderErrorMessage(); + CGUIWindow::Render(); +} + +void CGUIWindowSlideShow::RenderEx() +{ + if (m_slides.at(m_iCurrentSlide)->IsVideo()) + { + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + appPlayer->Render(false, 255, false); + } + + CGUIWindow::RenderEx(); +} + +int CGUIWindowSlideShow::GetNextSlide() +{ + if (m_slides.size() <= 1) + return m_iCurrentSlide; + int step = m_iDirection >= 0 ? 1 : -1; + int nextSlide = (m_iCurrentSlide + step + m_slides.size()) % m_slides.size(); + while (nextSlide != m_iCurrentSlide) + { + if (!m_slides.at(nextSlide)->HasProperty("unplayable")) + return nextSlide; + nextSlide = (nextSlide + step + m_slides.size()) % m_slides.size(); + } + return m_iCurrentSlide; +} + +EVENT_RESULT CGUIWindowSlideShow::OnMouseEvent(const CPoint &point, const CMouseEvent &event) +{ + if (event.m_id == ACTION_GESTURE_NOTIFY) + { + int result = EVENT_RESULT_ROTATE | EVENT_RESULT_ZOOM; + if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally) + result |= EVENT_RESULT_SWIPE; + else + result |= EVENT_RESULT_PAN_HORIZONTAL; + + if (m_Image[m_iCurrentPic].m_bCanMoveVertically) + result |= EVENT_RESULT_PAN_VERTICAL; + + return (EVENT_RESULT)result; + } + else if (event.m_id == ACTION_GESTURE_BEGIN) + { + m_firstGesturePoint = point; + m_fInitialZoom = m_fZoom; + m_fInitialRotate = m_fRotate; + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_PAN) + { + // zoomed in - free move mode + if (m_iZoomFactor != 1 && + (m_Image[m_iCurrentPic].m_bCanMoveHorizontally || m_Image[m_iCurrentPic].m_bCanMoveVertically)) + { + Move(PICTURE_MOVE_AMOUNT_TOUCH / m_iZoomFactor * (m_firstGesturePoint.x - point.x), PICTURE_MOVE_AMOUNT_TOUCH / m_iZoomFactor * (m_firstGesturePoint.y - point.y)); + m_firstGesturePoint = point; + } + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_SWIPE_LEFT || event.m_id == ACTION_GESTURE_SWIPE_RIGHT) + { + if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally) + { + // on zoomlevel 1 just detect swipe left and right + if (event.m_id == ACTION_GESTURE_SWIPE_LEFT) + OnAction(CAction(ACTION_NEXT_PICTURE)); + else + OnAction(CAction(ACTION_PREV_PICTURE)); + } + } + else if (event.m_id == ACTION_GESTURE_END || event.m_id == ACTION_GESTURE_ABORT) + { + if (m_fRotate != 0.0f) + { + // "snap" to nearest of 0, 90, 180 and 270 if the + // difference in angle is +/-10 degrees + float reminder = fmodf(m_fRotate, 90.0f); + if (fabs(reminder) < ROTATION_SNAP_RANGE) + Rotate(-reminder); + else if (reminder > 90.0f - ROTATION_SNAP_RANGE) + Rotate(90.0f - reminder); + else if (-reminder > 90.0f - ROTATION_SNAP_RANGE) + Rotate(-90.0f - reminder); + } + + m_fInitialZoom = 0.0f; + m_fInitialRotate = 0.0f; + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_ZOOM) + { + ZoomRelative(m_fInitialZoom * event.m_offsetX, true); + return EVENT_RESULT_HANDLED; + } + else if (event.m_id == ACTION_GESTURE_ROTATE) + { + Rotate(m_fInitialRotate + event.m_offsetX - m_fRotate, true); + return EVENT_RESULT_HANDLED; + } + return EVENT_RESULT_UNHANDLED; +} + +bool CGUIWindowSlideShow::OnAction(const CAction &action) +{ + switch (action.GetID()) + { + case ACTION_SHOW_INFO: + { + CGUIDialogPictureInfo *pictureInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPictureInfo>(WINDOW_DIALOG_PICTURE_INFO); + if (pictureInfo) + { + // no need to set the picture here, it's done in Render() + pictureInfo->Open(); + } + } + break; + case ACTION_STOP: + { + if (m_slides.size()) + AnnouncePlayerStop(m_slides.at(m_iCurrentSlide)); + auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo()) + appPlayer->ClosePlayer(); + Close(); + break; + } + + case ACTION_NEXT_PICTURE: + ShowNext(); + break; + + case ACTION_PREV_PICTURE: + ShowPrevious(); + break; + + case ACTION_MOVE_RIGHT: + if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally) + ShowNext(); + else + Move(PICTURE_MOVE_AMOUNT, 0); + break; + + case ACTION_MOVE_LEFT: + if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveHorizontally) + ShowPrevious(); + else + Move( -PICTURE_MOVE_AMOUNT, 0); + break; + + case ACTION_MOVE_DOWN: + Move(0, PICTURE_MOVE_AMOUNT); + break; + + case ACTION_MOVE_UP: + Move(0, -PICTURE_MOVE_AMOUNT); + break; + + case ACTION_PAUSE: + case ACTION_PLAYER_PLAY: + if (m_slides.size() == 0) + break; + if (m_slides.at(m_iCurrentSlide)->IsVideo()) + { + if (!m_bPlayingVideo) + { + if (m_bSlideShow) + { + SetDirection(1); + m_bPause = false; + } + PlayVideo(); + } + } + else if (!m_bSlideShow || m_bPause) + { + m_bSlideShow = true; + m_bPause = false; + SetDirection(1); + if (m_Image[m_iCurrentPic].IsLoaded()) + { + CSlideShowPic::DISPLAY_EFFECT effect = GetDisplayEffect(m_iCurrentSlide); + if (m_Image[m_iCurrentPic].DisplayEffectNeedChange(effect)) + m_Image[m_iCurrentPic].Reset(effect); + } + AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide)); + } + else if (action.GetID() == ACTION_PAUSE) + { + m_bPause = true; + AnnouncePlayerPause(m_slides.at(m_iCurrentSlide)); + } + break; + + case ACTION_ZOOM_OUT: + Zoom(m_iZoomFactor - 1); + break; + + case ACTION_ZOOM_IN: + Zoom(m_iZoomFactor + 1); + break; + + case ACTION_GESTURE_SWIPE_UP: + case ACTION_GESTURE_SWIPE_DOWN: + if (m_iZoomFactor == 1 || !m_Image[m_iCurrentPic].m_bCanMoveVertically) + { + bool swipeOnLeft = action.GetAmount() < CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth() / 2.0f; + bool swipeUp = action.GetID() == ACTION_GESTURE_SWIPE_UP; + if (swipeUp == swipeOnLeft) + Rotate(90.0f); + else + Rotate(-90.0f); + } + break; + + case ACTION_ROTATE_PICTURE_CW: + Rotate(90.0f); + break; + + case ACTION_ROTATE_PICTURE_CCW: + Rotate(-90.0f); + break; + + case ACTION_ZOOM_LEVEL_NORMAL: + case ACTION_ZOOM_LEVEL_1: + case ACTION_ZOOM_LEVEL_2: + case ACTION_ZOOM_LEVEL_3: + case ACTION_ZOOM_LEVEL_4: + case ACTION_ZOOM_LEVEL_5: + case ACTION_ZOOM_LEVEL_6: + case ACTION_ZOOM_LEVEL_7: + case ACTION_ZOOM_LEVEL_8: + case ACTION_ZOOM_LEVEL_9: + Zoom((action.GetID() - ACTION_ZOOM_LEVEL_NORMAL) + 1); + break; + + case ACTION_ANALOG_MOVE: + // this action is used and works, when CAction object provides both x and y coordinates + Move(action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, -action.GetAmount(1)*PICTURE_MOVE_AMOUNT_ANALOG); + break; + case ACTION_ANALOG_MOVE_X_LEFT: + Move(-action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, 0.0f); + break; + case ACTION_ANALOG_MOVE_X_RIGHT: + Move(action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG, 0.0f); + break; + case ACTION_ANALOG_MOVE_Y_UP: + Move(0.0f, -action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG); + break; + case ACTION_ANALOG_MOVE_Y_DOWN: + Move(0.0f, action.GetAmount()*PICTURE_MOVE_AMOUNT_ANALOG); + break; + + default: + return CGUIDialog::OnAction(action); + } + return true; +} + +void CGUIWindowSlideShow::RenderErrorMessage() +{ + if (!m_bErrorMessage) + return ; + + const CGUIControl *control = GetControl(LABEL_ROW1); + if (NULL == control || control->GetControlType() != CGUIControl::GUICONTROL_LABEL) + { + return; + } + + CGUIFont *pFont = static_cast<const CGUILabelControl*>(control)->GetLabelInfo().font; + CGUITextLayout::DrawText(pFont, 0.5f*CServiceBroker::GetWinSystem()->GetGfxContext().GetWidth(), 0.5f*CServiceBroker::GetWinSystem()->GetGfxContext().GetHeight(), 0xffffffff, 0, g_localizeStrings.Get(747), XBFONT_CENTER_X | XBFONT_CENTER_Y); +} + +bool CGUIWindowSlideShow::OnMessage(CGUIMessage& message) +{ + switch ( message.GetMessage() ) + { + case GUI_MSG_WINDOW_INIT: + { + m_Resolution = (RESOLUTION) CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_PICTURES_DISPLAYRESOLUTION); + + //FIXME: Use GUI resolution for now + if (false /*m_Resolution != CDisplaySettings::GetInstance().GetCurrentResolution() && m_Resolution != INVALID && m_Resolution!=AUTORES*/) + CServiceBroker::GetWinSystem()->GetGfxContext().SetVideoResolution(m_Resolution, false); + else + m_Resolution = CServiceBroker::GetWinSystem()->GetGfxContext().GetVideoResolution(); + + CGUIDialog::OnMessage(message); + + // turn off slideshow if we only have 1 image + if (m_slides.size() <= 1) + m_bSlideShow = false; + + return true; + } + break; + + case GUI_MSG_SHOW_PICTURE: + { + const std::string& strFile = message.GetStringParam(); + Reset(); + CFileItem item(strFile, false); + Add(&item); + RunSlideShow("", false, false, true, "", false); + } + break; + + case GUI_MSG_START_SLIDESHOW: + { + const std::string& strFolder = message.GetStringParam(); + unsigned int iParams = message.GetParam1(); + const std::string& beginSlidePath = message.GetStringParam(1); + //decode params + bool bRecursive = false; + bool bRandom = false; + bool bNotRandom = false; + bool bPause = false; + if (iParams > 0) + { + if ((iParams & 1) == 1) + bRecursive = true; + if ((iParams & 2) == 2) + bRandom = true; + if ((iParams & 4) == 4) + bNotRandom = true; + if ((iParams & 8) == 8) + bPause = true; + } + RunSlideShow(strFolder, bRecursive, bRandom, bNotRandom, beginSlidePath, !bPause); + } + break; + + case GUI_MSG_PLAYLISTPLAYER_STOPPED: + { + } + break; + + case GUI_MSG_PLAYBACK_STOPPED: + { + if (m_bPlayingVideo) + { + m_bPlayingVideo = false; + m_iVideoSlide = -1; + if (m_bSlideShow) + m_bPause = true; + } + } + break; + + case GUI_MSG_PLAYBACK_ENDED: + { + if (m_bPlayingVideo) + { + m_bPlayingVideo = false; + m_iVideoSlide = -1; + if (m_bSlideShow) + { + m_bPause = false; + if (m_iCurrentSlide == m_iNextSlide) + break; + m_Image[m_iCurrentPic].Close(); + m_iCurrentPic = 1 - m_iCurrentPic; + m_iCurrentSlide = m_iNextSlide; + m_iNextSlide = GetNextSlide(); + AnnouncePlayerPlay(m_slides.at(m_iCurrentSlide)); + m_iZoomFactor = 1; + m_fZoom = 1.0f; + m_fRotate = 0.0f; + } + } + } + break; + } + return CGUIDialog::OnMessage(message); +} + +void CGUIWindowSlideShow::RenderPause() +{ // display the pause icon + if (m_bPause) + { + SET_CONTROL_VISIBLE(CONTROL_PAUSE); + } + else + { + SET_CONTROL_HIDDEN(CONTROL_PAUSE); + } +} + +void CGUIWindowSlideShow::Rotate(float fAngle, bool immediate /* = false */) +{ + if (m_Image[m_iCurrentPic].DrawNextImage()) + return; + + m_fRotate += fAngle; + + m_Image[m_iCurrentPic].Rotate(fAngle, immediate); +} + +void CGUIWindowSlideShow::Zoom(int iZoom) +{ + if (iZoom > MAX_ZOOM_FACTOR || iZoom < 1) + return; + + ZoomRelative(zoomamount[iZoom - 1]); +} + +void CGUIWindowSlideShow::ZoomRelative(float fZoom, bool immediate /* = false */) +{ + if (fZoom < zoomamount[0]) + fZoom = zoomamount[0]; + else if (fZoom > zoomamount[MAX_ZOOM_FACTOR - 1]) + fZoom = zoomamount[MAX_ZOOM_FACTOR - 1]; + + if (m_Image[m_iCurrentPic].DrawNextImage()) + return; + + m_fZoom = fZoom; + + // find the nearest zoom factor + for (unsigned int i = 1; i < MAX_ZOOM_FACTOR; i++) + { + if (m_fZoom > zoomamount[i]) + continue; + + if (fabs(m_fZoom - zoomamount[i - 1]) < fabs(m_fZoom - zoomamount[i])) + m_iZoomFactor = i; + else + m_iZoomFactor = i + 1; + + break; + } + + m_Image[m_iCurrentPic].Zoom(m_fZoom, immediate); +} + +void CGUIWindowSlideShow::Move(float fX, float fY) +{ + if (m_Image[m_iCurrentPic].IsLoaded() && m_Image[m_iCurrentPic].GetZoom() > 1) + { // we move in the opposite direction, due to the fact we are moving + // the viewing window, not the picture. + m_Image[m_iCurrentPic].Move( -fX, -fY); + } +} + +bool CGUIWindowSlideShow::PlayVideo() +{ + CFileItemPtr item = m_slides.at(m_iCurrentSlide); + if (!item || !item->IsVideo()) + return false; + CLog::Log(LOGDEBUG, "Playing current video slide {}", item->GetPath()); + m_bPlayingVideo = true; + m_iVideoSlide = m_iCurrentSlide; + bool ret = g_application.PlayFile(*item, ""); + if (ret == true) + return true; + else + { + CLog::Log(LOGINFO, "set video {} unplayable", item->GetPath()); + item->SetProperty("unplayable", true); + } + m_bPlayingVideo = false; + m_iVideoSlide = -1; + return false; +} + +CSlideShowPic::DISPLAY_EFFECT CGUIWindowSlideShow::GetDisplayEffect(int iSlideNumber) const +{ + if (m_bSlideShow && !m_bPause && !m_slides.at(iSlideNumber)->IsVideo()) + return CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_DISPLAYEFFECTS) ? CSlideShowPic::EFFECT_RANDOM : CSlideShowPic::EFFECT_NONE; + else + return CSlideShowPic::EFFECT_NO_TIMEOUT; +} + +void CGUIWindowSlideShow::OnLoadPic(int iPic, + int iSlideNumber, + const std::string& strFileName, + std::unique_ptr<CTexture> pTexture, + bool bFullSize) +{ + if (pTexture) + { + // set the pic's texture + size etc. + if (iSlideNumber >= static_cast<int>(m_slides.size()) || GetPicturePath(m_slides.at(iSlideNumber).get()) != strFileName) + { // throw this away - we must have cleared the slideshow while we were still loading + return; + } + CLog::Log(LOGDEBUG, "Finished background loading slot {}, {}: {}", iPic, iSlideNumber, + m_slides.at(iSlideNumber)->GetPath()); + m_Image[iPic].SetOriginalSize(pTexture->GetOriginalWidth(), pTexture->GetOriginalHeight(), bFullSize); + m_Image[iPic].SetTexture(iSlideNumber, std::move(pTexture), GetDisplayEffect(iSlideNumber)); + + m_Image[iPic].m_bIsComic = false; + if (URIUtils::IsInRAR(m_slides.at(m_iCurrentSlide)->GetPath()) || URIUtils::IsInZIP(m_slides.at(m_iCurrentSlide)->GetPath())) // move to top for cbr/cbz + { + CURL url(m_slides.at(m_iCurrentSlide)->GetPath()); + const std::string& strHostName = url.GetHostName(); + if (URIUtils::HasExtension(strHostName, ".cbr|.cbz")) + { + m_Image[iPic].m_bIsComic = true; + m_Image[iPic].Move((float)m_Image[iPic].GetOriginalWidth(),(float)m_Image[iPic].GetOriginalHeight()); + } + } + } + else if (iSlideNumber >= static_cast<int>(m_slides.size()) || GetPicturePath(m_slides.at(iSlideNumber).get()) != strFileName) + { // Failed to load image. and not match values calling LoadPic, then something is changed, ignore. + CLog::Log(LOGDEBUG, + "CGUIWindowSlideShow::OnLoadPic({}, {}, {}) on failure not match current state (cur " + "{}, next {}, curpic {}, pic[0, 1].slidenumber={}, {}, {})", + iPic, iSlideNumber, strFileName, m_iCurrentSlide, m_iNextSlide, m_iCurrentPic, + m_Image[0].SlideNumber(), m_Image[1].SlideNumber(), + iSlideNumber >= static_cast<int>(m_slides.size()) + ? "" + : m_slides.at(iSlideNumber)->GetPath()); + } + else + { // Failed to load image. What should be done?? + // We should wait for the current pic to finish rendering, then transition it out, + // release the texture, and try and reload this pic from scratch + m_bErrorMessage = true; + } + MarkDirtyRegion(); +} + +void CGUIWindowSlideShow::Shuffle() +{ + KODI::UTILS::RandomShuffle(m_slides.begin(), m_slides.end()); + m_iCurrentSlide = 0; + m_iNextSlide = GetNextSlide(); + m_bShuffled = true; + + AnnouncePropertyChanged("shuffled", true); +} + +int CGUIWindowSlideShow::NumSlides() const +{ + return m_slides.size(); +} + +int CGUIWindowSlideShow::CurrentSlide() const +{ + return m_iCurrentSlide + 1; +} + +void CGUIWindowSlideShow::AddFromPath(const std::string &strPath, + bool bRecursive, + SortBy method, SortOrder order, SortAttribute sortAttributes, + const std::string &strExtensions) +{ + if (strPath!="") + { + // reset the slideshow + Reset(); + if (bRecursive) + { + path_set recursivePaths; + AddItems(strPath, &recursivePaths, method, order, sortAttributes); + } + else + AddItems(strPath, NULL, method, order, sortAttributes); + } +} + +void CGUIWindowSlideShow::RunSlideShow(const std::string &strPath, + bool bRecursive /* = false */, bool bRandom /* = false */, + bool bNotRandom /* = false */, const std::string &beginSlidePath /* = "" */, + bool startSlideShow /* = true */, SortBy method /* = SortByLabel */, + SortOrder order /* = SortOrderAscending */, SortAttribute sortAttributes /* = SortAttributeNone */, + const std::string &strExtensions) +{ + // stop any video + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + if (appPlayer->IsPlayingVideo()) + g_application.StopPlaying(); + + AddFromPath(strPath, bRecursive, method, order, sortAttributes, strExtensions); + + if (!NumSlides()) + return; + + // mutually exclusive options + // if both are set, clear both and use the gui setting + if (bRandom && bNotRandom) + bRandom = bNotRandom = false; + + // NotRandom overrides the window setting + if ((!bNotRandom && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_SHUFFLE)) || bRandom) + Shuffle(); + + if (!beginSlidePath.empty()) + Select(beginSlidePath); + + if (startSlideShow) + StartSlideShow(); + else + { + CVariant param; + param["player"]["speed"] = 0; + param["player"]["playerid"] = PLAYLIST::TYPE_PICTURE; + CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Player, "OnPlay", + GetCurrentSlide(), param); + } + + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_SLIDESHOW); +} + +void CGUIWindowSlideShow::AddItems(const std::string &strPath, path_set *recursivePaths, SortBy method, SortOrder order, SortAttribute sortAttributes) +{ + // check whether we've already added this path + if (recursivePaths) + { + std::string path(strPath); + URIUtils::RemoveSlashAtEnd(path); + if (recursivePaths->find(path) != recursivePaths->end()) + return; + recursivePaths->insert(path); + } + + CFileItemList items; + CGUIViewStateWindowPictures viewState(items); + + // fetch directory and sort accordingly + if (!CDirectory::GetDirectory(strPath, items, viewState.GetExtensions(), DIR_FLAG_NO_FILE_DIRS)) + return; + + items.Sort(method, order, sortAttributes); + + // need to go into all subdirs + for (int i = 0; i < items.Size(); i++) + { + CFileItemPtr item = items[i]; + if (item->m_bIsFolder && recursivePaths) + { + AddItems(item->GetPath(), recursivePaths); + } + else if (!item->m_bIsFolder && !URIUtils::IsRAR(item->GetPath()) && !URIUtils::IsZIP(item->GetPath())) + { // add to the slideshow + Add(item.get()); + } + } +} + +void CGUIWindowSlideShow::GetCheckedSize(float width, float height, int &maxWidth, int &maxHeight) +{ + maxWidth = CServiceBroker::GetRenderSystem()->GetMaxTextureSize(); + maxHeight = CServiceBroker::GetRenderSystem()->GetMaxTextureSize(); +} + +std::string CGUIWindowSlideShow::GetPicturePath(CFileItem *item) +{ + bool isVideo = item->IsVideo(); + std::string picturePath = item->GetDynPath(); + if (isVideo) + { + picturePath = item->GetArt("thumb"); + if (picturePath.empty() && !item->HasProperty("nothumb")) + { + CPictureThumbLoader thumbLoader; + thumbLoader.LoadItem(item); + picturePath = item->GetArt("thumb"); + if (picturePath.empty()) + item->SetProperty("nothumb", true); + } + } + return picturePath; +} + + +void CGUIWindowSlideShow::RunSlideShow(const std::vector<std::string>& paths, int start /* = 0*/) +{ + auto dialog = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIWindowSlideShow>(WINDOW_SLIDESHOW); + if (dialog) + { + std::vector<CFileItemPtr> items; + items.reserve(paths.size()); + for (const auto& path : paths) + items.push_back(std::make_shared<CFileItem>(CTextureUtils::GetWrappedImageURL(path), false)); + + dialog->Reset(); + dialog->m_bPause = true; + dialog->m_bSlideShow = false; + dialog->m_iDirection = 1; + dialog->m_iCurrentSlide = start; + dialog->m_iNextSlide = (start + 1) % items.size(); + dialog->m_slides = std::move(items); + dialog->Open(); + } +} diff --git a/xbmc/pictures/GUIWindowSlideShow.h b/xbmc/pictures/GUIWindowSlideShow.h new file mode 100644 index 0000000..6955fb4 --- /dev/null +++ b/xbmc/pictures/GUIWindowSlideShow.h @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "SlideShowPicture.h" +#include "guilib/GUIDialog.h" +#include "threads/Event.h" +#include "threads/Thread.h" +#include "utils/SortUtils.h" + +#include <memory> +#include <set> + +class CFileItemList; +class CVariant; + +class CGUIWindowSlideShow; + +class CBackgroundPicLoader : public CThread +{ +public: + CBackgroundPicLoader(); + ~CBackgroundPicLoader() override; + + void Create(CGUIWindowSlideShow *pCallback); + void LoadPic(int iPic, int iSlideNumber, const std::string &strFileName, const int maxWidth, const int maxHeight); + bool IsLoading() { return m_isLoading; } + int SlideNumber() const { return m_iSlideNumber; } + int Pic() const { return m_iPic; } + +private: + void Process() override; + int m_iPic; + int m_iSlideNumber; + std::string m_strFileName; + int m_maxWidth; + int m_maxHeight; + + CEvent m_loadPic; + bool m_isLoading; + + CGUIWindowSlideShow *m_pCallback; +}; + +class CGUIWindowSlideShow : public CGUIDialog +{ +public: + CGUIWindowSlideShow(void); + ~CGUIWindowSlideShow() override = default; + + bool OnMessage(CGUIMessage& message) override; + EVENT_RESULT OnMouseEvent(const CPoint &point, const CMouseEvent &event) override; + bool OnAction(const CAction &action) override; + void Render() override; + void RenderEx() override; + void Process(unsigned int currentTime, CDirtyRegionList ®ions) override; + void OnDeinitWindow(int nextWindowID) override; + + void Reset(); + void Add(const CFileItem *picture); + bool IsPlaying() const; + void Select(const std::string& strPicture); + void GetSlideShowContents(CFileItemList &list); + std::shared_ptr<const CFileItem> GetCurrentSlide(); + void RunSlideShow(const std::string &strPath, bool bRecursive = false, + bool bRandom = false, bool bNotRandom = false, + const std::string &beginSlidePath="", bool startSlideShow = true, + SortBy method = SortByLabel, + SortOrder order = SortOrderAscending, + SortAttribute sortAttributes = SortAttributeNone, + const std::string &strExtensions=""); + void AddFromPath(const std::string &strPath, bool bRecursive, + SortBy method = SortByLabel, + SortOrder order = SortOrderAscending, + SortAttribute sortAttributes = SortAttributeNone, + const std::string &strExtensions=""); + void StartSlideShow(); + bool InSlideShow() const; + void OnLoadPic(int iPic, + int iSlideNumber, + const std::string& strFileName, + std::unique_ptr<CTexture> pTexture, + bool bFullSize); + int NumSlides() const; + int CurrentSlide() const; + void Shuffle(); + bool IsPaused() const { return m_bPause; } + bool IsShuffled() const { return m_bShuffled; } + int GetDirection() const { return m_iDirection; } + + static void RunSlideShow(const std::vector<std::string>& paths, int start = 0); + +private: + void ShowNext(); + void ShowPrevious(); + void SetDirection(int direction); // -1: rewind, 1: forward + + typedef std::set<std::string> path_set; // set to track which paths we're adding + void AddItems(const std::string &strPath, path_set *recursivePaths, + SortBy method = SortByLabel, + SortOrder order = SortOrderAscending, + SortAttribute sortAttributes = SortAttributeNone); + bool PlayVideo(); + CSlideShowPic::DISPLAY_EFFECT GetDisplayEffect(int iSlideNumber) const; + void RenderPause(); + void RenderErrorMessage(); + void Rotate(float fAngle, bool immediate = false); + void Zoom(int iZoom); + void ZoomRelative(float fZoom, bool immediate = false); + void Move(float fX, float fY); + void GetCheckedSize(float width, float height, int &maxWidth, int &maxHeight); + std::string GetPicturePath(CFileItem *item); + int GetNextSlide(); + + void AnnouncePlayerPlay(const CFileItemPtr& item); + void AnnouncePlayerPause(const CFileItemPtr& item); + void AnnouncePlayerStop(const CFileItemPtr& item); + void AnnouncePlaylistClear(); + void AnnouncePlaylistAdd(const CFileItemPtr& item, int pos); + void AnnouncePropertyChanged(const std::string &strProperty, const CVariant &value); + + int m_iCurrentSlide; + int m_iNextSlide; + int m_iDirection; + float m_fRotate; + float m_fInitialRotate; + int m_iZoomFactor; + float m_fZoom; + float m_fInitialZoom; + + bool m_bShuffled; + bool m_bSlideShow; + bool m_bPause; + bool m_bPlayingVideo; + int m_iVideoSlide = -1; + bool m_bErrorMessage; + + std::vector<CFileItemPtr> m_slides; + + CSlideShowPic m_Image[2]; + + int m_iCurrentPic; + // background loader + std::unique_ptr<CBackgroundPicLoader> m_pBackgroundLoader; + int m_iLastFailedNextSlide; + bool m_bLoadNextPic; + RESOLUTION m_Resolution; + CPoint m_firstGesturePoint; +}; diff --git a/xbmc/pictures/IptcParse.cpp b/xbmc/pictures/IptcParse.cpp new file mode 100644 index 0000000..dc66f27 --- /dev/null +++ b/xbmc/pictures/IptcParse.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +//-------------------------------------------------------------------------- +// Module to pull IPTC information out of various types of digital images. +//-------------------------------------------------------------------------- + +//-------------------------------------------------------------------------- +// Process IPTC data. +//-------------------------------------------------------------------------- + +#include "IptcParse.h" + +#include "ExifParse.h" + +#ifdef TARGET_WINDOWS +#include <windows.h> +#else +#include <string.h> +#endif + +#include <stdio.h> + +#ifndef min +#define min(a,b) (a)>(b)?(b):(a) +#endif + +// Supported IPTC entry types +#define IPTC_RECORD_VERSION 0x00 +#define IPTC_SUPLEMENTAL_CATEGORIES 0x14 +#define IPTC_KEYWORDS 0x19 +#define IPTC_CAPTION 0x78 +#define IPTC_AUTHOR 0x7A +#define IPTC_HEADLINE 0x69 +#define IPTC_SPECIAL_INSTRUCTIONS 0x28 +#define IPTC_CATEGORY 0x0F +#define IPTC_BYLINE 0x50 +#define IPTC_BYLINE_TITLE 0x55 +#define IPTC_CREDIT 0x6E +#define IPTC_SOURCE 0x73 +#define IPTC_COPYRIGHT_NOTICE 0x74 +#define IPTC_OBJECT_NAME 0x05 +#define IPTC_CITY 0x5A +#define IPTC_STATE 0x5F +#define IPTC_COUNTRY 0x65 +#define IPTC_TRANSMISSION_REFERENCE 0x67 +#define IPTC_DATE 0x37 +#define IPTC_URGENCY 0x0A +#define IPTC_COUNTRY_CODE 0x64 +#define IPTC_REFERENCE_SERVICE 0x2D +#define IPTC_TIME_CREATED 0x3C +#define IPTC_SUB_LOCATION 0x5C +#define IPTC_IMAGE_TYPE 0x82 + + +//-------------------------------------------------------------------------- +// Process IPTC marker. Return FALSE if unable to process marker. +// +// IPTC block consists of: +// - Marker: 1 byte (0xED) +// - Block length: 2 bytes +// - IPTC Signature: 14 bytes ("Photoshop 3.0\0") +// - 8BIM Signature 4 bytes ("8BIM") +// - IPTC Block start 2 bytes (0x04, 0x04) +// - IPTC Header length 1 byte +// - IPTC header Header is padded to even length, counting the length byte +// - Length 4 bytes +// - IPTC Data which consists of a number of entries, each of which has the following format: +// - Signature 2 bytes (0x1C02) +// - Entry type 1 byte (for defined entry types, see #defines above) +// - entry length 2 bytes +// - entry data 'entry length' bytes +// +//-------------------------------------------------------------------------- +bool CIptcParse::Process (const unsigned char* const Data, const unsigned short itemlen, IPTCInfo_t *info) +{ + if (!info) return false; + + const char IptcSignature1[] = "Photoshop 3.0"; + const char IptcSignature2[] = "8BIM"; + const char IptcSignature3[] = {0x04, 0x04}; + + // Check IPTC signatures + const char* pos = (const char*)(Data + sizeof(short)); // position data pointer after length field + const char* maxpos = (const char*)(Data+itemlen); + unsigned char headerLen = 0; + unsigned char dataLen = 0; + memset(info, 0, sizeof(IPTCInfo_t)); + + if (itemlen < 25) return false; + + if (memcmp(pos, IptcSignature1, strlen(IptcSignature1)-1) != 0) return false; + pos += sizeof(IptcSignature1); // move data pointer to the next field + + if (memcmp(pos, IptcSignature2, strlen(IptcSignature2)-1) != 0) return false; + pos += sizeof(IptcSignature2)-1; // move data pointer to the next field + + while (memcmp(pos, IptcSignature3, sizeof(IptcSignature3)) != 0) { // loop on valid Photoshop blocks + + pos += sizeof(IptcSignature3); // move data pointer to the Header Length + // Skip header + headerLen = *pos; // get header length and move data pointer to the next field + pos += (headerLen & 0xfe) + 2; // move data pointer to the next field (Header is padded to even length, counting the length byte) + + pos += 3; // move data pointer to length, assume only one byte, TODO: use all 4 bytes + + dataLen = *pos++; + pos += dataLen; // skip data section + + if (memcmp(pos, IptcSignature2, sizeof(IptcSignature2) - 1) != 0) return false; + pos += sizeof(IptcSignature2) - 1; // move data pointer to the next field + } + + pos += sizeof(IptcSignature3); // move data pointer to the next field + if (pos >= maxpos) return false; + + // IPTC section found + + // Skip header + headerLen = *pos++; // get header length and move data pointer to the next field + pos += headerLen + 1 - (headerLen % 2); // move data pointer to the next field (Header is padded to even length, counting the length byte) + + if (pos + 4 >= maxpos) return false; + + pos += 4; // move data pointer to the next field + + // Now read IPTC data + while (pos < (const char*)(Data + itemlen-5)) + { + if (pos + 5 > maxpos) return false; + + short signature = (*pos << 8) + (*(pos+1)); + + pos += 2; + if (signature != 0x1C01 && signature != 0x1C02) + break; + + unsigned char type = *pos++; + unsigned short length = (*pos << 8) + (*(pos+1)); + pos += 2; // Skip tag length + + if (pos + length > maxpos) return false; + + // Process tag here + char *tag = NULL; + if (signature == 0x1C02) + { + switch (type) + { + case IPTC_RECORD_VERSION: tag = info->RecordVersion; break; + case IPTC_SUPLEMENTAL_CATEGORIES: tag = info->SupplementalCategories; break; + case IPTC_KEYWORDS: tag = info->Keywords; break; + case IPTC_CAPTION: tag = info->Caption; break; + case IPTC_AUTHOR: tag = info->Author; break; + case IPTC_HEADLINE: tag = info->Headline; break; + case IPTC_SPECIAL_INSTRUCTIONS: tag = info->SpecialInstructions; break; + case IPTC_CATEGORY: tag = info->Category; break; + case IPTC_BYLINE: tag = info->Byline; break; + case IPTC_BYLINE_TITLE: tag = info->BylineTitle; break; + case IPTC_CREDIT: tag = info->Credit; break; + case IPTC_SOURCE: tag = info->Source; break; + case IPTC_COPYRIGHT_NOTICE: tag = info->CopyrightNotice; break; + case IPTC_OBJECT_NAME: tag = info->ObjectName; break; + case IPTC_CITY: tag = info->City; break; + case IPTC_STATE: tag = info->State; break; + case IPTC_COUNTRY: tag = info->Country; break; + case IPTC_TRANSMISSION_REFERENCE: tag = info->TransmissionReference; break; + case IPTC_DATE: tag = info->Date; break; + case IPTC_URGENCY: tag = info->Urgency; break; + case IPTC_REFERENCE_SERVICE: tag = info->ReferenceService; break; + case IPTC_COUNTRY_CODE: tag = info->CountryCode; break; + case IPTC_TIME_CREATED: tag = info->TimeCreated; break; + case IPTC_SUB_LOCATION: tag = info->SubLocation; break; + case IPTC_IMAGE_TYPE: tag = info->ImageType; break; + default: + printf("IptcParse: Unrecognised IPTC tag: 0x%02x", type); + break; + } + } + + if (tag) + { + if (type != IPTC_KEYWORDS || *tag == 0) + { + strncpy(tag, pos, min(length, MAX_IPTC_STRING - 1)); + tag[min(length, MAX_IPTC_STRING - 1)] = 0; + } + else if (type == IPTC_KEYWORDS) + { + // there may be multiple keywords - lets join them + size_t maxLen = MAX_IPTC_STRING - strlen(tag); + if (maxLen > 2) + { + strcat(tag, ", "); + strncat(tag, pos, min(length, maxLen - 3)); + } + } +/* if (id == SLIDESHOW_IPTC_CAPTION) + { + CExifParse::FixComment(m_IptcInfo[id]); // Ensure comment is printable + }*/ + } + pos += length; + } + return true; +} + diff --git a/xbmc/pictures/IptcParse.h b/xbmc/pictures/IptcParse.h new file mode 100644 index 0000000..ba00135 --- /dev/null +++ b/xbmc/pictures/IptcParse.h @@ -0,0 +1,10 @@ +#pragma once + +#include "libexif.h" + +class CIptcParse +{ + public: + static bool Process(const unsigned char* const Data, const unsigned short length, IPTCInfo_t *info); +}; + diff --git a/xbmc/pictures/JpegParse.cpp b/xbmc/pictures/JpegParse.cpp new file mode 100644 index 0000000..718ac54 --- /dev/null +++ b/xbmc/pictures/JpegParse.cpp @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +//-------------------------------------------------------------------------- +// This module gathers information about a digital image file. This includes: +// - File name and path +// - File size +// - Resolution (if available) +// - IPTC information (if available) +// - EXIF information (if available) +// All gathered information is stored in a vector of 'description' and 'value' +// pairs (where both description and value fields are of CStdString types). +//-------------------------------------------------------------------------- + +#include "JpegParse.h" + +#include "filesystem/File.h" + +#ifdef TARGET_WINDOWS +#include <windows.h> +#else +#include <memory.h> +#include <cstring> +typedef unsigned char BYTE; +#endif + +#ifndef min +#define min(a,b) (a)>(b)?(b):(a) +#endif + +using namespace XFILE; + +//-------------------------------------------------------------------------- +#define JPEG_PARSE_STRING_ID_BASE 21500 +enum { + ProcessUnknown = JPEG_PARSE_STRING_ID_BASE, + ProcessSof0, + ProcessSof1, + ProcessSof2, + ProcessSof3, + ProcessSof5, + ProcessSof6, + ProcessSof7, + ProcessSof9, + ProcessSof10, + ProcessSof11, + ProcessSof13, + ProcessSof14, + ProcessSof15, +}; + + + + +//-------------------------------------------------------------------------- +// Constructor +//-------------------------------------------------------------------------- +CJpegParse::CJpegParse(): + m_SectionBuffer(NULL) +{ + memset(&m_ExifInfo, 0, sizeof(m_ExifInfo)); + memset(&m_IPTCInfo, 0, sizeof(m_IPTCInfo)); +} + +//-------------------------------------------------------------------------- +// Process a SOFn marker. This is useful for the image dimensions +//-------------------------------------------------------------------------- +void CJpegParse::ProcessSOFn (void) +{ + m_ExifInfo.Height = CExifParse::Get16(m_SectionBuffer+3); + m_ExifInfo.Width = CExifParse::Get16(m_SectionBuffer+5); + + unsigned char num_components = m_SectionBuffer[7]; + if (num_components != 3) + { + m_ExifInfo.IsColor = 0; + } + else + { + m_ExifInfo.IsColor = 1; + } +} + + +//-------------------------------------------------------------------------- +// Read a section from a JPEG file. Note that this function allocates memory. +// It must be called in pair with ReleaseSection +//-------------------------------------------------------------------------- +bool CJpegParse::GetSection (CFile& infile, const unsigned short sectionLength) +{ + if (sectionLength < 2) + { + printf("JpgParse: invalid section length"); + return false; + } + + m_SectionBuffer = new unsigned char[sectionLength]; + if (m_SectionBuffer == NULL) + { + printf("JpgParse: could not allocate memory"); + return false; + } + // Store first two pre-read bytes. + m_SectionBuffer[0] = (unsigned char)(sectionLength >> 8); + m_SectionBuffer[1] = (unsigned char)(sectionLength & 0x00FF); + + unsigned int len = (unsigned int)sectionLength; + + size_t bytesRead = infile.Read(m_SectionBuffer+sizeof(sectionLength), len-sizeof(sectionLength)); + if (bytesRead != sectionLength-sizeof(sectionLength)) + { + printf("JpgParse: premature end of file?"); + ReleaseSection(); + return false; + } + return true; +} + +//-------------------------------------------------------------------------- +// Deallocate memory allocated in GetSection. This function must always +// be paired by a preceding GetSection call. +//-------------------------------------------------------------------------- +void CJpegParse::ReleaseSection (void) +{ + delete[] m_SectionBuffer; + m_SectionBuffer = NULL; +} + +//-------------------------------------------------------------------------- +// Parse the marker stream until SOS or EOI is seen; infile has already been +// successfully open +//-------------------------------------------------------------------------- +bool CJpegParse::ExtractInfo (CFile& infile) +{ + // Get file marker (two bytes - must be 0xFFD8 for JPEG files + BYTE a; + size_t bytesRead = infile.Read(&a, sizeof(BYTE)); + if ((bytesRead != sizeof(BYTE)) || (a != 0xFF)) + { + return false; + } + bytesRead = infile.Read(&a, sizeof(BYTE)); + if ((bytesRead != sizeof(BYTE)) || (a != M_SOI)) + { + return false; + } + + for(;;) + { + BYTE marker = 0; + for (a=0; a<7; a++) { + bytesRead = infile.Read(&marker, sizeof(BYTE)); + if (marker != 0xFF) + break; + + if (a >= 6) + { + printf("JpgParse: too many padding bytes"); + return false; + } + marker = 0; + } + + // Read the length of the section. + unsigned short itemlen = 0; + bytesRead = infile.Read(&itemlen, sizeof(itemlen)); + itemlen = CExifParse::Get16(&itemlen); + + if ((bytesRead != sizeof(itemlen)) || (itemlen < sizeof(itemlen))) + { + printf("JpgParse: invalid marker"); + return false; + } + + switch(marker) + { + case M_SOS: // stop before hitting compressed data + return true; + + case M_EOI: // in case it's a tables-only JPEG stream + printf("JpgParse: No image in jpeg!"); + return false; + break; + + case M_COM: // Comment section + GetSection(infile, itemlen); + if (m_SectionBuffer != NULL) + { + // CExifParse::FixComment(comment); // Ensure comment is printable + unsigned short length = min(itemlen - 2, MAX_COMMENT); + strncpy(m_ExifInfo.FileComment, (char *)&m_SectionBuffer[2], length); + m_ExifInfo.FileComment[length] = '\0'; + } + ReleaseSection(); + break; + + case M_SOF0: + case M_SOF1: + case M_SOF2: + case M_SOF3: + case M_SOF5: + case M_SOF6: + case M_SOF7: + case M_SOF9: + case M_SOF10: + case M_SOF11: + case M_SOF13: + case M_SOF14: + case M_SOF15: + GetSection(infile, itemlen); + if ((m_SectionBuffer != NULL) && (itemlen >= 7)) + { + ProcessSOFn(); + m_ExifInfo.Process = marker; + } + ReleaseSection(); + break; + + case M_IPTC: + GetSection(infile, itemlen); + if (m_SectionBuffer != NULL) + { + CIptcParse::Process(m_SectionBuffer, itemlen, &m_IPTCInfo); + } + ReleaseSection(); + break; + + case M_EXIF: + // Seen files from some 'U-lead' software with Vivitar scanner + // that uses marker 31 for non exif stuff. Thus make sure + // it says 'Exif' in the section before treating it as exif. + GetSection(infile, itemlen); + if (m_SectionBuffer != NULL) + { + CExifParse exif; + exif.Process(m_SectionBuffer, itemlen, &m_ExifInfo); + } + ReleaseSection(); + break; + + case M_JFIF: + // Regular jpegs always have this tag, exif images have the exif + // marker instead, although ACDsee will write images with both markers. + // this program will re-create this marker on absence of exif marker. + // hence no need to keep the copy from the file. + // fall through to default case + default: + // Skip any other sections. + GetSection(infile, itemlen); + ReleaseSection(); + break; + } + } + return true; +} + +//-------------------------------------------------------------------------- +// Process a file. Check if it is JPEG. Extract exif/iptc info if it is. +//-------------------------------------------------------------------------- +bool CJpegParse::Process (const char *picFileName) +{ + CFile file; + + if (!file.Open(picFileName)) + return false; + + // File exists and successfully opened. Start processing + // Gather all information about the file + +/* // Get file name... + CStdString tmp, urlFName, path; + CURL url(picFileName); + url.GetURLWithoutUserDetails(urlFName); + CUtil::Split(urlFName, path, tmp); + m_JpegInfo[SLIDESHOW_FILE_NAME] = tmp; + // ...then path... + m_JpegInfo[SLIDESHOW_FILE_PATH] = path; + + // ...then size... + __stat64 fileStat; + CFile::Stat(picFileName, &fileStat); + float fileSize = (float)fileStat.st_size; + tmp = ""; + if (fileSize > 1024) + { + fileSize /= 1024; + tmp = "KB"; + } + if (fileSize > 1024) + { + fileSize /= 1024; + tmp = "MB"; + } + if (fileSize > 1024) + { + fileSize /= 1024; + tmp = "GB"; + } + tmp.Format("%.2f %s", fileSize, tmp); + m_JpegInfo[SLIDESHOW_FILE_SIZE] = tmp; + + // ...then date and time... + CDateTime date((time_t)fileStat.st_mtime); + tmp.Format("%s %s", date.GetAsLocalizedDate(), date.GetAsLocalizedTime()); + m_JpegInfo[SLIDESHOW_FILE_DATE] = tmp;*/ + + bool result = ExtractInfo(file); + file.Close(); + return result; +} + diff --git a/xbmc/pictures/JpegParse.h b/xbmc/pictures/JpegParse.h new file mode 100644 index 0000000..11d2ca6 --- /dev/null +++ b/xbmc/pictures/JpegParse.h @@ -0,0 +1,63 @@ +#pragma once + +#include "ExifParse.h" +#include "IptcParse.h" + +#include <stdio.h> + +//-------------------------------------------------------------------------- +// JPEG markers consist of one or more 0xFF bytes, followed by a marker +// code byte (which is not an FF). Here are the marker codes of interest +// in this application. +//-------------------------------------------------------------------------- + +#define M_SOF0 0xC0 // Start Of Frame N +#define M_SOF1 0xC1 // N indicates which compression process +#define M_SOF2 0xC2 // Only SOF0-SOF2 are now in common use +#define M_SOF3 0xC3 +#define M_SOF5 0xC5 // NB: codes C4 and CC are NOT SOF markers +#define M_SOF6 0xC6 +#define M_SOF7 0xC7 +#define M_SOF9 0xC9 +#define M_SOF10 0xCA +#define M_SOF11 0xCB +#define M_SOF13 0xCD +#define M_SOF14 0xCE +#define M_SOF15 0xCF +#define M_SOI 0xD8 // Start Of Image (beginning of datastream) +#define M_EOI 0xD9 // End Of Image (end of datastream) +#define M_SOS 0xDA // Start Of Scan (begins compressed data) +#define M_JFIF 0xE0 // Jfif marker +#define M_EXIF 0xE1 // Exif marker +#define M_COM 0xFE // COMment +#define M_DQT 0xDB +#define M_DHT 0xC4 +#define M_DRI 0xDD +#define M_IPTC 0xED // IPTC marker + +namespace XFILE +{ + class CFile; +} + + +class CJpegParse +{ + public: + CJpegParse(); + ~CJpegParse(void) = default; + bool Process(const char *picFileName); + const ExifInfo_t* GetExifInfo() const { return &m_ExifInfo; } + const IPTCInfo_t* GetIptcInfo() const { return &m_IPTCInfo; } + + private: + bool ExtractInfo(XFILE::CFile& infile); + bool GetSection(XFILE::CFile& infile, const unsigned short sectionLength); + void ReleaseSection(void); + void ProcessSOFn(void); + + unsigned char* m_SectionBuffer; + ExifInfo_t m_ExifInfo; + IPTCInfo_t m_IPTCInfo; +}; + diff --git a/xbmc/pictures/Picture.cpp b/xbmc/pictures/Picture.cpp new file mode 100644 index 0000000..9c7ec1e --- /dev/null +++ b/xbmc/pictures/Picture.cpp @@ -0,0 +1,588 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include <algorithm> + +#include "Picture.h" +#include "URL.h" +#include "ServiceBroker.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "FileItem.h" +#include "filesystem/File.h" +#include "utils/log.h" +#include "utils/URIUtils.h" +#include "guilib/Texture.h" +#include "guilib/imagefactory.h" + +extern "C" { +#include <libswscale/swscale.h> +} + +using namespace XFILE; + +bool CPicture::GetThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile, uint8_t* &result, size_t& result_size) +{ + unsigned char *thumb = NULL; + unsigned int thumbsize = 0; + + // get an image handler + IImage* image = ImageFactory::CreateLoader(thumbFile); + if (image == NULL || + !image->CreateThumbnailFromSurface(const_cast<unsigned char*>(buffer), width, height, + XB_FMT_A8R8G8B8, stride, thumbFile, thumb, thumbsize)) + { + delete image; + return false; + } + + // copy the resulting buffer + result_size = thumbsize; + result = new uint8_t[result_size]; + memcpy(result, thumb, result_size); + + // release the image buffer and the image handler + image->ReleaseThumbnailBuffer(); + delete image; + + return true; +} + +bool CPicture::CreateThumbnailFromSurface(const unsigned char *buffer, int width, int height, int stride, const std::string &thumbFile) +{ + CLog::Log(LOGDEBUG, "cached image '{}' size {}x{}", CURL::GetRedacted(thumbFile), width, height); + + unsigned char *thumb = NULL; + unsigned int thumbsize=0; + IImage* pImage = ImageFactory::CreateLoader(thumbFile); + if(pImage == NULL || !pImage->CreateThumbnailFromSurface(const_cast<unsigned char*>(buffer), width, height, XB_FMT_A8R8G8B8, stride, thumbFile.c_str(), thumb, thumbsize)) + { + CLog::Log(LOGERROR, "Failed to CreateThumbnailFromSurface for {}", + CURL::GetRedacted(thumbFile)); + delete pImage; + return false; + } + + XFILE::CFile file; + const bool ret = file.OpenForWrite(thumbFile, true) && + file.Write(thumb, thumbsize) == static_cast<ssize_t>(thumbsize); + + pImage->ReleaseThumbnailBuffer(); + delete pImage; + + return ret; +} + +CThumbnailWriter::CThumbnailWriter(unsigned char* buffer, int width, int height, int stride, const std::string& thumbFile): + m_thumbFile(thumbFile) +{ + m_buffer = buffer; + m_width = width; + m_height = height; + m_stride = stride; +} + +CThumbnailWriter::~CThumbnailWriter() +{ + delete m_buffer; +} + +bool CThumbnailWriter::DoWork() +{ + bool success = true; + + if (!CPicture::CreateThumbnailFromSurface(m_buffer, m_width, m_height, m_stride, m_thumbFile)) + { + CLog::Log(LOGERROR, "CThumbnailWriter::DoWork unable to write {}", + CURL::GetRedacted(m_thumbFile)); + success = false; + } + + delete [] m_buffer; + m_buffer = NULL; + + return success; +} + +bool CPicture::ResizeTexture(const std::string& image, + CTexture* texture, + uint32_t& dest_width, + uint32_t& dest_height, + uint8_t*& result, + size_t& result_size, + CPictureScalingAlgorithm::Algorithm + scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */) +{ + if (image.empty() || texture == NULL) + return false; + + return ResizeTexture(image, texture->GetPixels(), texture->GetWidth(), texture->GetHeight(), texture->GetPitch(), + dest_width, dest_height, result, result_size, + scalingAlgorithm); +} + +bool CPicture::ResizeTexture(const std::string &image, uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, + uint32_t &dest_width, uint32_t &dest_height, uint8_t* &result, size_t& result_size, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */) +{ + if (image.empty() || pixels == NULL) + return false; + + dest_width = std::min(width, dest_width); + dest_height = std::min(height, dest_height); + + // if no max width or height is specified, don't resize + if (dest_width == 0 && dest_height == 0) + { + dest_width = width; + dest_height = height; + } + else if (dest_width == 0) + { + double factor = (double)dest_height / (double)height; + dest_width = (uint32_t)(width * factor); + } + else if (dest_height == 0) + { + double factor = (double)dest_width / (double)width; + dest_height = (uint32_t)(height * factor); + } + + // nothing special to do if the dimensions already match + if (dest_width >= width || dest_height >= height) + return GetThumbnailFromSurface(pixels, dest_width, dest_height, pitch, image, result, result_size); + + // create a buffer large enough for the resulting image + GetScale(width, height, dest_width, dest_height); + + // Let's align so that stride is always divisible by 16, and then add some 32 bytes more on top + // See: https://github.com/FFmpeg/FFmpeg/blob/75638fe9402f70645bdde4d95672fa640a327300/libswscale/tests/swscale.c#L157 + uint32_t dest_width_aligned = ((dest_width + 15) & ~0x0f); + uint32_t stride = dest_width_aligned * sizeof(uint32_t); + + uint32_t* buffer = new uint32_t[dest_width_aligned * dest_height + 4]; + if (buffer == NULL) + { + result = NULL; + result_size = 0; + return false; + } + + if (!ScaleImage(pixels, width, height, pitch, AV_PIX_FMT_BGRA, (uint8_t*)buffer, dest_width, + dest_height, stride, AV_PIX_FMT_BGRA, scalingAlgorithm)) + { + delete[] buffer; + result = NULL; + result_size = 0; + return false; + } + + bool success = GetThumbnailFromSurface((unsigned char*)buffer, dest_width, dest_height, stride, + image, result, result_size); + delete[] buffer; + + if (!success) + { + result = NULL; + result_size = 0; + } + + return success; +} + +bool CPicture::CacheTexture(CTexture* texture, + uint32_t& dest_width, + uint32_t& dest_height, + const std::string& dest, + CPictureScalingAlgorithm::Algorithm + scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */) +{ + return CacheTexture(texture->GetPixels(), texture->GetWidth(), texture->GetHeight(), texture->GetPitch(), + texture->GetOrientation(), dest_width, dest_height, dest, scalingAlgorithm); +} + +bool CPicture::CacheTexture(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, int orientation, + uint32_t &dest_width, uint32_t &dest_height, const std::string &dest, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */) +{ + const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings(); + + // if no max width or height is specified, don't resize + if (dest_width == 0) + dest_width = width; + if (dest_height == 0) + dest_height = height; + if (scalingAlgorithm == CPictureScalingAlgorithm::NoAlgorithm) + scalingAlgorithm = advancedSettings->m_imageScalingAlgorithm; + + uint32_t max_height = advancedSettings->m_imageRes; + if (advancedSettings->m_fanartRes > advancedSettings->m_imageRes) + { // 16x9 images larger than the fanart res use that rather than the image res + if (fabsf(static_cast<float>(width) / static_cast<float>(height) / (16.0f / 9.0f) - 1.0f) + <= 0.01f) + { + max_height = advancedSettings->m_fanartRes; // use height defined in fanartRes + } + } + + uint32_t max_width = max_height * 16/9; + + dest_height = std::min(dest_height, max_height); + dest_width = std::min(dest_width, max_width); + + if (width > dest_width || height > dest_height || orientation) + { + bool success = false; + + dest_width = std::min(width, dest_width); + dest_height = std::min(height, dest_height); + + // create a buffer large enough for the resulting image + GetScale(width, height, dest_width, dest_height); + + // Let's align so that stride is always divisible by 16, and then add some 32 bytes more on top + // See: https://github.com/FFmpeg/FFmpeg/blob/75638fe9402f70645bdde4d95672fa640a327300/libswscale/tests/swscale.c#L157 + uint32_t dest_width_aligned = ((dest_width + 15) & ~0x0f); + uint32_t stride = dest_width_aligned * sizeof(uint32_t); + + uint32_t* buffer = new uint32_t[dest_width_aligned * dest_height + 4]; + if (buffer) + { + if (ScaleImage(pixels, width, height, pitch, AV_PIX_FMT_BGRA, (uint8_t*)buffer, dest_width, + dest_height, stride, AV_PIX_FMT_BGRA, scalingAlgorithm)) + { + if (!orientation || + OrientateImage(buffer, dest_width, dest_height, orientation, dest_width_aligned)) + { + success = CreateThumbnailFromSurface((unsigned char*)buffer, dest_width, dest_height, + dest_width_aligned * 4, dest); + } + } + delete[] buffer; + } + return success; + } + else + { // no orientation needed + dest_width = width; + dest_height = height; + return CreateThumbnailFromSurface(pixels, width, height, pitch, dest); + } + return false; +} + +bool CPicture::CreateTiledThumb(const std::vector<std::string> &files, const std::string &thumb) +{ + if (!files.size()) + return false; + + unsigned int num_across = (unsigned int)ceil(sqrt((float)files.size())); + unsigned int num_down = (files.size() + num_across - 1) / num_across; + + unsigned int imageRes = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes; + + unsigned int tile_width = imageRes / num_across; + unsigned int tile_height = imageRes / num_down; + unsigned int tile_gap = 1; + bool success = false; + + // create a buffer for the resulting thumb + uint32_t *buffer = static_cast<uint32_t *>(calloc(imageRes * imageRes, 4)); + if (!buffer) + return false; + for (unsigned int i = 0; i < files.size(); ++i) + { + int x = i % num_across; + int y = i / num_across; + // load in the image + unsigned int width = tile_width - 2*tile_gap, height = tile_height - 2*tile_gap; + std::unique_ptr<CTexture> texture = CTexture::LoadFromFile(files[i], width, height, true); + if (texture && texture->GetWidth() && texture->GetHeight()) + { + GetScale(texture->GetWidth(), texture->GetHeight(), width, height); + + // scale appropriately + uint32_t *scaled = new uint32_t[width * height]; + if (ScaleImage(texture->GetPixels(), texture->GetWidth(), texture->GetHeight(), + texture->GetPitch(), AV_PIX_FMT_BGRA, (uint8_t*)scaled, width, height, + width * 4, AV_PIX_FMT_BGRA)) + { + unsigned int stridePixels{width}; + if (!texture->GetOrientation() || + OrientateImage(scaled, width, height, texture->GetOrientation(), stridePixels)) + { + success = true; // Flag that we at least had one successful image processed + // drop into the texture + unsigned int posX = x*tile_width + (tile_width - width)/2; + unsigned int posY = y*tile_height + (tile_height - height)/2; + uint32_t *dest = buffer + posX + posY * imageRes; + uint32_t *src = scaled; + for (unsigned int y = 0; y < height; ++y) + { + memcpy(dest, src, width*4); + dest += imageRes; + src += stridePixels; + } + } + } + delete[] scaled; + } + } + // now save to a file + if (success) + success = CreateThumbnailFromSurface((uint8_t *)buffer, imageRes, imageRes, imageRes * 4, thumb); + + free(buffer); + return success; +} + +void CPicture::GetScale(unsigned int width, unsigned int height, unsigned int &out_width, unsigned int &out_height) +{ + float aspect = (float)width / height; + if ((unsigned int)(out_width / aspect + 0.5f) > out_height) + out_width = (unsigned int)(out_height * aspect + 0.5f); + else + out_height = (unsigned int)(out_width / aspect + 0.5f); +} + +bool CPicture::ScaleImage(uint8_t* in_pixels, + unsigned int in_width, + unsigned int in_height, + unsigned int in_pitch, + AVPixelFormat in_format, + uint8_t* out_pixels, + unsigned int out_width, + unsigned int out_height, + unsigned int out_pitch, + AVPixelFormat out_format, + CPictureScalingAlgorithm::Algorithm + scalingAlgorithm /* = CPictureScalingAlgorithm::NoAlgorithm */) +{ + struct SwsContext* context = + sws_getContext(in_width, in_height, in_format, out_width, out_height, out_format, + CPictureScalingAlgorithm::ToSwscale(scalingAlgorithm), NULL, NULL, NULL); + + uint8_t *src[] = { in_pixels, 0, 0, 0 }; + int srcStride[] = { (int)in_pitch, 0, 0, 0 }; + uint8_t *dst[] = { out_pixels , 0, 0, 0 }; + int dstStride[] = { (int)out_pitch, 0, 0, 0 }; + + if (context) + { + sws_scale(context, src, srcStride, 0, in_height, dst, dstStride); + sws_freeContext(context); + return true; + } + return false; +} + +bool CPicture::OrientateImage(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + int orientation, + unsigned int& stridePixels) +{ + // ideas for speeding these functions up: http://cgit.freedesktop.org/pixman/tree/pixman/pixman-fast-path.c + bool out = false; + switch (orientation) + { + case 1: + out = FlipHorizontal(pixels, width, height, stridePixels); + break; + case 2: + out = Rotate180CCW(pixels, width, height, stridePixels); + break; + case 3: + out = FlipVertical(pixels, width, height, stridePixels); + break; + case 4: + out = Transpose(pixels, width, height, stridePixels); + break; + case 5: + out = Rotate270CCW(pixels, width, height, stridePixels); + break; + case 6: + out = TransposeOffAxis(pixels, width, height, stridePixels); + break; + case 7: + out = Rotate90CCW(pixels, width, height, stridePixels); + break; + default: + CLog::Log(LOGERROR, "Unknown orientation {}", orientation); + break; + } + return out; +} + +bool CPicture::FlipHorizontal(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels) +{ + // this can be done in-place easily enough + for (unsigned int y = 0; y < height; ++y) + { + uint32_t* line = pixels + y * stridePixels; + for (unsigned int x = 0; x < width / 2; ++x) + std::swap(line[x], line[width - 1 - x]); + } + return true; +} + +bool CPicture::FlipVertical(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels) +{ + // this can be done in-place easily enough + for (unsigned int y = 0; y < height / 2; ++y) + { + uint32_t* line1 = pixels + y * stridePixels; + uint32_t* line2 = pixels + (height - 1 - y) * stridePixels; + for (unsigned int x = 0; x < width; ++x) + std::swap(*line1++, *line2++); + } + return true; +} + +bool CPicture::Rotate180CCW(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels) +{ + // this can be done in-place easily enough + for (unsigned int y = 0; y < height / 2; ++y) + { + uint32_t* line1 = pixels + y * stridePixels; + uint32_t* line2 = pixels + (height - 1 - y) * stridePixels + width - 1; + for (unsigned int x = 0; x < width; ++x) + std::swap(*line1++, *line2--); + } + if (height % 2) + { // height is odd, so flip the middle row as well + uint32_t* line = pixels + (height - 1) / 2 * stridePixels; + for (unsigned int x = 0; x < width / 2; ++x) + std::swap(line[x], line[width - 1 - x]); + } + return true; +} + +bool CPicture::Rotate90CCW(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels) +{ + uint32_t *dest = new uint32_t[width * height * 4]; + if (dest) + { + unsigned int d_height = width, d_width = height; + for (unsigned int y = 0; y < d_height; y++) + { + const uint32_t *src = pixels + (d_height - 1 - y); // y-th col from right, starting at top + uint32_t *dst = dest + d_width * y; // y-th row from top, starting at left + for (unsigned int x = 0; x < d_width; x++) + { + *dst++ = *src; + src += stridePixels; + } + } + delete[] pixels; + pixels = dest; + std::swap(width, height); + stridePixels = width; + return true; + } + return false; +} + +bool CPicture::Rotate270CCW(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels) +{ + uint32_t *dest = new uint32_t[width * height * 4]; + if (!dest) + return false; + + unsigned int d_height = width, d_width = height; + for (unsigned int y = 0; y < d_height; y++) + { + const uint32_t* src = + pixels + stridePixels * (d_width - 1) + y; // y-th col from left, starting at bottom + uint32_t* dst = dest + d_width * y; // y-th row from top, starting at left + for (unsigned int x = 0; x < d_width; x++) + { + *dst++ = *src; + src -= stridePixels; + } + } + + delete[] pixels; + pixels = dest; + std::swap(width, height); + stridePixels = width; + return true; +} + +bool CPicture::Transpose(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels) +{ + uint32_t *dest = new uint32_t[width * height * 4]; + if (!dest) + return false; + + unsigned int d_height = width, d_width = height; + for (unsigned int y = 0; y < d_height; y++) + { + const uint32_t *src = pixels + y; // y-th col from left, starting at top + uint32_t *dst = dest + d_width * y; // y-th row from top, starting at left + for (unsigned int x = 0; x < d_width; x++) + { + *dst++ = *src; + src += stridePixels; + } + } + + delete[] pixels; + pixels = dest; + std::swap(width, height); + stridePixels = width; + return true; +} + +bool CPicture::TransposeOffAxis(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels) +{ + uint32_t *dest = new uint32_t[width * height * 4]; + if (!dest) + return false; + + unsigned int d_height = width, d_width = height; + for (unsigned int y = 0; y < d_height; y++) + { + const uint32_t* src = pixels + stridePixels * (d_width - 1) + + (d_height - 1 - y); // y-th col from right, starting at bottom + uint32_t *dst = dest + d_width * y; // y-th row, starting at left + for (unsigned int x = 0; x < d_width; x++) + { + *dst++ = *src; + src -= stridePixels; + } + } + + delete[] pixels; + pixels = dest; + std::swap(width, height); + stridePixels = width; + return true; +} diff --git a/xbmc/pictures/Picture.h b/xbmc/pictures/Picture.h new file mode 100644 index 0000000..192982a --- /dev/null +++ b/xbmc/pictures/Picture.h @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pictures/PictureScalingAlgorithm.h" +#include "utils/Job.h" + +#include <cstddef> +#include <cstdint> +#include <string> +#include <vector> + +extern "C" +{ +#include <libavutil/pixfmt.h> +} + +class CTexture; + +class CPicture +{ +public: + static bool GetThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile, uint8_t* &result, size_t& result_size); + static bool CreateThumbnailFromSurface(const unsigned char* buffer, int width, int height, int stride, const std::string &thumbFile); + + /*! \brief Create a tiled thumb of the given files + \param files the files to create the thumb from + \param thumb the filename of the thumb + */ + static bool CreateTiledThumb(const std::vector<std::string> &files, const std::string &thumb); + + static bool ResizeTexture( + const std::string& image, + CTexture* texture, + uint32_t& dest_width, + uint32_t& dest_height, + uint8_t*& result, + size_t& result_size, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm); + static bool ResizeTexture(const std::string &image, uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, + uint32_t &dest_width, uint32_t &dest_height, uint8_t* &result, size_t& result_size, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm); + + /*! \brief Cache a texture, resizing, rotating and flipping as needed, and saving as a JPG or PNG + \param texture a pointer to a CTexture + \param dest_width [in/out] maximum width in pixels of cached version - replaced with actual cached width + \param dest_height [in/out] maximum height in pixels of cached version - replaced with actual cached height + \param dest the output cache file + \return true if successful, false otherwise + */ + static bool CacheTexture( + CTexture* texture, + uint32_t& dest_width, + uint32_t& dest_height, + const std::string& dest, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm); + static bool CacheTexture(uint8_t *pixels, uint32_t width, uint32_t height, uint32_t pitch, int orientation, + uint32_t &dest_width, uint32_t &dest_height, const std::string &dest, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm); + + static void GetScale(unsigned int width, unsigned int height, unsigned int &out_width, unsigned int &out_height); + static bool ScaleImage( + uint8_t* in_pixels, + unsigned int in_width, + unsigned int in_height, + unsigned int in_pitch, + AVPixelFormat in_format, + uint8_t* out_pixels, + unsigned int out_width, + unsigned int out_height, + unsigned int out_pitch, + AVPixelFormat out_format, + CPictureScalingAlgorithm::Algorithm scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm); + +private: + static bool OrientateImage(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + int orientation, + unsigned int& stridePixels); + + static bool FlipHorizontal(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels); + static bool FlipVertical(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels); + static bool Rotate90CCW(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels); + static bool Rotate270CCW(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels); + static bool Rotate180CCW(uint32_t*& pixels, + const unsigned int& width, + const unsigned int& height, + const unsigned int& stridePixels); + static bool Transpose(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& width_aligned); + static bool TransposeOffAxis(uint32_t*& pixels, + unsigned int& width, + unsigned int& height, + unsigned int& stridePixels); +}; + +//this class calls CreateThumbnailFromSurface in a CJob, so a png file can be written without halting the render thread +class CThumbnailWriter : public CJob +{ + public: + //WARNING: buffer is deleted from DoWork() + CThumbnailWriter(unsigned char* buffer, int width, int height, int stride, const std::string& thumbFile); + ~CThumbnailWriter() override; + bool DoWork() override; + + private: + unsigned char* m_buffer; + int m_width; + int m_height; + int m_stride; + std::string m_thumbFile; +}; + diff --git a/xbmc/pictures/PictureInfoLoader.cpp b/xbmc/pictures/PictureInfoLoader.cpp new file mode 100644 index 0000000..81b8cfa --- /dev/null +++ b/xbmc/pictures/PictureInfoLoader.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PictureInfoLoader.h" + +#include "FileItem.h" +#include "PictureInfoTag.h" +#include "ServiceBroker.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" + +CPictureInfoLoader::CPictureInfoLoader() +{ + m_mapFileItems = new CFileItemList; + m_tagReads = 0; +} + +CPictureInfoLoader::~CPictureInfoLoader() +{ + StopThread(); + delete m_mapFileItems; +} + +void CPictureInfoLoader::OnLoaderStart() +{ + // Load previously cached items from HD + m_mapFileItems->SetPath(m_pVecItems->GetPath()); + m_mapFileItems->Load(); + m_mapFileItems->SetFastLookup(true); + + m_tagReads = 0; + m_loadTags = CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_PICTURES_USETAGS); + + if (m_pProgressCallback) + m_pProgressCallback->SetProgressMax(m_pVecItems->GetFileCount()); +} + +bool CPictureInfoLoader::LoadItem(CFileItem* pItem) +{ + bool result = LoadItemCached(pItem); + result |= LoadItemLookup(pItem); + + return result; +} + +bool CPictureInfoLoader::LoadItemCached(CFileItem* pItem) +{ + if (!pItem->IsPicture() || pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBR() || pItem->IsCBZ() || pItem->IsInternetStream() || pItem->IsVideo()) + return false; + + if (pItem->HasPictureInfoTag()) + return true; + + // Check the cached item + CFileItemPtr mapItem = (*m_mapFileItems)[pItem->GetPath()]; + if (mapItem && mapItem->m_dateTime==pItem->m_dateTime && mapItem->HasPictureInfoTag()) + { // Query map if we previously cached the file on HD + *pItem->GetPictureInfoTag() = *mapItem->GetPictureInfoTag(); + pItem->SetArt("thumb", mapItem->GetArt("thumb")); + return true; + } + + return true; +} + +bool CPictureInfoLoader::LoadItemLookup(CFileItem* pItem) +{ + if (m_pProgressCallback && !pItem->m_bIsFolder) + m_pProgressCallback->SetProgressAdvance(); + + if (!pItem->IsPicture() || pItem->IsZIP() || pItem->IsRAR() || pItem->IsCBR() || pItem->IsCBZ() || pItem->IsInternetStream() || pItem->IsVideo()) + return false; + + if (pItem->HasPictureInfoTag()) + return false; + + if (m_loadTags) + { // Nothing found, load tag from file + pItem->GetPictureInfoTag()->Load(pItem->GetPath()); + m_tagReads++; + } + + return true; +} + +void CPictureInfoLoader::OnLoaderFinish() +{ + // cleanup cache loaded from HD + m_mapFileItems->Clear(); + + // Save loaded items to HD + if (!m_bStop && m_tagReads > 0) + m_pVecItems->Save(); +} diff --git a/xbmc/pictures/PictureInfoLoader.h b/xbmc/pictures/PictureInfoLoader.h new file mode 100644 index 0000000..0171fcb --- /dev/null +++ b/xbmc/pictures/PictureInfoLoader.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "BackgroundInfoLoader.h" + +#include <string> + +class CPictureInfoLoader : public CBackgroundInfoLoader +{ +public: + CPictureInfoLoader(); + ~CPictureInfoLoader() override; + + void UseCacheOnHD(const std::string& strFileName); + bool LoadItem(CFileItem* pItem) override; + bool LoadItemCached(CFileItem* pItem) override; + bool LoadItemLookup(CFileItem* pItem) override; + +protected: + void OnLoaderStart() override; + void OnLoaderFinish() override; + + CFileItemList* m_mapFileItems; + unsigned int m_tagReads; + bool m_loadTags; +}; + diff --git a/xbmc/pictures/PictureInfoTag.cpp b/xbmc/pictures/PictureInfoTag.cpp new file mode 100644 index 0000000..4aab3e0 --- /dev/null +++ b/xbmc/pictures/PictureInfoTag.cpp @@ -0,0 +1,764 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PictureInfoTag.h" + +#include "ServiceBroker.h" +#include "addons/ExtsMimeSupportList.h" +#include "addons/ImageDecoder.h" +#include "addons/addoninfo/AddonType.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "utils/Archive.h" +#include "utils/CharsetConverter.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <vector> + +using namespace KODI::ADDONS; + +CPictureInfoTag::ExifInfo::ExifInfo(const ExifInfo_t& other) + : CameraMake(other.CameraMake), + CameraModel(other.CameraModel), + DateTime(other.DateTime), + Height(other.Height), + Width(other.Width), + Orientation(other.Orientation), + IsColor(other.IsColor), + Process(other.Process), + FlashUsed(other.FlashUsed), + FocalLength(other.FocalLength), + ExposureTime(other.ExposureTime), + ApertureFNumber(other.ApertureFNumber), + Distance(other.Distance), + CCDWidth(other.CCDWidth), + ExposureBias(other.ExposureBias), + DigitalZoomRatio(other.DigitalZoomRatio), + FocalLength35mmEquiv(other.FocalLength35mmEquiv), + Whitebalance(other.Whitebalance), + MeteringMode(other.MeteringMode), + ExposureProgram(other.ExposureProgram), + ExposureMode(other.ExposureMode), + ISOequivalent(other.ISOequivalent), + LightSource(other.LightSource), + CommentsCharset(EXIF_COMMENT_CHARSET_CONVERTED), + XPCommentsCharset(EXIF_COMMENT_CHARSET_CONVERTED), + Comments(Convert(other.CommentsCharset, other.Comments)), + FileComment(Convert(EXIF_COMMENT_CHARSET_UNKNOWN, other.FileComment)), + XPComment(Convert(other.XPCommentsCharset, other.XPComment)), + Description(other.Description), + ThumbnailOffset(other.ThumbnailOffset), + ThumbnailSize(other.ThumbnailSize), + LargestExifOffset(other.LargestExifOffset), + ThumbnailAtEnd(other.ThumbnailAtEnd), + ThumbnailSizeOffset(other.ThumbnailSizeOffset), + DateTimeOffsets(other.DateTimeOffsets, other.DateTimeOffsets + other.numDateTimeTags), + GpsInfoPresent(other.GpsInfoPresent), + GpsLat(other.GpsLat), + GpsLong(other.GpsLong), + GpsAlt(other.GpsAlt) +{ +} + +std::string CPictureInfoTag::ExifInfo::Convert(int charset, const char* data) +{ + std::string value; + + // The charset used for the UserComment is stored in CommentsCharset: + // Ascii, Unicode (UCS2), JIS (X208-1990), Unknown (application specific) + if (charset == EXIF_COMMENT_CHARSET_UNICODE) + { + g_charsetConverter.ucs2ToUTF8(std::u16string(reinterpret_cast<const char16_t*>(data)), value); + } + else + { + // Ascii doesn't need to be converted (EXIF_COMMENT_CHARSET_ASCII) + // Unknown data can't be converted as it could be any codec (EXIF_COMMENT_CHARSET_UNKNOWN) + // JIS data can't be converted as CharsetConverter and iconv lacks support (EXIF_COMMENT_CHARSET_JIS) + g_charsetConverter.unknownToUTF8(data, value); + } + + return value; +} + +CPictureInfoTag::IPTCInfo::IPTCInfo(const IPTCInfo_t& other) + : RecordVersion(other.RecordVersion), + SupplementalCategories(other.SupplementalCategories), + Keywords(other.Keywords), + Caption(other.Caption), + Author(other.Author), + Headline(other.Headline), + SpecialInstructions(other.SpecialInstructions), + Category(other.Category), + Byline(other.Byline), + BylineTitle(other.BylineTitle), + Credit(other.Credit), + Source(other.Source), + CopyrightNotice(other.CopyrightNotice), + ObjectName(other.ObjectName), + City(other.City), + State(other.State), + Country(other.Country), + TransmissionReference(other.TransmissionReference), + Date(other.Date), + Urgency(other.Urgency), + ReferenceService(other.ReferenceService), + CountryCode(other.CountryCode), + TimeCreated(other.TimeCreated), + SubLocation(other.SubLocation), + ImageType(other.ImageType) +{ +} + +void CPictureInfoTag::Reset() +{ + m_exifInfo = {}; + m_iptcInfo = {}; + m_isLoaded = false; + m_isInfoSetExternally = false; + m_dateTimeTaken.Reset(); +} + +bool CPictureInfoTag::Load(const std::string &path) +{ + m_isLoaded = false; + + // Get file extensions to find addon related to it. + std::string strExtension = URIUtils::GetExtension(path); + StringUtils::ToLower(strExtension); + if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp()) + { + // Load via available image decoder addons + auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos( + strExtension, CExtsMimeSupportList::FilterSelect::all); + for (const auto& addonInfo : addonInfos) + { + if (addonInfo.first != ADDON::AddonType::IMAGEDECODER) + continue; + + std::unique_ptr<CImageDecoder> result = std::make_unique<CImageDecoder>(addonInfo.second, ""); + if (result->LoadInfoTag(path, this)) + { + m_isLoaded = true; + break; + } + } + } + + // Load by Kodi's included own way + if (!m_isLoaded) + { + ExifInfo_t exifInfo; + IPTCInfo_t iptcInfo; + + if (process_jpeg(path.c_str(), &exifInfo, &iptcInfo)) + { + m_exifInfo = ExifInfo(exifInfo); + m_iptcInfo = IPTCInfo(iptcInfo); + m_isLoaded = true; + } + } + + ConvertDateTime(); + + return m_isLoaded; +} + +void CPictureInfoTag::Archive(CArchive& ar) +{ + if (ar.IsStoring()) + { + ar << m_isLoaded; + ar << m_isInfoSetExternally; + ar << m_exifInfo.ApertureFNumber; + ar << m_exifInfo.CameraMake; + ar << m_exifInfo.CameraModel; + ar << m_exifInfo.CCDWidth; + ar << m_exifInfo.Comments; + ar << m_exifInfo.Description; + ar << m_exifInfo.DateTime; + for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i) + { + if (i < m_exifInfo.DateTimeOffsets.size()) + ar << m_exifInfo.DateTimeOffsets[i]; + else + ar << static_cast<int>(0); + } + ar << m_exifInfo.DigitalZoomRatio; + ar << m_exifInfo.Distance; + ar << m_exifInfo.ExposureBias; + ar << m_exifInfo.ExposureMode; + ar << m_exifInfo.ExposureProgram; + ar << m_exifInfo.ExposureTime; + ar << m_exifInfo.FlashUsed; + ar << m_exifInfo.FocalLength; + ar << m_exifInfo.FocalLength35mmEquiv; + ar << m_exifInfo.GpsInfoPresent; + ar << m_exifInfo.GpsAlt; + ar << m_exifInfo.GpsLat; + ar << m_exifInfo.GpsLong; + ar << m_exifInfo.Height; + ar << m_exifInfo.IsColor; + ar << m_exifInfo.ISOequivalent; + ar << m_exifInfo.LargestExifOffset; + ar << m_exifInfo.LightSource; + ar << m_exifInfo.MeteringMode; + ar << static_cast<int>(m_exifInfo.DateTimeOffsets.size()); + ar << m_exifInfo.Orientation; + ar << m_exifInfo.Process; + ar << m_exifInfo.ThumbnailAtEnd; + ar << m_exifInfo.ThumbnailOffset; + ar << m_exifInfo.ThumbnailSize; + ar << m_exifInfo.ThumbnailSizeOffset; + ar << m_exifInfo.Whitebalance; + ar << m_exifInfo.Width; + ar << m_dateTimeTaken; + + ar << m_iptcInfo.Author; + ar << m_iptcInfo.Byline; + ar << m_iptcInfo.BylineTitle; + ar << m_iptcInfo.Caption; + ar << m_iptcInfo.Category; + ar << m_iptcInfo.City; + ar << m_iptcInfo.Urgency; + ar << m_iptcInfo.CopyrightNotice; + ar << m_iptcInfo.Country; + ar << m_iptcInfo.CountryCode; + ar << m_iptcInfo.Credit; + ar << m_iptcInfo.Date; + ar << m_iptcInfo.Headline; + ar << m_iptcInfo.Keywords; + ar << m_iptcInfo.ObjectName; + ar << m_iptcInfo.ReferenceService; + ar << m_iptcInfo.Source; + ar << m_iptcInfo.SpecialInstructions; + ar << m_iptcInfo.State; + ar << m_iptcInfo.SupplementalCategories; + ar << m_iptcInfo.TransmissionReference; + ar << m_iptcInfo.TimeCreated; + ar << m_iptcInfo.SubLocation; + ar << m_iptcInfo.ImageType; + } + else + { + ar >> m_isLoaded; + ar >> m_isInfoSetExternally; + ar >> m_exifInfo.ApertureFNumber; + ar >> m_exifInfo.CameraMake; + ar >> m_exifInfo.CameraModel; + ar >> m_exifInfo.CCDWidth; + ar >> m_exifInfo.Comments; + m_exifInfo.CommentsCharset = EXIF_COMMENT_CHARSET_CONVERTED; // Store and restore the comment charset converted + ar >> m_exifInfo.Description; + ar >> m_exifInfo.DateTime; + m_exifInfo.DateTimeOffsets.clear(); + m_exifInfo.DateTimeOffsets.reserve(MAX_DATE_COPIES); + for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i) + { + int dateTimeOffset; + ar >> dateTimeOffset; + m_exifInfo.DateTimeOffsets.push_back(dateTimeOffset); + } + ar >> m_exifInfo.DigitalZoomRatio; + ar >> m_exifInfo.Distance; + ar >> m_exifInfo.ExposureBias; + ar >> m_exifInfo.ExposureMode; + ar >> m_exifInfo.ExposureProgram; + ar >> m_exifInfo.ExposureTime; + ar >> m_exifInfo.FlashUsed; + ar >> m_exifInfo.FocalLength; + ar >> m_exifInfo.FocalLength35mmEquiv; + ar >> m_exifInfo.GpsInfoPresent; + ar >> m_exifInfo.GpsAlt; + ar >> m_exifInfo.GpsLat; + ar >> m_exifInfo.GpsLong; + ar >> m_exifInfo.Height; + ar >> m_exifInfo.IsColor; + ar >> m_exifInfo.ISOequivalent; + ar >> m_exifInfo.LargestExifOffset; + ar >> m_exifInfo.LightSource; + ar >> m_exifInfo.MeteringMode; + int numDateTimeTags; + ar >> numDateTimeTags; + m_exifInfo.DateTimeOffsets.resize(numDateTimeTags); + ar >> m_exifInfo.Orientation; + ar >> m_exifInfo.Process; + ar >> m_exifInfo.ThumbnailAtEnd; + ar >> m_exifInfo.ThumbnailOffset; + ar >> m_exifInfo.ThumbnailSize; + ar >> m_exifInfo.ThumbnailSizeOffset; + ar >> m_exifInfo.Whitebalance; + ar >> m_exifInfo.Width; + ar >> m_dateTimeTaken; + + ar >> m_iptcInfo.Author; + ar >> m_iptcInfo.Byline; + ar >> m_iptcInfo.BylineTitle; + ar >> m_iptcInfo.Caption; + ar >> m_iptcInfo.Category; + ar >> m_iptcInfo.City; + ar >> m_iptcInfo.Urgency; + ar >> m_iptcInfo.CopyrightNotice; + ar >> m_iptcInfo.Country; + ar >> m_iptcInfo.CountryCode; + ar >> m_iptcInfo.Credit; + ar >> m_iptcInfo.Date; + ar >> m_iptcInfo.Headline; + ar >> m_iptcInfo.Keywords; + ar >> m_iptcInfo.ObjectName; + ar >> m_iptcInfo.ReferenceService; + ar >> m_iptcInfo.Source; + ar >> m_iptcInfo.SpecialInstructions; + ar >> m_iptcInfo.State; + ar >> m_iptcInfo.SupplementalCategories; + ar >> m_iptcInfo.TransmissionReference; + ar >> m_iptcInfo.TimeCreated; + ar >> m_iptcInfo.SubLocation; + ar >> m_iptcInfo.ImageType; + } +} + +void CPictureInfoTag::Serialize(CVariant& value) const +{ + value["aperturefnumber"] = m_exifInfo.ApertureFNumber; + value["cameramake"] = m_exifInfo.CameraMake; + value["cameramodel"] = m_exifInfo.CameraModel; + value["ccdwidth"] = m_exifInfo.CCDWidth; + value["comments"] = m_exifInfo.Comments; + value["description"] = m_exifInfo.Description; + value["datetime"] = m_exifInfo.DateTime; + for (std::vector<int>::size_type i = 0; i < MAX_DATE_COPIES; ++i) + { + if (i < m_exifInfo.DateTimeOffsets.size()) + value["datetimeoffsets"][static_cast<int>(i)] = m_exifInfo.DateTimeOffsets[i]; + else + value["datetimeoffsets"][static_cast<int>(i)] = static_cast<int>(0); + } + value["digitalzoomratio"] = m_exifInfo.DigitalZoomRatio; + value["distance"] = m_exifInfo.Distance; + value["exposurebias"] = m_exifInfo.ExposureBias; + value["exposuremode"] = m_exifInfo.ExposureMode; + value["exposureprogram"] = m_exifInfo.ExposureProgram; + value["exposuretime"] = m_exifInfo.ExposureTime; + value["flashused"] = m_exifInfo.FlashUsed; + value["focallength"] = m_exifInfo.FocalLength; + value["focallength35mmequiv"] = m_exifInfo.FocalLength35mmEquiv; + value["gpsinfopresent"] = m_exifInfo.GpsInfoPresent; + value["gpsinfo"]["alt"] = m_exifInfo.GpsAlt; + value["gpsinfo"]["lat"] = m_exifInfo.GpsLat; + value["gpsinfo"]["long"] = m_exifInfo.GpsLong; + value["height"] = m_exifInfo.Height; + value["iscolor"] = m_exifInfo.IsColor; + value["isoequivalent"] = m_exifInfo.ISOequivalent; + value["largestexifoffset"] = m_exifInfo.LargestExifOffset; + value["lightsource"] = m_exifInfo.LightSource; + value["meteringmode"] = m_exifInfo.MeteringMode; + value["numdatetimetags"] = static_cast<int>(m_exifInfo.DateTimeOffsets.size()); + value["orientation"] = m_exifInfo.Orientation; + value["process"] = m_exifInfo.Process; + value["thumbnailatend"] = m_exifInfo.ThumbnailAtEnd; + value["thumbnailoffset"] = m_exifInfo.ThumbnailOffset; + value["thumbnailsize"] = m_exifInfo.ThumbnailSize; + value["thumbnailsizeoffset"] = m_exifInfo.ThumbnailSizeOffset; + value["whitebalance"] = m_exifInfo.Whitebalance; + value["width"] = m_exifInfo.Width; + + value["author"] = m_iptcInfo.Author; + value["byline"] = m_iptcInfo.Byline; + value["bylinetitle"] = m_iptcInfo.BylineTitle; + value["caption"] = m_iptcInfo.Caption; + value["category"] = m_iptcInfo.Category; + value["city"] = m_iptcInfo.City; + value["urgency"] = m_iptcInfo.Urgency; + value["copyrightnotice"] = m_iptcInfo.CopyrightNotice; + value["country"] = m_iptcInfo.Country; + value["countrycode"] = m_iptcInfo.CountryCode; + value["credit"] = m_iptcInfo.Credit; + value["date"] = m_iptcInfo.Date; + value["headline"] = m_iptcInfo.Headline; + value["keywords"] = m_iptcInfo.Keywords; + value["objectname"] = m_iptcInfo.ObjectName; + value["referenceservice"] = m_iptcInfo.ReferenceService; + value["source"] = m_iptcInfo.Source; + value["specialinstructions"] = m_iptcInfo.SpecialInstructions; + value["state"] = m_iptcInfo.State; + value["supplementalcategories"] = m_iptcInfo.SupplementalCategories; + value["transmissionreference"] = m_iptcInfo.TransmissionReference; + value["timecreated"] = m_iptcInfo.TimeCreated; + value["sublocation"] = m_iptcInfo.SubLocation; + value["imagetype"] = m_iptcInfo.ImageType; +} + +void CPictureInfoTag::ToSortable(SortItem& sortable, Field field) const +{ + if (field == FieldDateTaken && m_dateTimeTaken.IsValid()) + sortable[FieldDateTaken] = m_dateTimeTaken.GetAsDBDateTime(); +} + +const std::string CPictureInfoTag::GetInfo(int info) const +{ + if (!m_isLoaded && !m_isInfoSetExternally) // If no metadata has been loaded from the picture file or set with SetInfo(), just return + return ""; + + std::string value; + switch (info) + { + case SLIDESHOW_RESOLUTION: + value = StringUtils::Format("{} x {}", m_exifInfo.Width, m_exifInfo.Height); + break; + case SLIDESHOW_COLOUR: + value = m_exifInfo.IsColor ? "Colour" : "Black and White"; + break; + case SLIDESHOW_PROCESS: + switch (m_exifInfo.Process) + { + case M_SOF0: + // don't show it if its the plain old boring 'baseline' process, but do + // show it if its something else, like 'progressive' (used on web sometimes) + value = "Baseline"; + break; + case M_SOF1: value = "Extended sequential"; break; + case M_SOF2: value = "Progressive"; break; + case M_SOF3: value = "Lossless"; break; + case M_SOF5: value = "Differential sequential"; break; + case M_SOF6: value = "Differential progressive"; break; + case M_SOF7: value = "Differential lossless"; break; + case M_SOF9: value = "Extended sequential, arithmetic coding"; break; + case M_SOF10: value = "Progressive, arithmetic coding"; break; + case M_SOF11: value = "Lossless, arithmetic coding"; break; + case M_SOF13: value = "Differential sequential, arithmetic coding"; break; + case M_SOF14: value = "Differential progressive, arithmetic coding"; break; + case M_SOF15: value = "Differential lossless, arithmetic coding"; break; + default: value = "Unknown"; break; + } + break; + case SLIDESHOW_COMMENT: + value = m_exifInfo.FileComment; + break; + case SLIDESHOW_EXIF_COMMENT: + value = m_exifInfo.Comments; + break; + case SLIDESHOW_EXIF_XPCOMMENT: + value = m_exifInfo.XPComment; + break; + case SLIDESHOW_EXIF_LONG_DATE_TIME: + if (m_dateTimeTaken.IsValid()) + value = m_dateTimeTaken.GetAsLocalizedDateTime(true); + break; + case SLIDESHOW_EXIF_DATE_TIME: + if (m_dateTimeTaken.IsValid()) + value = m_dateTimeTaken.GetAsLocalizedDateTime(); + break; + case SLIDESHOW_EXIF_LONG_DATE: + if (m_dateTimeTaken.IsValid()) + value = m_dateTimeTaken.GetAsLocalizedDate(true); + break; + case SLIDESHOW_EXIF_DATE: + if (m_dateTimeTaken.IsValid()) + value = m_dateTimeTaken.GetAsLocalizedDate(); + break; + case SLIDESHOW_EXIF_DESCRIPTION: + value = m_exifInfo.Description; + break; + case SLIDESHOW_EXIF_CAMERA_MAKE: + value = m_exifInfo.CameraMake; + break; + case SLIDESHOW_EXIF_CAMERA_MODEL: + value = m_exifInfo.CameraModel; + break; +// case SLIDESHOW_EXIF_SOFTWARE: +// value = m_exifInfo.Software; + case SLIDESHOW_EXIF_APERTURE: + if (m_exifInfo.ApertureFNumber) + value = StringUtils::Format("{:3.1f}", m_exifInfo.ApertureFNumber); + break; + case SLIDESHOW_EXIF_ORIENTATION: + switch (m_exifInfo.Orientation) + { + case 1: value = "Top Left"; break; + case 2: value = "Top Right"; break; + case 3: value = "Bottom Right"; break; + case 4: value = "Bottom Left"; break; + case 5: value = "Left Top"; break; + case 6: value = "Right Top"; break; + case 7: value = "Right Bottom"; break; + case 8: value = "Left Bottom"; break; + } + break; + case SLIDESHOW_EXIF_FOCAL_LENGTH: + if (m_exifInfo.FocalLength) + { + value = StringUtils::Format("{:4.2f}mm", m_exifInfo.FocalLength); + if (m_exifInfo.FocalLength35mmEquiv != 0) + value += StringUtils::Format(" (35mm Equivalent = {}mm)", m_exifInfo.FocalLength35mmEquiv); + } + break; + case SLIDESHOW_EXIF_FOCUS_DIST: + if (m_exifInfo.Distance < 0) + value = "Infinite"; + else if (m_exifInfo.Distance > 0) + value = StringUtils::Format("{:4.2f}m", m_exifInfo.Distance); + break; + case SLIDESHOW_EXIF_EXPOSURE: + switch (m_exifInfo.ExposureProgram) + { + case 1: value = "Manual"; break; + case 2: value = "Program (Auto)"; break; + case 3: value = "Aperture priority (Semi-Auto)"; break; + case 4: value = "Shutter priority (semi-auto)"; break; + case 5: value = "Creative Program (based towards depth of field)"; break; + case 6: value = "Action program (based towards fast shutter speed)"; break; + case 7: value = "Portrait Mode"; break; + case 8: value = "Landscape Mode"; break; + } + break; + case SLIDESHOW_EXIF_EXPOSURE_TIME: + if (m_exifInfo.ExposureTime) + { + if (m_exifInfo.ExposureTime < 0.010f) + value = StringUtils::Format("{:6.4f}s", m_exifInfo.ExposureTime); + else + value = StringUtils::Format("{:5.3f}s", m_exifInfo.ExposureTime); + if (m_exifInfo.ExposureTime <= 0.5f) + value += StringUtils::Format(" (1/{})", static_cast<int>(0.5f + 1 / m_exifInfo.ExposureTime)); + } + break; + case SLIDESHOW_EXIF_EXPOSURE_BIAS: + if (m_exifInfo.ExposureBias != 0) + value = StringUtils::Format("{:4.2f} EV", m_exifInfo.ExposureBias); + break; + case SLIDESHOW_EXIF_EXPOSURE_MODE: + switch (m_exifInfo.ExposureMode) + { + case 0: value = "Automatic"; break; + case 1: value = "Manual"; break; + case 2: value = "Auto bracketing"; break; + } + break; + case SLIDESHOW_EXIF_FLASH_USED: + if (m_exifInfo.FlashUsed >= 0) + { + if (m_exifInfo.FlashUsed & 1) + { + value = "Yes"; + switch (m_exifInfo.FlashUsed) + { + case 0x5: value = "Yes (Strobe light not detected)"; break; + case 0x7: value = "Yes (Strobe light detected)"; break; + case 0x9: value = "Yes (Manual)"; break; + case 0xd: value = "Yes (Manual, return light not detected)"; break; + case 0xf: value = "Yes (Manual, return light detected)"; break; + case 0x19: value = "Yes (Auto)"; break; + case 0x1d: value = "Yes (Auto, return light not detected)"; break; + case 0x1f: value = "Yes (Auto, return light detected)"; break; + case 0x41: value = "Yes (Red eye reduction mode)"; break; + case 0x45: value = "Yes (Red eye reduction mode return light not detected)"; break; + case 0x47: value = "Yes (Red eye reduction mode return light detected)"; break; + case 0x49: value = "Yes (Manual, red eye reduction mode)"; break; + case 0x4d: value = "Yes (Manual, red eye reduction mode, return light not detected)"; break; + case 0x4f: value = "Yes (Manual, red eye reduction mode, return light detected)"; break; + case 0x59: value = "Yes (Auto, red eye reduction mode)"; break; + case 0x5d: value = "Yes (Auto, red eye reduction mode, return light not detected)"; break; + case 0x5f: value = "Yes (Auto, red eye reduction mode, return light detected)"; break; + } + } + else + value = m_exifInfo.FlashUsed == 0x18 ? "No (Auto)" : "No"; + } + break; + case SLIDESHOW_EXIF_WHITE_BALANCE: + return m_exifInfo.Whitebalance ? "Manual" : "Auto"; + case SLIDESHOW_EXIF_LIGHT_SOURCE: + switch (m_exifInfo.LightSource) + { + case 1: value = "Daylight"; break; + case 2: value = "Fluorescent"; break; + case 3: value = "Incandescent"; break; + case 4: value = "Flash"; break; + case 9: value = "Fine Weather"; break; + case 11: value = "Shade"; break; + default:; //Quercus: 17-1-2004 There are many more modes for this, check Exif2.2 specs + // If it just says 'unknown' or we don't know it, then + // don't bother showing it - it doesn't add any useful information. + } + break; + case SLIDESHOW_EXIF_METERING_MODE: + switch (m_exifInfo.MeteringMode) + { + case 2: value = "Center weight"; break; + case 3: value = "Spot"; break; + case 5: value = "Matrix"; break; + } + break; + case SLIDESHOW_EXIF_ISO_EQUIV: + if (m_exifInfo.ISOequivalent) + value = StringUtils::Format("{:2}", m_exifInfo.ISOequivalent); + break; + case SLIDESHOW_EXIF_DIGITAL_ZOOM: + if (m_exifInfo.DigitalZoomRatio) + value = StringUtils::Format("{:1.3f}x", m_exifInfo.DigitalZoomRatio); + break; + case SLIDESHOW_EXIF_CCD_WIDTH: + if (m_exifInfo.CCDWidth) + value = StringUtils::Format("{:4.2f}mm", m_exifInfo.CCDWidth); + break; + case SLIDESHOW_EXIF_GPS_LATITUDE: + value = m_exifInfo.GpsLat; + break; + case SLIDESHOW_EXIF_GPS_LONGITUDE: + value = m_exifInfo.GpsLong; + break; + case SLIDESHOW_EXIF_GPS_ALTITUDE: + value = m_exifInfo.GpsAlt; + break; + case SLIDESHOW_IPTC_SUP_CATEGORIES: value = m_iptcInfo.SupplementalCategories; break; + case SLIDESHOW_IPTC_KEYWORDS: value = m_iptcInfo.Keywords; break; + case SLIDESHOW_IPTC_CAPTION: value = m_iptcInfo.Caption; break; + case SLIDESHOW_IPTC_AUTHOR: value = m_iptcInfo.Author; break; + case SLIDESHOW_IPTC_HEADLINE: value = m_iptcInfo.Headline; break; + case SLIDESHOW_IPTC_SPEC_INSTR: value = m_iptcInfo.SpecialInstructions; break; + case SLIDESHOW_IPTC_CATEGORY: value = m_iptcInfo.Category; break; + case SLIDESHOW_IPTC_BYLINE: value = m_iptcInfo.Byline; break; + case SLIDESHOW_IPTC_BYLINE_TITLE: value = m_iptcInfo.BylineTitle; break; + case SLIDESHOW_IPTC_CREDIT: value = m_iptcInfo.Credit; break; + case SLIDESHOW_IPTC_SOURCE: value = m_iptcInfo.Source; break; + case SLIDESHOW_IPTC_COPYRIGHT_NOTICE: value = m_iptcInfo.CopyrightNotice; break; + case SLIDESHOW_IPTC_OBJECT_NAME: value = m_iptcInfo.ObjectName; break; + case SLIDESHOW_IPTC_CITY: value = m_iptcInfo.City; break; + case SLIDESHOW_IPTC_STATE: value = m_iptcInfo.State; break; + case SLIDESHOW_IPTC_COUNTRY: value = m_iptcInfo.Country; break; + case SLIDESHOW_IPTC_TX_REFERENCE: value = m_iptcInfo.TransmissionReference; break; + case SLIDESHOW_IPTC_DATE: value = m_iptcInfo.Date; break; + case SLIDESHOW_IPTC_URGENCY: value = m_iptcInfo.Urgency; break; + case SLIDESHOW_IPTC_COUNTRY_CODE: value = m_iptcInfo.CountryCode; break; + case SLIDESHOW_IPTC_REF_SERVICE: value = m_iptcInfo.ReferenceService; break; + case SLIDESHOW_IPTC_TIMECREATED: value = m_iptcInfo.TimeCreated; break; + case SLIDESHOW_IPTC_SUBLOCATION: value = m_iptcInfo.SubLocation; break; + case SLIDESHOW_IPTC_IMAGETYPE: value = m_iptcInfo.ImageType; break; + default: + break; + } + return value; +} + +int CPictureInfoTag::TranslateString(const std::string &info) +{ + if (StringUtils::EqualsNoCase(info, "filename")) return SLIDESHOW_FILE_NAME; + else if (StringUtils::EqualsNoCase(info, "path")) return SLIDESHOW_FILE_PATH; + else if (StringUtils::EqualsNoCase(info, "filesize")) return SLIDESHOW_FILE_SIZE; + else if (StringUtils::EqualsNoCase(info, "filedate")) return SLIDESHOW_FILE_DATE; + else if (StringUtils::EqualsNoCase(info, "slideindex")) return SLIDESHOW_INDEX; + else if (StringUtils::EqualsNoCase(info, "resolution")) return SLIDESHOW_RESOLUTION; + else if (StringUtils::EqualsNoCase(info, "slidecomment")) return SLIDESHOW_COMMENT; + else if (StringUtils::EqualsNoCase(info, "colour")) return SLIDESHOW_COLOUR; + else if (StringUtils::EqualsNoCase(info, "process")) return SLIDESHOW_PROCESS; + else if (StringUtils::EqualsNoCase(info, "exiftime")) return SLIDESHOW_EXIF_DATE_TIME; + else if (StringUtils::EqualsNoCase(info, "exifdate")) return SLIDESHOW_EXIF_DATE; + else if (StringUtils::EqualsNoCase(info, "longexiftime")) return SLIDESHOW_EXIF_LONG_DATE_TIME; + else if (StringUtils::EqualsNoCase(info, "longexifdate")) return SLIDESHOW_EXIF_LONG_DATE; + else if (StringUtils::EqualsNoCase(info, "exifdescription")) return SLIDESHOW_EXIF_DESCRIPTION; + else if (StringUtils::EqualsNoCase(info, "cameramake")) return SLIDESHOW_EXIF_CAMERA_MAKE; + else if (StringUtils::EqualsNoCase(info, "cameramodel")) return SLIDESHOW_EXIF_CAMERA_MODEL; + else if (StringUtils::EqualsNoCase(info, "exifcomment")) return SLIDESHOW_EXIF_COMMENT; + else if (StringUtils::EqualsNoCase(info, "exifsoftware")) return SLIDESHOW_EXIF_SOFTWARE; + else if (StringUtils::EqualsNoCase(info, "aperture")) return SLIDESHOW_EXIF_APERTURE; + else if (StringUtils::EqualsNoCase(info, "focallength")) return SLIDESHOW_EXIF_FOCAL_LENGTH; + else if (StringUtils::EqualsNoCase(info, "focusdistance")) return SLIDESHOW_EXIF_FOCUS_DIST; + else if (StringUtils::EqualsNoCase(info, "exposure")) return SLIDESHOW_EXIF_EXPOSURE; + else if (StringUtils::EqualsNoCase(info, "exposuretime")) return SLIDESHOW_EXIF_EXPOSURE_TIME; + else if (StringUtils::EqualsNoCase(info, "exposurebias")) return SLIDESHOW_EXIF_EXPOSURE_BIAS; + else if (StringUtils::EqualsNoCase(info, "exposuremode")) return SLIDESHOW_EXIF_EXPOSURE_MODE; + else if (StringUtils::EqualsNoCase(info, "flashused")) return SLIDESHOW_EXIF_FLASH_USED; + else if (StringUtils::EqualsNoCase(info, "whitebalance")) return SLIDESHOW_EXIF_WHITE_BALANCE; + else if (StringUtils::EqualsNoCase(info, "lightsource")) return SLIDESHOW_EXIF_LIGHT_SOURCE; + else if (StringUtils::EqualsNoCase(info, "meteringmode")) return SLIDESHOW_EXIF_METERING_MODE; + else if (StringUtils::EqualsNoCase(info, "isoequivalence")) return SLIDESHOW_EXIF_ISO_EQUIV; + else if (StringUtils::EqualsNoCase(info, "digitalzoom")) return SLIDESHOW_EXIF_DIGITAL_ZOOM; + else if (StringUtils::EqualsNoCase(info, "ccdwidth")) return SLIDESHOW_EXIF_CCD_WIDTH; + else if (StringUtils::EqualsNoCase(info, "orientation")) return SLIDESHOW_EXIF_ORIENTATION; + else if (StringUtils::EqualsNoCase(info, "supplementalcategories")) return SLIDESHOW_IPTC_SUP_CATEGORIES; + else if (StringUtils::EqualsNoCase(info, "keywords")) return SLIDESHOW_IPTC_KEYWORDS; + else if (StringUtils::EqualsNoCase(info, "caption")) return SLIDESHOW_IPTC_CAPTION; + else if (StringUtils::EqualsNoCase(info, "author")) return SLIDESHOW_IPTC_AUTHOR; + else if (StringUtils::EqualsNoCase(info, "headline")) return SLIDESHOW_IPTC_HEADLINE; + else if (StringUtils::EqualsNoCase(info, "specialinstructions")) return SLIDESHOW_IPTC_SPEC_INSTR; + else if (StringUtils::EqualsNoCase(info, "category")) return SLIDESHOW_IPTC_CATEGORY; + else if (StringUtils::EqualsNoCase(info, "byline")) return SLIDESHOW_IPTC_BYLINE; + else if (StringUtils::EqualsNoCase(info, "bylinetitle")) return SLIDESHOW_IPTC_BYLINE_TITLE; + else if (StringUtils::EqualsNoCase(info, "credit")) return SLIDESHOW_IPTC_CREDIT; + else if (StringUtils::EqualsNoCase(info, "source")) return SLIDESHOW_IPTC_SOURCE; + else if (StringUtils::EqualsNoCase(info, "copyrightnotice")) return SLIDESHOW_IPTC_COPYRIGHT_NOTICE; + else if (StringUtils::EqualsNoCase(info, "objectname")) return SLIDESHOW_IPTC_OBJECT_NAME; + else if (StringUtils::EqualsNoCase(info, "city")) return SLIDESHOW_IPTC_CITY; + else if (StringUtils::EqualsNoCase(info, "state")) return SLIDESHOW_IPTC_STATE; + else if (StringUtils::EqualsNoCase(info, "country")) return SLIDESHOW_IPTC_COUNTRY; + else if (StringUtils::EqualsNoCase(info, "transmissionreference")) return SLIDESHOW_IPTC_TX_REFERENCE; + else if (StringUtils::EqualsNoCase(info, "iptcdate")) return SLIDESHOW_IPTC_DATE; + else if (StringUtils::EqualsNoCase(info, "urgency")) return SLIDESHOW_IPTC_URGENCY; + else if (StringUtils::EqualsNoCase(info, "countrycode")) return SLIDESHOW_IPTC_COUNTRY_CODE; + else if (StringUtils::EqualsNoCase(info, "referenceservice")) return SLIDESHOW_IPTC_REF_SERVICE; + else if (StringUtils::EqualsNoCase(info, "latitude")) return SLIDESHOW_EXIF_GPS_LATITUDE; + else if (StringUtils::EqualsNoCase(info, "longitude")) return SLIDESHOW_EXIF_GPS_LONGITUDE; + else if (StringUtils::EqualsNoCase(info, "altitude")) return SLIDESHOW_EXIF_GPS_ALTITUDE; + else if (StringUtils::EqualsNoCase(info, "timecreated")) return SLIDESHOW_IPTC_TIMECREATED; + else if (StringUtils::EqualsNoCase(info, "sublocation")) return SLIDESHOW_IPTC_SUBLOCATION; + else if (StringUtils::EqualsNoCase(info, "imagetype")) return SLIDESHOW_IPTC_IMAGETYPE; + return 0; +} + +void CPictureInfoTag::SetInfo(const std::string &key, const std::string& value) +{ + int info = TranslateString(key); + + switch (info) + { + case SLIDESHOW_RESOLUTION: + { + std::vector<std::string> dimension; + StringUtils::Tokenize(value, dimension, ","); + if (dimension.size() == 2) + { + m_exifInfo.Width = atoi(dimension[0].c_str()); + m_exifInfo.Height = atoi(dimension[1].c_str()); + m_isInfoSetExternally = true; // Set the internal state to show metadata has been set by call to SetInfo + } + break; + } + case SLIDESHOW_EXIF_DATE_TIME: + { + m_exifInfo.DateTime = value; + m_isInfoSetExternally = true; // Set the internal state to show metadata has been set by call to SetInfo + ConvertDateTime(); + break; + } + default: + break; + } +} + +const CDateTime& CPictureInfoTag::GetDateTimeTaken() const +{ + return m_dateTimeTaken; +} + +void CPictureInfoTag::ConvertDateTime() +{ + const std::string& dateTime = m_exifInfo.DateTime; + if (dateTime.length() >= 19 && dateTime[0] != ' ') + { + int year = atoi(dateTime.substr(0, 4).c_str()); + int month = atoi(dateTime.substr(5, 2).c_str()); + int day = atoi(dateTime.substr(8, 2).c_str()); + int hour = atoi(dateTime.substr(11,2).c_str()); + int min = atoi(dateTime.substr(14,2).c_str()); + int sec = atoi(dateTime.substr(17,2).c_str()); + m_dateTimeTaken.SetDateTime(year, month, day, hour, min, sec); + } +} diff --git a/xbmc/pictures/PictureInfoTag.h b/xbmc/pictures/PictureInfoTag.h new file mode 100644 index 0000000..eff58fb --- /dev/null +++ b/xbmc/pictures/PictureInfoTag.h @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" +#include "libexif.h" +#include "utils/IArchivable.h" +#include "utils/ISerializable.h" +#include "utils/ISortable.h" + +#include <string> +#include <vector> + +namespace KODI +{ +namespace ADDONS +{ +class CImageDecoder; +} /* namespace ADDONS */ +} /* namespace KODI */ + +class CVariant; + +class CPictureInfoTag : public IArchivable, public ISerializable, public ISortable +{ + friend class KODI::ADDONS::CImageDecoder; + + // Mimic structs from libexif.h but with C++ types instead of arrays + struct ExifInfo + { + ExifInfo() = default; + ExifInfo(const ExifInfo&) = default; + ExifInfo(ExifInfo&&) = default; + ExifInfo(const ExifInfo_t& other); + + ExifInfo& operator=(const ExifInfo&) = default; + ExifInfo& operator=(ExifInfo&&) = default; + + std::string CameraMake; + std::string CameraModel; + std::string DateTime; + int Height{}; + int Width{}; + int Orientation{}; + int IsColor{}; + int Process{}; + int FlashUsed{}; + float FocalLength{}; + float ExposureTime{}; + float ApertureFNumber{}; + float Distance{}; + float CCDWidth{}; + float ExposureBias{}; + float DigitalZoomRatio{}; + int FocalLength35mmEquiv{}; + int Whitebalance{}; + int MeteringMode{}; + int ExposureProgram{}; + int ExposureMode{}; + int ISOequivalent{}; + int LightSource{}; + int CommentsCharset{}; + int XPCommentsCharset{}; + std::string Comments; + std::string FileComment; + std::string XPComment; + std::string Description; + + unsigned ThumbnailOffset{}; + unsigned ThumbnailSize{}; + unsigned LargestExifOffset{}; + + char ThumbnailAtEnd{}; + int ThumbnailSizeOffset{}; + + std::vector<int> DateTimeOffsets; + + int GpsInfoPresent{}; + std::string GpsLat; + std::string GpsLong; + std::string GpsAlt; + + private: + static std::string Convert(int charset, const char* data); + }; + + struct IPTCInfo + { + IPTCInfo() = default; + IPTCInfo(const IPTCInfo&) = default; + IPTCInfo(IPTCInfo&&) = default; + IPTCInfo(const IPTCInfo_t& other); + + IPTCInfo& operator=(const IPTCInfo&) = default; + IPTCInfo& operator=(IPTCInfo&&) = default; + + std::string RecordVersion; + std::string SupplementalCategories; + std::string Keywords; + std::string Caption; + std::string Author; + std::string Headline; + std::string SpecialInstructions; + std::string Category; + std::string Byline; + std::string BylineTitle; + std::string Credit; + std::string Source; + std::string CopyrightNotice; + std::string ObjectName; + std::string City; + std::string State; + std::string Country; + std::string TransmissionReference; + std::string Date; + std::string Urgency; + std::string ReferenceService; + std::string CountryCode; + std::string TimeCreated; + std::string SubLocation; + std::string ImageType; + }; + +public: + CPictureInfoTag() { Reset(); } + virtual ~CPictureInfoTag() = default; + void Reset(); + void Archive(CArchive& ar) override; + void Serialize(CVariant& value) const override; + void ToSortable(SortItem& sortable, Field field) const override; + const std::string GetInfo(int info) const; + + bool Loaded() const { return m_isLoaded; } + bool Load(const std::string &path); + + void SetInfo(const std::string& key, const std::string& value); + + /** + * GetDateTimeTaken() -- Returns the EXIF DateTimeOriginal for current picture + * + * The exif library returns DateTimeOriginal if available else the other + * DateTime tags. See libexif CExifParse::ProcessDir for details. + */ + const CDateTime& GetDateTimeTaken() const; +private: + static int TranslateString(const std::string &info); + + ExifInfo m_exifInfo; + IPTCInfo m_iptcInfo; + bool m_isLoaded; // Set to true if metadata has been loaded from the picture file successfully + bool m_isInfoSetExternally; // Set to true if metadata has been set by an external call to SetInfo + CDateTime m_dateTimeTaken; + void ConvertDateTime(); +}; + diff --git a/xbmc/pictures/PictureScalingAlgorithm.cpp b/xbmc/pictures/PictureScalingAlgorithm.cpp new file mode 100644 index 0000000..65cfcd9 --- /dev/null +++ b/xbmc/pictures/PictureScalingAlgorithm.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include <algorithm> + +extern "C" { +#include <libswscale/swscale.h> +} + +#include "PictureScalingAlgorithm.h" +#include "utils/StringUtils.h" + +CPictureScalingAlgorithm::Algorithm CPictureScalingAlgorithm::Default = CPictureScalingAlgorithm::Bicubic; + +CPictureScalingAlgorithm::AlgorithmMap CPictureScalingAlgorithm::m_algorithms = { + { FastBilinear, { "fast_bilinear", SWS_FAST_BILINEAR } }, + { Bilinear, { "bilinear", SWS_BILINEAR } }, + { Bicubic, { "bicubic", SWS_BICUBIC } }, + { Experimental, { "experimental", SWS_X } }, + { NearestNeighbor, { "nearest_neighbor", SWS_POINT } }, + { AveragingArea, { "averaging_area", SWS_AREA } }, + { Bicublin, { "bicublin", SWS_BICUBLIN } }, + { Gaussian, { "gaussian", SWS_GAUSS } }, + { Sinc, { "sinc", SWS_SINC } }, + { Lanczos, { "lanczos", SWS_LANCZOS } }, + { BicubicSpline, { "bicubic_spline", SWS_SPLINE } }, +}; + +CPictureScalingAlgorithm::Algorithm CPictureScalingAlgorithm::FromString(const std::string& scalingAlgorithm) +{ + const auto& algorithm = std::find_if(m_algorithms.begin(), m_algorithms.end(), + [&scalingAlgorithm](const std::pair<Algorithm, ScalingAlgorithm>& algo) { return StringUtils::EqualsNoCase(algo.second.name, scalingAlgorithm); }); + if (algorithm != m_algorithms.end()) + return algorithm->first; + + return NoAlgorithm; +} + +std::string CPictureScalingAlgorithm::ToString(Algorithm scalingAlgorithm) +{ + const auto& algorithm = m_algorithms.find(scalingAlgorithm); + if (algorithm != m_algorithms.end()) + return algorithm->second.name; + + return ""; +} + +int CPictureScalingAlgorithm::ToSwscale(const std::string& scalingAlgorithm) +{ + return ToSwscale(FromString(scalingAlgorithm)); +} + +int CPictureScalingAlgorithm::ToSwscale(Algorithm scalingAlgorithm) +{ + const auto& algorithm = m_algorithms.find(scalingAlgorithm); + if (algorithm != m_algorithms.end()) + return algorithm->second.swscale; + + return ToSwscale(Default); +} diff --git a/xbmc/pictures/PictureScalingAlgorithm.h b/xbmc/pictures/PictureScalingAlgorithm.h new file mode 100644 index 0000000..d5aa543 --- /dev/null +++ b/xbmc/pictures/PictureScalingAlgorithm.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <map> +#include <string> + +class CPictureScalingAlgorithm +{ +public: + typedef enum Algorithm + { + NoAlgorithm, + FastBilinear, + Bilinear, + Bicubic, + Experimental, + NearestNeighbor, + AveragingArea, + Bicublin, + Gaussian, + Sinc, + Lanczos, + BicubicSpline + } Algorithm; + + static Algorithm Default; + + static Algorithm FromString(const std::string& scalingAlgorithm); + static std::string ToString(Algorithm scalingAlgorithm); + static int ToSwscale(const std::string& scalingAlgorithm); + static int ToSwscale(Algorithm scalingAlgorithm); + +private: + CPictureScalingAlgorithm(); + + typedef struct ScalingAlgorithm + { + std::string name; + int swscale; + } ScalingAlgorithm; + + typedef std::map<CPictureScalingAlgorithm::Algorithm, CPictureScalingAlgorithm::ScalingAlgorithm> AlgorithmMap; + static AlgorithmMap m_algorithms; +}; diff --git a/xbmc/pictures/PictureThumbLoader.cpp b/xbmc/pictures/PictureThumbLoader.cpp new file mode 100644 index 0000000..0cdd56c --- /dev/null +++ b/xbmc/pictures/PictureThumbLoader.cpp @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PictureThumbLoader.h" + +#include "FileItem.h" +#include "GUIUserMessages.h" +#include "Picture.h" +#include "ServiceBroker.h" +#include "TextureCache.h" +#include "URL.h" +#include "filesystem/Directory.h" +#include "filesystem/MultiPathDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileExtensionProvider.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" +#include "video/VideoThumbLoader.h" + +using namespace XFILE; + +CPictureThumbLoader::CPictureThumbLoader() : CThumbLoader(), CJobQueue(true, 1, CJob::PRIORITY_LOW_PAUSABLE) +{ + m_regenerateThumbs = false; +} + +CPictureThumbLoader::~CPictureThumbLoader() +{ + StopThread(); +} + +void CPictureThumbLoader::OnLoaderFinish() +{ + m_regenerateThumbs = false; + CThumbLoader::OnLoaderFinish(); +} + +bool CPictureThumbLoader::LoadItem(CFileItem* pItem) +{ + bool result = LoadItemCached(pItem); + result |= LoadItemLookup(pItem); + + return result; +} + +bool CPictureThumbLoader::LoadItemCached(CFileItem* pItem) +{ + if (pItem->m_bIsShareOrDrive + || pItem->IsParentFolder()) + return false; + + if (pItem->HasArt("thumb") && m_regenerateThumbs) + { + CServiceBroker::GetTextureCache()->ClearCachedImage(pItem->GetArt("thumb")); + if (m_textureDatabase->Open()) + { + m_textureDatabase->ClearTextureForPath(pItem->GetPath(), "thumb"); + m_textureDatabase->Close(); + } + pItem->SetArt("thumb", ""); + } + + std::string thumb; + if (pItem->IsPicture() && !pItem->IsZIP() && !pItem->IsRAR() && !pItem->IsCBZ() && !pItem->IsCBR() && !pItem->IsPlayList()) + { // load the thumb from the image file + thumb = pItem->HasArt("thumb") ? pItem->GetArt("thumb") : CTextureUtils::GetWrappedThumbURL(pItem->GetPath()); + } + else if (pItem->IsVideo() && !pItem->IsZIP() && !pItem->IsRAR() && !pItem->IsCBZ() && !pItem->IsCBR() && !pItem->IsPlayList()) + { // video + CVideoThumbLoader loader; + if (!loader.FillThumb(*pItem)) + { + std::string thumbURL = CVideoThumbLoader::GetEmbeddedThumbURL(*pItem); + if (CServiceBroker::GetTextureCache()->HasCachedImage(thumbURL)) + { + thumb = thumbURL; + } + else if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTTHUMB) && CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_MYVIDEOS_EXTRACTFLAGS)) + { + CFileItem item(*pItem); + CThumbExtractor* extract = new CThumbExtractor(item, pItem->GetPath(), true, thumbURL); + AddJob(extract); + thumb.clear(); + } + } + } + else if (!pItem->HasArt("thumb")) + { // folder, zip, cbz, rar, cbr, playlist may have a previously cached image + thumb = GetCachedImage(*pItem, "thumb"); + } + if (!thumb.empty()) + { + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb); + pItem->SetArt("thumb", thumb); + } + pItem->FillInDefaultIcon(); + return true; +} + +bool CPictureThumbLoader::LoadItemLookup(CFileItem* pItem) +{ + return false; +} + +void CPictureThumbLoader::OnJobComplete(unsigned int jobID, bool success, CJob* job) +{ + if (success) + { + CThumbExtractor* loader = static_cast<CThumbExtractor*>(job); + loader->m_item.SetPath(loader->m_listpath); + CFileItemPtr pItem(new CFileItem(loader->m_item)); + CGUIMessage msg(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_ITEM, 0, pItem); + CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(msg); + } + CJobQueue::OnJobComplete(jobID, success, job); +} + +void CPictureThumbLoader::ProcessFoldersAndArchives(CFileItem *pItem) +{ + if (pItem->HasArt("thumb")) + return; + + CTextureDatabase db; + db.Open(); + if (pItem->IsCBR() || pItem->IsCBZ()) + { + std::string strTBN(URIUtils::ReplaceExtension(pItem->GetPath(),".tbn")); + if (CFileUtils::Exists(strTBN)) + { + db.SetTextureForPath(pItem->GetPath(), "thumb", strTBN); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(strTBN); + pItem->SetArt("thumb", strTBN); + return; + } + } + if ((pItem->m_bIsFolder || pItem->IsCBR() || pItem->IsCBZ()) && !pItem->m_bIsShareOrDrive + && !pItem->IsParentFolder() && !pItem->IsPath("add")) + { + // first check for a folder.jpg + std::string thumb = "folder.jpg"; + CURL pathToUrl = pItem->GetURL(); + if (pItem->IsCBR()) + { + pathToUrl = URIUtils::CreateArchivePath("rar",pItem->GetURL(),""); + thumb = "cover.jpg"; + } + if (pItem->IsCBZ()) + { + pathToUrl = URIUtils::CreateArchivePath("zip",pItem->GetURL(),""); + thumb = "cover.jpg"; + } + if (pItem->IsMultiPath()) + pathToUrl = CURL(CMultiPathDirectory::GetFirstPath(pItem->GetPath())); + thumb = URIUtils::AddFileToFolder(pathToUrl.Get(), thumb); + if (CFileUtils::Exists(thumb)) + { + db.SetTextureForPath(pItem->GetPath(), "thumb", thumb); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb); + pItem->SetArt("thumb", thumb); + return; + } + if (!pItem->IsPlugin()) + { + // we load the directory, grab 4 random thumb files (if available) and then generate + // the thumb. + + CFileItemList items; + + CDirectory::GetDirectory(pathToUrl, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS); + + // create the folder thumb by choosing 4 random thumbs within the folder and putting + // them into one thumb. + // count the number of images + for (int i=0; i < items.Size();) + { + if (!items[i]->IsPicture() || items[i]->IsZIP() || items[i]->IsRAR() || items[i]->IsPlayList()) + { + items.Remove(i); + } + else + i++; + } + + if (items.IsEmpty()) + { + if (pItem->IsCBZ() || pItem->IsCBR()) + { + CDirectory::GetDirectory(pathToUrl, items, CServiceBroker::GetFileExtensionProvider().GetPictureExtensions(), DIR_FLAG_NO_FILE_DIRS); + for (int i=0;i<items.Size();++i) + { + CFileItemPtr item = items[i]; + if (item->m_bIsFolder) + { + ProcessFoldersAndArchives(item.get()); + pItem->SetArt("thumb", items[i]->GetArt("thumb")); + pItem->SetArt("icon", items[i]->GetArt("icon")); + return; + } + } + } + return; // no images in this folder + } + + // randomize them + items.Randomize(); + + if (items.Size() < 4 || pItem->IsCBR() || pItem->IsCBZ()) + { // less than 4 items, so just grab the first thumb + items.Sort(SortByLabel, SortOrderAscending); + std::string thumb = CTextureUtils::GetWrappedThumbURL(items[0]->GetPath()); + db.SetTextureForPath(pItem->GetPath(), "thumb", thumb); + CServiceBroker::GetTextureCache()->BackgroundCacheImage(thumb); + pItem->SetArt("thumb", thumb); + } + else + { + // ok, now we've got the files to get the thumbs from, lets create it... + // we basically load the 4 images and combine them + std::vector<std::string> files; + files.reserve(4); + for (int thumb = 0; thumb < 4; thumb++) + files.push_back(items[thumb]->GetPath()); + std::string thumb = CTextureUtils::GetWrappedImageURL(pItem->GetPath(), "picturefolder"); + std::string relativeCacheFile = CTextureCache::GetCacheFile(thumb) + ".png"; + if (CPicture::CreateTiledThumb(files, CTextureCache::GetCachedPath(relativeCacheFile))) + { + CTextureDetails details; + details.file = relativeCacheFile; + details.width = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes; + details.height = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes; + CServiceBroker::GetTextureCache()->AddCachedTexture(thumb, details); + db.SetTextureForPath(pItem->GetPath(), "thumb", thumb); + pItem->SetArt("thumb", CTextureCache::GetCachedPath(relativeCacheFile)); + } + } + } + // refill in the icon to get it to update + pItem->FillInDefaultIcon(); + } +} diff --git a/xbmc/pictures/PictureThumbLoader.h b/xbmc/pictures/PictureThumbLoader.h new file mode 100644 index 0000000..664968e --- /dev/null +++ b/xbmc/pictures/PictureThumbLoader.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "ThumbLoader.h" +#include "utils/JobManager.h" + +class CPictureThumbLoader : public CThumbLoader, public CJobQueue +{ +public: + CPictureThumbLoader(); + ~CPictureThumbLoader() override; + + bool LoadItem(CFileItem* pItem) override; + bool LoadItemCached(CFileItem* pItem) override; + bool LoadItemLookup(CFileItem* pItem) override; + void SetRegenerateThumbs(bool regenerate) { m_regenerateThumbs = regenerate; } + static void ProcessFoldersAndArchives(CFileItem *pItem); + + /*! + \brief Callback from CThumbExtractor on completion of a generated image + + Performs the callbacks and updates the GUI. + + \sa CImageLoader, IJobCallback + */ + void OnJobComplete(unsigned int jobID, bool success, CJob *job) override; + +protected: + void OnLoaderFinish() override; + +private: + bool m_regenerateThumbs; +}; diff --git a/xbmc/pictures/SlideShowPicture.cpp b/xbmc/pictures/SlideShowPicture.cpp new file mode 100644 index 0000000..63ec8b9 --- /dev/null +++ b/xbmc/pictures/SlideShowPicture.cpp @@ -0,0 +1,1016 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "SlideShowPicture.h" + +#include "ServiceBroker.h" +#include "guilib/Texture.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "windowing/GraphicContext.h" +#include "windowing/WinSystem.h" + +#include <mutex> + +#ifndef _USE_MATH_DEFINES +#define _USE_MATH_DEFINES +#endif +#include <math.h> + +#if defined(HAS_GL) +#include "rendering/gl/RenderSystemGL.h" +#include "utils/GLUtils.h" +#elif defined(HAS_GLES) +#include "rendering/gles/RenderSystemGLES.h" +#include "utils/GLUtils.h" +#elif defined(TARGET_WINDOWS) +#include "guilib/TextureDX.h" +#include "rendering/dx/DeviceResources.h" +#include "rendering/dx/RenderContext.h" + +#include <DirectXMath.h> +using namespace DirectX; +using namespace Microsoft::WRL; +#endif + +#include <cstddef> + +#define IMMEDIATE_TRANSITION_TIME 20 + +#define PICTURE_MOVE_AMOUNT 0.02f +#define PICTURE_MOVE_AMOUNT_ANALOG 0.01f +#define PICTURE_VIEW_BOX_COLOR 0xffffff00 // YELLOW +#define PICTURE_VIEW_BOX_BACKGROUND 0xff000000 // BLACK + +#define FPS 25 + +static float zoomamount[10] = { 1.0f, 1.2f, 1.5f, 2.0f, 2.8f, 4.0f, 6.0f, 9.0f, 13.5f, 20.0f }; + +CSlideShowPic::CSlideShowPic() : m_pImage(nullptr) +{ + m_bIsLoaded = false; + m_bIsFinished = false; + m_bDrawNextImage = false; + m_bTransitionImmediately = false; + + m_bCanMoveHorizontally = false; + m_bCanMoveVertically = false; +} + +CSlideShowPic::~CSlideShowPic() +{ + Close(); +} + +void CSlideShowPic::Close() +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + m_pImage.reset(); + m_bIsLoaded = false; + m_bIsFinished = false; + m_bDrawNextImage = false; + m_bTransitionImmediately = false; + m_bIsDirty = true; + m_alpha = 0; +#ifdef HAS_DX + m_vb = nullptr; +#endif +} + +void CSlideShowPic::Reset(DISPLAY_EFFECT dispEffect, TRANSITION_EFFECT transEffect) +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + if (m_pImage) + SetTexture_Internal(m_iSlideNumber, std::move(m_pImage), dispEffect, transEffect); + else + Close(); +} + +bool CSlideShowPic::DisplayEffectNeedChange(DISPLAY_EFFECT newDispEffect) const +{ + if (m_displayEffect == newDispEffect) + return false; + if (newDispEffect == EFFECT_RANDOM && m_displayEffect != EFFECT_NONE && m_displayEffect != EFFECT_NO_TIMEOUT) + return false; + return true; +} + +void CSlideShowPic::SetTexture(int iSlideNumber, + std::unique_ptr<CTexture> pTexture, + DISPLAY_EFFECT dispEffect, + TRANSITION_EFFECT transEffect) +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + Close(); + SetTexture_Internal(iSlideNumber, std::move(pTexture), dispEffect, transEffect); +} + +void CSlideShowPic::SetTexture_Internal(int iSlideNumber, + std::unique_ptr<CTexture> pTexture, + DISPLAY_EFFECT dispEffect, + TRANSITION_EFFECT transEffect) +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + m_bPause = false; + m_bNoEffect = false; + m_bTransitionImmediately = false; + m_iSlideNumber = iSlideNumber; + + m_bIsDirty = true; + m_pImage = std::move(pTexture); + m_fWidth = static_cast<float>(m_pImage->GetWidth()); + m_fHeight = static_cast<float>(m_pImage->GetHeight()); + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_SLIDESHOW_HIGHQUALITYDOWNSCALING)) + { // activate mipmapping when high quality downscaling is 'on' + m_pImage->SetMipmapping(); + } + // reset our counter + m_iCounter = 0; + // initialize our transition effect + m_transitionStart.type = transEffect; + m_transitionStart.start = 0; + + // initialize our display effect + if (dispEffect == EFFECT_RANDOM) + { + if (((m_fWidth / m_fHeight) > 1.9f) || ((m_fHeight / m_fWidth) > 1.9f)) + m_displayEffect = EFFECT_PANORAMA; + else + m_displayEffect = (DISPLAY_EFFECT)((rand() % (EFFECT_RANDOM - 1)) + 1); + } + else + m_displayEffect = dispEffect; + + // the +1's make sure it actually occurs + float fadeTime = 0.2f; + if (m_displayEffect != EFFECT_NO_TIMEOUT) + fadeTime = std::min(0.2f*CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME), 3.0f); + m_transitionStart.length = (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * fadeTime); // transition time in frames + m_transitionEnd.type = transEffect; + m_transitionEnd.length = m_transitionStart.length; + m_transitionTemp.type = TRANSITION_NONE; + m_fTransitionAngle = 0; + m_fTransitionZoom = 0; + m_fAngle = 0.0f; + if (m_pImage->GetOrientation() == 7) + { // rotate to 270 degrees + m_fAngle = 270.0f; + } + if (m_pImage->GetOrientation() == 2) + { // rotate to 180 degrees + m_fAngle = 180.0f; + } + if (m_pImage->GetOrientation() == 5) + { // rotate to 90 degrees + m_fAngle = 90.0f; + } + m_fZoomAmount = 1; + m_fZoomLeft = 0; + m_fZoomTop = 0; + m_fPosX = m_fPosY = 0.0f; + m_fPosZ = 1.0f; + m_fVelocityX = m_fVelocityY = m_fVelocityZ = 0.0f; + int iFrames = std::max((int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME)), 1); + if (m_displayEffect == EFFECT_PANORAMA) + { + RESOLUTION_INFO res = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + float fScreenWidth = (float)res.Overscan.right - res.Overscan.left; + float fScreenHeight = (float)res.Overscan.bottom - res.Overscan.top; + + if (m_fWidth > m_fHeight) + { + iFrames = (int)(iFrames * (m_fWidth - m_fHeight) / m_fHeight); + m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames; + + m_fPosX = 0.5f - (fScreenWidth / fScreenHeight) * (m_fHeight / m_fWidth) * 0.5f; + if (rand() % 2) + m_fPosX = -m_fPosX; + m_fVelocityX = -m_fPosX * 2.0f / m_iTotalFrames; + } + else + { + iFrames = (int)(iFrames * (m_fHeight - (0.5f * m_fWidth)) / m_fWidth); + m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames; + + m_fPosY = 0.5f - (fScreenHeight / fScreenWidth) * (m_fWidth / m_fHeight) * 0.5f; + if (rand() % 2) + m_fPosY = -m_fPosY; + m_fVelocityY = -m_fPosY * 2.0f / m_iTotalFrames; + } + } + else + { + m_iTotalFrames = m_transitionStart.length + m_transitionEnd.length + iFrames; + + if (m_displayEffect == EFFECT_FLOAT) + { + // Calculate start and end positions + // choose a random direction + float angle = (rand() % 1000) / 1000.0f * 2 * (float)M_PI; + m_fPosX = cos(angle) * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.00005f; + m_fPosY = sin(angle) * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.00005f; + m_fVelocityX = -m_fPosX * 2.0f / m_iTotalFrames; + m_fVelocityY = -m_fPosY * 2.0f / m_iTotalFrames; + } + else if (m_displayEffect == EFFECT_ZOOM) + { + m_fPosZ = 1.0f; + m_fVelocityZ = 0.0001f * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowZoomAmount; + } + } + + m_transitionEnd.start = m_transitionStart.length + iFrames; + + m_bIsFinished = false; + m_bDrawNextImage = false; + m_bIsLoaded = true; +} + +void CSlideShowPic::SetOriginalSize(int iOriginalWidth, int iOriginalHeight, bool bFullSize) +{ + m_iOriginalWidth = iOriginalWidth; + m_iOriginalHeight = iOriginalHeight; + m_bFullSize = bFullSize; +} + +int CSlideShowPic::GetOriginalWidth() +{ + int iAngle = (int)(m_fAngle / 90.0f + 0.4f); + if (iAngle % 2) + return m_iOriginalHeight; + else + return m_iOriginalWidth; +} + +int CSlideShowPic::GetOriginalHeight() +{ + int iAngle = (int)(m_fAngle / 90.0f + 0.4f); + if (iAngle % 2) + return m_iOriginalWidth; + else + return m_iOriginalHeight; +} + +void CSlideShowPic::UpdateTexture(std::unique_ptr<CTexture> pTexture) +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + m_pImage = std::move(pTexture); + m_fWidth = static_cast<float>(m_pImage->GetWidth()); + m_fHeight = static_cast<float>(m_pImage->GetHeight()); + m_bIsDirty = true; +} + +static CRect GetRectangle(const float x[4], const float y[4]) +{ + CRect rect; + rect.x1 = *std::min_element(x, x+4); + rect.y1 = *std::min_element(y, y+4); + rect.x2 = *std::max_element(x, x+4); + rect.y2 = *std::max_element(y, y+4); + return rect; +} + +void CSlideShowPic::UpdateVertices(float cur_x[4], float cur_y[4], const float new_x[4], const float new_y[4], CDirtyRegionList &dirtyregions) +{ + const size_t count = sizeof(float)*4; + if(memcmp(cur_x, new_x, count) + || memcmp(cur_y, new_y, count) + || m_bIsDirty) + { + dirtyregions.push_back(CDirtyRegion(GetRectangle(cur_x, cur_y))); + dirtyregions.push_back(CDirtyRegion(GetRectangle(new_x, new_y))); + memcpy(cur_x, new_x, count); + memcpy(cur_y, new_y, count); + } +} + +void CSlideShowPic::Process(unsigned int currentTime, CDirtyRegionList &dirtyregions) +{ + if (!m_pImage || !m_bIsLoaded || m_bIsFinished) return ; + UTILS::COLOR::Color alpha = m_alpha; + if (m_iCounter <= m_transitionStart.length) + { // do start transition + if (m_transitionStart.type == CROSSFADE) + { // fade in at 1x speed + alpha = (UTILS::COLOR::Color)((float)m_iCounter / (float)m_transitionStart.length * 255.0f); + } + else if (m_transitionStart.type == FADEIN_FADEOUT) + { // fade in at 2x speed, then keep solid + alpha = + (UTILS::COLOR::Color)((float)m_iCounter / (float)m_transitionStart.length * 255.0f * 2); + if (alpha > 255) alpha = 255; + } + else // m_transitionEffect == TRANSITION_NONE + { + alpha = 0xFF; // opaque + } + } + bool bPaused = m_bPause | (m_fZoomAmount != 1.0f); + // check if we're doing a temporary effect (such as rotate + zoom) + if (m_transitionTemp.type != TRANSITION_NONE) + { + bPaused = true; + if (m_iCounter >= m_transitionTemp.start) + { + if (m_iCounter >= m_transitionTemp.start + m_transitionTemp.length) + { // we're finished this transition + if (m_transitionTemp.type == TRANSITION_ZOOM) + { // correct for any introduced inaccuracies. + int i; + for (i = 0; i < 10; i++) + { + if (fabs(m_fZoomAmount - zoomamount[i]) < 0.01f * zoomamount[i]) + { + m_fZoomAmount = zoomamount[i]; + break; + } + } + m_bNoEffect = (m_fZoomAmount != 1.0f); // turn effect rendering back on. + } + m_transitionTemp.type = TRANSITION_NONE; + } + else + { + if (m_transitionTemp.type == TRANSITION_ROTATE) + m_fAngle += m_fTransitionAngle; + if (m_transitionTemp.type == TRANSITION_ZOOM) + m_fZoomAmount += m_fTransitionZoom; + } + } + } + // now just display + if (!m_bNoEffect && !bPaused) + { + if (m_displayEffect == EFFECT_PANORAMA) + { + m_fPosX += m_fVelocityX; + m_fPosY += m_fVelocityY; + } + else if (m_displayEffect == EFFECT_FLOAT) + { + m_fPosX += m_fVelocityX; + m_fPosY += m_fVelocityY; + float fMoveAmount = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.0001f; + if (m_fPosX > fMoveAmount) + { + m_fPosX = fMoveAmount; + m_fVelocityX = -m_fVelocityX; + } + if (m_fPosX < -fMoveAmount) + { + m_fPosX = -fMoveAmount; + m_fVelocityX = -m_fVelocityX; + } + if (m_fPosY > fMoveAmount) + { + m_fPosY = fMoveAmount; + m_fVelocityY = -m_fVelocityY; + } + if (m_fPosY < -fMoveAmount) + { + m_fPosY = -fMoveAmount; + m_fVelocityY = -m_fVelocityY; + } + } + else if (m_displayEffect == EFFECT_ZOOM) + { + m_fPosZ += m_fVelocityZ; +/* if (m_fPosZ > 1.0f + 0.01f*CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("Slideshow.ZoomAmount")) + { + m_fPosZ = 1.0f + 0.01f * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt("Slideshow.ZoomAmount"); + m_fVelocityZ = -m_fVelocityZ; + } + if (m_fPosZ < 1.0f) + { + m_fPosZ = 1.0f; + m_fVelocityZ = -m_fVelocityZ; + }*/ + } + } + if (m_displayEffect != EFFECT_NO_TIMEOUT && bPaused && !m_bTransitionImmediately) + { // paused - increment the last transition start time + m_transitionEnd.start++; + } + if (m_iCounter >= m_transitionEnd.start) + { // do end transition +// CLog::Log(LOGDEBUG,"Transitioning"); + m_bDrawNextImage = true; + if (m_transitionEnd.type == CROSSFADE) + { // fade out at 1x speed + alpha = 255 - (UTILS::COLOR::Color)((float)(m_iCounter - m_transitionEnd.start) / + (float)m_transitionEnd.length * 255.0f); + } + else if (m_transitionEnd.type == FADEIN_FADEOUT) + { // keep solid, then fade out at 2x speed + alpha = (UTILS::COLOR::Color)( + (float)(m_transitionEnd.length - m_iCounter + m_transitionEnd.start) / + (float)m_transitionEnd.length * 255.0f * 2); + if (alpha > 255) alpha = 255; + } + else // m_transitionEffect == TRANSITION_NONE + { + alpha = 0xFF; // opaque + } + } + if (alpha != m_alpha) + { + m_alpha = alpha; + m_bIsDirty = true; + } + if (m_displayEffect != EFFECT_NO_TIMEOUT || m_iCounter < m_transitionStart.length || m_iCounter >= m_transitionEnd.start || (m_iCounter >= m_transitionTemp.start && m_iCounter < m_transitionTemp.start + m_transitionTemp.length)) + { + /* this really annoying. there's non-stop logging when viewing a pic outside of the slideshow + if (m_displayEffect == EFFECT_NO_TIMEOUT) + CLog::Log(LOGDEBUG, "Incrementing counter ({}) while not in slideshow (startlength={},endstart={},endlength={})", m_iCounter, m_transitionStart.length, m_transitionEnd.start, m_transitionEnd.length); + */ + m_iCounter++; + } + if (m_iCounter > m_transitionEnd.start + m_transitionEnd.length) + m_bIsFinished = true; + + RESOLUTION_INFO info = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo(); + + // calculate where we should render (and how large it should be) + // calculate aspect ratio correction factor + float fOffsetX = (float)info.Overscan.left; + float fOffsetY = (float)info.Overscan.top; + float fScreenWidth = (float)info.Overscan.right - info.Overscan.left; + float fScreenHeight = (float)info.Overscan.bottom - info.Overscan.top; + float fPixelRatio = info.fPixelRatio; + + // Rotate the image as needed + float x[4]; + float y[4]; + float si = sin(m_fAngle / 180.0f * static_cast<float>(M_PI)); + float co = cos(m_fAngle / 180.0f * static_cast<float>(M_PI)); + x[0] = -m_fWidth * co + m_fHeight * si; + y[0] = -m_fWidth * si - m_fHeight * co; + x[1] = m_fWidth * co + m_fHeight * si; + y[1] = m_fWidth * si - m_fHeight * co; + x[2] = m_fWidth * co - m_fHeight * si; + y[2] = m_fWidth * si + m_fHeight * co; + x[3] = -m_fWidth * co - m_fHeight * si; + y[3] = -m_fWidth * si + m_fHeight * co; + + // calculate our scale amounts + float fSourceAR = m_fWidth / m_fHeight; + float fSourceInvAR = 1 / fSourceAR; + float fAR = si * si * (fSourceInvAR - fSourceAR) + fSourceAR; + + //float fOutputFrameAR = fAR / fPixelRatio; + + float fScaleNorm = fScreenWidth / m_fWidth; + float fScaleInv = fScreenWidth / m_fHeight; + + bool bFillScreen = false; + float fComp = 1.0f + 0.01f * CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowBlackBarCompensation; + float fScreenRatio = fScreenWidth / fScreenHeight * fPixelRatio; + // work out if we should be compensating the zoom to minimize blackbars + // we should compute this based on the % of black bars on screen perhaps?? + //! @todo change m_displayEffect != EFFECT_NO_TIMEOUT to whether we're running the slideshow + if (m_displayEffect != EFFECT_NO_TIMEOUT && m_displayEffect != EFFECT_NONE && fScreenRatio < fSourceAR * fComp && fSourceAR < fScreenRatio * fComp) + bFillScreen = true; + if ((!bFillScreen && fScreenWidth*fPixelRatio > fScreenHeight*fSourceAR) || (bFillScreen && fScreenWidth*fPixelRatio < fScreenHeight*fSourceAR)) + fScaleNorm = fScreenHeight / (m_fHeight * fPixelRatio); + bFillScreen = false; + if (m_displayEffect != EFFECT_NO_TIMEOUT && m_displayEffect != EFFECT_NONE && fScreenRatio < fSourceInvAR * fComp && fSourceInvAR < fScreenRatio * fComp) + bFillScreen = true; + if ((!bFillScreen && fScreenWidth*fPixelRatio > fScreenHeight*fSourceInvAR) || (bFillScreen && fScreenWidth*fPixelRatio < fScreenHeight*fSourceInvAR)) + fScaleInv = fScreenHeight / (m_fWidth * fPixelRatio); + + float fScale = si * si * (fScaleInv - fScaleNorm) + fScaleNorm; + // scale if we need to due to the effect we're using + if (m_displayEffect == EFFECT_PANORAMA) + { + if (m_fWidth > m_fHeight) + fScale *= m_fWidth / fScreenWidth * fScreenHeight / m_fHeight; + else + fScale *= m_fHeight / fScreenHeight * fScreenWidth / m_fWidth; + } + if (m_displayEffect == EFFECT_FLOAT) + fScale *= (1.0f + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_slideshowPanAmount * m_iTotalFrames * 0.0001f); + if (m_displayEffect == EFFECT_ZOOM) + fScale *= m_fPosZ; + // zoom image + fScale *= m_fZoomAmount; + + // calculate the resultant coordinates + for (int i = 0; i < 4; i++) + { + x[i] *= fScale * 0.5f; // as the offsets x[] and y[] are from center + y[i] *= fScale * fPixelRatio * 0.5f; + // center it + x[i] += 0.5f * fScreenWidth + fOffsetX; + y[i] += 0.5f * fScreenHeight + fOffsetY; + } + // shift if we're zooming + if (m_fZoomAmount > 1) + { + float minx = x[0]; + float maxx = x[0]; + float miny = y[0]; + float maxy = y[0]; + for (int i = 1; i < 4; i++) + { + if (x[i] < minx) minx = x[i]; + if (x[i] > maxx) maxx = x[i]; + if (y[i] < miny) miny = y[i]; + if (y[i] > maxy) maxy = y[i]; + } + float w = maxx - minx; + float h = maxy - miny; + m_bCanMoveHorizontally = (w >= fScreenWidth); + m_bCanMoveVertically = (h >= fScreenHeight); + if (w >= fScreenWidth) + { // must have no black bars + if (minx + m_fZoomLeft*w > fOffsetX) + m_fZoomLeft = (fOffsetX - minx) / w; + if (maxx + m_fZoomLeft*w < fOffsetX + fScreenWidth) + m_fZoomLeft = (fScreenWidth + fOffsetX - maxx) / w; + for (float& i : x) + i += w * m_fZoomLeft; + } + if (h >= fScreenHeight) + { // must have no black bars + if (miny + m_fZoomTop*h > fOffsetY) + m_fZoomTop = (fOffsetY - miny) / h; + if (maxy + m_fZoomTop*h < fOffsetY + fScreenHeight) + m_fZoomTop = (fScreenHeight + fOffsetY - maxy) / h; + for (float& i : y) + i += m_fZoomTop * h; + } + } + // add offset from display effects + for (int i = 0; i < 4; i++) + { + x[i] += m_fPosX * m_fWidth * fScale; + y[i] += m_fPosY * m_fHeight * fScale; + } + + UpdateVertices(m_ax, m_ay, x, y, dirtyregions); + + // now render the image in the top right corner if we're zooming + if (m_fZoomAmount == 1 || m_bIsComic) + { + const float empty[4] = {}; + UpdateVertices(m_bx, m_by, empty, empty, dirtyregions); + UpdateVertices(m_sx, m_sy, empty, empty, dirtyregions); + UpdateVertices(m_ox, m_oy, empty, empty, dirtyregions); + m_bIsDirty = false; + return; + } + + float sx[4], sy[4]; + sx[0] = -m_fWidth * co + m_fHeight * si; + sy[0] = -m_fWidth * si - m_fHeight * co; + sx[1] = m_fWidth * co + m_fHeight * si; + sy[1] = m_fWidth * si - m_fHeight * co; + sx[2] = m_fWidth * co - m_fHeight * si; + sy[2] = m_fWidth * si + m_fHeight * co; + sx[3] = -m_fWidth * co - m_fHeight * si; + sy[3] = -m_fWidth * si + m_fHeight * co; + // convert to the appropriate scale + float fSmallArea = fScreenWidth * fScreenHeight / 50; + float fSmallWidth = sqrt(fSmallArea * fAR / fPixelRatio); // fAR*height = width, so total area*far = width*width + float fSmallHeight = fSmallArea / fSmallWidth; + float fSmallX = fOffsetX + fScreenWidth * 0.95f - fSmallWidth * 0.5f; + float fSmallY = fOffsetY + fScreenHeight * 0.05f + fSmallHeight * 0.5f; + fScaleNorm = fSmallWidth / m_fWidth; + fScaleInv = fSmallWidth / m_fHeight; + fScale = si * si * (fScaleInv - fScaleNorm) + fScaleNorm; + for (int i = 0; i < 4; i++) + { + sx[i] *= fScale * 0.5f; + sy[i] *= fScale * fPixelRatio * 0.5f; + } + // calculate a black border + float bx[4]; + float by[4]; + for (int i = 0; i < 4; i++) + { + if (sx[i] > 0) + bx[i] = sx[i] + 1; + else + bx[i] = sx[i] - 1; + if (sy[i] > 0) + by[i] = sy[i] + 1; + else + by[i] = sy[i] - 1; + sx[i] += fSmallX; + sy[i] += fSmallY; + bx[i] += fSmallX; + by[i] += fSmallY; + } + + fSmallX -= fSmallWidth * 0.5f; + fSmallY -= fSmallHeight * 0.5f; + + UpdateVertices(m_bx, m_by, bx, by, dirtyregions); + UpdateVertices(m_sx, m_sy, sx, sy, dirtyregions); + + // now we must render the wireframe image of the view window + // work out the direction of the top of pic vector + float scale; + if (fabs(x[1] - x[0]) > fabs(x[3] - x[0])) + scale = (sx[1] - sx[0]) / (x[1] - x[0]); + else + scale = (sx[3] - sx[0]) / (x[3] - x[0]); + float ox[4]; + float oy[4]; + ox[0] = (fOffsetX - x[0]) * scale + sx[0]; + oy[0] = (fOffsetY - y[0]) * scale + sy[0]; + ox[1] = (fOffsetX + fScreenWidth - x[0]) * scale + sx[0]; + oy[1] = (fOffsetY - y[0]) * scale + sy[0]; + ox[2] = (fOffsetX + fScreenWidth - x[0]) * scale + sx[0]; + oy[2] = (fOffsetY + fScreenHeight - y[0]) * scale + sy[0]; + ox[3] = (fOffsetX - x[0]) * scale + sx[0]; + oy[3] = (fOffsetY + fScreenHeight - y[0]) * scale + sy[0]; + // crop to within the range of our piccy + for (int i = 0; i < 4; i++) + { + if (ox[i] < fSmallX) ox[i] = fSmallX; + if (ox[i] > fSmallX + fSmallWidth) ox[i] = fSmallX + fSmallWidth; + if (oy[i] < fSmallY) oy[i] = fSmallY; + if (oy[i] > fSmallY + fSmallHeight) oy[i] = fSmallY + fSmallHeight; + } + + UpdateVertices(m_ox, m_oy, ox, oy, dirtyregions); + m_bIsDirty = false; +} + +void CSlideShowPic::Keep() +{ + // this is called if we need to keep the current pic on screen + // to wait for the next pic to load + if (!m_bDrawNextImage) return ; // don't need to keep pic + // hold off the start of the next frame + m_transitionEnd.start = m_iCounter; +} + +bool CSlideShowPic::StartTransition() +{ + // this is called if we need to start transitioning immediately to the new picture + if (m_bDrawNextImage) return false; // don't need to do anything as we are already transitioning + // decrease the number of display frame + m_transitionEnd.start = m_iCounter; + m_bTransitionImmediately = true; + return true; +} + +void CSlideShowPic::Pause(bool bPause) +{ + if (!m_bDrawNextImage) + m_bPause = bPause; +} + +void CSlideShowPic::SetInSlideshow(bool slideshow) +{ + if (slideshow && m_displayEffect == EFFECT_NO_TIMEOUT) + m_displayEffect = EFFECT_NONE; +} + +int CSlideShowPic::GetTransitionTime(int iType) const +{ + if (iType == 0) // start transition + return m_transitionStart.length; + else // iType == 1 // end transition + return m_transitionEnd.length; +} + +void CSlideShowPic::SetTransitionTime(int iType, int iTime) +{ + if (iType == 0) // start transition + m_transitionStart.length = iTime; + else // iType == 1 // end transition + m_transitionEnd.length = iTime; +} + +void CSlideShowPic::Rotate(float fRotateAngle, bool immediate /* = false */) +{ + if (m_bDrawNextImage) return; + if (m_transitionTemp.type == TRANSITION_ZOOM) return; + if (immediate) + { + m_fAngle += fRotateAngle; + return; + } + + // if there is a rotation ongoing already + // add the new angle to the old destination angle + if (m_transitionTemp.type == TRANSITION_ROTATE && + m_transitionTemp.start + m_transitionTemp.length > m_iCounter) + { + int remainder = m_transitionTemp.start + m_transitionTemp.length - m_iCounter; + fRotateAngle += m_fTransitionAngle * remainder; + } + + m_transitionTemp.type = TRANSITION_ROTATE; + m_transitionTemp.start = m_iCounter; + m_transitionTemp.length = IMMEDIATE_TRANSITION_TIME; + m_fTransitionAngle = fRotateAngle / (float)m_transitionTemp.length; + // reset the timer + m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME)); +} + +void CSlideShowPic::Zoom(float fZoom, bool immediate /* = false */) +{ + if (m_bDrawNextImage) return; + if (m_transitionTemp.type == TRANSITION_ROTATE) return; + if (immediate) + { + m_fZoomAmount = fZoom; + return; + } + m_transitionTemp.type = TRANSITION_ZOOM; + m_transitionTemp.start = m_iCounter; + m_transitionTemp.length = IMMEDIATE_TRANSITION_TIME; + m_fTransitionZoom = (fZoom - m_fZoomAmount) / (float)m_transitionTemp.length; + // reset the timer + m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME)); + // turn off the render effects until we're back down to normal zoom + m_bNoEffect = true; +} + +void CSlideShowPic::Move(float fDeltaX, float fDeltaY) +{ + m_fZoomLeft += fDeltaX; + m_fZoomTop += fDeltaY; + // reset the timer + // m_transitionEnd.start = m_iCounter + m_transitionStart.length + (int)(CServiceBroker::GetWinSystem()->GetGfxContext().GetFPS() * CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_SLIDESHOW_STAYTIME)); +} + +void CSlideShowPic::Render() +{ + std::unique_lock<CCriticalSection> lock(m_textureAccess); + + Render(m_ax, m_ay, m_pImage.get(), (m_alpha << 24) | 0xFFFFFF); + + // now render the image in the top right corner if we're zooming + if (m_fZoomAmount == 1.0f || m_bIsComic) return ; + + Render(m_bx, m_by, NULL, PICTURE_VIEW_BOX_BACKGROUND); + Render(m_sx, m_sy, m_pImage.get(), 0xFFFFFFFF); + Render(m_ox, m_oy, NULL, PICTURE_VIEW_BOX_COLOR); +} + +#ifdef HAS_DX +bool CSlideShowPic::UpdateVertexBuffer(Vertex* vertices) +{ + if (!m_vb) // create new + { + CD3D11_BUFFER_DESC desc(sizeof(Vertex) * 5, D3D11_BIND_VERTEX_BUFFER, D3D11_USAGE_DYNAMIC, D3D11_CPU_ACCESS_WRITE); + D3D11_SUBRESOURCE_DATA initData = {}; + initData.pSysMem = vertices; + initData.SysMemPitch = sizeof(Vertex) * 5; + if (SUCCEEDED(DX::DeviceResources::Get()->GetD3DDevice()->CreateBuffer(&desc, &initData, m_vb.ReleaseAndGetAddressOf()))) + return true; + } + else // update + { + ID3D11DeviceContext* pContext = DX::DeviceResources::Get()->GetD3DContext(); + D3D11_MAPPED_SUBRESOURCE res; + if (SUCCEEDED(pContext->Map(m_vb.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &res))) + { + memcpy(res.pData, vertices, sizeof(Vertex) * 5); + pContext->Unmap(m_vb.Get(), 0); + return true; + } + } + + return false; +} +#endif + +void CSlideShowPic::Render(float* x, float* y, CTexture* pTexture, UTILS::COLOR::Color color) +{ +#ifdef HAS_DX + Vertex vertex[5]; + for (int i = 0; i < 4; i++) + { + vertex[i].pos = XMFLOAT3( x[i], y[i], 0); + CD3DHelper::XMStoreColor(&vertex[i].color, color); + vertex[i].texCoord = XMFLOAT2(0.0f, 0.0f); + vertex[i].texCoord2 = XMFLOAT2(0.0f, 0.0f); + } + + if (pTexture) + { + vertex[1].texCoord.x = vertex[2].texCoord.x = (float) pTexture->GetWidth() / pTexture->GetTextureWidth(); + vertex[2].texCoord.y = vertex[3].texCoord.y = (float) pTexture->GetHeight() / pTexture->GetTextureHeight(); + } + else + { + vertex[1].texCoord.x = vertex[2].texCoord.x = 1.0f; + vertex[2].texCoord.y = vertex[3].texCoord.y = 1.0f; + } + vertex[4] = vertex[0]; // Not used when pTexture != NULL + + CGUIShaderDX* pGUIShader = DX::Windowing()->GetGUIShader(); + pGUIShader->Begin(SHADER_METHOD_RENDER_TEXTURE_BLEND); + + // Set state to render the image + if (pTexture) + { + pTexture->LoadToGPU(); + CDXTexture* dxTexture = reinterpret_cast<CDXTexture*>(pTexture); + ID3D11ShaderResourceView* shaderRes = dxTexture->GetShaderResource(); + pGUIShader->SetShaderViews(1, &shaderRes); + pGUIShader->DrawQuad(vertex[0], vertex[1], vertex[2], vertex[3]); + } + else + { + if (!UpdateVertexBuffer(vertex)) + return; + + ComPtr<ID3D11DeviceContext> pContext = DX::DeviceResources::Get()->GetD3DContext(); + + unsigned stride = sizeof(Vertex); + unsigned offset = 0; + pContext->IASetVertexBuffers(0, 1, m_vb.GetAddressOf(), &stride, &offset); + pContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP); + + pGUIShader->Draw(5, 0); + pGUIShader->RestoreBuffers(); + } + +#elif defined(HAS_GL) + CRenderSystemGL *renderSystem = dynamic_cast<CRenderSystemGL*>(CServiceBroker::GetRenderSystem()); + if (pTexture) + { + pTexture->LoadToGPU(); + pTexture->BindToUnit(0); + + glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + renderSystem->EnableShader(ShaderMethodGL::SM_TEXTURE); + } + else + { + renderSystem->EnableShader(ShaderMethodGL::SM_DEFAULT); + } + + float u1 = 0, u2 = 1, v1 = 0, v2 = 1; + if (pTexture) + { + u2 = (float)pTexture->GetWidth() / pTexture->GetTextureWidth(); + v2 = (float)pTexture->GetHeight() / pTexture->GetTextureHeight(); + } + + GLubyte colour[4]; + GLubyte idx[4] = {0, 1, 3, 2}; //determines order of the vertices + GLuint vertexVBO; + GLuint indexVBO; + struct PackedVertex + { + float x, y, z; + float u1, v1; + } vertex[4]; + + // Setup vertex position values + vertex[0].x = x[0]; + vertex[0].y = y[0]; + vertex[0].z = 0; + vertex[0].u1 = u1; + vertex[0].v1 = v1; + + vertex[1].x = x[1]; + vertex[1].y = y[1]; + vertex[1].z = 0; + vertex[1].u1 = u2; + vertex[1].v1 = v1; + + vertex[2].x = x[2]; + vertex[2].y = y[2]; + vertex[2].z = 0; + vertex[2].u1 = u2; + vertex[2].v1 = v2; + + vertex[3].x = x[3]; + vertex[3].y = y[3]; + vertex[3].z = 0; + vertex[3].u1 = u1; + vertex[3].v1 = v2; + + GLint posLoc = renderSystem->ShaderGetPos(); + GLint tex0Loc = renderSystem->ShaderGetCoord0(); + GLint uniColLoc= renderSystem->ShaderGetUniCol(); + + glGenBuffers(1, &vertexVBO); + glBindBuffer(GL_ARRAY_BUFFER, vertexVBO); + glBufferData(GL_ARRAY_BUFFER, sizeof(PackedVertex)*4, &vertex[0], GL_STATIC_DRAW); + + glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, sizeof(PackedVertex), + reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, x))); + glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, sizeof(PackedVertex), + reinterpret_cast<const GLvoid*>(offsetof(PackedVertex, u1))); + + glEnableVertexAttribArray(posLoc); + glEnableVertexAttribArray(tex0Loc); + + // Setup Colour values + colour[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color); + colour[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color); + colour[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color); + colour[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color); + + glUniform4f(uniColLoc,(colour[0] / 255.0f), (colour[1] / 255.0f), + (colour[2] / 255.0f), (colour[3] / 255.0f)); + + glGenBuffers(1, &indexVBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVBO); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLubyte)*4, idx, GL_STATIC_DRAW); + + glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, 0); + + glDisableVertexAttribArray(posLoc); + glDisableVertexAttribArray(tex0Loc); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &vertexVBO); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glDeleteBuffers(1, &indexVBO); + + renderSystem->DisableShader(); + +#elif defined(HAS_GLES) + CRenderSystemGLES *renderSystem = dynamic_cast<CRenderSystemGLES*>(CServiceBroker::GetRenderSystem()); + if (pTexture) + { + pTexture->LoadToGPU(); + pTexture->BindToUnit(0); + + glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); // Turn Blending On + + renderSystem->EnableGUIShader(ShaderMethodGLES::SM_TEXTURE); + } + else + { + renderSystem->EnableGUIShader(ShaderMethodGLES::SM_DEFAULT); + } + + float u1 = 0, u2 = 1, v1 = 0, v2 = 1; + if (pTexture) + { + u2 = (float)pTexture->GetWidth() / pTexture->GetTextureWidth(); + v2 = (float)pTexture->GetHeight() / pTexture->GetTextureHeight(); + } + + GLubyte col[4]; + GLfloat ver[4][3]; + GLfloat tex[4][2]; + GLubyte idx[4] = {0, 1, 3, 2}; //determines order of triangle strip + + GLint posLoc = renderSystem->GUIShaderGetPos(); + GLint tex0Loc = renderSystem->GUIShaderGetCoord0(); + GLint uniColLoc= renderSystem->GUIShaderGetUniCol(); + + glVertexAttribPointer(posLoc, 3, GL_FLOAT, 0, 0, ver); + glVertexAttribPointer(tex0Loc, 2, GL_FLOAT, 0, 0, tex); + + glEnableVertexAttribArray(posLoc); + glEnableVertexAttribArray(tex0Loc); + + // Setup Colour values + col[0] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::R, color); + col[1] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::G, color); + col[2] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::B, color); + col[3] = KODI::UTILS::GL::GetChannelFromARGB(KODI::UTILS::GL::ColorChannel::A, color); + + if (CServiceBroker::GetWinSystem()->UseLimitedColor()) + { + col[0] = (235 - 16) * col[0] / 255 + 16; + col[1] = (235 - 16) * col[1] / 255 + 16; + col[2] = (235 - 16) * col[2] / 255 + 16; + } + + for (int i=0; i<4; i++) + { + // Setup vertex position values + ver[i][0] = x[i]; + ver[i][1] = y[i]; + ver[i][2] = 0.0f; + } + // Setup texture coordinates + tex[0][0] = tex[3][0] = u1; + tex[0][1] = tex[1][1] = v1; + tex[1][0] = tex[2][0] = u2; + tex[2][1] = tex[3][1] = v2; + + glUniform4f(uniColLoc,(col[0] / 255.0f), (col[1] / 255.0f), (col[2] / 255.0f), (col[3] / 255.0f)); + glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_BYTE, idx); + + glDisableVertexAttribArray(posLoc); + glDisableVertexAttribArray(tex0Loc); + + renderSystem->DisableGUIShader(); + +#endif +} diff --git a/xbmc/pictures/SlideShowPicture.h b/xbmc/pictures/SlideShowPicture.h new file mode 100644 index 0000000..b557cc5 --- /dev/null +++ b/xbmc/pictures/SlideShowPicture.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/DirtyRegion.h" +#include "threads/CriticalSection.h" +#include "utils/ColorUtils.h" + +#include <string> +#ifdef HAS_DX +#include "guilib/GUIShaderDX.h" +#include <wrl/client.h> +#endif + +#include <memory> + +class CTexture; + +class CSlideShowPic +{ +public: + enum DISPLAY_EFFECT { EFFECT_NONE = 0, EFFECT_FLOAT, EFFECT_ZOOM, EFFECT_RANDOM, EFFECT_PANORAMA, EFFECT_NO_TIMEOUT }; + enum TRANSITION_EFFECT { TRANSITION_NONE = 0, FADEIN_FADEOUT, CROSSFADE, TRANSITION_ZOOM, TRANSITION_ROTATE }; + + struct TRANSITION + { + TRANSITION_EFFECT type = TRANSITION_NONE; + int start = 0; + int length = 0; + }; + + CSlideShowPic(); + ~CSlideShowPic(); + + void SetTexture(int iSlideNumber, + std::unique_ptr<CTexture> pTexture, + DISPLAY_EFFECT dispEffect = EFFECT_RANDOM, + TRANSITION_EFFECT transEffect = FADEIN_FADEOUT); + void UpdateTexture(std::unique_ptr<CTexture> pTexture); + + bool IsLoaded() const { return m_bIsLoaded; } + void UnLoad() { m_bIsLoaded = false; } + void Process(unsigned int currentTime, CDirtyRegionList &dirtyregions); + void Render(); + void Close(); + void Reset(DISPLAY_EFFECT dispEffect = EFFECT_RANDOM, TRANSITION_EFFECT transEffect = FADEIN_FADEOUT); + DISPLAY_EFFECT DisplayEffect() const { return m_displayEffect; } + bool DisplayEffectNeedChange(DISPLAY_EFFECT newDispEffect) const; + bool IsStarted() const { return m_iCounter > 0; } + bool IsFinished() const { return m_bIsFinished; } + bool DrawNextImage() const { return m_bDrawNextImage; } + + int GetWidth() const { return (int)m_fWidth; } + int GetHeight() const { return (int)m_fHeight; } + + void Keep(); + bool StartTransition(); + int GetTransitionTime(int iType) const; + void SetTransitionTime(int iType, int iTime); + + int SlideNumber() const { return m_iSlideNumber; } + + void Zoom(float fZoomAmount, bool immediate = false); + void Rotate(float fRotateAngle, bool immediate = false); + void Pause(bool bPause); + void SetInSlideshow(bool slideshow); + void SetOriginalSize(int iOriginalWidth, int iOriginalHeight, bool bFullSize); + bool FullSize() const { return m_bFullSize; } + int GetOriginalWidth(); + int GetOriginalHeight(); + + void Move(float dX, float dY); + float GetZoom() const { return m_fZoomAmount; } + + bool m_bIsComic; + bool m_bCanMoveHorizontally; + bool m_bCanMoveVertically; +private: + void SetTexture_Internal(int iSlideNumber, + std::unique_ptr<CTexture> pTexture, + DISPLAY_EFFECT dispEffect = EFFECT_RANDOM, + TRANSITION_EFFECT transEffect = FADEIN_FADEOUT); + void UpdateVertices(float cur_x[4], float cur_y[4], const float new_x[4], const float new_y[4], CDirtyRegionList &dirtyregions); + void Render(float* x, float* y, CTexture* pTexture, UTILS::COLOR::Color color); + std::unique_ptr<CTexture> m_pImage; + + int m_iOriginalWidth; + int m_iOriginalHeight; + int m_iSlideNumber; + bool m_bIsLoaded; + bool m_bIsFinished; + bool m_bDrawNextImage; + bool m_bIsDirty; + std::string m_strFileName; + float m_fWidth; + float m_fHeight; + UTILS::COLOR::Color m_alpha = 0; + // stuff relative to middle position + float m_fPosX; + float m_fPosY; + float m_fPosZ; + float m_fVelocityX; + float m_fVelocityY; + float m_fVelocityZ; + float m_fZoomAmount; + float m_fZoomLeft; + float m_fZoomTop; + float m_ax[4]{}, m_ay[4]{}; + float m_sx[4]{}, m_sy[4]{}; + float m_bx[4]{}, m_by[4]{}; + float m_ox[4]{}, m_oy[4]{}; + + // transition and display effects + DISPLAY_EFFECT m_displayEffect = EFFECT_NONE; + TRANSITION m_transitionStart; + TRANSITION m_transitionEnd; + TRANSITION m_transitionTemp; // used for rotations + zooms + float m_fAngle; // angle (between 0 and 2pi to display the image) + float m_fTransitionAngle; + float m_fTransitionZoom; + int m_iCounter = 0; + int m_iTotalFrames; + bool m_bPause; + bool m_bNoEffect; + bool m_bFullSize; + bool m_bTransitionImmediately; + + CCriticalSection m_textureAccess; +#ifdef HAS_DX + Microsoft::WRL::ComPtr<ID3D11Buffer> m_vb; + bool UpdateVertexBuffer(Vertex *vertices); +#endif +}; diff --git a/xbmc/pictures/libexif.cpp b/xbmc/pictures/libexif.cpp new file mode 100644 index 0000000..3ee75b9 --- /dev/null +++ b/xbmc/pictures/libexif.cpp @@ -0,0 +1,35 @@ +// libexif.cpp : Defines the entry point for the console application. +// + +#include "libexif.h" + +#include "JpegParse.h" + +#ifdef TARGET_WINDOWS +#include <windows.h> +#else +#include <memory.h> +#include <cstring> +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +bool process_jpeg(const char *filename, ExifInfo_t *exifInfo, IPTCInfo_t *iptcInfo) +{ + if (!exifInfo || !iptcInfo) return false; + CJpegParse jpeg; + memset(exifInfo, 0, sizeof(ExifInfo_t)); + memset(iptcInfo, 0, sizeof(IPTCInfo_t)); + if (jpeg.Process(filename)) + { + memcpy(exifInfo, jpeg.GetExifInfo(), sizeof(ExifInfo_t)); + memcpy(iptcInfo, jpeg.GetIptcInfo(), sizeof(IPTCInfo_t)); + return true; + } + return false; +} +#ifdef __cplusplus +} +#endif diff --git a/xbmc/pictures/libexif.h b/xbmc/pictures/libexif.h new file mode 100644 index 0000000..567255e --- /dev/null +++ b/xbmc/pictures/libexif.h @@ -0,0 +1,138 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef _DLL +#ifdef WIN32 +#define EXIF_EXPORT __declspec(dllexport) +#else +#define EXIF_EXPORT +#endif +#else +#define EXIF_EXPORT +#endif + +//-------------------------------------------------------------------------- +// JPEG markers consist of one or more 0xFF bytes, followed by a marker +// code byte (which is not an FF). Here are the marker codes of interest +// in this application. +//-------------------------------------------------------------------------- + +#define M_SOF0 0xC0 // Start Of Frame N +#define M_SOF1 0xC1 // N indicates which compression process +#define M_SOF2 0xC2 // Only SOF0-SOF2 are now in common use +#define M_SOF3 0xC3 +#define M_SOF5 0xC5 // NB: codes C4 and CC are NOT SOF markers +#define M_SOF6 0xC6 +#define M_SOF7 0xC7 +#define M_SOF9 0xC9 +#define M_SOF10 0xCA +#define M_SOF11 0xCB +#define M_SOF13 0xCD +#define M_SOF14 0xCE +#define M_SOF15 0xCF +#define M_SOI 0xD8 // Start Of Image (beginning of datastream) +#define M_EOI 0xD9 // End Of Image (end of datastream) +#define M_SOS 0xDA // Start Of Scan (begins compressed data) +#define M_JFIF 0xE0 // Jfif marker +#define M_EXIF 0xE1 // Exif marker +#define M_COM 0xFE // COMment +#define M_DQT 0xDB +#define M_DHT 0xC4 +#define M_DRI 0xDD +#define M_IPTC 0xED // IPTC marker + +#define MAX_IPTC_STRING 256 + +typedef struct { + char RecordVersion[MAX_IPTC_STRING]; + char SupplementalCategories[MAX_IPTC_STRING]; + char Keywords[MAX_IPTC_STRING]; + char Caption[MAX_IPTC_STRING]; + char Author[MAX_IPTC_STRING]; + char Headline[MAX_IPTC_STRING]; + char SpecialInstructions[MAX_IPTC_STRING]; + char Category[MAX_IPTC_STRING]; + char Byline[MAX_IPTC_STRING]; + char BylineTitle[MAX_IPTC_STRING]; + char Credit[MAX_IPTC_STRING]; + char Source[MAX_IPTC_STRING]; + char CopyrightNotice[MAX_IPTC_STRING]; + char ObjectName[MAX_IPTC_STRING]; + char City[MAX_IPTC_STRING]; + char State[MAX_IPTC_STRING]; + char Country[MAX_IPTC_STRING]; + char TransmissionReference[MAX_IPTC_STRING]; + char Date[MAX_IPTC_STRING]; + char Urgency[MAX_IPTC_STRING]; + char ReferenceService[MAX_IPTC_STRING]; + char CountryCode[MAX_IPTC_STRING]; + char TimeCreated[MAX_IPTC_STRING]; + char SubLocation[MAX_IPTC_STRING]; + char ImageType[MAX_IPTC_STRING]; +} IPTCInfo_t; + +#define EXIF_COMMENT_CHARSET_CONVERTED -1 // Comments contains converted data +#define EXIF_COMMENT_CHARSET_UNKNOWN 0 // Exif: Unknown +#define EXIF_COMMENT_CHARSET_ASCII 2 // Exif: Ascii +#define EXIF_COMMENT_CHARSET_UNICODE 3 // Exif: Unicode (UTF-16) +#define EXIF_COMMENT_CHARSET_JIS 4 // Exif: JIS X208-1990 + +#define MAX_COMMENT 2000 +#define MAX_DATE_COPIES 10 + +typedef struct { + char CameraMake [33]; + char CameraModel [41]; + char DateTime [21]; + int Height, Width; + int Orientation; + int IsColor; + int Process; + int FlashUsed; + float FocalLength; + float ExposureTime; + float ApertureFNumber; + float Distance; + float CCDWidth; + float ExposureBias; + float DigitalZoomRatio; + int FocalLength35mmEquiv; // Exif 2.2 tag - usually not present. + int Whitebalance; + int MeteringMode; + int ExposureProgram; + int ExposureMode; + int ISOequivalent; + int LightSource; + int CommentsCharset; // EXIF_COMMENT_CHARSET_* + int XPCommentsCharset; + char Comments[MAX_COMMENT + 1]; // +1 for null termination + char FileComment[MAX_COMMENT + 1]; + char XPComment[MAX_COMMENT + 1]; + char Description[MAX_COMMENT + 1]; + + unsigned ThumbnailOffset; // Exif offset to thumbnail + unsigned ThumbnailSize; // Size of thumbnail. + unsigned LargestExifOffset; // Last exif data referenced (to check if thumbnail is at end) + + char ThumbnailAtEnd; // Exif header ends with the thumbnail + // (we can only modify the thumbnail if its at the end) + int ThumbnailSizeOffset; + + int DateTimeOffsets[MAX_DATE_COPIES]; + int numDateTimeTags; + + int GpsInfoPresent; + char GpsLat[31]; + char GpsLong[31]; + char GpsAlt[20]; +} ExifInfo_t; + +EXIF_EXPORT bool process_jpeg(const char *filename, ExifInfo_t *exifInfo, IPTCInfo_t *iptcInfo); + +#ifdef __cplusplus +} +#endif + |