Skip to content

Commit

Permalink
Introduce helper data structure and function to simplify logic. Scale…
Browse files Browse the repository at this point in the history
… image in dominant dimension (width or height)
  • Loading branch information
audiodude committed Nov 19, 2024
1 parent 10ccaa5 commit 666e60e
Show file tree
Hide file tree
Showing 2 changed files with 303 additions and 133 deletions.
149 changes: 98 additions & 51 deletions src/renderers/wikimedia-mobile.renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ import { RenderOpts, RenderOutput } from './abstract.renderer.js'
type PipeFunction = (value: DominoElement) => DominoElement | Promise<DominoElement>

const THUMB_WIDTH_REGEX = /\/(\d+)px-[^/]+$/
const THUMB_MAX_WIDTH = 320
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 {
Expand Down Expand Up @@ -96,67 +102,108 @@ export class WikimediaMobileRenderer extends MobileRenderer {
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
let scaleUsingHeight = widthHeightRatio < 1.0

Check failure on line 115 in src/renderers/wikimedia-mobile.renderer.ts

View workflow job for this annotation

GitHub Actions / ci-test (18.x)

'scaleUsingHeight' is never reassigned. Use 'const' instead

// 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)

// Set the attributes for the img element based on the data attributes in the span

// 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 originalWidth: number
let match: RegExpMatchArray | undefined
const originalSrc = span.getAttribute('data-data-file-original-src')
if (originalSrc) {
// Try to match against an image URL with a width in it.
match = THUMB_WIDTH_REGEX.exec(originalSrc)
if (match) {
originalWidth = parseInt(match[1], 10)
}
const widthToData = {
[preparedData.width]: preparedData,
[maxData.width]: maxData,
[originalData?.width || 0]: originalData,
}

// These are the attributes that were "prepared" for us by the mobile-html endpoint.
const preparedSrc = span.getAttribute('data-src')
const preparedWidth = parseInt(span.getAttribute('data-width') || '0', 10)
const preparedHeight = parseInt(span.getAttribute('data-height') || '0', 10)

let imgSrc = preparedSrc
let width = preparedWidth
if (originalWidth && match && originalWidth < preparedWidth) {
// There was a match on the originalSrc, and it is an image that is smaller than the prepared image.
width = originalWidth
imgSrc = originalSrc
}
if (THUMB_MAX_WIDTH < originalWidth || (!originalWidth && THUMB_MAX_WIDTH < preparedWidth)) {
// If both srcs are too big, try to either use the original URL hacking, or URL hacking on the
// "prepared" src, to get an image of the right size.
let srcToReplace = originalSrc
if (!match) {
// Try to match against the prepared URL, it might have sizing information.
match = THUMB_WIDTH_REGEX.exec(preparedSrc)
srcToReplace = preparedSrc
const minWidth = originalData ? Math.min(preparedData.width, maxData.width, originalData?.width) : Math.min(preparedData.width, maxData.width)
let selectedData = widthToData[minWidth]

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 there is no match, we will just use the prepared image as is.
if (match) {
width = THUMB_MAX_WIDTH
imgSrc = srcToReplace.replace(`${match[1]}px`, `${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]
}
}
// If the above ifs didn't execute, we're using the prepared image.
// This is a no-op if width == preparedWidth.
const height = Math.round((preparedHeight * width) / preparedWidth)

img.src = urlJoin(protocol, imgSrc)
// Create a new img element
const img = doc.createElement('img') as DominoElement
img.src = urlJoin(protocol, selectedData.src)
img.setAttribute('decoding', 'async')
img.width = width
img.height = 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 @@ -213,7 +260,7 @@ export class WikimediaMobileRenderer extends MobileRenderer {
}

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

0 comments on commit 666e60e

Please sign in to comment.