Skip to content

Commit 14448a7

Browse files
committed
improvement(content): cover GIF and warn when OG dimensions are unreadable
1 parent 0fafe0c commit 14448a7

3 files changed

Lines changed: 60 additions & 7 deletions

File tree

apps/sim/lib/content/image-dimensions.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ function webpVp8(width: number, height: number): Buffer {
5757
return buffer
5858
}
5959

60+
function gif(width: number, height: number, version: 'GIF87a' | 'GIF89a' = 'GIF89a'): Buffer {
61+
const buffer = Buffer.alloc(14)
62+
buffer.write(version, 0, 'latin1')
63+
buffer.writeUInt16LE(width, 6)
64+
buffer.writeUInt16LE(height, 8)
65+
return buffer
66+
}
67+
6068
describe('readImageDimensions', () => {
6169
it('reads PNG dimensions from IHDR', () => {
6270
expect(readImageDimensions(png(1200, 630))).toEqual({ width: 1200, height: 630 })
@@ -93,10 +101,30 @@ describe('readImageDimensions', () => {
93101
expect(readImageDimensions(webpVp8(512, 256))).toEqual({ width: 512, height: 256 })
94102
})
95103

104+
it('reads GIF dimensions from the logical screen descriptor', () => {
105+
expect(readImageDimensions(gif(800, 424))).toEqual({ width: 800, height: 424 })
106+
expect(readImageDimensions(gif(640, 722, 'GIF87a'))).toEqual({ width: 640, height: 722 })
107+
})
108+
96109
it('returns null for an unrecognized format', () => {
97110
expect(readImageDimensions(Buffer.from('not an image at all, really'))).toBeNull()
98111
})
99112

113+
/**
114+
* SVG and ICO are intentionally out of scope — neither is a valid `og:image`
115+
* for the social crawlers, and callers fall back to the OG default.
116+
*/
117+
it('returns null for SVG and ICO', () => {
118+
expect(readImageDimensions(Buffer.from('<svg width="222" height="222"></svg>'))).toBeNull()
119+
const ico = Buffer.alloc(16)
120+
ico.writeUInt16LE(0, 0)
121+
ico.writeUInt16LE(1, 2)
122+
ico.writeUInt16LE(1, 4)
123+
ico.writeUInt8(32, 6)
124+
ico.writeUInt8(32, 7)
125+
expect(readImageDimensions(ico)).toBeNull()
126+
})
127+
100128
it('returns null for a truncated buffer', () => {
101129
expect(readImageDimensions(png(100, 100).subarray(0, 20))).toBeNull()
102130
})

apps/sim/lib/content/image-dimensions.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
/**
2-
* Minimal intrinsic-dimension reader for the image formats the content
3-
* pipeline actually ships as OG covers (PNG, JPEG, WebP).
2+
* Minimal intrinsic-dimension reader for the raster formats that are valid as
3+
* social preview images: PNG, JPEG, WebP, and GIF.
44
*
55
* This replaces the `image-size` package, which is archived upstream and
66
* carries unpatched high-severity DoS advisories (GHSA-w3rx-r6r6-pgpr,
77
* GHSA-5p2g-fcmc-qvqq) in its ICNS/JXL/HEIF parsers — formats this app never
88
* reads. Only the JPEG marker scan loops at all, and it advances on every
9-
* iteration regardless of the declared lengths (see `readJpeg`); PNG and WebP
10-
* are fixed-offset header reads.
9+
* iteration regardless of the declared lengths (see `readJpeg`); the rest are
10+
* fixed-offset header reads.
11+
*
12+
* SVG and ICO are deliberately unsupported: neither is accepted as an
13+
* `og:image` by the major social crawlers, and reading SVG dimensions means
14+
* regex-matching untrusted-shaped XML, which is the failure class that
15+
* motivated removing the dependency in the first place. Callers are expected
16+
* to treat a null return as "fall back to the declared OG default".
1117
*/
1218

1319
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
1420

21+
const GIF_SIGNATURES = new Set(['GIF87a', 'GIF89a'])
22+
1523
/** JPEG frame markers that carry a size record, excluding DHT/JPG/DAC. */
1624
const JPEG_SOF_MARKERS = new Set([
1725
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
@@ -59,6 +67,11 @@ function readJpeg(buffer: Buffer): ImageDimensions | null {
5967
return null
6068
}
6169

70+
function readGif(buffer: Buffer): ImageDimensions | null {
71+
if (buffer.length < 10) return null
72+
return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) }
73+
}
74+
6275
function readWebp(buffer: Buffer): ImageDimensions | null {
6376
const chunkType = buffer.subarray(12, 16).toString('latin1')
6477

@@ -92,8 +105,9 @@ function readWebp(buffer: Buffer): ImageDimensions | null {
92105
}
93106

94107
/**
95-
* Reads intrinsic pixel dimensions from a PNG, JPEG, or WebP buffer. Returns
96-
* null for unrecognized formats, truncated buffers, or zero-valued dimensions.
108+
* Reads intrinsic pixel dimensions from a PNG, JPEG, WebP, or GIF buffer.
109+
* Returns null for unrecognized formats, truncated buffers, or zero-valued
110+
* dimensions.
97111
*/
98112
export function readImageDimensions(buffer: Buffer): ImageDimensions | null {
99113
if (buffer.length < 12) return null
@@ -108,6 +122,8 @@ export function readImageDimensions(buffer: Buffer): ImageDimensions | null {
108122
buffer.subarray(8, 12).toString('latin1') === 'WEBP'
109123
) {
110124
dimensions = readWebp(buffer)
125+
} else if (GIF_SIGNATURES.has(buffer.subarray(0, 6).toString('latin1'))) {
126+
dimensions = readGif(buffer)
111127
}
112128

113129
if (!dimensions || dimensions.width <= 0 || dimensions.height <= 0) return null

apps/sim/lib/content/registry-factory.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'fs/promises'
22
import path from 'path'
33
import { cache } from 'react'
4+
import { createLogger } from '@sim/logger'
45
import matter from 'gray-matter'
56
import { compileMDX } from 'next-mdx-remote/rsc'
67
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
@@ -13,6 +14,8 @@ import type { Author, ContentMeta, ContentPost, TagWithCount } from '@/lib/conte
1314
import { AuthorSchema, ContentFrontmatterSchema } from '@/lib/content/schema'
1415
import { byDateDesc, ensureContentDirs, toIsoDate } from '@/lib/content/utils'
1516

17+
const logger = createLogger('ContentRegistry')
18+
1619
/** Loads a post's custom MDX component overrides, keyed by slug. */
1720
export type ContentComponentLoaders = Record<
1821
string,
@@ -101,7 +104,13 @@ export function createContentRegistry(config: ContentRegistryConfig): ContentReg
101104
if (ogImage.startsWith('http')) return null
102105
try {
103106
const buffer = await fs.readFile(path.join(process.cwd(), 'public', ogImage))
104-
return readImageDimensions(buffer)
107+
const dimensions = readImageDimensions(buffer)
108+
if (!dimensions) {
109+
logger.warn('OG image dimensions could not be read; falling back to the OG default', {
110+
ogImage,
111+
})
112+
}
113+
return dimensions
105114
} catch {
106115
return null
107116
}

0 commit comments

Comments
 (0)