Skip to content

Commit

Permalink
Merge pull request #3005 from owid/image-small-variant
Browse files Browse the repository at this point in the history
🎉 Add support for small filename in gdocs images
  • Loading branch information
ikesau authored Dec 15, 2023
2 parents bc98d0b + 098068d commit b422e80
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 30 deletions.
3 changes: 3 additions & 0 deletions db/model/Gdoc/GdocBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export class GdocBase extends BaseEntity implements OwidGdocBaseInterface {
if ("filename" in item && item.filename) {
filenames.add(item.filename)
}
if (item.type === "image" && item.smallFilename) {
filenames.add(item.smallFilename)
}
if (item.type === "prominent-link" && item.thumbnail) {
filenames.add(item.thumbnail)
}
Expand Down
1 change: 1 addition & 0 deletions db/model/Gdoc/rawToEnriched.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ const parseImage = (image: RawBlockImage): EnrichedBlockImage => {
return {
type: "image",
filename,
smallFilename: image.value.smallFilename,
alt: image.value.alt,
caption,
size,
Expand Down
4 changes: 2 additions & 2 deletions packages/@ourworldindata/components/src/styles/variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $vertical-spacing: 16px;
:root {
--grid-gap: 24px;

@media (max-width: 798px) {
@media (max-width: 768px) {
--grid-gap: 16px;
}
}
Expand Down Expand Up @@ -52,7 +52,7 @@ $search-cta-height: 40px;
* Breakpoints
*/

// Calculations in Image.tsx rely on these variables. Please change them there if you change them here.
// Calculations in Image.tsx and image.ts rely on these variables. Please change them there if you change them here.
$sm: 768px;
$md: 960px;
$lg: 1280px;
Expand Down
31 changes: 31 additions & 0 deletions packages/@ourworldindata/utils/src/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,34 @@ export function getFilenameWithoutExtension(
export function getFilenameAsPng(filename: ImageMetadata["filename"]): string {
return `${getFilenameWithoutExtension(filename)}.png`
}

export type SourceProps = {
media: string | undefined
srcSet: string
}

/**
* When we have a small and large image, we want to generate two <source> elements.
* The first will only be active at small screen sizes, due to its `media` property.
* The second will be active at all screen sizes.
* These props work in conjuction with a `sizes` attribute on the <source> element.
*/
export function generateSourceProps(
smallImage: ImageMetadata | undefined,
regularImage: ImageMetadata
): SourceProps[] {
const props: SourceProps[] = []
if (smallImage) {
const smallSizes = getSizes(smallImage.originalWidth)
props.push({
media: "(max-width: 768px)",
srcSet: generateSrcSet(smallSizes, smallImage.filename),
})
}
const regularSizes = getSizes(regularImage.originalWidth)
props.push({
media: undefined,
srcSet: generateSrcSet(regularSizes, regularImage.filename),
})
return props
}
2 changes: 2 additions & 0 deletions packages/@ourworldindata/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,8 @@ export {
getFilenameAsPng,
type GDriveImageMetadata,
type ImageMetadata,
type SourceProps,
generateSourceProps,
} from "./image.js"

export { Tippy, TippyIfInteractive } from "./Tippy.js"
Expand Down
2 changes: 2 additions & 0 deletions packages/@ourworldindata/utils/src/owidTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ export type RawBlockImage = {
type: "image"
value: {
filename?: string
smallFilename?: string
alt?: string
caption?: string
size?: BlockImageSize
Expand All @@ -715,6 +716,7 @@ export type RawBlockImage = {
export type EnrichedBlockImage = {
type: "image"
filename: string
smallFilename?: string
alt?: string // optional as we can use the default alt from the file
caption?: Span[]
originalWidth?: number
Expand Down
33 changes: 31 additions & 2 deletions site/Lightbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ import { useTriggerOnEscape } from "./hooks.js"

export const LIGHTBOX_IMAGE_CLASS = "lightbox-image"

/**
* If the image is inside a <picture> element, get the URL of the largest
* source image that matches the current viewport size.
*/
function getActiveSourceImgUrl(img: HTMLImageElement): string | undefined {
if (img.closest("picture")) {
const sources = img.closest("picture")!.querySelectorAll("source")
const activeSource = Array.from(sources).find((s) =>
s.media ? window.matchMedia(s.media).matches : true
)
const sourceSrcset = activeSource?.getAttribute("srcset")
// split sourceSrcset into [src, width] pairs
const srcsetPairs = sourceSrcset
?.split(",")
.map((pair) => pair.trim().split(" "))
const largestImgSrc = srcsetPairs?.at(-1)?.[0]
return largestImgSrc
}
return undefined
}

const Lightbox = ({
children,
containerNode,
Expand Down Expand Up @@ -145,9 +166,17 @@ export const runLightbox = () => {

img.classList.add("lightbox-enabled")
img.addEventListener("click", () => {
// getAttribute doesn't automatically URI encode values, img.src does
// An attribute placed by our WP image formatter: the URL of the original image without any WxH suffix
const highResSrc = img.getAttribute("data-high-res-src")
const imgSrc = highResSrc ? encodeURI(highResSrc) : img.src
// If the image is a Gdoc Image with a smallFilename, get the source that is currently active
const activeSourceImgUrl = getActiveSourceImgUrl(img)
const imgSrc = highResSrc
? // getAttribute doesn't automatically URI encode values, img.src does
encodeURI(highResSrc)
: activeSourceImgUrl
? activeSourceImgUrl
: img.src

const imgAlt = img.alt
if (imgSrc) {
ReactDOM.render(
Expand Down
1 change: 1 addition & 0 deletions site/gdocs/ArticleBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export default function ArticleBlock({
>
<Image
filename={block.filename}
smallFilename={block.smallFilename}
alt={block.alt}
containerType={containerType}
/>
Expand Down
89 changes: 63 additions & 26 deletions site/gdocs/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, { useContext } from "react"
import {
getSizes,
generateSrcSet,
getFilenameWithoutExtension,
IMAGES_DIRECTORY,
generateSourceProps,
ImageMetadata,
} from "@ourworldindata/utils"
import { LIGHTBOX_IMAGE_CLASS } from "../Lightbox.js"
import cx from "classnames"
import {
IMAGE_HOSTING_BUCKET_SUBFOLDER_PATH,
IMAGE_HOSTING_CDN_URL,
Expand All @@ -27,11 +26,12 @@ const generateResponsiveSizes = (numberOfColumns: number): string =>
const gridSpan5 = generateResponsiveSizes(5)
const gridSpan6 = generateResponsiveSizes(6)
const gridSpan7 = generateResponsiveSizes(7)
const gridSpan8 = generateResponsiveSizes(8)

type ImageParentContainer = Container | "thumbnail" | "full-width"

const containerSizes: Record<ImageParentContainer, string> = {
["default"]: gridSpan6,
["default"]: gridSpan8,
["sticky-right-left-column"]: gridSpan5,
["sticky-right-right-column"]: gridSpan7,
["sticky-left-left-column"]: gridSpan7,
Expand All @@ -46,33 +46,43 @@ const containerSizes: Record<ImageParentContainer, string> = {

export default function Image(props: {
filename: string
smallFilename?: string
alt?: string
className?: string
containerType?: ImageParentContainer
shouldLightbox?: boolean
}) {
const {
filename,
smallFilename,
className = "",
containerType = "default",
shouldLightbox = true,
} = props
const { isPreviewing } = useContext(DocumentContext)
const image = useImage(filename)
const smallImage = useImage(smallFilename)
const renderImageError = (name: string) => (
<BlockErrorFallback
className={className}
error={{
name: "Image error",
message: `Image with filename "${name}" not found. This block will not render when the page is baked.`,
}}
/>
)

if (!image) {
if (isPreviewing) {
return (
<BlockErrorFallback
className={className}
error={{
name: "Image error",
message: `Image with filename "${filename}" not found. This block will not render when the page is baked.`,
}}
/>
)
return renderImageError(filename)
}
// Don't render anything if we're not previewing (i.e. a bake) and the image is not found
return null
}
// Here we can fall back to the regular image filename, so don't return null if not found
if (isPreviewing && smallFilename && !smallImage) {
return renderImageError(smallFilename)
}

const alt = props.alt ?? image.defaultAlt
const maybeLightboxClassName =
Expand All @@ -81,13 +91,36 @@ export default function Image(props: {
: LIGHTBOX_IMAGE_CLASS

if (isPreviewing) {
const previewSrc = `${IMAGE_HOSTING_CDN_URL}/${IMAGE_HOSTING_BUCKET_SUBFOLDER_PATH}/${filename}`
const makePreviewUrl = (f: string) =>
encodeURI(
`${IMAGE_HOSTING_CDN_URL}/${IMAGE_HOSTING_BUCKET_SUBFOLDER_PATH}/${f}`
)

const PreviewSource = (props: { i?: ImageMetadata; sm?: boolean }) => {
const { i, sm } = props
if (!i) return null

return (
<source
srcSet={`${makePreviewUrl(i.filename)} ${i.originalWidth}w`}
media={sm ? "(max-width: 768px)" : undefined}
type="image/webp"
sizes={
containerSizes[containerType] ?? containerSizes.default
}
/>
)
}
return (
<img
src={encodeURI(previewSrc)}
alt={alt}
className={cx(maybeLightboxClassName, className, "lazyload")}
/>
<picture className={className}>
<PreviewSource i={smallImage} sm />
<PreviewSource i={image} />
<img
src={makePreviewUrl(image.filename)}
alt={alt}
className={maybeLightboxClassName}
/>
</picture>
)
}

Expand All @@ -114,17 +147,21 @@ export default function Image(props: {
)
}

const sizes = getSizes(image.originalWidth)
const srcSet = generateSrcSet(sizes, filename)
const imageSrc = `${IMAGES_DIRECTORY}${filename}`
const sourceProps = generateSourceProps(smallImage, image)

return (
<picture className={className}>
<source
srcSet={srcSet}
type="image/webp"
sizes={containerSizes[containerType] ?? containerSizes.default}
/>
{sourceProps.map((props, i) => (
<source
key={i}
{...props}
type="image/webp"
sizes={
containerSizes[containerType] ?? containerSizes.default
}
/>
))}
<img
src={encodeURI(imageSrc)}
alt={alt}
Expand Down

0 comments on commit b422e80

Please sign in to comment.