diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index dbc74c8a56..9b216ad82e 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -690,7 +690,6 @@
"fair_usage_policy_applies": "",
"fast": "",
"fast_draft": "",
- "fast_optimize_images": "",
"feature_controls": "",
"feature_disabled_by_organization_contact_support": "",
"feature_enabled_or_disabled": "",
@@ -1399,6 +1398,7 @@
"open_project": "",
"open_settings": "",
"open_target": "",
+ "optimise_images_recommended": "",
"optional": "",
"or": "",
"or_enable_clipboard_access_in_your_browser_settings": "",
@@ -1937,6 +1937,7 @@
"sort_projects": "",
"speak": "",
"speech_input_not_available": "",
+ "speed_up_compiles_by_reusing_images": "",
"spellcheck": "",
"spellcheck_language": "",
"spelling_and_language": "",
diff --git a/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx b/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx
index 98b4c07fe4..ee89f5eba5 100644
--- a/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx
+++ b/services/web/frontend/js/features/ide-settings/components/compiler-settings/draft-setting.tsx
@@ -1,26 +1,17 @@
-import getMeta from '../../../../utils/meta'
import { useTranslation } from 'react-i18next'
import { useDetachCompileContext as useCompileContext } from '@/shared/context/detach-compile-context'
import { useCallback, useMemo } from 'react'
import DropdownSetting from '../dropdown-setting'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
-import { useProjectSettingsContext } from '@/features/ide-settings/context/project-settings-context'
-type CompileMode = 'normal' | 'png2pdf' | 'fast_draft'
+type CompileMode = 'normal' | 'fast_draft'
export default function DraftSetting() {
- const { draft, setDraft, png2pdf, setPng2pdf } = useCompileContext()
- const { setPng2pdf: persistPng2pdf } = useProjectSettingsContext()
+ const { draft, setDraft } = useCompileContext()
const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
- const png2pdfEnabled = getMeta('ol-canUsePng2Pdf')
- // The three modes are mutually exclusive, derived from the two underlying flags.
- const mode: CompileMode = png2pdf
- ? 'png2pdf'
- : draft
- ? 'fast_draft'
- : 'normal'
+ const mode: CompileMode = draft ? 'fast_draft' : 'normal'
const changeMode = useCallback(
(value: CompileMode) => {
@@ -28,29 +19,17 @@ export default function DraftSetting() {
setting: 'compile-mode',
settingVal: value,
})
- const enablePng2pdf = value === 'png2pdf'
setDraft(value === 'fast_draft')
- setPng2pdf(enablePng2pdf)
- // Draft mode is a local-only setting, so don't overwrite the project-wide
- // png2pdf preference (shared with all collaborators) when a user just
- // toggles draft for themselves. Only persist when the user explicitly
- // picks Normal or Fast [optimize images].
- if (value !== 'fast_draft') {
- persistPng2pdf(enablePng2pdf)
- }
},
- [sendEvent, setDraft, setPng2pdf, persistPng2pdf]
+ [sendEvent, setDraft]
)
const options = useMemo(
() => [
{ label: t('normal'), value: 'normal' as const },
- ...(png2pdfEnabled
- ? [{ label: t('fast_optimize_images'), value: 'png2pdf' as const }]
- : []),
{ label: t('fast_draft'), value: 'fast_draft' as const },
],
- [t, png2pdfEnabled]
+ [t]
)
return (
@@ -61,7 +40,7 @@ export default function DraftSetting() {
description={t('switch_compile_mode_for_faster_draft_compilation')}
value={mode}
onChange={changeMode}
- width={png2pdfEnabled ? 'wide' : 'default'}
+ width="default"
/>
)
}
diff --git a/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx b/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx
new file mode 100644
index 0000000000..b417525466
--- /dev/null
+++ b/services/web/frontend/js/features/ide-settings/components/compiler-settings/optimize-compile-setting.tsx
@@ -0,0 +1,28 @@
+import ToggleSetting from '../toggle-setting'
+import getMeta from '../../../../utils/meta'
+import { useTranslation } from 'react-i18next'
+import { useProjectSettingsContext } from '@/features/ide-settings/context/project-settings-context'
+import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
+import { useSetCompilationSettingWithEvent } from '@/features/ide-settings/hooks/use-set-compilation-setting'
+
+export default function OptimizeCompilesSetting() {
+ const { t } = useTranslation()
+ const png2pdfEnabled = getMeta('ol-canUsePng2Pdf')
+ const { png2pdf, setPng2pdf } = useProjectSettingsContext()
+ const { write } = usePermissionsContext()
+ const changePng2pdf = useSetCompilationSettingWithEvent(
+ 'optimize-compiles',
+ setPng2pdf
+ )
+
+ return png2pdfEnabled ? (
+
+ ) : null
+}
diff --git a/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx b/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx
index 0b9bff7044..e490ba7392 100644
--- a/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx
+++ b/services/web/frontend/js/features/ide-settings/context/settings-modal-context.tsx
@@ -47,6 +47,7 @@ import type {
import EditorTabsSetting from '../components/editor-settings/editor-tabs-setting'
import FloatingMenuSetting from '../components/editor-settings/floating-menu-setting'
import useEventListener from '@/shared/hooks/use-event-listener'
+import OptimizeCompileSetting from '../components/compiler-settings/optimize-compile-setting'
const [referenceSearchSettingModule] = importOverleafModules(
'referenceSearchSetting'
@@ -229,6 +230,10 @@ export const SettingsModalProvider: FC = ({
key: 'autoCompile',
component: ,
},
+ {
+ key: 'optimizeCompiles',
+ component: ,
+ },
],
},
],
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 36bdc9e9df..ede4d98a2f 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
@@ -1,4 +1,3 @@
-import getMeta from '../../../utils/meta'
import { useTranslation } from 'react-i18next'
import { memo, useCallback } from 'react'
import classNames from 'classnames'
@@ -40,11 +39,9 @@ function PdfCompileButton() {
autoCompile,
compiling,
draft,
- png2pdf,
hasChanges,
setAutoCompile,
setDraft,
- setPng2pdf,
setStopOnValidationError,
stopOnFirstError,
stopOnValidationError,
@@ -58,20 +55,16 @@ function PdfCompileButton() {
const { t } = useTranslation()
- const png2pdfEnabled = getMeta('ol-canUsePng2Pdf')
-
- // The three compile modes (Normal / Fast [optimize images] / Fast [draft]) are
- // mutually exclusive, so each selection sets both underlying flags.
+ // The two compile modes (Normal / Fast [draft]) are mutually exclusive.
const setCompileMode = useCallback(
- (mode: 'normal' | 'png2pdf' | 'draft') => {
+ (mode: 'normal' | 'draft') => {
eventTracking.sendMB('recompile-setting-changed', {
setting: 'compile-mode',
settingVal: mode,
})
setDraft(mode === 'draft')
- setPng2pdf(mode === 'png2pdf')
},
- [setDraft, setPng2pdf]
+ [setDraft]
)
const { detachRole } = useLayoutContext()
@@ -205,23 +198,11 @@ function PdfCompileButton() {
setCompileMode('normal')}
- trailingIcon={!draft && !png2pdf ? 'check' : null}
+ trailingIcon={!draft ? 'check' : null}
>
{t('normal')}
- {png2pdfEnabled && (
-
- setCompileMode('png2pdf')}
- trailingIcon={png2pdf ? 'check' : null}
- >
- {t('fast')}
- [optimize images]
-
-
- )}
= ({
setAnimateCompileDropdownArrow: _setAnimateCompileDropdownArrow,
setAutoCompile: _setAutoCompile,
setDraft: _setDraft,
- setPng2pdf: _setPng2pdf,
setError: _setError,
setHasLintingError: _setHasLintingError,
setHighlights: _setHighlights,
@@ -268,12 +267,6 @@ export const DetachCompileProvider: FC = ({
'detached',
'detacher'
)
- const setPng2pdf = useDetachAction(
- 'setPng2pdf',
- _setPng2pdf,
- 'detached',
- 'detacher'
- )
const setError = useDetachAction(
'setError',
_setError,
@@ -454,7 +447,6 @@ export const DetachCompileProvider: FC = ({
setAutoCompile,
setCompiling,
setDraft,
- setPng2pdf,
setError,
setHasLintingError,
setHighlights,
@@ -513,7 +505,6 @@ export const DetachCompileProvider: FC = ({
setAutoCompile,
setCompiling,
setDraft,
- setPng2pdf,
setError,
setHasLintingError,
setHighlights,
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 ca4854cb78..a5f527b017 100644
--- a/services/web/frontend/js/shared/context/local-compile-context.tsx
+++ b/services/web/frontend/js/shared/context/local-compile-context.tsx
@@ -95,7 +95,6 @@ export type CompileContext = {
rawLog?: string
setAutoCompile: (value: boolean) => void
setDraft: (value: any) => void
- setPng2pdf: (value: any) => void
setError: (value: any) => void
setHasLintingError: (value: boolean) => void // only for storybook
setHighlights: (value: any) => void
@@ -279,23 +278,13 @@ export const LocalCompileProvider: FC = ({
// ol-canUsePng2Pdf is the single source of truth from the backend: it already
// accounts for both the split-test rollout and the premium entitlement.
const canUsePng2pdf = Boolean(getMeta('ol-canUsePng2Pdf'))
- const png2pdfDefault = canUsePng2pdf && !draft
- // whether the compile should optimize images (png2pdf conversion)
- const [png2pdfSetting, setPng2pdf] = usePersistedState(
- `png2pdf:${projectId}`,
- png2pdfDefault,
- {
- listen: true,
- }
- )
-
- // The compile modes (Normal / optimize-images / draft) are mutually
- // exclusive. Draft takes precedence, so png2pdf is never sent alongside draft
- // even when a stale local or server-persisted setting has it enabled (e.g. a
- // socket update applying project.png2pdf=true while the user is in local draft
- // mode, or a setting left over after a downgrade).
- const png2pdf = canUsePng2pdf && png2pdfSetting && !draft
+ // Optimise images is a project-wide setting, so it is read straight off the
+ // project rather than persisted per user. It is on by default for anyone who
+ // can use it and has not turned it off. Draft mode takes precedence: an
+ // optimized compile is never requested while draft is on, but the project
+ // setting is left alone so it resumes when draft is turned off again.
+ const png2pdf = canUsePng2pdf && (project?.png2pdf ?? true) && !draft
// whether compiling should stop on first error
const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
@@ -379,15 +368,6 @@ export const LocalCompileProvider: FC = ({
compiler.setOption('png2pdf', png2pdf)
}, [compiler, png2pdf])
- // Apply the server-persisted png2pdf setting when the project has one. When
- // it is unset, leave the locally-derived default (which accounts for draft
- // mode) in place rather than forcing it on.
- useEffect(() => {
- if (project?.png2pdf !== undefined) {
- setPng2pdf(project.png2pdf)
- }
- }, [project?.png2pdf, setPng2pdf])
-
// keep stop on first error setting in sync with the compiler
useEffect(() => {
compiler.setOption('stopOnFirstError', stopOnFirstError)
@@ -819,7 +799,6 @@ export const LocalCompileProvider: FC = ({
setAutoCompile,
setCompiling,
setDraft,
- setPng2pdf,
setError,
setHasLintingError, // only for stories
setHighlights,
@@ -877,7 +856,6 @@ export const LocalCompileProvider: FC = ({
setAnimateCompileDropdownArrow,
setAutoCompile,
setDraft,
- setPng2pdf,
setError,
setHasLintingError, // only for stories
setHighlights,
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 1bb9c3e634..0372e5d064 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -890,7 +890,6 @@
"faq_how_does_free_trial_works_answer": "You get full access to your chosen __appName__ plan during your __len__-day free trial. There is no obligation to continue beyond the trial. Your card will be charged at the end of your __len__ day trial unless you cancel before then. You can cancel via your subscription settings.",
"fast": "Fast",
"fast_draft": "Fast [draft]",
- "fast_optimize_images": "Fast [optimize images]",
"fastest": "Fastest",
"feature_controls": "Feature controls",
"feature_disabled_by_organization_contact_support": "This feature isn’t available for your group. <0>Contact Overleaf support0> to enable it.",
@@ -1817,6 +1816,7 @@
"opens_in_new_tab": "opens in new tab",
"operation": "Operation",
"opted_out_linking": "You’ve opted out from linking your __email__ __appName__ account to your institutional account.",
+ "optimise_images_recommended": "Optimise images (recommended)",
"optional": "Optional",
"or": "or",
"or_enable_clipboard_access_in_your_browser_settings": "Or enable clipboard access in your browser settings.",
@@ -2497,6 +2497,7 @@
"source": "Source",
"speak": "Speak",
"speech_input_not_available": "Speech input is not yet available in this browser",
+ "speed_up_compiles_by_reusing_images": "Speed up compiles by reusing already-processed PNGs, with no loss in quality.",
"spellcheck": "Spellcheck",
"spellcheck_language": "Spellcheck language",
"spelling_and_language": "Spelling and language",
diff --git a/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx b/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx
index 94f9159e42..100d1f0fe0 100644
--- a/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx
+++ b/services/web/test/frontend/features/ide-settings/settings/draft-setting.test.tsx
@@ -39,111 +39,12 @@ describe('', function () {
expect(!!localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal(
option.draft
)
- expect(!!localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal(
- false
- )
}
})
- it('offers the optimize-images option when png2pdf is available', async function () {
- window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
- render(
-
-
-
-
-
- )
-
- const select = screen.getByLabelText('Compile mode')
- const optionElement = within(select).getByText('Fast [optimize images]')
- expect(optionElement.getAttribute('value')).to.equal('png2pdf')
-
- await userEvent.selectOptions(select, [optionElement])
- expect((select as HTMLSelectElement).value).to.equal('png2pdf')
- expect(localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal(
- null
- )
- expect(!!localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal(
- false
- )
- })
-
- it('defaults to optimize-images mode when png2pdf is available', function () {
- window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
-
- render(
-
-
-
-
-
- )
-
- const select = screen.getByLabelText('Compile mode') as HTMLSelectElement
- expect(select.value).to.equal('png2pdf')
- expect(localStorage.getItem(`png2pdf:${projectDefaults._id}`)).to.equal(
- null
- )
- expect(localStorage.getItem(`draft:${projectDefaults._id}`)).to.equal(null)
- })
-
- it('respects a previous draft setting over the png2pdf default', function () {
- window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
- localStorage.setItem(`draft:${projectDefaults._id}`, 'true')
-
- render(
-
-
-
-
-
- )
-
- const select = screen.getByLabelText('Compile mode') as HTMLSelectElement
- expect(select.value).to.equal('fast_draft')
- })
-
- it('persists the png2pdf setting to the server when changed', async function () {
- window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
- render(
-
-
-
-
-
- )
-
- const saveSettingsMock = fetchMock.post(
- `express:/project/:projectId/settings`,
- { status: 200 },
- { delay: 0 }
- )
-
- const select = screen.getByLabelText('Compile mode')
-
- await userEvent.selectOptions(select, [
- within(select).getByText('Fast [optimize images]'),
- ])
- expect(
- saveSettingsMock.callHistory.called(
- `/project/${projectDefaults._id}/settings`,
- { body: { png2pdf: true } }
- )
- ).to.be.true
-
- await userEvent.selectOptions(select, [within(select).getByText('Normal')])
- expect(
- saveSettingsMock.callHistory.called(
- `/project/${projectDefaults._id}/settings`,
- { body: { png2pdf: false } }
- )
- ).to.be.true
- })
-
- it('does not persist png2pdf when switching to draft mode', async function () {
- // Draft is a local-only setting; toggling it must not overwrite the shared
- // project-wide png2pdf preference.
+ it('does not persist a project setting when switching to draft mode', async function () {
+ // Draft is a local-only setting; toggling it must not touch the shared
+ // project-wide Optimise images (recommended) preference.
window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
render(
diff --git a/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx b/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx
new file mode 100644
index 0000000000..1c85a6083e
--- /dev/null
+++ b/services/web/test/frontend/features/ide-settings/settings/optimize-compile-setting.test.tsx
@@ -0,0 +1,83 @@
+import { screen, render } from '@testing-library/react'
+import { expect } from 'chai'
+import fetchMock from 'fetch-mock'
+import { SettingsModalProvider } from '@/features/ide-settings/context/settings-modal-context'
+import {
+ EditorProviders,
+ projectDefaults,
+} from '../../../helpers/editor-providers'
+import userEvent from '@testing-library/user-event'
+import OptimizeCompilesSetting from '@/features/ide-settings/components/compiler-settings/optimize-compile-setting'
+import type { PermissionsLevel } from '@/features/ide-react/types/permissions'
+
+describe('', function () {
+ afterEach(function () {
+ window.metaAttributesCache.delete('ol-canUsePng2Pdf')
+ localStorage.clear()
+ fetchMock.removeRoutes().clearHistory()
+ })
+
+ function renderSetting(
+ png2pdf?: boolean,
+ permissionsLevel: PermissionsLevel = 'owner'
+ ) {
+ render(
+
+
+
+
+
+ )
+ }
+
+ it('is not rendered when png2pdf is unavailable', function () {
+ renderSetting()
+
+ expect(screen.queryByLabelText('Optimise images (recommended)')).to.be.null
+ })
+
+ it('is on by default when the project has no stored preference', function () {
+ window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
+ renderSetting()
+
+ const toggle = screen.getByLabelText('Optimise images (recommended)')
+ expect((toggle as HTMLInputElement).checked).to.be.true
+ })
+
+ it('is disabled for a collaborator without write access', function () {
+ window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
+ renderSetting(undefined, 'readOnly')
+
+ const toggle = screen.getByLabelText('Optimise images (recommended)')
+ expect((toggle as HTMLInputElement).disabled).to.be.true
+ })
+
+ it('reflects the stored project preference when it is off', function () {
+ window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
+ renderSetting(false)
+
+ const toggle = screen.getByLabelText('Optimise images (recommended)')
+ expect((toggle as HTMLInputElement).checked).to.be.false
+ })
+
+ it('persists the setting to the project when toggled', async function () {
+ window.metaAttributesCache.set('ol-canUsePng2Pdf', true)
+ renderSetting()
+
+ const saveSettingsMock = fetchMock.post(
+ `express:/project/:projectId/settings`,
+ { status: 200 },
+ { delay: 0 }
+ )
+
+ const toggle = screen.getByLabelText('Optimise images (recommended)')
+ await userEvent.click(toggle)
+
+ expect(
+ saveSettingsMock.callHistory.called(
+ `/project/${projectDefaults._id}/settings`,
+ { body: { png2pdf: false } }
+ )
+ ).to.be.true
+ })
+})
diff --git a/services/web/test/frontend/helpers/editor-providers.tsx b/services/web/test/frontend/helpers/editor-providers.tsx
index 81026a5dcb..2153164790 100644
--- a/services/web/test/frontend/helpers/editor-providers.tsx
+++ b/services/web/test/frontend/helpers/editor-providers.tsx
@@ -84,6 +84,7 @@ export type EditorProvidersProps = {
rootDocId?: string
imageName?: string
compiler?: ProjectCompiler
+ png2pdf?: boolean
socket?: Socket
isRestrictedTokenMember?: boolean
scope?: Record
@@ -164,6 +165,8 @@ export function EditorProviders({
rootDocId = projectDefaults.rootDocId,
imageName = projectDefaults.imageName,
compiler = projectDefaults.compiler,
+ // left unset by default so the "never chosen" path is the one under test
+ png2pdf,
socket = new SocketIOMock() as any as Socket,
isRestrictedTokenMember = false,
scope: defaultScope = {},
@@ -231,6 +234,7 @@ export function EditorProviders({
rootFolder,
imageName,
compiler,
+ png2pdf,
members: [],
invites: [],
trackChangesState: {} as TrackChangesStateData,
@@ -730,7 +734,6 @@ const BASE_COMPILE_CONTEXT_MOCK = {
rawLog: undefined,
recompileFromScratch: () => {},
setAnimateCompileDropdownArrow: () => {},
- setPng2pdf: () => {},
setHasLintingError: () => {},
setHighlights: () => {},
setPosition: () => {},