summaryrefslogtreecommitdiff
path: root/www/wiki/extensions/MultimediaViewer/resources/mmv/provider/mmv.provider.GuessedThumbnailInfo.js
blob: 5ad76dbdc324fbe4b14aa20231fb8faf4d2d3096 (plain)
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
/*
 * 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 ( mw, $ ) {
	/**
	 * 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 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;
}( mediaWiki, jQuery ) );