1 /*
2 * Copyright (C) 2012-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
7 */
8
9 #include "TextureCacheJob.h"
10 #include "ServiceBroker.h"
11 #include "TextureCache.h"
12 #include "guilib/Texture.h"
13 #include "settings/AdvancedSettings.h"
14 #include "settings/SettingsComponent.h"
15 #include "utils/log.h"
16 #include "filesystem/File.h"
17 #include "pictures/Picture.h"
18 #include "utils/URIUtils.h"
19 #include "utils/StringUtils.h"
20 #include "video/VideoThumbLoader.h"
21 #include "URL.h"
22 #include "FileItem.h"
23 #include "music/MusicThumbLoader.h"
24 #include "music/tags/MusicInfoTag.h"
25
26 #include <inttypes.h>
27
CTextureCacheJob(const std::string & url,const std::string & oldHash)28 CTextureCacheJob::CTextureCacheJob(const std::string &url, const std::string &oldHash):
29 m_url(url),
30 m_oldHash(oldHash),
31 m_cachePath(CTextureCache::GetCacheFile(m_url))
32 {
33 }
34
35 CTextureCacheJob::~CTextureCacheJob() = default;
36
operator ==(const CJob * job) const37 bool CTextureCacheJob::operator==(const CJob* job) const
38 {
39 if (strcmp(job->GetType(),GetType()) == 0)
40 {
41 const CTextureCacheJob* cacheJob = dynamic_cast<const CTextureCacheJob*>(job);
42 if (cacheJob && cacheJob->m_cachePath == m_cachePath)
43 return true;
44 }
45 return false;
46 }
47
DoWork()48 bool CTextureCacheJob::DoWork()
49 {
50 if (ShouldCancel(0, 0))
51 return false;
52 if (ShouldCancel(1, 0)) // HACK: second check is because we cancel the job in the first callback, but we don't detect it
53 return false; // until the second
54
55 // check whether we need cache the job anyway
56 bool needsRecaching = false;
57 std::string path(CTextureCache::GetInstance().CheckCachedImage(m_url, needsRecaching));
58 if (!path.empty() && !needsRecaching)
59 return false;
60 return CacheTexture();
61 }
62
CacheTexture(CTexture ** out_texture)63 bool CTextureCacheJob::CacheTexture(CTexture** out_texture)
64 {
65 // unwrap the URL as required
66 std::string additional_info;
67 unsigned int width, height;
68 CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
69 std::string image = DecodeImageURL(m_url, width, height, scalingAlgorithm, additional_info);
70
71 m_details.updateable = additional_info != "music" && UpdateableURL(image);
72
73 // generate the hash
74 m_details.hash = GetImageHash(image);
75 if (m_details.hash.empty())
76 return false;
77 else if (m_details.hash == m_oldHash)
78 return true;
79
80 CTexture* texture = LoadImage(image, width, height, additional_info, true);
81 if (texture)
82 {
83 if (texture->HasAlpha())
84 m_details.file = m_cachePath + ".png";
85 else
86 m_details.file = m_cachePath + ".jpg";
87
88 CLog::Log(LOGDEBUG, "%s image '%s' to '%s':", m_oldHash.empty() ? "Caching" : "Recaching", CURL::GetRedacted(image).c_str(), m_details.file.c_str());
89
90 if (CPicture::CacheTexture(texture, width, height, CTextureCache::GetCachedPath(m_details.file), scalingAlgorithm))
91 {
92 m_details.width = width;
93 m_details.height = height;
94 if (out_texture) // caller wants the texture
95 *out_texture = texture;
96 else
97 delete texture;
98 return true;
99 }
100 }
101 delete texture;
102 return false;
103 }
104
ResizeTexture(const std::string & url,uint8_t * & result,size_t & result_size)105 bool CTextureCacheJob::ResizeTexture(const std::string &url, uint8_t* &result, size_t &result_size)
106 {
107 result = NULL;
108 result_size = 0;
109
110 if (url.empty())
111 return false;
112
113 // unwrap the URL as required
114 std::string additional_info;
115 unsigned int width, height;
116 CPictureScalingAlgorithm::Algorithm scalingAlgorithm;
117 std::string image = DecodeImageURL(url, width, height, scalingAlgorithm, additional_info);
118 if (image.empty())
119 return false;
120
121 CTexture* texture = LoadImage(image, width, height, additional_info, true);
122 if (texture == NULL)
123 return false;
124
125 bool success = CPicture::ResizeTexture(image, texture, width, height, result, result_size, scalingAlgorithm);
126 delete texture;
127
128 return success;
129 }
130
DecodeImageURL(const std::string & url,unsigned int & width,unsigned int & height,CPictureScalingAlgorithm::Algorithm & scalingAlgorithm,std::string & additional_info)131 std::string CTextureCacheJob::DecodeImageURL(const std::string &url, unsigned int &width, unsigned int &height, CPictureScalingAlgorithm::Algorithm& scalingAlgorithm, std::string &additional_info)
132 {
133 // unwrap the URL as required
134 std::string image(url);
135 additional_info.clear();
136 width = height = 0;
137 scalingAlgorithm = CPictureScalingAlgorithm::NoAlgorithm;
138 if (StringUtils::StartsWith(url, "image://"))
139 {
140 // format is image://[type@]<url_encoded_path>?options
141 CURL thumbURL(url);
142
143 if (!CTextureCache::CanCacheImageURL(thumbURL))
144 return "";
145 if (thumbURL.GetUserName() == "music")
146 additional_info = "music";
147 if (StringUtils::StartsWith(thumbURL.GetUserName(), "video_"))
148 additional_info = thumbURL.GetUserName();
149
150 image = thumbURL.GetHostName();
151
152 if (thumbURL.HasOption("flipped"))
153 additional_info = "flipped";
154
155 if (thumbURL.GetOption("size") == "thumb")
156 width = height = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_imageRes;
157 else
158 {
159 if (thumbURL.HasOption("width") && StringUtils::IsInteger(thumbURL.GetOption("width")))
160 width = strtol(thumbURL.GetOption("width").c_str(), NULL, 0);
161 if (thumbURL.HasOption("height") && StringUtils::IsInteger(thumbURL.GetOption("height")))
162 height = strtol(thumbURL.GetOption("height").c_str(), NULL, 0);
163 }
164
165 if (thumbURL.HasOption("scaling_algorithm"))
166 scalingAlgorithm = CPictureScalingAlgorithm::FromString(thumbURL.GetOption("scaling_algorithm"));
167 }
168 return image;
169 }
170
LoadImage(const std::string & image,unsigned int width,unsigned int height,const std::string & additional_info,bool requirePixels)171 CTexture* CTextureCacheJob::LoadImage(const std::string& image,
172 unsigned int width,
173 unsigned int height,
174 const std::string& additional_info,
175 bool requirePixels)
176 {
177 if (additional_info == "music")
178 { // special case for embedded music images
179 EmbeddedArt art;
180 if (CMusicThumbLoader::GetEmbeddedThumb(image, art))
181 return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
182 height);
183 }
184
185 if (StringUtils::StartsWith(additional_info, "video_"))
186 {
187 EmbeddedArt art;
188 if (CVideoThumbLoader::GetEmbeddedThumb(image, additional_info.substr(6), art))
189 return CTexture::LoadFromFileInMemory(art.m_data.data(), art.m_size, art.m_mime, width,
190 height);
191 }
192
193 // Validate file URL to see if it is an image
194 CFileItem file(image, false);
195 file.FillInMimeType();
196 if (!(file.IsPicture() && !(file.IsZIP() || file.IsRAR() || file.IsCBR() || file.IsCBZ() ))
197 && !StringUtils::StartsWithNoCase(file.GetMimeType(), "image/") && !StringUtils::EqualsNoCase(file.GetMimeType(), "application/octet-stream")) // ignore non-pictures
198 return NULL;
199
200 CTexture* texture =
201 CTexture::LoadFromFile(image, width, height, requirePixels, file.GetMimeType());
202 if (!texture)
203 return NULL;
204
205 // EXIF bits are interpreted as: <flipXY><flipY*flipX><flipX>
206 // where to undo the operation we apply them in reverse order <flipX>*<flipY*flipX>*<flipXY>
207 // When flipped we have an additional <flipX> on the left, which is equivalent to toggling the last bit
208 if (additional_info == "flipped")
209 texture->SetOrientation(texture->GetOrientation() ^ 1);
210
211 return texture;
212 }
213
UpdateableURL(const std::string & url) const214 bool CTextureCacheJob::UpdateableURL(const std::string &url) const
215 {
216 // we don't constantly check online images
217 return !(StringUtils::StartsWith(url, "http://") || StringUtils::StartsWith(url, "https://"));
218 }
219
GetImageHash(const std::string & url)220 std::string CTextureCacheJob::GetImageHash(const std::string &url)
221 {
222 // silently ignore - we cannot state these
223 if (URIUtils::IsProtocol(url,"addons") || URIUtils::IsProtocol(url,"plugin"))
224 return "";
225
226 struct __stat64 st;
227 if (XFILE::CFile::Stat(url, &st) == 0)
228 {
229 int64_t time = st.st_mtime;
230 if (!time)
231 time = st.st_ctime;
232 if (time || st.st_size)
233 return StringUtils::Format("d%" PRId64"s%" PRId64, time, st.st_size);
234
235 // the image exists but we couldn't determine the mtime/ctime and/or size
236 // so set an obviously bad hash
237 return "BADHASH";
238 }
239 CLog::Log(LOGDEBUG, "%s - unable to stat url %s", __FUNCTION__, CURL::GetRedacted(url).c_str());
240 return "";
241 }
242
CTextureUseCountJob(const std::vector<CTextureDetails> & textures)243 CTextureUseCountJob::CTextureUseCountJob(const std::vector<CTextureDetails> &textures) : m_textures(textures)
244 {
245 }
246
operator ==(const CJob * job) const247 bool CTextureUseCountJob::operator==(const CJob* job) const
248 {
249 if (strcmp(job->GetType(),GetType()) == 0)
250 {
251 const CTextureUseCountJob* useJob = dynamic_cast<const CTextureUseCountJob*>(job);
252 if (useJob && useJob->m_textures == m_textures)
253 return true;
254 }
255 return false;
256 }
257
DoWork()258 bool CTextureUseCountJob::DoWork()
259 {
260 CTextureDatabase db;
261 if (db.Open())
262 {
263 db.BeginTransaction();
264 for (std::vector<CTextureDetails>::const_iterator i = m_textures.begin(); i != m_textures.end(); ++i)
265 db.IncrementUseCount(*i);
266 db.CommitTransaction();
267 }
268 return true;
269 }
270