ThumbnailInfo: support multi lingual SVG

Bug: T208564
Change-Id: Icf082ca0dc94bc4739fd31b795de76f8a0083b70
This commit is contained in:
Simon Legner 2024-05-28 11:07:27 +02:00 committed by Derk-Jan Hartman
parent 4e1c2e841f
commit 4672f2eb57
9 changed files with 98 additions and 59 deletions

View file

@ -2,7 +2,7 @@
"modules": [ "modules": [
{ {
"resourceModule": "mmv", "resourceModule": "mmv",
"maxSize": "26.6 kB" "maxSize": "26.9 kB"
}, },
{ {
"resourceModule": "mmv.ui.restriction", "resourceModule": "mmv.ui.restriction",

View file

@ -206,7 +206,7 @@ class MultimediaViewer {
this.currentIndex = image.index; this.currentIndex = image.index;
this.currentImageFileTitle = image.filePageTitle; this.currentImage = image;
if ( !this.isOpen ) { if ( !this.isOpen ) {
$( document ).trigger( $.Event( 'mmv-setup-overlay' ) ); $( document ).trigger( $.Event( 'mmv-setup-overlay' ) );
@ -632,9 +632,9 @@ class MultimediaViewer {
guessing = true; guessing = true;
thumbnailPromise = this.guessedThumbnailInfoProvider.get( thumbnailPromise = this.guessedThumbnailInfoProvider.get(
fileTitle, sampleUrl, width, originalWidth, originalHeight fileTitle, sampleUrl, width, originalWidth, originalHeight
).then( null, () => this.thumbnailInfoProvider.get( fileTitle, width ) ); ).then( null, () => this.thumbnailInfoProvider.get( fileTitle, sampleUrl, width ) );
} else { } else {
thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, width ); thumbnailPromise = this.thumbnailInfoProvider.get( fileTitle, sampleUrl, width );
} }
imagePromise = thumbnailPromise.then( ( thumbnail ) => this.imageProvider.get( thumbnail.url ) ); imagePromise = thumbnailPromise.then( ( thumbnail ) => this.imageProvider.get( thumbnail.url ) );
@ -644,7 +644,7 @@ class MultimediaViewer {
// As a side effect this introduces an extra (harmless) retry of a failed thumbnailInfoProvider.get call // As a side effect this introduces an extra (harmless) retry of a failed thumbnailInfoProvider.get call
// because thumbnailInfoProvider.get is already called above when guessedThumbnailInfoProvider.get fails. // because thumbnailInfoProvider.get is already called above when guessedThumbnailInfoProvider.get fails.
imagePromise = imagePromise imagePromise = imagePromise
.then( null, () => this.thumbnailInfoProvider.get( fileTitle, width ) .then( null, () => this.thumbnailInfoProvider.get( fileTitle, sampleUrl, width )
.then( ( thumbnail ) => this.imageProvider.get( thumbnail.url ) ) ); .then( ( thumbnail ) => this.imageProvider.get( thumbnail.url ) ) );
} }
@ -748,7 +748,7 @@ class MultimediaViewer {
*/ */
setTitle() { setTitle() {
// update title after route change, see T225387 // update title after route change, see T225387
document.title = this.createDocumentTitle( this.currentImageFileTitle ); document.title = this.createDocumentTitle( this.currentImage && this.currentImage.filePageTitle );
} }
/** /**
@ -813,13 +813,13 @@ class MultimediaViewer {
} ).on( 'mmv-resize-end.mmvp', () => { } ).on( 'mmv-resize-end.mmvp', () => {
this.resize( this.ui ); this.resize( this.ui );
} ).on( 'mmv-request-thumbnail.mmvp', ( e, size ) => { } ).on( 'mmv-request-thumbnail.mmvp', ( e, size ) => {
if ( this.currentImageFileTitle ) { if ( this.currentImage && this.currentImage.filePageTitle ) {
return this.thumbnailInfoProvider.get( this.currentImageFileTitle, size ); return this.thumbnailInfoProvider.get( this.currentImage.filePageTitle, this.currentImage.src, size );
} else { } else {
return $.Deferred().reject(); return $.Deferred().reject();
} }
} ).on( 'mmv-viewfile.mmvp', () => { } ).on( 'mmv-viewfile.mmvp', () => {
this.imageInfoProvider.get( this.currentImageFileTitle ).done( ( imageInfo ) => { this.imageInfoProvider.get( this.currentImage.filePageTitle ).done( ( imageInfo ) => {
document.location = imageInfo.url; document.location = imageInfo.url;
} ); } );
} ); } );

View file

@ -42,12 +42,15 @@ class ThumbnailInfo extends Api {
* is smaller). * is smaller).
* *
* @param {mw.Title} file * @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} width thumbnail width in pixels
* @param {number} height thumbnail height in pixels * @param {number} height thumbnail height in pixels
* @return {jQuery.Promise.<Thumbnail>} * @return {jQuery.Promise.<Thumbnail>}
*/ */
get( file, width, height ) { get( file, sampleUrl, width, height ) {
const cacheKey = `${ file.getPrefixedDb() }|${ width || '' }|${ height || '' }`; const match = sampleUrl.match( /(lang|page)([\d\-a-z]+)-(\d+)px/ ); // multi lingual SVG or PDF page
const iiurlparam = match ? `${ match[ 1 ] }${ match[ 2 ] }-${ width }px` : undefined;
const cacheKey = [ file.getPrefixedDb(), width || '', height || '', iiurlparam || '' ].join();
return this.getCachedPromise( cacheKey, () => this.apiGetWithMaxAge( { return this.getCachedPromise( cacheKey, () => this.apiGetWithMaxAge( {
formatversion: 2, formatversion: 2,
@ -55,6 +58,7 @@ class ThumbnailInfo extends Api {
prop: 'imageinfo', prop: 'imageinfo',
titles: file.getPrefixedDb(), titles: file.getPrefixedDb(),
iiprop: 'url', iiprop: 'url',
iiurlparam,
iiurlwidth: width, iiurlwidth: width,
iiurlheight: height iiurlheight: height
} ).then( ( data ) => this.getQueryPage( data ) ).then( ( page ) => { } ).then( ( data ) => this.getQueryPage( data ) ).then( ( page ) => {

View file

@ -739,7 +739,7 @@ class MetadataPanel extends UiElement {
this.$datetimeUpdatedLi.removeClass( 'empty' ); this.$datetimeUpdatedLi.removeClass( 'empty' );
} }
this.buttons.set( imageData ); this.buttons.set( image, imageData );
this.description.set( imageData.description, image.caption ); this.description.set( imageData.description, image.caption );
this.setLicense( imageData.license, imageData.descriptionUrl ); this.setLicense( imageData.license, imageData.descriptionUrl );

View file

@ -44,16 +44,29 @@ class StripeButtons extends UiElement {
/** /**
* @inheritdoc * @inheritdoc
* @param {LightboxImage} image
* @param {ImageModel} imageInfo * @param {ImageModel} imageInfo
*/ */
set( imageInfo ) { set( image, imageInfo ) {
const match = image && image.src ?
image.src.match( /(lang|page)([\d\-a-z]+)-(\d+)px/ ) : // multi lingual SVG or PDF page
null;
const params = {};
if ( match ) {
params[ match[ 1 ] ] = match[ 2 ];
}
let descriptionUrl = imageInfo.descriptionUrl; let descriptionUrl = imageInfo.descriptionUrl;
let isCommons = String( descriptionUrl ).includes( '//commons.wikimedia.org/' ); let isCommons = String( descriptionUrl ).includes( '//commons.wikimedia.org/' );
if ( imageInfo.pageID ) { if ( imageInfo.pageID ) {
// The file has a local description page, override the description URL // The file has a local description page, override the description URL
descriptionUrl = imageInfo.title.getUrl(); descriptionUrl = imageInfo.title.getUrl( params );
isCommons = false; isCommons = false;
} else {
const parsedUrl = new mw.Uri( descriptionUrl );
parsedUrl.extend( params );
descriptionUrl = parsedUrl.toString();
} }
this.$descriptionPage.empty() this.$descriptionPage.empty()

View file

@ -460,7 +460,7 @@ const { MultimediaViewerBootstrap } = require( 'mmv.bootstrap' );
const originalWidth = 50; const originalWidth = 50;
const viewer = getMultimediaViewer(); const viewer = getMultimediaViewer();
viewer.thumbnailInfoProvider.get = function ( fileTitle, width ) { viewer.thumbnailInfoProvider.get = function ( fileTitle, sampleUrl, width ) {
assert.strictEqual( width, expectedWidth ); assert.strictEqual( width, expectedWidth );
return $.Deferred().reject(); return $.Deferred().reject();
}; };
@ -603,7 +603,7 @@ const { MultimediaViewerBootstrap } = require( 'mmv.bootstrap' );
const oldDocumentTitle = document.title; const oldDocumentTitle = document.title;
this.sandbox.stub( mw.loader, 'using' ).returns( $.Deferred().resolve( viewer ) ); this.sandbox.stub( mw.loader, 'using' ).returns( $.Deferred().resolve( viewer ) );
viewer.currentImageFileTitle = title; viewer.currentImage = { filePageTitle: title };
bootstrap.setupEventHandlers(); bootstrap.setupEventHandlers();
viewer.setTitle(); viewer.setTitle();

View file

@ -84,55 +84,76 @@ const { ThumbnailInfo } = require( 'mmv' );
assert.true( thumbnailInfoProvider instanceof ThumbnailInfo ); assert.true( thumbnailInfoProvider instanceof ThumbnailInfo );
} ); } );
QUnit.test( 'ThumbnailInfo get test', ( assert ) => { QUnit.test( 'ThumbnailInfo get test', function ( assert ) {
let apiCallCount = 0; const api = { get: this.sandbox.stub().returns( $.Deferred().resolve( {
const api = { get: function () { query: {
apiCallCount++; pages: [
return $.Deferred().resolve( { {
query: { ns: 6,
pages: [ title: 'File:Stuff.jpg',
{ missing: true,
ns: 6, imagerepository: 'shared',
title: 'File:Stuff.jpg', imageinfo: [
missing: true, {
imagerepository: 'shared', thumburl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg',
imageinfo: [ thumbwidth: 95,
{ thumbheight: 200,
thumburl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg', url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg',
thumbwidth: 95, descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg'
thumbheight: 200, }
url: 'https://upload.wikimedia.org/wikipedia/commons/1/19/Stuff.jpg', ]
descriptionurl: 'https://commons.wikimedia.org/wiki/File:Stuff.jpg' }
} ]
] }
} } ) ) };
]
}
} );
} };
const file = new mw.Title( 'File:Stuff.jpg' ); const file = new mw.Title( 'File:Stuff.jpg' );
const thumbnailInfoProvider = new ThumbnailInfo( api ); const thumbnailInfoProvider = new ThumbnailInfo( api );
return thumbnailInfoProvider.get( file, 100 ).then( ( thumbnail ) => { return thumbnailInfoProvider.get( file, '', 100 ).then( ( thumbnail ) => {
assert.strictEqual( thumbnail.url, assert.strictEqual( thumbnail.url,
'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg', 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/19/Stuff.jpg/51px-Stuff.jpg',
'URL is set correctly' ); 'URL is set correctly' );
assert.strictEqual( thumbnail.width, 95, 'actual width is set correctly' ); assert.strictEqual( thumbnail.width, 95, 'actual width is set correctly' );
assert.strictEqual( thumbnail.height, 200, 'actual height is set correctly' ); assert.strictEqual( thumbnail.height, 200, 'actual height is set correctly' );
} ).then( () => { } ).then( () => {
assert.strictEqual( apiCallCount, 1 ); assert.strictEqual( api.get.calledOnce, true );
assert.deepEqual( api.get.getCall( 0 ).args[ 0 ], {
formatversion: 2,
action: 'query',
prop: 'imageinfo',
titles: 'File:Stuff.jpg',
iiprop: 'url',
iiurlheight: undefined,
iiurlparam: undefined,
iiurlwidth: 100
} );
// call the data provider a second time to check caching // call the data provider a second time to check caching
return thumbnailInfoProvider.get( file, 100 ); return thumbnailInfoProvider.get( file, '', 100 );
} ).then( () => { } ).then( () => {
assert.strictEqual( apiCallCount, 1 ); assert.strictEqual( api.get.calledOnce, true );
// call a third time with different size to check caching // call a third time with different size to check caching
return thumbnailInfoProvider.get( file, 110 ); return thumbnailInfoProvider.get( file, '', 110 );
} ).then( () => { } ).then( () => {
assert.strictEqual( apiCallCount, 2 ); assert.strictEqual( api.get.calledTwice, true );
// call it again, with a height specified, to check caching // call it again, with a height specified, to check caching
return thumbnailInfoProvider.get( file, 110, 100 ); return thumbnailInfoProvider.get( file, '', 110, 100 );
} ).then( () => { } ).then( () => {
assert.strictEqual( apiCallCount, 3 ); assert.strictEqual( api.get.calledThrice, true );
// call it again, with multi lingual SVG
const file2 = new mw.Title( 'File:Multilingual_SVG_example.svg' );
const sampleUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Multilingual_SVG_example.svg/langit-120px-Multilingual_SVG_example.svg.png';
return thumbnailInfoProvider.get( file2, sampleUrl, 100 );
} ).then( () => {
assert.deepEqual( api.get.getCall( api.get.callCount - 1 ).args[ 0 ], {
action: 'query',
formatversion: 2,
iiprop: 'url',
iiurlheight: undefined,
iiurlparam: 'langit-100px',
iiurlwidth: 100,
prop: 'imageinfo',
titles: 'File:Multilingual_SVG_example.svg'
} );
} ); } );
} ); } );
@ -144,7 +165,7 @@ const { ThumbnailInfo } = require( 'mmv' );
const done = assert.async(); const done = assert.async();
const thumbnailInfoProvider = new ThumbnailInfo( api ); const thumbnailInfoProvider = new ThumbnailInfo( api );
thumbnailInfoProvider.get( file, 100 ).fail( () => { thumbnailInfoProvider.get( file, '', 100 ).fail( () => {
assert.true( true, 'promise rejected when no data is returned' ); assert.true( true, 'promise rejected when no data is returned' );
done(); done();
} ); } );
@ -166,7 +187,7 @@ const { ThumbnailInfo } = require( 'mmv' );
const done = assert.async(); const done = assert.async();
const thumbnailInfoProvider = new ThumbnailInfo( api ); const thumbnailInfoProvider = new ThumbnailInfo( api );
thumbnailInfoProvider.get( file, 100 ).fail( () => { thumbnailInfoProvider.get( file, '', 100 ).fail( () => {
assert.true( true, 'promise rejected when imageinfo is missing' ); assert.true( true, 'promise rejected when imageinfo is missing' );
done(); done();
} ); } );
@ -190,7 +211,7 @@ const { ThumbnailInfo } = require( 'mmv' );
const done = assert.async(); const done = assert.async();
const thumbnailInfoProvider = new ThumbnailInfo( api ); const thumbnailInfoProvider = new ThumbnailInfo( api );
thumbnailInfoProvider.get( file ).fail( ( errorMessage ) => { thumbnailInfoProvider.get( file, '' ).fail( ( errorMessage ) => {
assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg', assert.strictEqual( errorMessage, 'file does not exist: File:Stuff.jpg',
'error message is set correctly for missing file' ); 'error message is set correctly for missing file' );
done(); done();
@ -216,7 +237,7 @@ const { ThumbnailInfo } = require( 'mmv' );
const done = assert.async(); const done = assert.async();
const thumbnailInfoProvider = new ThumbnailInfo( api ); const thumbnailInfoProvider = new ThumbnailInfo( api );
thumbnailInfoProvider.get( file, 100 ).fail( () => { thumbnailInfoProvider.get( file, '', 100 ).fail( () => {
assert.true( true, 'promise rejected when thumbnail info is missing' ); assert.true( true, 'promise rejected when thumbnail info is missing' );
done(); done();
} ); } );

View file

@ -112,7 +112,8 @@ QUnit.test( '.setImageInfo()', function ( assert ) {
); );
const title = 'Foo bar'; const title = 'Foo bar';
const image = { const image = {
filePageTitle: mw.Title.newFromText( 'File:' + title + '.jpg' ) filePageTitle: mw.Title.newFromText( 'File:' + title + '.jpg' ),
src: 'https://upload.wikimedia.org/wikipedia/commons/3/3a/Foobar.jpg'
}; };
const imageData = { const imageData = {
title: image.filePageTitle, title: image.filePageTitle,

View file

@ -48,7 +48,7 @@ const { StripeButtons } = require( 'mmv' );
const buttons = createStripeButtons(); const buttons = createStripeButtons();
const fakeImageInfo = { descriptionUrl: '//commons.wikimedia.org/wiki/File:Foo.jpg' }; const fakeImageInfo = { descriptionUrl: '//commons.wikimedia.org/wiki/File:Foo.jpg' };
buttons.set( fakeImageInfo ); buttons.set( null, fakeImageInfo );
buttons.empty(); buttons.empty();
assert.true( true, 'No error on set()/empty().' ); assert.true( true, 'No error on set()/empty().' );
@ -62,16 +62,16 @@ const { StripeButtons } = require( 'mmv' );
const descriptionUrlCommons = 'https://commons.wikimedia.org/desc'; const descriptionUrlCommons = 'https://commons.wikimedia.org/desc';
const descriptionUrl2 = 'http://example.com/different-desc'; const descriptionUrl2 = 'http://example.com/different-desc';
buttons.set( { descriptionUrl } ); buttons.set( null, { descriptionUrl } );
assert.strictEqual( $button.hasClass( 'mw-mmv-repo-button-commons' ), false, 'Button does not have commons class non-Commons files' ); assert.strictEqual( $button.hasClass( 'mw-mmv-repo-button-commons' ), false, 'Button does not have commons class non-Commons files' );
assert.strictEqual( $button.find( 'a' ).addBack().filter( 'a' ).attr( 'href' ), descriptionUrl, 'Description page link is correct' ); assert.strictEqual( $button.find( 'a' ).addBack().filter( 'a' ).attr( 'href' ), descriptionUrl, 'Description page link is correct' );
buttons.set( { descriptionUrl: descriptionUrlCommons } ); buttons.set( null, { descriptionUrl: descriptionUrlCommons } );
assert.strictEqual( $button.hasClass( 'mw-mmv-repo-button-commons' ), true, 'Button commons class for Commons files' ); assert.strictEqual( $button.hasClass( 'mw-mmv-repo-button-commons' ), true, 'Button commons class for Commons files' );
buttons.set( { buttons.set( null, {
descriptionUrl, descriptionUrl,
pageID: 1, pageID: 1,
title: { getUrl: () => descriptionUrl2 } title: { getUrl: () => descriptionUrl2 }