Repository: searxng/searxng
Pull request: [feat] images: show resolution in overview
## What does this PR do?
- the previous `img_format` will now be used for the real format of the image (e.g. jpg, png, webp, ...)
- there now is a new field called `resolution` that should be used with the format `width x height`
- I've also changed the documentation accordingly

## Why is this change important?
see #3177

## How to test this PR locally?
- search for images, and notice the resolution labels next to the source (only works for the here modified engines)

## Related issues
closes #3177

Problem statement:
Image search: show resolution beneath title or url
**Is your feature request related to a problem? Please describe.**
When searching for images it is quite annoying to click at all the images just to notice their resolution doesn't fit my need. Probably not all engines provide a resolution but for those who do, it would be great to show it beneath the image title or url.

**Describe the solution you'd like**
I would like to see the resolution of images on the image search overview where possible. An option to enable/disable would be even better.

**Describe alternatives you've considered**
Use another search engine for image search or click on every image and hope for the best. A filter function to only show certain resolutions would be another possible solution.

Hints from issue discussion:

Commit message: [refactor] images: add resolution, image format and filesize fields

Co-authored-by: Markus Heiser <markus.heiser@darmarit.de>

Patch to review:
diff --git a/docs/dev/engines/engine_overview.rst b/docs/dev/engines/engine_overview.rst
index 9c1ea04277e..091d217c06b 100644
--- a/docs/dev/engines/engine_overview.rst
+++ b/docs/dev/engines/engine_overview.rst
@@ -271,22 +271,54 @@ type.
 ``images``
 ----------
 
-.. table::  Parameter of the **images** media type:
+.. list-table:: Parameter of the **images** media type
+   :header-rows: 2
    :width: 100%
 
-   ========================= =====================================================
-   result-parameter          information
-   ------------------------- -----------------------------------------------------
-   template                  is set to ``images.html``
-   ========================= =====================================================
-   url                       string, url to the result site
-   title                     string, title of the result *(partly implemented)*
-   content                   *(partly implemented)*
-   publishedDate             :py:class:`datetime.datetime`,
-                             time of publish *(partly implemented)*
-   img\_src                  string, url to the result image
-   thumbnail\_src            string, url to a small-preview image
-   ========================= =====================================================
+   * - result-parameter
+     - Python type
+     - information
+
+   * - template
+     - :py:class:`str`
+     - is set to ``images.html``
+
+   * - url
+     - :py:class:`str`
+     - url to the result site
+
+   * - title
+     - :py:class:`str`
+     - title of the result
+
+   * - content
+     - :py:class:`str`
+     - description of the image
+
+   * - publishedDate
+     - :py:class:`datetime <datetime.datetime>`
+     - time of publish
+
+   * - img_src
+     - :py:class:`str`
+     - url to the result image
+
+   * - thumbnail_src
+     - :py:class:`str`
+     - url to a small-preview image
+
+   * - resolution
+     - :py:class:`str`
+     - the resolution of the image (e.g. ``1920 x 1080`` pixel)
+
+   * - img_format
+     - :py:class:`str`
+     - the format of the image (e.g. ``png``)
+
+   * - filesize
+     - :py:class:`str`
+     - size of bytes in :py:obj:`human readable <searx.humanize_bytes>` notation
+       (e.g. ``MB`` for 1024 \* 1024 Bytes filesize).
 
 
 .. _template videos:
diff --git a/searx/engines/artic.py b/searx/engines/artic.py
index c0ae0a5e7a9..506a9cea6d3 100644
--- a/searx/engines/artic.py
+++ b/searx/engines/artic.py
@@ -57,11 +57,10 @@ def response(resp):
         results.append(
             {
                 'url': 'https://artic.edu/artworks/%(id)s' % result,
-                'title': result['title'] + " (%(date_display)s) //  %(artist_display)s" % result,
-                'content': result['medium_display'],
+                'title': result['title'] + " (%(date_display)s) // %(artist_display)s" % result,
+                'content': "%(medium_display)s // %(dimensions)s" % result,
                 'author': ', '.join(result['artist_titles']),
                 'img_src': image_api + '/%(image_id)s/full/843,/0/default.jpg' % result,
-                'img_format': result['dimensions'],
                 'template': 'images.html',
             }
         )
diff --git a/searx/engines/bing_images.py b/searx/engines/bing_images.py
index faf0941c899..788c902789a 100644
--- a/searx/engines/bing_images.py
+++ b/searx/engines/bing_images.py
@@ -92,7 +92,7 @@ def response(resp):
 
         metadata = json.loads(result.xpath('.//a[@class="iusc"]/@m')[0])
         title = ' '.join(result.xpath('.//div[@class="infnmpt"]//a/text()')).strip()
-        img_format = ' '.join(result.xpath('.//div[@class="imgpt"]/div/span/text()')).strip()
+        img_format = ' '.join(result.xpath('.//div[@class="imgpt"]/div/span/text()')).strip().split(" · ")
         source = ' '.join(result.xpath('.//div[@class="imgpt"]//div[@class="lnkw"]//a/text()')).strip()
         results.append(
             {
@@ -103,7 +103,8 @@ def response(resp):
                 'content': metadata['desc'],
                 'title': title,
                 'source': source,
-                'img_format': img_format,
+                'resolution': img_format[0],
+                'img_format': img_format[1] if len(img_format) >= 2 else None,
             }
         )
     return results
diff --git a/searx/engines/brave.py b/searx/engines/brave.py
index 397af609445..8588c2efd21 100644
--- a/searx/engines/brave.py
+++ b/searx/engines/brave.py
@@ -367,9 +367,10 @@ def _parse_images(json_resp):
             'title': result['title'],
             'content': result['description'],
             'template': 'images.html',
-            'img_format': result['properties']['format'],
+            'resolution': result['properties']['format'],
             'source': result['source'],
             'img_src': result['properties']['url'],
+            'thumbnail_src': result['thumbnail']['src'],
         }
         result_list.append(item)
 
diff --git a/searx/engines/demo_online.py b/searx/engines/demo_online.py
index 08add5371d1..33cfde82295 100644
--- a/searx/engines/demo_online.py
+++ b/searx/engines/demo_online.py
@@ -88,11 +88,10 @@ def response(resp):
         results.append(
             {
                 'url': 'https://artic.edu/artworks/%(id)s' % result,
-                'title': result['title'] + " (%(date_display)s) //  %(artist_display)s" % result,
-                'content': result['medium_display'],
+                'title': result['title'] + " (%(date_display)s) // %(artist_display)s" % result,
+                'content': "%(medium_display)s // %(dimensions)s" % result,
                 'author': ', '.join(result['artist_titles']),
                 'img_src': image_api + '/%(image_id)s/full/843,/0/default.jpg' % result,
-                'img_format': result['dimensions'],
                 'template': 'images.html',
             }
         )
diff --git a/searx/engines/duckduckgo_extra.py b/searx/engines/duckduckgo_extra.py
index 25692add760..83ca38c260f 100644
--- a/searx/engines/duckduckgo_extra.py
+++ b/searx/engines/duckduckgo_extra.py
@@ -96,7 +96,7 @@ def _image_result(result):
         'content': '',
         'thumbnail_src': result['thumbnail'],
         'img_src': result['image'],
-        'img_format': '%s x %s' % (result['width'], result['height']),
+        'resolution': '%s x %s' % (result['width'], result['height']),
         'source': result['source'],
     }
 
diff --git a/searx/engines/flickr_noapi.py b/searx/engines/flickr_noapi.py
index 5299c604f9c..ebd61570fb3 100644
--- a/searx/engines/flickr_noapi.py
+++ b/searx/engines/flickr_noapi.py
@@ -110,7 +110,7 @@ def response(resp):  # pylint: disable=too-many-branches
             continue
 
         img_src = size_data['url']
-        img_format = f"{size_data['width']} x {size_data['height']}"
+        resolution = f"{size_data['width']} x {size_data['height']}"
 
         # For a bigger thumbnail, keep only the url_z, not the url_n
         if 'n' in photo['sizes']['data']:
@@ -131,7 +131,7 @@ def response(resp):  # pylint: disable=too-many-branches
             'img_src': img_src,
             'thumbnail_src': thumbnail_src,
             'source': source,
-            'img_format': img_format,
+            'resolution': resolution,
             'template': 'images.html',
         }
         result['author'] = author.encode(errors='ignore').decode()
diff --git a/searx/engines/google_images.py b/searx/engines/google_images.py
index d2d33d40826..31211b9b6a9 100644
--- a/searx/engines/google_images.py
+++ b/searx/engines/google_images.py
@@ -103,7 +103,7 @@ def response(resp):
             'title': item["result"]["page_title"],
             'content': item["text_in_grid"]["snippet"],
             'source': item["result"]["site_title"],
-            'img_format': f'{item["original_image"]["width"]} x {item["original_image"]["height"]}',
+            'resolution': f'{item["original_image"]["width"]} x {item["original_image"]["height"]}',
             'img_src': item["original_image"]["url"],
             'thumbnail_src': item["thumbnail"]["url"],
             'template': 'images.html',
diff --git a/searx/engines/qwant.py b/searx/engines/qwant.py
index 1f8556cf177..775a1129201 100644
--- a/searx/engines/qwant.py
+++ b/searx/engines/qwant.py
@@ -265,6 +265,8 @@ def parse_web_api(resp):
                         'template': 'images.html',
                         'thumbnail_src': thumbnail,
                         'img_src': img_src,
+                        'resolution': f"{item['width']} x {item['height']}",
+                        'img_format': item.get('thumb_type'),
                     }
                 )
 
diff --git a/searx/engines/seekr.py b/searx/engines/seekr.py
index 9250ac991a1..014b736dcdc 100644
--- a/searx/engines/seekr.py
+++ b/searx/engines/seekr.py
@@ -122,7 +122,7 @@ def _images_response(json):
                 'url': summary['refererurl'],
                 'title': result['title'],
                 'img_src': result['url'],
-                'img_format': f"{summary['width']}x{summary['height']}",
+                'resolution': f"{summary['width']}x{summary['height']}",
                 'thumbnail_src': 'https://media.seekr.com/engine/rp/' + summary['tg'] + '/?src= ' + result['thumbnail'],
             }
         )
diff --git a/searx/engines/wallhaven.py b/searx/engines/wallhaven.py
index f80b5d86d6a..94be1a6a7d3 100644
--- a/searx/engines/wallhaven.py
+++ b/searx/engines/wallhaven.py
@@ -8,6 +8,8 @@
 from datetime import datetime
 from urllib.parse import urlencode
 
+from searx.utils import humanize_bytes
+
 about = {
     'website': 'https://wallhaven.cc/',
     'official_api_documentation': 'https://wallhaven.cc/help/api',
@@ -68,6 +70,7 @@ def response(resp):
     json = resp.json()
 
     for result in json['data']:
+
         results.append(
             {
                 'template': 'images.html',
@@ -76,8 +79,10 @@ def response(resp):
                 'url': result['url'],
                 'img_src': result['path'],
                 'thumbnail_src': result['thumbs']['small'],
-                'img_format': result['resolution'],
+                'resolution': result['resolution'].replace('x', ' x '),
                 'publishedDate': datetime.strptime(result['created_at'], '%Y-%m-%d %H:%M:%S'),
+                'img_format': result['file_type'],
+                'filesize': humanize_bytes(result['file_size']),
             }
         )
 
diff --git a/searx/engines/wikicommons.py b/searx/engines/wikicommons.py
index a478f57d26a..9b08497160f 100644
--- a/searx/engines/wikicommons.py
+++ b/searx/engines/wikicommons.py
@@ -62,7 +62,7 @@ def response(resp):
             'title': title,
             'content': item["snippet"],
             'img_src': imageinfo["url"],
-            'img_format': f'{imageinfo["width"]} x {imageinfo["height"]}',
+            'resolution': f'{imageinfo["width"]} x {imageinfo["height"]}',
             'thumbnail_src': imageinfo["thumburl"],
             'template': 'images.html',
         }
diff --git a/searx/static/themes/simple/src/less/detail.less b/searx/static/themes/simple/src/less/detail.less
index b7894797cb1..253ffd5b273 100644
--- a/searx/static/themes/simple/src/less/detail.less
+++ b/searx/static/themes/simple/src/less/detail.less
@@ -49,8 +49,7 @@ article.result-images .detail {
 
   .result-images-labels {
     color: var(--color-result-detail-font);
-    max-height: 16rem;
-    min-height: 16rem;
+    height: 19rem;
 
     hr {
       border-top: 1px solid var(--color-result-detail-hr);
@@ -62,11 +61,13 @@ article.result-images .detail {
       overflow: hidden;
       text-overflow: ellipsis;
       font-size: 0.9rem;
+      margin-bottom: 0;
     }
 
     p {
       color: var(--color-result-detail-label-font);
       font-size: 0.9rem;
+      line-height: 0.9rem;
 
       span {
         display: inline-block;
@@ -82,6 +83,7 @@ article.result-images .detail {
 
     p.result-content {
       height: 2rem;
+      line-height: unset;
       overflow: hidden;
       text-overflow: ellipsis;
     }
diff --git a/searx/templates/simple/result_templates/images.html b/searx/templates/simple/result_templates/images.html
index 8aadbd2049e..0462588be5a 100644
--- a/searx/templates/simple/result_templates/images.html
+++ b/searx/templates/simple/result_templates/images.html
@@ -2,7 +2,10 @@
         <a {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} href="{{ result.img_src }}">{{- "" -}}
                 <img class="image_thumbnail" {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} src="{% if result.thumbnail_src %}{{ image_proxify(result.thumbnail_src) }}{% else %}{{ image_proxify(result.img_src) }}{% endif %}" alt="{{ result.title|striptags }}" loading="lazy" width="200" height="200">{{- "" -}}
                 <span class="title">{{ result.title|striptags }}</span>{{- "" -}}
-                <span class="source">{{ result.parsed_url.netloc }}</span>{{- "" -}}
+                <span class="source">
+                  {{- result.parsed_url.netloc -}}
+                  {%- if result.resolution %} - {{ result.resolution }}{%- endif -%}
+                </span>{{- "" -}}
         </a>{{- "" -}}
         <div class="detail">{{- "" -}}
                 <a class="result-detail-close" href="#">{{ icon('close') }}</a>{{- "" -}}
@@ -16,7 +19,9 @@ <h4>{{ result.title|striptags }}</h4>{{- "" -}}
                         <p class="result-content">{%- if result.content %}{{ result.content|striptags }}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
                         <hr>{{- "" -}}
                         <p class="result-author">{%- if result.author %}<span>{{ _('Author') }}:</span>{{ result.author|striptags }}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
+                        <p class="result-resolution">{%- if result.resolution %}<span>{{ _('Resolution') }}:</span>{{ result.resolution }}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
                         <p class="result-format">{%- if result.img_format %}<span>{{ _('Format') }}:</span>{{ result.img_format }}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
+                        <p class="result-filesize">{%- if result.filesize %}<span>{{ _('Filesize') }}:</span>{{ result.filesize}}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
                         <p class="result-source">{%- if result.source %}<span>{{ _('Source') }}:</span>{{ result.source }}{% else %}&nbsp;{% endif -%}</p>{{- "" -}}
                         <p class="result-engine"><span>{{ _('Engine') }}:</span>{{ result.engine }}</p>{{- "" -}}{{- "" -}}
                         <p class="result-url"><span>{{ _('View source') }}:</span><a {% if results_on_new_tab %}target="_blank" rel="noopener noreferrer"{% else %}rel="noreferrer"{% endif %} href="{{ result.url }}">{{ result.url }}</a></p>{{- "" -}}
diff --git a/searx/utils.py b/searx/utils.py
index c009c31445e..b0794c24408 100644
--- a/searx/utils.py
+++ b/searx/utils.py
@@ -353,6 +353,18 @@ def get_torrent_size(filesize: str, filesize_multiplier: str) -> Optional[int]:
         return None
 
 
+def humanize_bytes(size, precision=2):
+    """Determine the *human readable* value of bytes on 1024 base (1KB=1024B)."""
+    s = ['B ', 'KB', 'MB', 'GB', 'TB']
+
+    x = len(s)
+    p = 0
+    while size > 1024 and p < x:
+        p += 1
+        size = size / 1024.0
+    return "%.*f %s" % (precision, size, s[p])
+
+
 def convert_str_to_int(number_str: str) -> int:
     """Convert number_str to int or 0 if number_str is not a number."""
     if number_str.isdigit():


Code review comments:
diff_hunk: @@ -67,7 +67,9 @@ article.result-images .detail {
     p {
       color: var(--color-result-detail-label-font);
       font-size: 0.9rem;
-      line-height: 0.9rem;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      overflow: hidden;
path: searx/static/themes/simple/src/less/detail.less
text: @user1:
While I was looking into this I fond an old issue .. here I removed the `line-height: 0.9rem;` since setting this value causes the lower part of letters such as g to be cut off:

Before ..

![image](https://github.com/searxng/searxng/assets/554536/00e56ccc-622f-476f-adde-97ebcf812312)


After removing line-height ..

![image](https://github.com/searxng/searxng/assets/554536/9e15b4ba-b47c-48e0-a517-1b257261d30c)


@author:
Hmm, this change was meant to reduce the spacing between the individual entries/rows.
So now, the last entry is sometimes cut off a bit again.

I think we can achieve the same by using a smaller margin, I'll have a look.

@author:
Okay, `margin: 0.8rem 0;` fixes this (no cut off entries anymore and no text cut off).