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

Using book cover sizes to prevent layout shifting #10145

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
24 changes: 23 additions & 1 deletion openlibrary/book_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@
from typing import Generic, Literal, TypedDict, TypeVar, cast
from urllib import parse

import requests
import web
from web import uniq
from web.template import TemplateResult

from openlibrary.app import render_template
from openlibrary.plugins.upstream.models import Edition
from openlibrary.plugins.upstream.utils import get_coverstore_public_url
from openlibrary.plugins.upstream.utils import (
get_coverstore_public_url,
get_coverstore_url,
)
from openlibrary.utils import OrderedEnum, multisort_best

logger = logging.getLogger("openlibrary.book_providers")
Expand Down Expand Up @@ -577,6 +581,24 @@ def get_cover_url(ed_or_solr: Edition | dict) -> str | None:
return None


def get_dimensions_cover(url: str) -> tuple[int, int] | None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have access to the database that contains all the cover info, instead of making a network request, it would be faster to instead make a database request.

I think calling the details method should fix this:

https://github.com/internetarchive/openlibrary/blob/8b2a94044f01b9388b96ff094559c8e4637b6d4f/openlibrary/coverstore/db.py

"""
Get the dimensions of the cover image at a given URL
"""
url = url.replace(url.split("/b/")[0], get_coverstore_url())

# replace -M.jpg or -S.jpg or -L.jpg to .json
url = url.replace("-M.jpg", ".json")
url = url.replace("-S.jpg", ".json")
url = url.replace("-L.jpg", ".json")

try:
d = requests.get(url).json()
return d['width'], d['height']
except OSError:
return None


def is_non_ia_ocaid(ocaid: str) -> bool:
"""
Check if the ocaid "looks like" it's from another provider
Expand Down
44 changes: 43 additions & 1 deletion openlibrary/components/LibraryExplorer/components/OLCarousel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
</template>

<template v-slot:cover="{book}">
<slot name="cover" v-bind:book="book"/>
<div class="cover-container">
<slot name="cover" v-bind:book="book">
<img :src="`https://covers.openlibrary.org/b/id/${book.cover_i}-L.jpg`" alt="Cover" class="cover"/>
</slot>
</div>
</template>

<template #book-end-start>
Expand Down Expand Up @@ -319,4 +323,42 @@ export default {
overflow: clip;
position: relative;
}

.cover-container {
width: 180px;
height: 220px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}

img.cover {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}

@media (max-width: 600px) {
.cover-container {
width: 100px;
height: 150px;
}
}

@media (min-width: 601px) and (max-width: 1200px) {
.cover-container {
width: 120px;
height: 180px;
}
}

@media (min-width: 1201px) {
.cover-container {
width: 150px;
height: 200px;
}
}

</style>
10 changes: 10 additions & 0 deletions openlibrary/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ def url(self, size="M"):
def __repr__(self):
return "<image: %s/%d>" % (self.category, self.id)

def width(self):
"""Get the width of the image."""
info = self.info()
return info["width"] if info else None

def height(self):
"""Get the height of the image."""
info = self.info()
return info["height"] if info else None


ThingKey = str

Expand Down
39 changes: 37 additions & 2 deletions openlibrary/macros/SearchResultsWork.html
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here unfortunately, this will not be able to use/display the cover width/height until it is in solr.

Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,47 @@
<div class="sri__main">
<span class="bookcover $blur_cover">
$ cover = get_cover_url(selected_ed) or "/images/icons/avatar_book-sm.png"
<a href="$work_edition_url"><img
$ cover_dimensions = get_dimensions_cover(cover)
$ aspect_ratio = 1
$if cover_dimensions:
$ cover_height = cover_dimensions[1]
$ cover_width = cover_dimensions[0]
$else:
$ cover_height = 360
$ cover_width = 180
$ aspect_ratio = cover_width / cover_height

$ fixed_width = 175
$ desired_height_for_fixed_width = fixed_width / aspect_ratio
<a href="$work_edition_url">
<img
itemprop="image"
src="$cover"
alt="$_('Cover of: %(title)s', title=full_title)"
title="$_('Cover of: %(title)s', title=full_title)"
/></a>
/>
<style>
.searchResultItem {
.bookcover {
img {
width: 100%;
aspect-ratio: $aspect_ratio;
object-fit: cover;
}
}
}
@media only screen and (max-width: 768px) {
.searchResultItem {
.bookcover {
img {
width: ${fixed_width}px;
height: ${desired_height_for_fixed_width}px;
}
}
}
}
</style>
</a>
</span>

<div class="details">
Expand Down
2 changes: 2 additions & 0 deletions openlibrary/plugins/openlibrary/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ def setup_template_globals():
get_book_provider,
get_book_provider_by_name,
get_cover_url,
get_dimensions_cover,
)

def get_supported_languages():
Expand Down Expand Up @@ -1369,6 +1370,7 @@ def get_supported_languages():
'get_book_provider': get_book_provider,
'get_book_provider_by_name': get_book_provider_by_name,
'get_cover_url': get_cover_url,
'get_dimensions_cover': get_dimensions_cover,
# bad use of globals
'is_bot': is_bot,
'time': time,
Expand Down
5 changes: 3 additions & 2 deletions openlibrary/plugins/openlibrary/js/lists/ShowcaseItem.js
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cover is very rarely displayed, and fetching the cover size before rendering it is not worth the complexity and performance costs, I believe, so we can roll back the changes to this piece (and where ShowcaseItem is called).

Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,10 @@ function getSeedType(seed) {
* @param {string} seedKey
* @param {string} listTitle
* @param {string} [coverUrl]
* @param {string} desiredHeight
* @returns {HTMLLIElement}
*/
export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL) {
export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl = DEFAULT_COVER_URL, desiredHeight = '') {
if (!i18nStrings) {
const i18nInput = document.querySelector('input[name=list-i18n-strings]')
i18nStrings = JSON.parse(i18nInput.value)
Expand All @@ -244,7 +245,7 @@ export function createActiveShowcaseItem(listKey, seedKey, listTitle, coverUrl =
const seedType = getSeedType(seedKey)

const itemMarkUp = `<span class="image">
<a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}"/></a>
<a href="${listKey}"><img src="${coverUrl}" alt="${i18nStrings['cover_of']}${listTitle}" title="${i18nStrings['cover_of']}${listTitle}" width="22px" height="${desiredHeight}"/></a>
</span>
<span class="data">
<span class="label">
Expand Down
16 changes: 13 additions & 3 deletions openlibrary/plugins/openlibrary/js/my-books/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { removeChildren } from '../utils'

// XXX : jsdoc
// XXX : decompose
export function initMyBooksAffordances(dropperElements, showcaseElements) {
export async function initMyBooksAffordances(dropperElements, showcaseElements) {
const showcases = []
for (const elem of showcaseElements) {
const showcase = new ShowcaseItem(elem)
Expand Down Expand Up @@ -42,7 +42,7 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) {

getListPartials()
.then(response => response.json())
.then((data) => {
.then(async (data) => {
// XXX : convert this block to one or two function calls
const listData = data.listData
const activeShowcaseItems = []
Expand All @@ -56,7 +56,17 @@ export function initMyBooksAffordances(dropperElements, showcaseElements) {
const coverID = key.slice(key.indexOf('OL'))
const cover = `https://covers.openlibrary.org/b/olid/${coverID}-S.jpg`

activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover))
const img = new Image()
img.src = cover

await new Promise((resolve) => {
img.onload = () => {
let desiredHeight = 22 * img.height / img.width
desiredHeight = desiredHeight.toFixed(2)
activeShowcaseItems.push(createActiveShowcaseItem(listKey, seedKey, listData[listKey].listName, cover, desiredHeight))
resolve()
}
})
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion openlibrary/templates/covers/book_cover.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,30 @@
$ src = cover_url or '/images/icons/avatar_book.png'
$ srcset = '%s 2x' % (cover_lg or '/images/icons/avatar_book-lg.png')

$ cover_height = None
$ cover_width = 180 # Fixed width of the image
$ aspect_ratio = 1
$ cover = book.get_cover()
$if cover:
$ cover_height = cover.height()
$ cover_width = cover.width()
$ aspect_ratio = cover.width() / cover.height()
$else:
$ ia_cover_url = book.get_ia_cover(book.ocaid, "M")
$ parts = ia_cover_url.split('_h')
$ cover_height_str = parts[1].split('.')[0]
$ cover_height = int(cover_height_str)
$ cover_width_str = parts[0].split('_w')[1]
$ cover_width = int(cover_width_str)
$ aspect_ratio = cover_width / cover_height

$ fixed_width = 180 # Fixed width of the container
$ calculated_height = fixed_width / aspect_ratio

$if preload:
$add_metatag(tag="link", **{'rel': 'preload', 'as': 'image', 'href': src, 'imagesrcset': srcset})

<div class="coverMagic cover-animation">
<div class="coverMagic cover-animation" style="min-height: ${calculated_height}px;">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like we'll only need to specify the aspect ratio; e.g. style="aspect-ratio: $cover_width / $cover_height;" on the <img> element. @RayBB and I tested this, and it removed the layout shift! That should simplify this up quite a bit!

<div class="SRPCover bookCover" style="display: $cond(cover_url, 'block', 'none');">
<a
href="$cover_lg"
Expand Down
7 changes: 6 additions & 1 deletion openlibrary/templates/lists/home.html
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here unfortunately, this will have to be rolled back to avoid a performance hit.

Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ <h2>$_('Active Lists')</h2>
<div class="cover">
$ cover = list.get_cover() or list.get_default_cover()
$ cover_url = cover and cover.url("S") or "/images/icons/avatar_book-sm.png"
<img src="$cover_url" alt=""/>
$ cover_width = cover.width()
$ cover_height = cover.height()
$ cover_aspect_ratio = cover_width and cover_height and cover_width / cover_height
$ cover_width_for_58px_height = cover_aspect_ratio and int(58 * cover_aspect_ratio)
$ cover_height_for_25px_width = cover_width_for_58px_height and round(1450 / cover_width_for_58px_height)
<img src="$cover_url" alt="" width="25px" height="$cover_height_for_25px_width" />
</div>
$list.name
</a>
Expand Down
4 changes: 2 additions & 2 deletions static/css/components/book.less
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
margin: 0 auto;
box-shadow: 1px 2px 5px 0 @book-cover-shadow-color;
border-radius: 3px;
max-width: 100%;
max-height: 200px;
width: 100%;
height: 12rem;
}
}
3 changes: 1 addition & 2 deletions static/css/components/listLists.less
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ ul.listLists {
padding: 0 10px;
/* stylelint-disable max-nesting-depth */
img {
width: 32px;
max-width: 100%;
width: 22px;
}
/* stylelint-enable max-nesting-depth */
}
Expand Down
3 changes: 0 additions & 3 deletions static/css/layout/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
div.contentBody,
div#contentBody {
padding: 0 @contentBody-padding @contentBody-padding;
img {
max-width: 100%;
}
pre {
overflow-x: auto;
}
Expand Down
Loading