Merge pull request #92 from sambecker/matte

Add optional photo matting
This commit is contained in:
Sam Becker 2024-05-09 21:44:35 -05:00 committed by GitHub
commit 141ffb63a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 77 additions and 26 deletions

View File

@ -96,6 +96,7 @@ Application behavior can be changed by configuring the following environment var
- `NEXT_PUBLIC_PRO_MODE = 1` enables higher quality image storage (results in increased storage usage)
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES = 1` enables static optimization for pages, i.e., renders pages at build time (results in increased project usage)—⚠️ _Experimental_
- `NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES = 1` enables static optimization for OG images, i.e., renders images at build time (results in increased project usage)—⚠️ _Experimental_
- `NEXT_PUBLIC_MATTE_PHOTOS = 1` constrains the size of each photo, and enables a surrounding border (potentially useful for photos with tall aspect ratios)
- `NEXT_PUBLIC_BLUR_DISABLED = 1` prevents image blur data being stored and displayed (potentially useful for limiting Postgres usage)
- `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data
- `NEXT_PUBLIC_IGNORE_PRIORITY_ORDER = 1` prevents `priority_order` field affecting photo order
@ -208,10 +209,10 @@ FAQ
> This template statically optimizes core views such as `/` and `/grid` to minimize visitor load times. Consequently, when photos are added, edited, or removed, it might take several minutes for those changes to propagate. If it seems like a change is not taking effect, try navigating to `/admin/configuration` and clicking "Clear Cache."
#### Why dont my OG images load when I share a link?
> Many services such as iMessage, Slack, and X, require near-instant responses when unfurling link-based content. In order to guarantee sufficient responsiveness, consider rendering pages and image assets ahead of time by enabling static optimization by setting `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES` and `NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES` to `1`. Keep in mind that this will increase platform usage.
> Many services such as iMessage, Slack, and X, require near-instant responses when unfurling link-based content. In order to guarantee sufficient responsiveness, consider rendering pages and image assets ahead of time by enabling static optimization by setting `NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES = 1` and `NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES = 1`. Keep in mind that this will increase platform usage.
#### Why are my thumbnails square?
> Absent configuration, the default grid aspect ratio is `1`. It can be set to any number (for instance `1.5` for 3:2 images) via `NEXT_PUBLIC_GRID_ASPECT_RATIO` or ignored entirely by setting to `0`.
#### Why do my vertical images take up so much space?
> By default, all photos are shown full-width, regardless of orientation. Enable matting to showcase horizontal and vertical photos at a similar scale by setting `NEXT_PUBLIC_MATTE_PHOTOS = 1`.
#### My images/content have fallen out of sync with my database and/or my production site no longer matches local development. What do I do?
> Navigate to `/admin/configuration` and click "Clear Cache."
@ -219,6 +220,9 @@ FAQ
#### I'm seeing server-side runtime errors when loading a page after updating my fork. What do I do?
> Navigate to `/admin/configuration` and click "Clear Cache." If this doesn't help, [open an issue](https://github.com/sambecker/exif-photo-blog/issues/new).
#### Why are my thumbnails square?
> Absent configuration, the default grid aspect ratio is `1`. `NEXT_PUBLIC_GRID_ASPECT_RATIO` can be set to any number (for instance, `1.5` for 3:2 images) or ignored by setting to `0`.
#### Why aren't my Fujifilm simulations importing alongside EXIF data?
> Fujifilm simulation data is stored in vendor-specific Makernote binaries embedded in EXIF data. Under certain circumstances an intermediary may strip out this data. For instance, there is a known issue on iOS where editing an image, e.g., cropping it, causes Makernote data loss. If your simulation data appears to be missing, try importing the original file as it was stored by the camera. Additionally, if you can confirm the simulation mode on camera, you can then edit the photo record and manually select it.

View File

@ -29,7 +29,7 @@ export default function ChecklistRow({
/>
<div className="flex flex-col min-w-0">
<div className={clsx(
'flex flex-wrap items-center gap-2 pb-1',
'flex flex-wrap items-center gap-2 pb-0.5',
'font-bold dark:text-gray-300',
)}>
{title}

View File

@ -70,11 +70,13 @@ export default function CommandKClient({
isUserSignedIn,
setUserEmail,
isCommandKOpen: isOpen,
arePhotosMatted,
shouldShowBaselineGrid,
shouldDebugBlur,
setIsCommandKOpen: setIsOpen,
setShouldRespondToKeyboardCommands,
setShouldShowBaselineGrid,
setArePhotosMatted,
setShouldDebugBlur,
} = useAppState();
@ -197,6 +199,10 @@ export default function CommandKClient({
heading: 'Debug Tools',
accessory: <RiToolsFill size={16} className="translate-x-[-1px]" />,
items: [{
label: 'Toggle Photo Matting',
action: () => setArePhotosMatted?.(prev => !prev),
annotation: arePhotosMatted ? <FaCheck size={12} /> : undefined,
}, {
label: 'Toggle Blur Debug',
action: () => setShouldDebugBlur?.(prev => !prev),
annotation: shouldDebugBlur ? <FaCheck size={12} /> : undefined,

View File

@ -8,13 +8,15 @@ import Image, { ImageProps } from 'next/image';
import { useCallback, useEffect, useRef, useState } from 'react';
export default function ImageBlurFallback(props: ImageProps & {
blurCompatibilityLevel?: 'none' | 'low' | 'high';
blurCompatibilityLevel?: 'none' | 'low' | 'high'
imgClassName?: string
}) {
const {
className,
priority,
blurDataURL,
blurCompatibilityLevel = 'low',
imgClassName = 'object-cover h-full',
...rest
} = props;
@ -29,8 +31,6 @@ export default function ImageBlurFallback(props: ImageProps & {
const [hideBlurPlaceholder, setHideBlurPlaceholder] = useState(false);
const imageClassName = 'object-cover h-full';
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
@ -75,8 +75,9 @@ export default function ImageBlurFallback(props: ImageProps & {
<div className={clsx(
'@container',
'absolute inset-0',
'bg-main overflow-hidden',
'overflow-hidden',
'transition-opacity duration-300 ease-in',
!(BLUR_ENABLED && props.blurDataURL) && 'bg-main',
(isLoading || shouldDebugBlur) ? 'opacity-100' : 'opacity-0',
)}>
{(BLUR_ENABLED && props.blurDataURL)
@ -84,7 +85,7 @@ export default function ImageBlurFallback(props: ImageProps & {
...rest,
src: blurDataURL,
className: clsx(
imageClassName,
imgClassName,
getBlurClass(),
),
}} />
@ -97,7 +98,7 @@ export default function ImageBlurFallback(props: ImageProps & {
...rest,
ref: imgRef,
priority,
className: imageClassName,
className: imgClassName,
onLoad,
onError,
}} />

View File

@ -3,6 +3,7 @@ import ImageBlurFallback from './ImageBlurFallback';
export default function ImageLarge({
className,
imgClassName,
src,
alt,
aspectRatio,
@ -11,6 +12,7 @@ export default function ImageLarge({
priority,
}: {
className?: string
imgClassName?: string
src: string
alt: string
aspectRatio: number
@ -21,6 +23,7 @@ export default function ImageLarge({
return (
<ImageBlurFallback {...{
className,
imgClassName,
src,
alt,
blurDataURL: blurData,

View File

@ -26,6 +26,7 @@ import { RevalidatePhoto } from './InfinitePhotoScroll';
import { useRef } from 'react';
import useOnVisible from '@/utility/useOnVisible';
import PhotoDate from './PhotoDate';
import { useAppState } from '@/state/AppState';
export default function PhotoLarge({
photo,
@ -68,24 +69,38 @@ export default function PhotoLarge({
useOnVisible(ref, onVisible);
const { arePhotosMatted } = useAppState();
return (
<SiteGrid
containerRef={ref}
contentMain={
<Link
href={pathForPhoto(photo)}
className="active:brightness-75"
className={clsx(arePhotosMatted &&
'flex items-center aspect-[3/2] bg-gray-100',
)}
prefetch={prefetch}
>
<ImageLarge
className="w-full"
alt={altTextForPhoto(photo)}
src={photo.url}
aspectRatio={photo.aspectRatio}
blurData={photo.blurData}
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
priority={priority}
/>
<div className={clsx(
arePhotosMatted &&
'flex items-center justify-center w-full',
arePhotosMatted && photo.aspectRatio >= 1
? 'h-[80%]'
: 'h-[90%]',
)}>
<ImageLarge
className={clsx(arePhotosMatted && 'h-full')}
imgClassName={clsx(arePhotosMatted &&
'object-contain w-full h-full')}
alt={altTextForPhoto(photo)}
src={photo.url}
aspectRatio={photo.aspectRatio}
blurData={photo.blurData}
blurCompatibilityMode={doesPhotoNeedBlurCompatibility(photo)}
priority={priority}
/>
</div>
</Link>}
contentSide={
<DivDebugBaselineGrid className={clsx(

View File

@ -44,6 +44,7 @@ export default function SiteChecklistClient({
isStaticallyOptimized,
arePagesStaticallyOptimized,
areOGImagesStaticallyOptimized,
arePhotosMatted,
isBlurEnabled,
isGeoPrivacyEnabled,
isPriorityOrderEnabled,
@ -122,9 +123,9 @@ export default function SiteChecklistClient({
>
<span className="inline-flex items-center gap-1">
<span className={clsx(
'text-xs font-medium tracking-wide',
'px-0.5 py-0.5',
'rounded-sm',
'text-[11px] font-medium tracking-wide',
'px-0.5 py-[0.5px]',
'rounded-[5px]',
'bg-gray-100 dark:bg-gray-800',
)}>
`{variable}`
@ -134,7 +135,7 @@ export default function SiteChecklistClient({
</div>;
const renderEnvVars = (variables: string[]) =>
<div className="py-1 space-y-1">
<div className="py-0.5">
{variables.map(envVar => renderEnvVar(envVar))}
</div>;
@ -369,14 +370,25 @@ export default function SiteChecklistClient({
{renderSubStatus(
arePagesStaticallyOptimized ? 'checked' : 'optional',
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES']),
'translate-y-[4.5px]',
'translate-y-[3.5px]',
)}
{renderSubStatus(
areOGImagesStaticallyOptimized ? 'checked' : 'optional',
renderEnvVars(['NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES']),
'translate-y-[4.5px]',
'translate-y-[3.5px]',
)}
</ChecklistRow>
<ChecklistRow
title="Photo Matting"
status={arePhotosMatted}
isPending={isPendingPage}
optional
>
Set environment variable to {'"1"'} to constrain the size
{' '}
of each photo, and enable a surrounding border:
{renderEnvVars(['NEXT_PUBLIC_MATTE_PHOTOS'])}
</ChecklistRow>
<ChecklistRow
title="Image Blur"
status={isBlurEnabled}

View File

@ -114,6 +114,8 @@ export const STATICALLY_OPTIMIZED_PAGES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_PAGES === '1';
export const STATICALLY_OPTIMIZED_OG_IMAGES =
process.env.NEXT_PUBLIC_STATICALLY_OPTIMIZE_OG_IMAGES === '1';
export const MATTE_PHOTOS =
process.env.NEXT_PUBLIC_MATTE_PHOTOS === '1';
export const BLUR_ENABLED =
process.env.NEXT_PUBLIC_BLUR_DISABLED !== '1';
export const GEO_PRIVACY_ENABLED =
@ -177,6 +179,7 @@ export const CONFIG_CHECKLIST_STATUS = {
),
arePagesStaticallyOptimized: STATICALLY_OPTIMIZED_PAGES,
areOGImagesStaticallyOptimized: STATICALLY_OPTIMIZED_OG_IMAGES,
arePhotosMatted: MATTE_PHOTOS,
isBlurEnabled: BLUR_ENABLED,
isGeoPrivacyEnabled: GEO_PRIVACY_ENABLED,
isAiTextGenerationEnabled: AI_TEXT_GENERATION_ENABLED,

View File

@ -4,6 +4,8 @@ import { AnimationConfig } from '@/components/AnimateItems';
export interface AppStateContext {
previousPathname?: string
hasLoaded?: boolean
arePhotosMatted?: boolean
setArePhotosMatted?: Dispatch<SetStateAction<boolean>>
swrTimestamp?: number
invalidateSwr?: () => void
userEmail?: string

View File

@ -6,6 +6,7 @@ import { AnimationConfig } from '@/components/AnimateItems';
import usePathnames from '@/utility/usePathnames';
import { getAuthAction } from '@/auth/actions';
import useSWR from 'swr';
import { MATTE_PHOTOS } from '@/site/config';
export default function AppStateProvider({
children,
@ -16,6 +17,8 @@ export default function AppStateProvider({
const [hasLoaded, setHasLoaded] =
useState(false);
const [arePhotosMatted, setArePhotosMatted] =
useState(MATTE_PHOTOS);
const [swrTimestamp, setSwrTimestamp] =
useState(Date.now());
const [userEmail, setUserEmail] =
@ -50,6 +53,8 @@ export default function AppStateProvider({
value={{
previousPathname,
hasLoaded,
arePhotosMatted,
setArePhotosMatted,
swrTimestamp,
invalidateSwr,
setHasLoaded,