ImageProcessing-ElectronicPublications / scantailor-experimental

Scan Tailor Experimental is an interactive post-processing tool for scanned pages.
https://github.com/Tulon/scantailor/tree/experimental
GNU General Public License v3.0
30 stars 0 forks source link

BUG: Qt6 build cannot load JPEGs or PNGs whose pixel count is 67,108,865 (2^26+1) pixels or more #34

Closed fusefib closed 2 months ago

fusefib commented 2 months ago

Description: The limitation described only affects Qt6 builds, not regular builds. I've conducted testing on Windows builds ranging from 0.2023.10.24-X86-64-Qt6 to 0.2024.05.09-X86-64-Qt6, and all Qt6 versions exhibit the same behavior. This constraint specifically impacts JPEG and PNG files, while TIFF files remain unaffected. For JPEGs and PNGs, the canvas is restricted to 2^26 or 67,108,864 pixels. In other words, for JPEGs and PNGs:

TIFFs, on the other hand, load without any issues regardless of their pixel count (at least within "reasonable" limits, I tested 20000x20000 and could output bitonal with ScanTailor-Experimental Qt6 just fine).

Expected Behavior: These images should load as in normal (Qt5?) builds.

I would add that these resolutions can be encountered in real-life. I came across this issue with a 600dpi scan of a large magazine via a RICOH MP C3004ex device, which resulted in 7016x9921 JPEG files (double page scans, likely A3 size @ 600dpi).

If it's a permanent limitation owing to Qt6, perhaps a better warning message should be given here (see below).

Actual Behavior: When trying to load a JPEG or PNG whose pixel count is 67,108,865 (2^26+1) pixels or more, it doesn't display or work with the image, and we see the following vague warning message instead: The following file could not be loaded: {the file path}

Environment:

Steps to reproduce:

  1. Requirements: Python 3, pip install Pillow
  2. Save this as resize_img.py:
    
    import argparse
    import os
    from PIL import Image
    import locale

def resize_image(input_image, output_image, dimensions, quality): image = Image.open(input_image) resized_image = image.resize(dimensions, Image.LANCZOS) if output_image.endswith(".png"): resized_image.save(output_image) elif output_image.endswith(".tif"): resized_image.save(output_image, compression="tiff_deflate") else: resized_image.save(output_image, quality=quality)

# Get file size
file_size = os.path.getsize(output_image)

# Get dimensions and number of pixels
width, height = resized_image.size
num_pixels = width * height

# Add thousands separator to file size and number of pixels
locale.setlocale(locale.LC_ALL, '')
file_size_str = locale.format_string("%d", file_size, grouping=True)
num_pixels_str = locale.format_string("%d", num_pixels, grouping=True)

print(f"Resized image dimensions: {width}x{height}")
print(f"Number of pixels: {num_pixels_str}")
print(f"File size: {file_size_str} bytes")

if name == "main": parser = argparse.ArgumentParser(description='Resize an image to arbitrary dimensions.') parser.add_argument('input', help='Input image file') parser.add_argument('--dimensions', required=True, help='Desired dimensions in the format "XxY"') parser.add_argument('--quality', type=int, default=80, help='Output quality (0-100 for JPEG, ignored for PNG and TIFF)') parser.add_argument('--output-png', action='store_true', help='Output image as PNG (default is JPEG)') parser.add_argument('--output-tif', action='store_true', help='Output image as TIFF') args = parser.parse_args()

input_image = args.input
output_format = "png" if args.output_png else "jpg" if not args.output_tif else "tif"
output_image = f"{input_image.split('.')[0]}_resized_{args.dimensions}.{output_format}"

width, height = map(int, args.dimensions.split('x'))
dimensions = (width, height)

resize_image(input_image, output_image, dimensions, args.quality)
3. Take any existing image (e.g. `input.jpg`) and run the following:

python resize_img.py --dimensions 8192x8192 input.jpg python resize_img.py --dimensions 8192x8192 input.jpg --output-tif python resize_img.py --dimensions 8192x8192 input.jpg --output-png python resize_img.py --dimensions 8192x8193 input.jpg python resize_img.py --dimensions 8192x8193 input.jpg --output-tif python resize_img.py --dimensions 8192x8193 input.jpg --output-png


4. Using a Qt6 build, try adding the six newly created files to a ScanTailor-Experimental project. 
5. This will be observed:
- The files generated by commands 1, 2, 3, and 5 currently work.
- The files generated by commands 4 and 6 currently don't work, because they're JPEGs or PNGs exceeding 2^26 pixels.
zvezdochiot commented 2 months ago

This is problem Qt6, not STEX!

plzombie commented 2 months ago

@fusefib It looks like Qt6 build needs to be recompiled with this https://doc.qt.io/qt-6/qimagereader.html#setAllocationLimit function called with 0 (or something large enough)

zvezdochiot commented 2 months ago

@plzombie , а где баг то? STEX не падает. А не "читает" изображение Qt6.

plzombie commented 2 months ago

@zvezdochiot Ну технически это регрессия с переходом на Qt6, так как теперь изображения по умолчанию могут весить не более 128MB, и надо ставить #ifdef QT6 в main и вызывать функцию, которая возвращает всё обратно или вообще убирает ограничение

zvezdochiot commented 2 months ago

@plzombie , а баг то где? Где падение, переполнение, либо уб? Спам это, а не баг.

plzombie commented 2 months ago

@zvezdochiot Баг в том, что у него в Qt6 не открываются большие изображения. И фиксить это надо не на стороне Qt6, а на стороне STEX, добавив вызов функции, убирающей ограничения

zvezdochiot commented 2 months ago

@plzombie say:

Баг в том, что

@zvezdochiot say:

Где падение, переполнение, либо уб?

plzombie commented 2 months ago

@zvezdochiot Хочешь чтобы падало? Попробуй изображение со стороной 65500 загрузить. Я тут ещё один баг могу подкинуть. Программа не разделяет несколько файлов с одинаковым именем, но разным расширением (8193.jpg, 8193.png, 8193.tif). В папке out один файл на три изображения (и кэш у них тоже один). Ещё падает, если один и тот же файл два раза добавляешь

plzombie commented 2 months ago

@fusefib Please try this build staging.zip

zvezdochiot commented 2 months ago

@plzombie say:

Попробуй изображение со стороной 65500 загрузить.

Попробую. Только 65500/215мм*25,4=7738dpi (сюр какой то).

@plzombie say:

Программа не разделяет несколько файлов с одинаковым именем.

Это не баг.

@plzombie say:

Ещё падает, если один и тот же файл два раза добавляешь.

Вот это уже не хорошо. Но связываться с итераторами не хочется, а как сделать "по-иначе" я не соображу.

fusefib commented 2 months ago

Thank you, the new build works. So did changing the environment variable - 256 MB was the default or equivalent, and higher value improved things.

Apologies for the 'bug' wording. STEX's handling of the Qt6 upstream change was good considering the situation - no instability, no crash. I'm testing Qt6 builds specifically because I know Qt6 can create problems.

The same-filename collision also occurs if you subsequently load an image with the same filename (and extension) from a different folder; I used it at some point or another to pick a re-scan and then select the better photo and delete the other. The interface works, and this functionality is useful as-is if the user knows what's going on.