1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
|
/*
* This file is part of the MediaWiki extension MediaViewer.
*
* MediaViewer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* MediaViewer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MediaViewer. If not, see <http://www.gnu.org/licenses/>.
*/
( function () {
/**
* This provider is similar to mw.mmv.provider.ThumbnailInfo, but instead of making an API call
* to get the thumbnail URL, it tries to guess it. There are two failure modes:
* - known failure: in the given situation it does not seem possible or safe to guess the URL.
* It is up to the caller to obtain it by falling back to the normal provider.
* - unexpected failure: we guess an URL but it does not work. The current implementation is
* conservative so at least on WMF wikis this probably won't happen, but should be reckoned
* with anyway. On other wikis (especially ones which do not generate thumbnails on demand
* via the 404 handler) this could be more frequent. Again, it is the caller's resonsibility
* to handle this by detecting image loading errors and falling back to the normal provider.
*
* @class mw.mmv.provider.GuessedThumbnailInfo
* @constructor
*/
function GuessedThumbnailInfo() {}
/**
* File extensions which are vector types (as opposed to bitmap).
* Thumbnails of vector types can be larger than the original file.
*
* @property {Object.<string, number>}
*/
GuessedThumbnailInfo.prototype.vectorExtensions = {
svg: 1
};
/**
* File extensions which can be displayed in the browser.
* Other file types need to be thumbnailed even if the size of the original file would be right.
*
* @property {Object.<string, number>}
*/
GuessedThumbnailInfo.prototype.displayableExtensions = {
png: 1,
jpg: 1,
jpeg: 1,
gif: 1
};
/**
* Try to guess the thumbnailinfo for a thumbnail without doing an API request.
* An existing thumbnail URL is required.
*
* There is no guarantee this function will be successful - in some cases, it is impossible
* to guess how the URL would look. If that's the case, the promise just rejects.
*
* @param {mw.Title} file
* @param {string} sampleUrl a thumbnail URL for the same file (but with different size).
* @param {number} width thumbnail width in pixels
* @param {number} originalWidth width of original image in pixels
* @param {number} originalHeight height of original image in pixels
* @return {jQuery.Promise.<mw.mmv.model.Thumbnail>}
*/
GuessedThumbnailInfo.prototype.get = function ( file, sampleUrl, width, originalWidth, originalHeight ) {
var url = this.getUrl( file, sampleUrl, width, originalWidth );
if ( url ) {
return $.Deferred().resolve( new mw.mmv.model.Thumbnail(
url,
this.guessWidth( file, width, originalWidth ),
this.guessHeight( file, width, originalWidth, originalHeight )
) );
} else {
return $.Deferred().reject( 'Could not guess thumbnail URL' );
}
};
/**
* Try to guess the URL of a thumbnail without doing an API request.
* See #get().
*
* @param {mw.Title} file
* @param {string} sampleUrl a thumbnail URL for the same file (but with different size)
* @param {number} width thumbnail width in pixels
* @param {number} originalWidth width of original image in pixels
* @return {string|undefined} a thumbnail URL or nothing
*/
GuessedThumbnailInfo.prototype.getUrl = function ( file, sampleUrl, width, originalWidth ) {
var needsFullSize = this.needsOriginal( file, width, originalWidth ),
sampleIsFullSize = this.isFullSizeUrl( sampleUrl, file );
if ( sampleIsFullSize && needsFullSize ) {
// sample thumbnail uses full size, and we need full size as well - the sample URL
// happens to be just the right one for us
return sampleUrl;
} else if ( !sampleIsFullSize && !needsFullSize ) {
// need to convert a scaled thumbnail URL to another scaled thumbnail URL
return this.replaceSize( file, sampleUrl, width );
} else if ( !sampleIsFullSize && needsFullSize ) {
if ( this.canBeDisplayedInBrowser( file ) ) {
// the size requested is larger than the original - we need to return an URL
// to the original file instead
return this.guessFullUrl( file, sampleUrl );
} else {
// the size requested is larger than the original, but this file type cannot
// be displayed by all browsers, so needs to be thumbnailed anyway,
// but the thumbnail still cannot be larger than the original file
return this.replaceSize( file, sampleUrl, originalWidth );
}
} else { // sampleIsFullSize && !needsOriginal
return this.guessThumbUrl( file, sampleUrl, width );
}
};
/**
* True if the original image needs to be used as a thumbnail.
*
* @protected
* @param {mw.Title} file
* @param {number} width thumbnail width in pixels
* @param {number} originalWidth width of original image in pixels
* @return {boolean}
*/
GuessedThumbnailInfo.prototype.needsOriginal = function ( file, width, originalWidth ) {
return width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file );
};
/**
* Checks if a given thumbnail URL is full-size (the original image) or scaled
*
* @protected
* @param {string} url a thumbnail URL
* @param {mw.Title} file
* @return {boolean}
*/
GuessedThumbnailInfo.prototype.isFullSizeUrl = function ( url, file ) {
return !this.obscureFilename( url, file ).match( '/thumb/' );
};
/**
* Removes the filename in a reversible way. This is useful because the filename can be nearly
* anything and could cause false positives when looking for patterns.
*
* @protected
* @param {string} url a thumbnail URL
* @param {mw.Title} file
* @return {string} thumbnnail URL with occurences of the filename replaced by `<filename>`
*/
GuessedThumbnailInfo.prototype.obscureFilename = function ( url, file ) {
// corresponds to File::getUrlRel() which uses rawurlencode()
var filenameInUrl = mw.util.rawurlencode( file.getMain() );
// In the URL to the original file the filename occurs once. In a thumbnail URL it usually
// occurs twice, but can occur once if it is too short. We replace twice, can't hurt.
return url.replace( filenameInUrl, '<filename>' ).replace( filenameInUrl, '<filename>' );
};
/**
* Undoes #obscureFilename().
*
* @protected
* @param {string} url a thumbnail URL (with obscured filename)
* @param {mw.Title} file
* @return {string} original thumbnnail URL
*/
GuessedThumbnailInfo.prototype.restoreFilename = function ( url, file ) {
// corresponds to File::getUrlRel() which uses rawurlencode()
var filenameInUrl = mw.util.rawurlencode( file.getMain() );
// <> cannot be used in titles, so this is safe
return url.replace( '<filename>', filenameInUrl ).replace( '<filename>', filenameInUrl );
};
/**
* True if the file is of a type for which the thumbnail can be scaled beyond the original size.
*
* @protected
* @param {mw.Title} file
* @return {boolean}
*/
GuessedThumbnailInfo.prototype.canHaveLargerThumbnailThanOriginal = function ( file ) {
return ( file.getExtension().toLowerCase() in this.vectorExtensions );
};
/**
* True if the file type can be displayed in most browsers, false if it needs thumbnailing
*
* @protected
* @param {mw.Title} file
* @return {boolean}
*/
GuessedThumbnailInfo.prototype.canBeDisplayedInBrowser = function ( file ) {
return ( file.getExtension().toLowerCase() in this.displayableExtensions );
};
/**
* Guess what will be the width of the thumbnail. (Thumbnails for most file formats cannot be
* larger than the original file so this might be smaller than the requested width.)
*
* @protected
* @param {mw.Title} file
* @param {number} width thumbnail width in pixels
* @param {number} originalWidth width of original image in pixels
* @return {number} guessed width
*/
GuessedThumbnailInfo.prototype.guessWidth = function ( file, width, originalWidth ) {
if ( width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file ) ) {
return originalWidth;
} else {
return width;
}
};
/**
* Guess what will be the height of the thumbnail, given its width.
*
* @protected
* @param {mw.Title} file
* @param {number} width thumbnail width in pixels
* @param {number} originalWidth width of original image in pixels
* @param {number} originalHeight height of original image in pixels
* @return {number} guessed height
*/
GuessedThumbnailInfo.prototype.guessHeight = function ( file, width, originalWidth, originalHeight ) {
if ( width >= originalWidth && !this.canHaveLargerThumbnailThanOriginal( file ) ) {
return originalHeight;
} else {
// might be off 1px due to rounding (we don't know what exact scaling method the
// backend uses) but that should not cause any issues
return Math.round( width * ( originalHeight / originalWidth ) );
}
};
/**
* Given a thumbnail URL with a wrong size, returns one with the right size.
*
* @protected
* @param {mw.Title} file
* @param {string} sampleUrl a thumbnail URL for the same file (but with different size)
* @param {number} width thumbnail width in pixels
* @return {string|undefined} thumbnail URL with the correct size
*/
GuessedThumbnailInfo.prototype.replaceSize = function ( file, sampleUrl, width ) {
var url = this.obscureFilename( sampleUrl, file ),
sizeRegexp = /\b\d{1,5}px\b/;
// this should never happen, but let's play it safe - returning the sample URL and believing
// it is the resized one would be bad. Returning a wrong filename is not catastrophical
// as long as we return a non-working wrong filename, which would not be the case here.
if ( !url.match( sizeRegexp ) ) {
return undefined;
}
// we are assuming here that the other thumbnail parameters do not look like a size
url = url.replace( sizeRegexp, width + 'px' );
return this.restoreFilename( url, file );
};
/**
* Try to guess the original URL to the file, from a thumb URL.
*
* @protected
* @param {mw.Title} file
* @param {string} thumbnailUrl
* @return {string} URL of the original file
*/
GuessedThumbnailInfo.prototype.guessFullUrl = function ( file, thumbnailUrl ) {
var url = this.obscureFilename( thumbnailUrl, file );
if ( url === thumbnailUrl ) {
// Did not find the filename, maybe due to URL encoding issues. Bail out.
return undefined;
}
// this depends on some config settings, but will work with default or WMF settings.
url = url.replace( /<filename>.*/, '<filename>' );
url = url.replace( '/thumb', '' );
return this.restoreFilename( url, file );
};
/**
* Hardest version: try to guess thumbnail URL from original
*
* @protected
* @param {mw.Title} file
* @param {string} originalUrl URL for the original file
* @param {number} width thumbnail width in pixels
* @return {string|undefined} thumbnail URL
*/
GuessedThumbnailInfo.prototype.guessThumbUrl = function () {
// Not implemented. This can be very complicated (the thumbnail might have other
// parameters than the size, which are impossible to guess, might be converted to some
// other format, might have a special shortened format depending on the length of the
// filename) and it is unlikely to be useful - it would be only called when we need
// a thumbnail that is smaller than the sample (the thumbnail which is already on the page).
return undefined;
};
mw.mmv.provider.GuessedThumbnailInfo = GuessedThumbnailInfo;
}() );
|