Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-work image resizing #2101

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 105 additions & 14 deletions src/renderers/wikimedia-mobile.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@

type PipeFunction = (value: DominoElement) => DominoElement | Promise<DominoElement>

const THUMB_WIDTH_REGEX = /\/(\d+)px-[^/]+$/
const THUMB_MAX_DIMENSION = 320

declare interface ImageMetadata {
src: string | null
width: number
height: number
}

// Represent 'https://{wikimedia-wiki}/api/rest_v1/page/mobile-html/'
export class WikimediaMobileRenderer extends MobileRenderer {
constructor() {
Expand Down Expand Up @@ -93,26 +102,108 @@
return doc
}

private calculateImageDimensions(span: DominoElement) {
// These are the attributes that were "prepared" for us by the mobile-html endpoint.
const preparedData = {
src: span.getAttribute('data-src'),
width: parseInt(span.getAttribute('data-width') || '0', 10),
height: parseInt(span.getAttribute('data-height') || '0', 10),
}

// Calculate the ratio so we know if we're scaling down in the width or height dimension.
const widthHeightRatio = preparedData.width / preparedData.height
const scaleUsingHeight = widthHeightRatio > 1.0

// The data-data-file-original-src attribute is the URL of the image that was used in the original article.
// It is preferred over the data-src attribute, which is a "mobile" image that may be scaled up in order to
// be "full width" on mobile devices. However, if the mobile API didn't scale the image up, then the
// data-data-file-original-src attribute will be missing, and we should use the data-src.
// See https://github.com/openzim/mwoffliner/issues/1925.
let originalData: ImageMetadata | undefined
const originalSrc = span.getAttribute('data-data-file-original-src')
if (originalSrc) {
// Try to match against an image URL with a width in it.
const match = THUMB_WIDTH_REGEX.exec(originalSrc)
if (match) {
const originalWidth = parseInt(match[1], 10)
originalData = {
src: originalSrc,
width: originalWidth,
height: Math.round(originalWidth / widthHeightRatio),
}
}
}

let maxData: ImageMetadata | undefined
if (scaleUsingHeight) {
maxData = {
src: null,
width: Math.round(THUMB_MAX_DIMENSION * widthHeightRatio),
height: THUMB_MAX_DIMENSION,
}
} else {
maxData = {
src: null,
width: THUMB_MAX_DIMENSION,
height: Math.round(THUMB_MAX_DIMENSION / widthHeightRatio),
}
}

return {
preparedData,
originalData,
maxData,
}
}

private convertLazyLoadToImagesImpl(doc: DominoElement) {
const protocol = 'https://'
const spans = doc.querySelectorAll('.pcs-lazy-load-placeholder')

spans.forEach((span: DominoElement) => {
// Create a new img element
const img = doc.createElement('img') as DominoElement
const { preparedData, originalData, maxData } = this.calculateImageDimensions(span)

const widthToData = {
[preparedData.width]: preparedData,
[maxData.width]: maxData,
[originalData?.width || 0]: originalData,
}

// Set the attributes for the img element based on the data attributes in the span
const minWidth = originalData ? Math.min(preparedData.width, maxData.width, originalData?.width) : Math.min(preparedData.width, maxData.width)
let selectedData = widthToData[minWidth]

// The data-data-file-original-src attribute is the URL of the image that was used in the original article.
// It is preferred over the data-src attribute, which is a "mobile" image that may be scaled up to 320px
// or 640px in order to be "full width" on mobile devices. However, if the mobile API didn't scale the
// image up, then the data-data-file-original-src attribute will be missing, and we should use the data-src.
// See https://github.com/openzim/mwoffliner/issues/1925.
const imgSrc = span.getAttribute('data-data-file-original-src') || span.getAttribute('data-src')
img.src = urlJoin(protocol, imgSrc)
if (selectedData === maxData) {
// We've decided to scale down the image. Use URL hacking to create an image that scales to the size we want.
if (originalData) {
const match = THUMB_WIDTH_REGEX.exec(originalData.src)
if (match) {
selectedData.src = originalData.src.replace(`${match[1]}px`, `${selectedData.width}px`)
}
} else {
// No original src, or original src cannot be URL hacked.
const match = THUMB_WIDTH_REGEX.exec(preparedData.src)
if (match) {
selectedData.src = preparedData.src.replace(`${match[1]}px`, `${selectedData.width}px`)
}
}
}

if (selectedData.src === null) {
// We couldn't find a URL to hack, so use the smaller of the original or prepared data.
if (!originalData) {
selectedData = preparedData
} else {
const newMinWidth = Math.min(preparedData.width, originalData.width)
selectedData = widthToData[newMinWidth]

Check warning on line 197 in src/renderers/wikimedia-mobile.renderer.ts

View check run for this annotation

Codecov / codecov/patch

src/renderers/wikimedia-mobile.renderer.ts#L195-L197

Added lines #L195 - L197 were not covered by tests
}
}

// Create a new img element
const img = doc.createElement('img') as DominoElement
img.src = urlJoin(protocol, selectedData.src)
img.setAttribute('decoding', 'async')
img.width = span.getAttribute('data-width')
img.height = span.getAttribute('data-height')
img.width = selectedData.width
img.height = selectedData.height
img.className = span.getAttribute('data-class')

// Replace the span with the img element
Expand Down Expand Up @@ -169,7 +260,7 @@
}

public readonly INTERNAL = {
convertLazyLoadToImages: this.convertLazyLoadToImagesImpl,
unhideSections: this.unhideSectionsImpl,
convertLazyLoadToImages: this.convertLazyLoadToImagesImpl.bind(this),
unhideSections: this.unhideSectionsImpl.bind(this),
}
}
Loading
Loading