feat: generate first-slide thumbnail for Quarto RevealJS presentations
Build and Deploy Verso / deploy (push) Successful in 14m10s
Build and Deploy Verso / deploy (push) Successful in 14m10s
thumbnailFromBuild() now tries output.pdf → output-slides.pdf → decktape on output.html (slide 1 only). The web service's ThumbnailManager already calls this endpoint fire-and-forget on every successful compile, so RevealJS project cards will show the first slide thumbnail automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,26 +256,73 @@ async function convertProjectToDocument(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generates a JPEG thumbnail of page 1 of the compiled output.pdf using
|
// Generates a JPEG thumbnail of page 1 of the compiled output using
|
||||||
// pdftocairo (poppler-utils, installed in the CLSI base image). Does NOT
|
// pdftocairo (poppler-utils). Tries output.pdf first (LaTeX / Quarto-PDF),
|
||||||
// require ENABLE_PDF_CONVERSIONS or the pdftocairo Docker image.
|
// then output-slides.pdf (Quarto RevealJS after a PDF-export compile), then
|
||||||
|
// falls back to rendering slide 1 of output.html via decktape (normal
|
||||||
|
// RevealJS preview compile). All temp dirs are cleaned up in finally.
|
||||||
async function thumbnailFromBuild(req, res) {
|
async function thumbnailFromBuild(req, res) {
|
||||||
const { project_id: projectId, user_id: userId, build_id: buildId } = req.params
|
const { project_id: projectId, user_id: userId, build_id: buildId } = req.params
|
||||||
if (!buildId?.match(OutputCacheManager.BUILD_REGEX)) return res.sendStatus(400)
|
if (!buildId?.match(OutputCacheManager.BUILD_REGEX)) return res.sendStatus(400)
|
||||||
|
|
||||||
const compileName = userId ? `${projectId}-${userId}` : projectId
|
const compileName = userId ? `${projectId}-${userId}` : projectId
|
||||||
const pdfPath = Path.join(
|
const buildDir = Path.join(
|
||||||
Settings.path.outputDir,
|
Settings.path.outputDir,
|
||||||
compileName,
|
compileName,
|
||||||
OutputCacheManager.CACHE_SUBDIR,
|
OutputCacheManager.CACHE_SUBDIR,
|
||||||
buildId,
|
buildId
|
||||||
'output.pdf'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
let pdfPath = null
|
||||||
await fs.access(pdfPath)
|
let deckTapeDir = null
|
||||||
} catch {
|
|
||||||
return res.sendStatus(404)
|
for (const name of ['output.pdf', 'output-slides.pdf']) {
|
||||||
|
try {
|
||||||
|
const p = Path.join(buildDir, name)
|
||||||
|
await fs.access(p)
|
||||||
|
pdfPath = p
|
||||||
|
break
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pdfPath) {
|
||||||
|
const htmlPath = Path.join(buildDir, 'output.html')
|
||||||
|
try {
|
||||||
|
await fs.access(htmlPath)
|
||||||
|
deckTapeDir = await fs.mkdtemp(Path.join(os.tmpdir(), 'clsi-deck-'))
|
||||||
|
const chromeHome = Path.join(deckTapeDir, 'chrome')
|
||||||
|
await fs.mkdir(chromeHome, { recursive: true })
|
||||||
|
const slidePdf = Path.join(deckTapeDir, 'slide1.pdf')
|
||||||
|
await execFileAsync(
|
||||||
|
'decktape',
|
||||||
|
[
|
||||||
|
'--slides', '1',
|
||||||
|
'--chrome-arg=--no-sandbox',
|
||||||
|
'--chrome-arg=--disable-dev-shm-usage',
|
||||||
|
'--chrome-arg=--disable-gpu',
|
||||||
|
`--chrome-arg=--user-data-dir=${chromeHome}/data`,
|
||||||
|
htmlPath,
|
||||||
|
slidePdf,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
timeout: 60000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
HOME: chromeHome,
|
||||||
|
XDG_CONFIG_HOME: chromeHome,
|
||||||
|
XDG_CACHE_HOME: chromeHome,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
pdfPath = slidePdf
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ err, projectId, buildId }, 'decktape slide1 thumbnail failed')
|
||||||
|
if (deckTapeDir) {
|
||||||
|
await fs.rm(deckTapeDir, { recursive: true, force: true }).catch(() => {})
|
||||||
|
deckTapeDir = null
|
||||||
|
}
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tmpDir = await fs.mkdtemp(Path.join(os.tmpdir(), 'clsi-thumb-'))
|
const tmpDir = await fs.mkdtemp(Path.join(os.tmpdir(), 'clsi-thumb-'))
|
||||||
@@ -311,6 +358,9 @@ async function thumbnailFromBuild(req, res) {
|
|||||||
if (!res.headersSent) res.sendStatus(500)
|
if (!res.headersSent) res.sendStatus(500)
|
||||||
} finally {
|
} finally {
|
||||||
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {})
|
||||||
|
if (deckTapeDir) {
|
||||||
|
await fs.rm(deckTapeDir, { recursive: true, force: true }).catch(() => {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user