mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +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
200 lines
5.5 KiB
JavaScript
200 lines
5.5 KiB
JavaScript
/* eslint-disable
|
|
no-return-assign,
|
|
*/
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
// Fix any style issues and re-enable lint.
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
import UrlFetcher from './UrlFetcher.js'
|
|
|
|
import Settings from '@overleaf/settings'
|
|
import fs from 'node:fs'
|
|
import Path from 'node:path'
|
|
import { callbackify } from 'node:util'
|
|
import Metrics from '@overleaf/metrics'
|
|
|
|
const PENDING_DOWNLOADS = new Map()
|
|
|
|
function getProjectDir(projectId) {
|
|
return Path.join(Settings.path.clsiCacheDir, projectId)
|
|
}
|
|
|
|
function getCachePath(projectId, url, lastModified) {
|
|
// The url is a filestore URL.
|
|
// It is sufficient to look at the path and mtime for uniqueness.
|
|
const mtime = (lastModified && lastModified.getTime()) || 0
|
|
const key = new URL(url).pathname.replace(/\//g, '-') + '-' + mtime
|
|
return Path.join(getProjectDir(projectId), key)
|
|
}
|
|
|
|
async function clearProject(projectId, options) {
|
|
const timer = new Metrics.Timer('url_cache', {
|
|
status: options?.reason || 'unknown',
|
|
path: 'delete',
|
|
})
|
|
await fs.promises.rm(getProjectDir(projectId), {
|
|
force: true,
|
|
recursive: true,
|
|
})
|
|
timer.done()
|
|
}
|
|
|
|
async function createProjectDir(projectId) {
|
|
await fs.promises.mkdir(getProjectDir(projectId), { recursive: true })
|
|
}
|
|
|
|
async function tryCopyFile(src, dest) {
|
|
try {
|
|
await fs.promises.copyFile(src, dest)
|
|
return true
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') return false
|
|
throw err
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copy a url's file from the cache into destPath, downloading it first if it is
|
|
* not cached yet.
|
|
*
|
|
* When `conversionSuffix` is set, the optimized result is cached alongside the
|
|
* original at `<cachePath>.opt`. The file is downloaded to a per-user conversion
|
|
* path and NOT copied to destPath: the caller is expected to process it (e.g.
|
|
* convert a slow png to pdf) and then commit it with commitConversion (which
|
|
* renames it onto `<cachePath>.opt`). In that case a
|
|
* `{ conversionPath, cachePath, destPath }` handle is returned for the files that
|
|
* were freshly downloaded (a cache hit returns undefined, as the already-final
|
|
* file has been copied to destPath).
|
|
*
|
|
* @param {string} projectId
|
|
* @param {string} url
|
|
* @param {string|null} fallbackURL
|
|
* @param {string} destPath
|
|
* @param {Date} lastModified
|
|
* @param {string} [conversionSuffix] when set the download is deferred to a
|
|
* conversion path for processing + commitConversion instead of being copied
|
|
* to destPath
|
|
* @return {Promise<{ conversionPath: string, cachePath: string, destPath: string }|undefined>}
|
|
*/
|
|
async function downloadUrlToFile(
|
|
projectId,
|
|
url,
|
|
fallbackURL,
|
|
destPath,
|
|
lastModified,
|
|
conversionSuffix = ''
|
|
) {
|
|
// When converting, the optimized result is cached alongside the original at
|
|
// `<cachePath>.opt`, so switching modes can reuse either variant.
|
|
const cachePath =
|
|
getCachePath(projectId, url, lastModified) +
|
|
(conversionSuffix ? '.opt' : '')
|
|
|
|
{
|
|
const timer = new Metrics.Timer('url_cache', {
|
|
status: 'cache-hit',
|
|
path: 'copy',
|
|
})
|
|
let copied = await tryCopyFile(cachePath, destPath)
|
|
if (!copied && fallbackURL) {
|
|
copied = await tryCopyFile(
|
|
getCachePath(projectId, fallbackURL, lastModified) +
|
|
(conversionSuffix ? '.opt' : ''),
|
|
destPath
|
|
)
|
|
}
|
|
// the metric is only updated if the file is present in the cache
|
|
if (copied) {
|
|
timer.done()
|
|
return
|
|
}
|
|
}
|
|
|
|
const conversionPath = cachePath + conversionSuffix
|
|
{
|
|
const timer = new Metrics.Timer('url_cache', {
|
|
status: 'cache-miss',
|
|
path: 'download',
|
|
})
|
|
try {
|
|
await download(url, fallbackURL, conversionPath)
|
|
} finally {
|
|
timer.done()
|
|
}
|
|
}
|
|
|
|
if (conversionSuffix) {
|
|
return { conversionPath, cachePath, destPath }
|
|
}
|
|
|
|
{
|
|
const timer = new Metrics.Timer('url_cache', {
|
|
status: 'cache-miss',
|
|
path: 'copy',
|
|
})
|
|
await fs.promises.copyFile(cachePath, destPath)
|
|
timer.done()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download a url to `targetPath`. UrlFetcher writes atomically (to its own
|
|
* `targetPath + '~'` then renames onto targetPath). Concurrent downloads of the
|
|
* same target are deduplicated.
|
|
*/
|
|
async function download(url, fallbackURL, targetPath) {
|
|
let pending = PENDING_DOWNLOADS.get(targetPath)
|
|
if (pending) {
|
|
return pending
|
|
}
|
|
|
|
pending = UrlFetcher.promises.pipeUrlToFileWithRetry(
|
|
url,
|
|
fallbackURL,
|
|
targetPath
|
|
)
|
|
PENDING_DOWNLOADS.set(targetPath, pending)
|
|
try {
|
|
await pending
|
|
} finally {
|
|
PENDING_DOWNLOADS.delete(targetPath)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Commit a converted cache file: atomically rename it onto the cache path, then
|
|
* copy it to the destination.
|
|
*
|
|
* @param {string} conversionPath
|
|
* @param {string} cachePath
|
|
* @param {string} destPath
|
|
* @return {Promise<void>}
|
|
*/
|
|
async function commitConversion(conversionPath, cachePath, destPath) {
|
|
await fs.promises.rename(conversionPath, cachePath)
|
|
const timer = new Metrics.Timer('url_cache', {
|
|
status: 'cache-miss',
|
|
path: 'copy',
|
|
})
|
|
await fs.promises.copyFile(cachePath, destPath)
|
|
timer.done()
|
|
}
|
|
|
|
export default {
|
|
clearProject: callbackify(clearProject),
|
|
createProjectDir: callbackify(createProjectDir),
|
|
downloadUrlToFile: callbackify(downloadUrlToFile),
|
|
getProjectCacheDir: getProjectDir,
|
|
promises: {
|
|
clearProject,
|
|
createProjectDir,
|
|
downloadUrlToFile,
|
|
commitConversion,
|
|
},
|
|
}
|