diff --git a/qubes-rpc/qubes.GetImageRGBA b/qubes-rpc/qubes.GetImageRGBA index b56ab02c..aa8b3aab 100755 --- a/qubes-rpc/qubes.GetImageRGBA +++ b/qubes-rpc/qubes.GetImageRGBA @@ -21,22 +21,29 @@ elif ! [ -r "${filename}" ]; then exit 1 fi -s="$(gm identify -format '%w %h %m' "$filename")" -w="$(echo "$s"|cut -d " " -f 1)" -h="$(echo "$s"|cut -d " " -f 2)" -m="$(echo "$s"|cut -d " " -f 3)" +read -r w h m << EOF +$(gm identify -format '%w %h %m' "$filename") +EOF if [ "$m" = SVG ]; then tmpfile2="$(mktemp /tmp/qimg-XXXXXXXX.png)" - rsvg-convert -w "$w" -h "$h" -o "$tmpfile2" "$filename" + if [ -n "$w" ] && [ "$w" -gt 0 ] && [ -n "$h" ] && [ "$h" -gt 0 ]; then + rsvg-convert -w "$w" -h "$h" -o "$tmpfile2" "$filename" + else + rsvg-convert -o "$tmpfile2" "$filename" + # re-read dimensions from the rendered PNG since SVG had no explicit size + read -r w h << EOF +$(gm identify -format '%w %h' "$tmpfile2") +EOF + fi # downscale the image if necessary if [ -n "$forcemaxsize" ] && \ { [ "$w" -gt "$forcemaxsize" ] || [ "$h" -gt "$forcemaxsize" ]; }; then gm convert "$tmpfile2" -scale "${forcemaxsize}x${forcemaxsize}" "$tmpfile2" fi # read the size again, because icon may not be a square or could have changed with convert - s="$(gm identify -format '%w %h' "$tmpfile2")" - w="$(echo "$s"|cut -d " " -f 1)" - h="$(echo "$s"|cut -d " " -f 2)" + read -r w h << EOF +$(gm identify -format '%w %h' "$tmpfile2") +EOF filename="$tmpfile2" fi echo "$w $h" diff --git a/qubesagent/test_getimgrgba.py b/qubesagent/test_getimgrgba.py new file mode 100644 index 00000000..48812fd8 --- /dev/null +++ b/qubesagent/test_getimgrgba.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# The Qubes OS Project, https://www.qubes-os.org/ +# +# Copyright (C) 2026 Jayant Saxena +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +"""Unit tests for qubes.GetImageRGBA script. + +Tests the fix for QubesOS/qubes-issues#9145: SVG images without explicit +width/height attributes should be converted successfully. +""" + +import os +import shutil +import subprocess +import tempfile +import unittest + +SCRIPT_PATH = os.path.join( + os.path.dirname(__file__), '../qubes-rpc/qubes.GetImageRGBA') + +# Minimal SVG with explicit width/height +SVG_WITH_DIMENSIONS = b'''\ + + + + +''' + +# SVG with only viewBox, no explicit width/height (reproduces issue #9145) +SVG_WITHOUT_DIMENSIONS = b'''\ + + + + +''' + + +def _run_script(svg_content): + """Run qubes.GetImageRGBA with given SVG content, return (returncode, stdout, stderr).""" + with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f: + f.write(svg_content) + svg_path = f.name + try: + result = subprocess.run( + [SCRIPT_PATH], + input=svg_path.encode() + b'\n', + capture_output=True, + timeout=10, + ) + return result.returncode, result.stdout, result.stderr + finally: + os.unlink(svg_path) + + +@unittest.skipUnless( + os.path.exists(SCRIPT_PATH), 'qubes.GetImageRGBA script not found') +@unittest.skipUnless( + shutil.which('gm'), 'GraphicsMagick (gm) not installed') +@unittest.skipUnless( + shutil.which('rsvg-convert'), 'rsvg-convert not installed') +class TestGetImageRGBA(unittest.TestCase): + + def _assert_valid_rgba_output(self, stdout, stderr): + """Assert stdout contains valid 'W H\\nRGBA_DATA' output.""" + self.assertEqual(b'', stderr, f'Unexpected stderr: {stderr.decode()}') + lines = stdout.split(b'\n', 1) + self.assertGreaterEqual(len(lines), 1) + dims = lines[0].decode().split() + self.assertEqual(len(dims), 2, f'Expected "W H" on first line, got: {lines[0]}') + width, height = int(dims[0]), int(dims[1]) + self.assertGreater(width, 0) + self.assertGreater(height, 0) + if len(lines) > 1: + expected = width * height * 4 + self.assertGreaterEqual(len(lines[1]), expected) + return width, height + + def test_svg_with_explicit_dimensions(self): + """SVG with explicit width/height should convert successfully.""" + rc, stdout, stderr = _run_script(SVG_WITH_DIMENSIONS) + self.assertEqual(rc, 0, f'Script failed: {stderr.decode()}') + self._assert_valid_rgba_output(stdout, b'') + + def test_svg_without_dimensions(self): + """SVG without explicit width/height (only viewBox) should convert successfully. + + Regression test for QubesOS/qubes-issues#9145. + """ + rc, stdout, stderr = _run_script(SVG_WITHOUT_DIMENSIONS) + self.assertEqual(rc, 0, f'Script failed: {stderr.decode()}') + self._assert_valid_rgba_output(stdout, b'') + + def test_nonexistent_file(self): + """Non-existent file should cause script to exit with non-zero code.""" + result = subprocess.run( + [SCRIPT_PATH], + input=b'/nonexistent/file.svg\n', + capture_output=True, + timeout=10, + ) + self.assertNotEqual(result.returncode, 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/rpm_spec/core-agent.spec.in b/rpm_spec/core-agent.spec.in index a474c9ee..5e6c9f4a 100644 --- a/rpm_spec/core-agent.spec.in +++ b/rpm_spec/core-agent.spec.in @@ -1079,6 +1079,7 @@ rm -f %{name}-%{version} %{python3_sitelib}/qubesagent/xdg.py* %{python3_sitelib}/qubesagent/test_xdg.py* %{python3_sitelib}/qubesagent/test_tools.py* +%{python3_sitelib}/qubesagent/test_getimgrgba.py* %dir /usr/share/qubes/mime-override /usr/share/qubes/mime-override/globs