Skip to content

Commit 65257c6

Browse files
authored
Fix custom logo quality in header and About modal (#1419)
* fix: improve SVG logo quality in header and About modal - Created buildLogoSrc helper to handle logo rendering - SVG logos now decoded from base64 and URI-encoded for crisp rendering - Raster images (PNG/JPEG) keep base64 encoding - Added CSS improvements for better image scaling - Comprehensive test coverage for all logo formats Fixes image quality issues when using custom branding logos in DevSpaces Signed-off-by: Oleksii Orel <oorel@redhat.com> * fix: resolve CSS linting errors in app.css - Fix CSS property order in .pf-c-page__header-brand-link .pf-c-brand - Move .pf-c-about-modal-box__brand img selector before more specific selectors - Add proper empty lines before comments and properties - Ensure width comes before max-width, and max-width before height Signed-off-by: Oleksii Orel <oorel@redhat.com> --------- Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent df1705f commit 65257c6

File tree

5 files changed

+178
-8
lines changed

5 files changed

+178
-8
lines changed

packages/dashboard-frontend/src/Layout/Header/Tools/AboutMenu/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import React from 'react';
2020

2121
import { AboutModal } from '@/Layout/Header/Tools/AboutMenu/Modal';
2222
import { BrandingData } from '@/services/bootstrap/branding.constant';
23+
import { buildLogoSrc } from '@/services/helpers/brandingLogo';
2324

2425
type Props = {
2526
branding: BrandingData;
@@ -95,10 +96,7 @@ export class AboutMenu extends React.PureComponent<Props, State> {
9596

9697
const { logoFile, name, productVersion } = this.props.branding;
9798

98-
const logoSrc =
99-
dashboardLogo !== undefined
100-
? `data:${dashboardLogo.mediatype};base64,${dashboardLogo.base64data}`
101-
: logoFile;
99+
const logoSrc = buildLogoSrc(dashboardLogo, logoFile);
102100

103101
return (
104102
<>

packages/dashboard-frontend/src/Layout/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ROUTE } from '@/Routes';
2929
import { AppAlerts } from '@/services/alerts/appAlerts';
3030
import { IssuesReporterService } from '@/services/bootstrap/issuesReporter';
3131
import { WarningsReporterService } from '@/services/bootstrap/warningsReporter';
32+
import { buildLogoSrc } from '@/services/helpers/brandingLogo';
3233
import { signOut } from '@/services/helpers/login';
3334
import { RootState } from '@/store';
3435
import { selectBranding } from '@/store/Branding/selectors';
@@ -137,10 +138,7 @@ export class Layout extends React.PureComponent<Props, State> {
137138
const { isHeaderVisible, isSidebarVisible } = this.state;
138139
const { history, branding, dashboardLogo } = this.props;
139140

140-
const logoSrc =
141-
dashboardLogo !== undefined
142-
? `data:${dashboardLogo.mediatype};base64,${dashboardLogo.base64data}`
143-
: branding.logoFile;
141+
const logoSrc = buildLogoSrc(dashboardLogo, branding.logoFile);
144142

145143
return (
146144
<ToggleBarsContext.Provider

packages/dashboard-frontend/src/app.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ div.tippy-popper a:hover {
4747
--pf-global--link--Color: #2b9af3;
4848
}
4949

50+
/* Ensure About modal logo renders with high quality */
51+
.pf-c-about-modal-box__brand img {
52+
max-width: 100%;
53+
height: auto;
54+
55+
object-fit: contain;
56+
57+
image-rendering: -webkit-optimize-contrast;
58+
image-rendering: crisp-edges;
59+
}
60+
5061
div.main-page-loader {
5162
position: absolute;
5263
z-index: 80;
@@ -147,7 +158,15 @@ div.main-page-loader .ide-page-loader-content img {
147158
}
148159

149160
.pf-c-page__header-brand-link .pf-c-brand {
161+
width: auto;
162+
max-width: 200px;
150163
height: 3.17rem;
164+
165+
object-fit: contain;
166+
167+
/* Improve rendering quality for scaled images */
168+
image-rendering: -webkit-optimize-contrast;
169+
image-rendering: crisp-edges;
151170
}
152171

153172
@keyframes hide {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright (c) 2018-2025 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
13+
import { buildLogoSrc } from '@/services/helpers/brandingLogo';
14+
15+
describe('brandingLogo helper', () => {
16+
const fallbackLogo = './assets/branding/che-logo.svg';
17+
18+
describe('when dashboardLogo is undefined', () => {
19+
it('should return fallback logo', () => {
20+
const result = buildLogoSrc(undefined, fallbackLogo);
21+
expect(result).toBe(fallbackLogo);
22+
});
23+
});
24+
25+
describe('when dashboardLogo is SVG', () => {
26+
it('should decode base64 and encode with URI encoding', () => {
27+
const svgContent = '<svg><circle r="10"/></svg>';
28+
const base64Svg = btoa(svgContent);
29+
const dashboardLogo = {
30+
base64data: base64Svg,
31+
mediatype: 'image/svg+xml',
32+
};
33+
34+
const result = buildLogoSrc(dashboardLogo, fallbackLogo);
35+
36+
expect(result).toContain('data:image/svg+xml;charset=utf-8,');
37+
expect(result).toContain(encodeURIComponent(svgContent));
38+
expect(result).not.toContain('base64');
39+
});
40+
41+
it('should fallback to base64 if decoding fails', () => {
42+
const invalidBase64 = 'invalid!!!base64';
43+
const dashboardLogo = {
44+
base64data: invalidBase64,
45+
mediatype: 'image/svg+xml',
46+
};
47+
48+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
49+
50+
const result = buildLogoSrc(dashboardLogo, fallbackLogo);
51+
52+
expect(result).toBe(`data:image/svg+xml;base64,${invalidBase64}`);
53+
expect(consoleErrorSpy).toHaveBeenCalledWith(
54+
'Failed to decode SVG logo, falling back to base64:',
55+
expect.any(Error),
56+
);
57+
58+
consoleErrorSpy.mockRestore();
59+
});
60+
});
61+
62+
describe('when dashboardLogo is PNG', () => {
63+
it('should return base64 data URL', () => {
64+
const base64data =
65+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
66+
const dashboardLogo = {
67+
base64data,
68+
mediatype: 'image/png',
69+
};
70+
71+
const result = buildLogoSrc(dashboardLogo, fallbackLogo);
72+
73+
expect(result).toBe(`data:image/png;base64,${base64data}`);
74+
});
75+
});
76+
77+
describe('when dashboardLogo is JPEG', () => {
78+
it('should return base64 data URL', () => {
79+
const base64data =
80+
'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wgALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//aAAgBAQABPxA=';
81+
const dashboardLogo = {
82+
base64data,
83+
mediatype: 'image/jpeg',
84+
};
85+
86+
const result = buildLogoSrc(dashboardLogo, fallbackLogo);
87+
88+
expect(result).toBe(`data:image/jpeg;base64,${base64data}`);
89+
});
90+
});
91+
92+
describe('SVG encoding quality', () => {
93+
it('should preserve SVG structure after encoding/decoding', () => {
94+
const svgContent =
95+
'<svg width="100" height="100"><rect x="10" y="10" width="80" height="80" fill="red"/></svg>';
96+
const base64Svg = btoa(svgContent);
97+
const dashboardLogo = {
98+
base64data: base64Svg,
99+
mediatype: 'image/svg+xml',
100+
};
101+
102+
const result = buildLogoSrc(dashboardLogo, fallbackLogo);
103+
104+
// Extract the encoded part
105+
const encodedPart = result.replace('data:image/svg+xml;charset=utf-8,', '');
106+
const decoded = decodeURIComponent(encodedPart);
107+
108+
expect(decoded).toBe(svgContent);
109+
});
110+
});
111+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (c) 2018-2025 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
13+
/**
14+
* Converts dashboard logo with base64 data to a proper data URL.
15+
* Handles SVG images specially to ensure crisp rendering.
16+
*
17+
* @param dashboardLogo - Logo object with base64data and mediatype
18+
* @param fallbackLogo - Fallback logo path if dashboardLogo is undefined
19+
* @returns Data URL string for use in img src or Brand component
20+
*/
21+
export function buildLogoSrc(
22+
dashboardLogo: { base64data: string; mediatype: string } | undefined,
23+
fallbackLogo: string,
24+
): string {
25+
if (dashboardLogo === undefined) {
26+
return fallbackLogo;
27+
}
28+
29+
// Special handling for SVG to ensure crisp rendering
30+
// SVG images render better when decoded and URI-encoded rather than base64
31+
if (dashboardLogo.mediatype === 'image/svg+xml') {
32+
try {
33+
const decodedSvg = atob(dashboardLogo.base64data);
34+
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(decodedSvg)}`;
35+
} catch (e) {
36+
console.error('Failed to decode SVG logo, falling back to base64:', e);
37+
// Fallback to base64 if decoding fails
38+
return `data:${dashboardLogo.mediatype};base64,${dashboardLogo.base64data}`;
39+
}
40+
}
41+
42+
// For non-SVG formats (PNG, JPEG, etc.), use base64 as-is
43+
return `data:${dashboardLogo.mediatype};base64,${dashboardLogo.base64data}`;
44+
}

0 commit comments

Comments
 (0)