diff --git a/services/clsi/app/js/CompileManager.js b/services/clsi/app/js/CompileManager.js
index 963fca401b..d6fea14e0a 100644
--- a/services/clsi/app/js/CompileManager.js
+++ b/services/clsi/app/js/CompileManager.js
@@ -318,8 +318,8 @@ async function doCompile(request, stats, timings) {
'done compile'
)
- if (request.compressPdf) {
- await _compressOutputPdf(compileDir)
+ if (request.compilePdfQuality && request.compilePdfQuality !== 'none') {
+ await _compressOutputPdf(compileDir, request.compilePdfQuality)
}
const { outputFiles, buildId } = await _saveOutputFiles({
@@ -449,7 +449,8 @@ async function clearExpiredProjects(maxCacheAgeMs) {
}
}
-async function _compressOutputPdf(compileDir) {
+async function _compressOutputPdf(compileDir, quality) {
+ const preset = quality === 'screen' ? '/screen' : '/ebook'
const src = Path.join(compileDir, 'output.pdf')
const tmp = Path.join(compileDir, 'output-gs-tmp.pdf')
const t0 = Date.now()
@@ -461,7 +462,7 @@ async function _compressOutputPdf(compileDir) {
'-q', '-dNOPAUSE', '-dBATCH',
'-sDEVICE=pdfwrite',
'-dCompatibilityLevel=1.7',
- '-dPDFSETTINGS=/ebook',
+ `-dPDFSETTINGS=${preset}`,
`-sOutputFile=${tmp}`,
src,
],
diff --git a/services/clsi/app/js/RequestParser.js b/services/clsi/app/js/RequestParser.js
index 05f2e4bf11..fecf4c7891 100644
--- a/services/clsi/app/js/RequestParser.js
+++ b/services/clsi/app/js/RequestParser.js
@@ -91,10 +91,10 @@ function parse(body, callback) {
default: false,
type: 'boolean',
})
- response.compressPdf = _parseAttribute(
- 'compressPdf',
- compile.options.compressPdf,
- { default: false, type: 'boolean' }
+ response.compilePdfQuality = _parseAttribute(
+ 'compilePdfQuality',
+ compile.options.compilePdfQuality,
+ { default: 'none', type: 'string' }
)
response.stopOnFirstError = _parseAttribute(
'stopOnFirstError',
diff --git a/services/web/app/src/Features/Compile/CompressedPdfController.mjs b/services/web/app/src/Features/Compile/CompressedPdfController.mjs
new file mode 100644
index 0000000000..e52e934df0
--- /dev/null
+++ b/services/web/app/src/Features/Compile/CompressedPdfController.mjs
@@ -0,0 +1,100 @@
+import { pipeline } from 'node:stream/promises'
+import { createWriteStream, createReadStream } from 'node:fs'
+import { unlink, mkdtemp } from 'node:fs/promises'
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+import { spawn } from 'node:child_process'
+import logger from '@overleaf/logger'
+import Settings from '@overleaf/settings'
+import { expressify } from '@overleaf/promise-utils'
+import SessionManager from '../Authentication/SessionManager.mjs'
+import ClsiManager from './ClsiManager.mjs'
+import ProjectGetter from '../Project/ProjectGetter.mjs'
+
+const GS_PRESETS = { ebook: '/ebook', screen: '/screen' }
+
+async function _compressStream(inputStream, quality) {
+ const preset = GS_PRESETS[quality] || '/ebook'
+ const dir = await mkdtemp(join(tmpdir(), 'verso-pdf-'))
+ const inPath = join(dir, 'input.pdf')
+ const outPath = join(dir, 'output.pdf')
+
+ try {
+ await pipeline(inputStream, createWriteStream(inPath))
+
+ await new Promise((resolve, reject) => {
+ const proc = spawn(
+ 'gs',
+ [
+ '-q', '-dNOPAUSE', '-dBATCH',
+ '-sDEVICE=pdfwrite',
+ '-dCompatibilityLevel=1.7',
+ `-dPDFSETTINGS=${preset}`,
+ `-sOutputFile=${outPath}`,
+ inPath,
+ ],
+ { stdio: 'ignore' }
+ )
+ proc.on('error', reject)
+ proc.on('close', code => {
+ if (code === 0) resolve()
+ else reject(new Error(`gs exited ${code}`))
+ })
+ })
+
+ return { stream: createReadStream(outPath), cleanup: () => Promise.all([unlink(inPath), unlink(outPath)]).catch(() => {}) }
+ } catch (err) {
+ await Promise.all([unlink(inPath).catch(() => {}), unlink(outPath).catch(() => {})])
+ throw err
+ }
+}
+
+async function downloadCompressedPdf(req, res) {
+ const projectId = req.params.Project_id
+ const buildId = req.params.build_id
+ const quality = req.query.quality
+ const clsiServerId = req.query.clsiserverid || null
+ const editorId = req.query.editorId || null
+
+ if (!GS_PRESETS[quality]) {
+ return res.status(400).send('Invalid quality. Use "ebook" or "screen".')
+ }
+
+ const userId = SessionManager.getLoggedInUserId(req.session)
+ const compileAsUser = Settings.disablePerUserCompiles ? undefined : userId
+
+ let cleanup = null
+ try {
+ const inputStream = await ClsiManager.promises.getOutputFileStream(
+ projectId,
+ compileAsUser,
+ clsiServerId,
+ buildId,
+ 'output.pdf'
+ )
+
+ const { stream, cleanup: _cleanup } = await _compressStream(inputStream, quality)
+ cleanup = _cleanup
+
+ const project = await ProjectGetter.promises.getProject(projectId, { name: 1 })
+ const safeName = (project?.name || 'document')
+ .replace(/[^\p{L}\p{Nd}]/gu, '_')
+
+ res.setHeader('Content-Type', 'application/pdf')
+ res.setHeader('Cache-Control', 'no-store')
+ res.setHeader('Content-Disposition', `attachment; filename="${safeName}.pdf"`)
+
+ await pipeline(stream, res)
+ } catch (err) {
+ logger.error({ err, projectId, quality }, 'compressed pdf download failed')
+ if (!res.headersSent) {
+ res.status(500).send('Compression failed. Please try compiling first.')
+ }
+ } finally {
+ if (cleanup) await cleanup()
+ }
+}
+
+export default {
+ downloadCompressedPdf: expressify(downloadCompressedPdf),
+}
diff --git a/services/web/app/src/router.mjs b/services/web/app/src/router.mjs
index 3fed51f239..61ed9c4b3b 100644
--- a/services/web/app/src/router.mjs
+++ b/services/web/app/src/router.mjs
@@ -5,6 +5,7 @@ import ProjectController from './Features/Project/ProjectController.mjs'
import ProjectApiController from './Features/Project/ProjectApiController.mjs'
import PublishedPresentationController from './Features/PublishedPresentation/PublishedPresentationController.mjs'
import PresentationExportController from './Features/Compile/PresentationExportController.mjs'
+import CompressedPdfController from './Features/Compile/CompressedPdfController.mjs'
import PythonRequirementsController from './Features/Compile/PythonRequirementsController.mjs'
import ProjectListController from './Features/Project/ProjectListController.mjs'
import SpellingController from './Features/Spelling/SpellingController.mjs'
@@ -671,6 +672,13 @@ async function initialize(webRouter, privateApiRouter, publicApiRouter) {
CompileController.downloadPdf
)
+ // On-demand GS-compressed PDF download (quality=ebook|screen)
+ webRouter.get(
+ '/download/project/:Project_id/build/:build_id/output/output.pdf/compressed',
+ AuthorizationMiddleware.ensureUserCanReadProject,
+ CompressedPdfController.downloadCompressedPdf
+ )
+
// Align with limits defined in CompileController.downloadPdf
const rateLimiterMiddlewareOutputFiles = RateLimiterMiddleware.rateLimit(
rateLimiters.miscOutputDownload,
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index d9339ec12d..f880b8e469 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -2289,9 +2289,17 @@
"typst_preview_mode": "",
"typst_preview_pdf": "",
"typst_preview_wasm": "",
- "pdf_size": "",
- "pdf_size_normal": "",
- "pdf_size_compressed": "",
+ "pdf_quality": "",
+ "pdf_quality_normal": "",
+ "pdf_quality_compressed": "",
+ "pdf_quality_draft": "",
+ "pdf_download_quality_label": "",
+ "pdf_download_normal_desc": "",
+ "pdf_download_ebook_desc": "",
+ "pdf_download_screen_desc": "",
+ "pdf_download_ghostscript_note": "",
+ "pdf_download_compression_failed": "",
+ "downloading": "",
"experimental": "",
"typst_wasm_error": "",
"typst_wasm_loading": "",
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx
index 731961215a..2ce4cf6f8c 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-compile-button.tsx
@@ -38,11 +38,11 @@ function PdfCompileButton() {
animateCompileDropdownArrow,
autoCompile,
compiling,
- compressPdf,
+ compilePdfQuality,
draft,
hasChanges,
setAutoCompile,
- setCompressPdf,
+ setCompilePdfQuality,
setDraft,
setStopOnValidationError,
stopOnFirstError,
@@ -203,27 +203,38 @@ function PdfCompileButton() {
- {t('pdf_size')}
+ {t('pdf_quality')}
- sendEventAndSet(false, setCompressPdf, 'pdf-size')
+ sendEventAndSet('none', setCompilePdfQuality, 'pdf-quality')
}
- trailingIcon={!compressPdf ? 'check' : null}
+ trailingIcon={compilePdfQuality === 'none' ? 'check' : null}
>
- {t('pdf_size_normal')}
+ {t('pdf_quality_normal')}
- sendEventAndSet(true, setCompressPdf, 'pdf-size')
+ sendEventAndSet('ebook', setCompilePdfQuality, 'pdf-quality')
}
- trailingIcon={compressPdf ? 'check' : null}
+ trailingIcon={compilePdfQuality === 'ebook' ? 'check' : null}
>
- {t('pdf_size_compressed')}
+ {t('pdf_quality_compressed')}
+
+
+
+
+ sendEventAndSet('screen', setCompilePdfQuality, 'pdf-quality')
+ }
+ trailingIcon={compilePdfQuality === 'screen' ? 'check' : null}
+ >
+ {t('pdf_quality_draft')}
>
diff --git a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
index 0c9eb21a40..42b38f3b46 100644
--- a/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
+++ b/services/web/frontend/js/features/pdf-preview/components/pdf-hybrid-download-button.tsx
@@ -23,6 +23,7 @@ import {
import LoadingSpinner from '@/shared/components/loading-spinner'
type ExportFormat = 'html' | 'pdf'
+type PdfQuality = 'normal' | 'ebook' | 'screen'
function filenameFromDisposition(disposition: string | null, ext: string) {
const match = disposition?.match(/filename="?([^"]+)"?/)
@@ -44,6 +45,58 @@ function PdfHybridDownloadButton() {
// after they've closed it doesn't pop the modal back open.
const requestIdRef = useRef(0)
+ // Compressed PDF download modal (for regular PDF, not presentations)
+ const [showDownloadModal, setShowDownloadModal] = useState(false)
+ const [downloadQuality, setDownloadQuality] = useState('normal')
+ const [downloading, setDownloading] = useState(false)
+ const [downloadError, setDownloadError] = useState(null)
+
+ const startCompressedDownload = useCallback(async () => {
+ if (!pdfDownloadUrl) return
+ if (downloadQuality === 'normal') {
+ // Direct download — no server-side compression needed
+ const link = document.createElement('a')
+ link.href = pdfDownloadUrl
+ link.download = ''
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ setShowDownloadModal(false)
+ return
+ }
+ setDownloading(true)
+ setDownloadError(null)
+ try {
+ // Derive the compressed URL from the existing pdfDownloadUrl by appending /compressed
+ const compressedUrl = pdfDownloadUrl.replace(
+ /\/output\.pdf(\?|$)/,
+ `/output.pdf/compressed$1`
+ ) + (pdfDownloadUrl.includes('?') ? '&' : '?') + `quality=${downloadQuality}`
+ const response = await fetch(compressedUrl, { credentials: 'same-origin' })
+ if (!response.ok) {
+ setDownloadError(t('pdf_download_compression_failed'))
+ return
+ }
+ const blob = await response.blob()
+ const disposition = response.headers.get('Content-Disposition')
+ const match = disposition?.match(/filename="?([^"]+)"?/)
+ const filename = match ? match[1] : 'document.pdf'
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = filename
+ document.body.appendChild(link)
+ link.click()
+ link.remove()
+ URL.revokeObjectURL(url)
+ setShowDownloadModal(false)
+ } catch (err) {
+ setDownloadError(err instanceof Error ? err.message : String(err))
+ } finally {
+ setDownloading(false)
+ }
+ }, [pdfDownloadUrl, downloadQuality, t])
+
const dismiss = useCallback(() => {
requestIdRef.current += 1
setExporting(null)
@@ -179,42 +232,103 @@ function PdfHybridDownloadButton() {
? t('download_pdf')
: t('please_compile_pdf_before_download')
- function handleOnClick(e: React.MouseEvent) {
- const event = e as React.MouseEvent
- if (event.currentTarget.dataset.disabled === 'true') {
- event.preventDefault()
- return
- }
-
- sendEvent('download-pdf-button-click', {
- projectId,
- location: 'pdf-preview',
- isSmallDevice,
- })
- }
-
return (
-
-
+
-
-
-
+ {
+ if (!pdfDownloadUrl) return
+ sendEvent('download-pdf-button-click', {
+ projectId,
+ location: 'pdf-preview',
+ isSmallDevice,
+ })
+ setShowDownloadModal(true)
+ }}
+ variant="link"
+ className="pdf-toolbar-btn"
+ draggable={false}
+ data-disabled={!pdfDownloadUrl}
+ disabled={!pdfDownloadUrl}
+ style={{ pointerEvents: 'auto' }}
+ aria-label={t('download_pdf')}
+ >
+
+
+
+
+ {
+ setShowDownloadModal(false)
+ setDownloadError(null)
+ }}
+ >
+
+ {t('download_pdf')}
+
+
+ {t('pdf_download_quality_label')}
+
+ {(
+ [
+ { value: 'normal', label: t('pdf_quality_normal'), desc: t('pdf_download_normal_desc') },
+ { value: 'ebook', label: t('pdf_quality_compressed'), desc: t('pdf_download_ebook_desc') },
+ { value: 'screen', label: t('pdf_quality_draft'), desc: t('pdf_download_screen_desc') },
+ ] as { value: PdfQuality; label: string; desc: string }[]
+ ).map(opt => (
+
+ ))}
+
+ {downloadQuality !== 'normal' && (
+
+ {t('pdf_download_ghostscript_note')}
+
+ )}
+ {downloadError && (
+ {downloadError}
+ )}
+
+
+ {
+ setShowDownloadModal(false)
+ setDownloadError(null)
+ }}
+ >
+ {t('cancel')}
+
+
+ {t('download')}
+
+
+
+ >
)
}
diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.ts b/services/web/frontend/js/features/pdf-preview/util/compiler.ts
index 5883506aa9..c89ee98ac0 100644
--- a/services/web/frontend/js/features/pdf-preview/util/compiler.ts
+++ b/services/web/frontend/js/features/pdf-preview/util/compiler.ts
@@ -147,7 +147,7 @@ export default class DocumentCompiler {
// if there was previously a server error
incrementalCompilesEnabled: !this.error,
stopOnFirstError: options.stopOnFirstError,
- compressPdf: options.compressPdf,
+ compilePdfQuality: options.compilePdfQuality,
editorId: EDITOR_SESSION_ID,
}
diff --git a/services/web/frontend/js/shared/context/detach-compile-context.tsx b/services/web/frontend/js/shared/context/detach-compile-context.tsx
index 5ddb8db0a9..814d32fb35 100644
--- a/services/web/frontend/js/shared/context/detach-compile-context.tsx
+++ b/services/web/frontend/js/shared/context/detach-compile-context.tsx
@@ -81,8 +81,8 @@ export const DetachCompileProvider: FC = ({
changedAt: _changedAt,
typstPreviewMode: _typstPreviewMode,
setTypstPreviewMode: _setTypstPreviewMode,
- compressPdf: _compressPdf,
- setCompressPdf: _setCompressPdf,
+ compilePdfQuality: _compilePdfQuality,
+ setCompilePdfQuality: _setCompilePdfQuality,
} = localCompileContext
const [animateCompileDropdownArrow] = useDetachStateWatcher(
@@ -449,15 +449,15 @@ export const DetachCompileProvider: FC = ({
'detacher'
)
- const [compressPdf] = useDetachStateWatcher(
- 'compressPdf',
- _compressPdf,
+ const [compilePdfQuality] = useDetachStateWatcher(
+ 'compilePdfQuality',
+ _compilePdfQuality,
'detacher',
'detached'
)
- const setCompressPdf = useDetachAction(
- 'setCompressPdf',
- _setCompressPdf,
+ const setCompilePdfQuality = useDetachAction(
+ 'setCompilePdfQuality',
+ _setCompilePdfQuality,
'detached',
'detacher'
)
@@ -530,8 +530,8 @@ export const DetachCompileProvider: FC = ({
changedAt,
typstPreviewMode,
setTypstPreviewMode,
- compressPdf,
- setCompressPdf,
+ compilePdfQuality,
+ setCompilePdfQuality,
}),
[
animateCompileDropdownArrow,
@@ -595,8 +595,8 @@ export const DetachCompileProvider: FC = ({
changedAt,
typstPreviewMode,
setTypstPreviewMode,
- compressPdf,
- setCompressPdf,
+ compilePdfQuality,
+ setCompilePdfQuality,
]
)
diff --git a/services/web/frontend/js/shared/context/local-compile-context.tsx b/services/web/frontend/js/shared/context/local-compile-context.tsx
index b3af09bb3d..95869496bf 100644
--- a/services/web/frontend/js/shared/context/local-compile-context.tsx
+++ b/services/web/frontend/js/shared/context/local-compile-context.tsx
@@ -91,8 +91,8 @@ export type CompileContext = {
pdfViewer?: string
position?: PdfScrollPosition
rawLog?: string
- compressPdf: boolean
- setCompressPdf: (value: boolean) => void
+ compilePdfQuality: 'none' | 'ebook' | 'screen'
+ setCompilePdfQuality: (value: 'none' | 'ebook' | 'screen') => void
setAutoCompile: (value: boolean) => void
setDraft: (value: any) => void
smoothPdfTransition: boolean
@@ -280,12 +280,10 @@ export const LocalCompileProvider: FC = ({
listen: true,
})
- // whether to compress the PDF with ghostscript (for low-bandwidth connections)
- const [compressPdf, setCompressPdf] = usePersistedState(
- `compress_pdf:${projectId}`,
- false,
- { listen: true }
- )
+ // PDF quality for compile-time GS compression ('none' | 'ebook' | 'screen')
+ const [compilePdfQuality, setCompilePdfQuality] = usePersistedState<
+ 'none' | 'ebook' | 'screen'
+ >(`pdf_quality:${projectId}`, 'none', { listen: true })
// whether compiling should stop on first error
const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
@@ -422,10 +420,10 @@ export const LocalCompileProvider: FC = ({
compiler.setOption('draft', draft)
}, [compiler, draft])
- // keep compressPdf setting in sync with the compiler
+ // keep compilePdfQuality setting in sync with the compiler
useEffect(() => {
- compiler.setOption('compressPdf', compressPdf)
- }, [compiler, compressPdf])
+ compiler.setOption('compilePdfQuality', compilePdfQuality)
+ }, [compiler, compilePdfQuality])
// keep stop on first error setting in sync with the compiler
useEffect(() => {
@@ -875,7 +873,7 @@ export const LocalCompileProvider: FC = ({
setAnimateCompileDropdownArrow,
setAutoCompile,
setCompiling,
- setCompressPdf,
+ setCompilePdfQuality,
setDraft,
setError,
setHasLintingError, // only for stories
@@ -910,8 +908,8 @@ export const LocalCompileProvider: FC = ({
changedAt,
typstPreviewMode,
setTypstPreviewMode,
- compressPdf,
- setCompressPdf,
+ compilePdfQuality,
+ setCompilePdfQuality,
}),
[
animateCompileDropdownArrow,
@@ -921,7 +919,7 @@ export const LocalCompileProvider: FC = ({
clsiServerId,
codeCheckFailed,
compiling,
- compressPdf,
+ compilePdfQuality,
deliveryLatencies,
draft,
editedSinceCompileStarted,
@@ -941,7 +939,7 @@ export const LocalCompileProvider: FC = ({
recompileFromScratch,
setAnimateCompileDropdownArrow,
setAutoCompile,
- setCompressPdf,
+ setCompilePdfQuality,
setDraft,
setError,
setHasLintingError, // only for stories
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 154a550a03..d122147402 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -2923,9 +2923,17 @@
"typst_preview_mode": "Preview mode",
"typst_preview_pdf": "PDF (server)",
"typst_preview_wasm": "Live (browser)",
- "pdf_size": "PDF size",
- "pdf_size_normal": "Normal",
- "pdf_size_compressed": "Compressed (low bandwidth)",
+ "pdf_quality": "PDF quality",
+ "pdf_quality_normal": "Normal",
+ "pdf_quality_compressed": "Compressed",
+ "pdf_quality_draft": "Draft",
+ "pdf_download_quality_label": "Choose the quality for this download:",
+ "pdf_download_normal_desc": "Full quality, original file size.",
+ "pdf_download_ebook_desc": "Reduced file size, 150 dpi images. Good for sharing.",
+ "pdf_download_screen_desc": "Smallest file size, 72 dpi images. For low-bandwidth or slow devices.",
+ "pdf_download_ghostscript_note": "Compression is applied server-side using Ghostscript.",
+ "pdf_download_compression_failed": "Compression failed. Please try again.",
+ "downloading": "Downloading",
"experimental": "experimental",
"typst_wasm_error": "Preview error",
"typst_wasm_loading": "Loading Typst compiler…",
diff --git a/services/web/locales/fr.json b/services/web/locales/fr.json
index 8a13996e07..0d52b2b790 100644
--- a/services/web/locales/fr.json
+++ b/services/web/locales/fr.json
@@ -2928,9 +2928,17 @@
"typst_preview_mode": "Mode de prévisualisation",
"typst_preview_pdf": "PDF (serveur)",
"typst_preview_wasm": "Direct (navigateur)",
- "pdf_size": "Taille du PDF",
- "pdf_size_normal": "Normale",
- "pdf_size_compressed": "Compressé (faible débit)",
+ "pdf_quality": "Qualité du PDF",
+ "pdf_quality_normal": "Normale",
+ "pdf_quality_compressed": "Compressé",
+ "pdf_quality_draft": "Brouillon",
+ "pdf_download_quality_label": "Choisissez la qualité pour ce téléchargement :",
+ "pdf_download_normal_desc": "Qualité maximale, taille de fichier originale.",
+ "pdf_download_ebook_desc": "Taille réduite, images à 150 dpi. Idéal pour le partage.",
+ "pdf_download_screen_desc": "Taille minimale, images à 72 dpi. Pour connexions lentes ou appareils peu puissants.",
+ "pdf_download_ghostscript_note": "La compression est appliquée côté serveur via Ghostscript.",
+ "pdf_download_compression_failed": "La compression a échoué. Veuillez réessayer.",
+ "downloading": "Téléchargement en cours",
"experimental": "expérimental",
"typst_wasm_error": "Erreur de prévisualisation",
"typst_wasm_loading": "Chargement du compilateur Typst…",
diff --git a/services/web/types/compile.ts b/services/web/types/compile.ts
index 544968ee39..f3797b0c1c 100644
--- a/services/web/types/compile.ts
+++ b/services/web/types/compile.ts
@@ -81,7 +81,7 @@ export type CompileResponseData = {
export type CompileOptions = {
draft?: boolean
stopOnFirstError?: boolean
- compressPdf?: boolean
+ compilePdfQuality?: 'none' | 'ebook' | 'screen'
isAutoCompileOnLoad?: boolean
isAutoCompileOnChange?: boolean
rootResourcePath?: string