mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-13 05:33:43 +01:00
* Add png to pdf converter for optimising image loading in pdftex * refactor * move fixtures * refactor and add unit tests * Remove gamma correction from PNG processing and update related tests * Add crc32fast dependency and enhance PNG processing tests * Add fixture test cases for PNG processing * fix alpha channel to match pdftex pdftex always uses an 8 bit alpha channel in the output even for 16 bit images * Refactor PNG processing to use Option for alpha stream and update related logic * Enhance PDF writing by adding DecodeParms for alpha stream and optimize PNG encoding process * Add pixel dimensions support in PNG processing and PDF writing * [copybara] move clsi excludes * [clsi] move gitignore rules * [png2pdf] move into dockerfiles folder * [png2pdf] use atomic writes * [clsi] integrate png2pdf * [saas-e2e] add coverage for png2pdf * [png2pdf] make compatible with seccomp profile in clsi * [copybara] remove exclude following move of png2pdf * [copybara] remove spurious excludes * fix broken detection of malformed chunk headers Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * [project-history] add build script flag for depending on doc-updater types * [png2pdf] run tests as part of clsi pipeline * [png2pdf] fix calculation of PDF page size * [web] put png2pdf behind feature flag and make it opt-in * [web] fix ClsiManager.promises.sendRequest invocation after merge * extend is_copy_safe function to follow pdftex see the conditions in pdftex's writepng.c https://svn.tug.org:8369/texlive/trunk/Build/source/texk/web2c/pdftexdir/writepng.c?revision=78081&view=markup#l575 the criteria are not an exact match because the png crate does not support all the png chunk types https://github.com/image-rs/image-png/issues/116 if the criteria result in a png being converted or skipped the final result should be the same. in one case the conversion will be done by pdftex (at runtime) in the other case the conversion will be done by png2pdf - both conversions should give the same result. * include tests in png2pdf Dockerfile * [clsi] explicitly disable png2pdf conversion for project conversion * [clsi] png2pdf: count converted files * [png2pdf] bump date in docker image tag * [web] make draft-setting option wider when png2pdf is available --------- Co-authored-by: Brian Gough <brian.gough@overleaf.com> GitOrigin-RevId: d0d952ee009bf93c72bba2ffe763ae0160d396e4
262 lines
8.1 KiB
TypeScript
262 lines
8.1 KiB
TypeScript
import getMeta from '../../../utils/meta'
|
|
import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
|
|
import { debounce, DebouncedFunc } from 'lodash'
|
|
import { EDITOR_SESSION_ID, trackPdfDownload } from './metrics'
|
|
import { enablePdfCaching } from './pdf-caching-flags'
|
|
import { debugConsole } from '@/utils/debugging'
|
|
import { signalWithTimeout } from '@/utils/abort-signal'
|
|
import { Dispatch, SetStateAction } from 'react'
|
|
import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
|
|
import { CompileOptions, CompileResponseData } from '@ol-types/compile'
|
|
import { DeliveryLatencies } from './types'
|
|
import { RootDocInfo } from '@/shared/hooks/use-root-doc'
|
|
|
|
const AUTO_COMPILE_MAX_WAIT = 5000
|
|
// We add a 2 second debounce to sending user changes to server if they aren't
|
|
// collaborating with anyone. This needs to be higher than SINGLE_USER_FLUSH_DELAY, and allow for
|
|
// client to server latency, otherwise we compile before the op reaches the server
|
|
// and then again on ack.
|
|
const AUTO_COMPILE_DEBOUNCE = 2500
|
|
|
|
// If there is a pending op, wait for it to be saved before compiling
|
|
const PENDING_OP_MAX_WAIT = 10000
|
|
|
|
const searchParams = new URLSearchParams(window.location.search)
|
|
|
|
export default class DocumentCompiler {
|
|
compilingRef: React.MutableRefObject<boolean>
|
|
projectId: string
|
|
setChangedAt: Dispatch<SetStateAction<number>>
|
|
setCompiling: Dispatch<SetStateAction<boolean>>
|
|
setData: Dispatch<SetStateAction<CompileResponseData | undefined>>
|
|
setFirstRenderDone: Dispatch<SetStateAction<() => void>>
|
|
setDeliveryLatencies: Dispatch<SetStateAction<DeliveryLatencies>>
|
|
setError: Dispatch<SetStateAction<string | undefined>>
|
|
cleanupCompileResult: () => void
|
|
signal: AbortSignal
|
|
openDocs: OpenDocuments
|
|
clsiServerId: string | null
|
|
error: Error | undefined
|
|
timer: number
|
|
defaultOptions: CompileOptions
|
|
debouncedAutoCompile: DebouncedFunc<() => void>
|
|
getRootDocInfo: () => RootDocInfo
|
|
|
|
constructor({
|
|
compilingRef,
|
|
projectId,
|
|
setChangedAt,
|
|
setCompiling,
|
|
setData,
|
|
setFirstRenderDone,
|
|
setDeliveryLatencies,
|
|
setError,
|
|
cleanupCompileResult,
|
|
signal,
|
|
openDocs,
|
|
getRootDocInfo,
|
|
}: {
|
|
compilingRef: React.MutableRefObject<boolean>
|
|
projectId: string
|
|
setChangedAt: Dispatch<SetStateAction<number>>
|
|
setCompiling: Dispatch<SetStateAction<boolean>>
|
|
setData: Dispatch<SetStateAction<CompileResponseData | undefined>>
|
|
setFirstRenderDone: Dispatch<SetStateAction<() => void>>
|
|
setDeliveryLatencies: Dispatch<SetStateAction<DeliveryLatencies>>
|
|
setError: Dispatch<SetStateAction<string | undefined>>
|
|
cleanupCompileResult: () => void
|
|
signal: AbortSignal
|
|
openDocs: OpenDocuments
|
|
getRootDocInfo: () => RootDocInfo
|
|
}) {
|
|
this.compilingRef = compilingRef
|
|
this.projectId = projectId
|
|
this.setChangedAt = setChangedAt
|
|
this.setCompiling = setCompiling
|
|
this.setData = setData
|
|
this.setFirstRenderDone = setFirstRenderDone
|
|
this.setDeliveryLatencies = setDeliveryLatencies
|
|
this.setError = setError
|
|
this.cleanupCompileResult = cleanupCompileResult
|
|
this.signal = signal
|
|
this.openDocs = openDocs
|
|
this.getRootDocInfo = getRootDocInfo
|
|
|
|
this.clsiServerId = null
|
|
this.error = undefined
|
|
this.timer = 0
|
|
this.defaultOptions = {
|
|
draft: false,
|
|
png2pdf: false,
|
|
stopOnFirstError: false,
|
|
}
|
|
|
|
this.debouncedAutoCompile = debounce(
|
|
() => {
|
|
this.compile({ isAutoCompileOnChange: true })
|
|
},
|
|
AUTO_COMPILE_DEBOUNCE,
|
|
{
|
|
maxWait: AUTO_COMPILE_MAX_WAIT,
|
|
}
|
|
)
|
|
}
|
|
|
|
// The main "compile" function.
|
|
// Call this directly to run a compile now, otherwise call debouncedAutoCompile.
|
|
async compile(options: CompileOptions = {}) {
|
|
options = { ...this.defaultOptions, ...options }
|
|
|
|
if (options.isAutoCompileOnLoad && getMeta('ol-preventCompileOnLoad')) {
|
|
return
|
|
}
|
|
|
|
// set "compiling" to true (in the React component's state), and return if it was already true
|
|
const wasCompiling = this.compilingRef.current
|
|
this.setCompiling(true)
|
|
|
|
if (wasCompiling) {
|
|
if (options.isAutoCompileOnChange) {
|
|
this.debouncedAutoCompile()
|
|
}
|
|
return
|
|
}
|
|
|
|
try {
|
|
await this.openDocs.awaitBufferedOps(
|
|
signalWithTimeout(this.signal, PENDING_OP_MAX_WAIT)
|
|
)
|
|
|
|
// reset values
|
|
this.setChangedAt(0) // TODO: wait for doc:saved?
|
|
|
|
const params = this.buildCompileParams(options)
|
|
|
|
const t0 = performance.now()
|
|
|
|
const { rootDocId, rootResourcePath } = this.getRootDocInfo()
|
|
|
|
const body = {
|
|
rootDoc_id: rootDocId,
|
|
rootResourcePath,
|
|
draft: options.draft,
|
|
png2pdf: options.png2pdf,
|
|
check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
|
|
// use incremental compile for all users but revert to a full compile
|
|
// if there was previously a server error
|
|
incrementalCompilesEnabled: !this.error,
|
|
stopOnFirstError: options.stopOnFirstError,
|
|
editorId: EDITOR_SESSION_ID,
|
|
}
|
|
|
|
const data: CompileResponseData = await postJSON(
|
|
`/project/${this.projectId}/compile?${params}`,
|
|
{ body, signal: this.signal }
|
|
)
|
|
|
|
const compileTimeClientE2E = Math.ceil(performance.now() - t0)
|
|
const { deliveryLatencies, firstRenderDone } = trackPdfDownload(
|
|
data,
|
|
compileTimeClientE2E,
|
|
t0
|
|
)
|
|
this.setDeliveryLatencies(() => deliveryLatencies)
|
|
this.setFirstRenderDone(() => firstRenderDone)
|
|
|
|
// unset the error before it's set again later, so that components are recreated and events are tracked
|
|
this.setError(undefined)
|
|
|
|
data.options = {
|
|
...options,
|
|
rootResourcePath,
|
|
}
|
|
data.rootDocId = rootDocId
|
|
if (data.clsiServerId) {
|
|
this.clsiServerId = data.clsiServerId
|
|
}
|
|
this.setData(data)
|
|
} catch (error: any) {
|
|
debugConsole.error(error)
|
|
this.cleanupCompileResult()
|
|
this.setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
|
|
} finally {
|
|
this.setCompiling(false)
|
|
}
|
|
}
|
|
|
|
// build the query parameters added to post-compile requests
|
|
buildPostCompileParams() {
|
|
const params = new URLSearchParams()
|
|
|
|
// the id of the CLSI server that processed the previous compile request
|
|
if (this.clsiServerId) {
|
|
params.set('clsiserverid', this.clsiServerId)
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
// build the query parameters for the compile request
|
|
buildCompileParams(options: CompileOptions) {
|
|
const params = new URLSearchParams()
|
|
|
|
// note: no clsiserverid query param is set on "compile" requests,
|
|
// as this is added in the backend by the web api
|
|
|
|
// tell the server whether this is an automatic or manual compile request
|
|
if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
|
|
params.set('auto_compile', 'true')
|
|
}
|
|
|
|
// use the feature flag to enable PDF caching
|
|
if (enablePdfCaching) {
|
|
params.set('enable_pdf_caching', 'true')
|
|
}
|
|
|
|
// use the feature flag to enable "file line errors"
|
|
if (searchParams.get('file_line_errors') === 'true') {
|
|
params.set('file_line_errors', 'true')
|
|
}
|
|
|
|
return params
|
|
}
|
|
|
|
// send a request to stop the current compile
|
|
stopCompile() {
|
|
// NOTE: no stoppingCompile state, as this should happen fairly quickly
|
|
// and doesn't matter if it runs twice.
|
|
|
|
const params = this.buildPostCompileParams()
|
|
|
|
return postJSON(`/project/${this.projectId}/compile/stop?${params}`, {
|
|
signal: this.signal,
|
|
})
|
|
.catch(error => {
|
|
debugConsole.error(error)
|
|
this.setError('error')
|
|
})
|
|
.finally(() => {
|
|
this.setCompiling(false)
|
|
})
|
|
}
|
|
|
|
// send a request to clear the cache
|
|
clearCache() {
|
|
const params = this.buildPostCompileParams()
|
|
|
|
return deleteJSON(`/project/${this.projectId}/output?${params}`, {
|
|
signal: this.signal,
|
|
}).catch(error => {
|
|
debugConsole.error(error)
|
|
this.setError('clear-cache')
|
|
})
|
|
}
|
|
|
|
setOption<Key extends keyof CompileOptions>(
|
|
option: Key,
|
|
value: CompileOptions[Key]
|
|
) {
|
|
this.defaultOptions[option] = value
|
|
}
|
|
}
|