mirror of
https://github.com/rbreu/beeref.git
synced 2026-03-11 08:54:28 +00:00
Add setting to change default image allocation limit.
This commit is contained in:
parent
65942c08cf
commit
13192987c6
12 changed files with 163 additions and 20 deletions
|
|
@ -1,3 +1,15 @@
|
|||
0.3.4-dev (unreleased)
|
||||
======================
|
||||
|
||||
Added
|
||||
-----
|
||||
|
||||
* Added a setting to change the default memory limit for individual
|
||||
images. If a big image won't load, increase this limit. This
|
||||
setting can be overridden by Qt's default environment variable
|
||||
QT_IMAGEIO_MAXALLOC
|
||||
|
||||
|
||||
0.3.3 - 2024-05-05
|
||||
==================
|
||||
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ def main():
|
|||
settings = BeeSettings()
|
||||
logger.info(f'Using settings: {settings.fileName()}')
|
||||
logger.info(f'Logging to: {logfile_name()}')
|
||||
settings.on_startup()
|
||||
args = CommandlineArgs(with_check=True) # Force checking
|
||||
assert not args.debug_raise_error, args.debug_raise_error
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@
|
|||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from PyQt6 import QtCore
|
||||
from PyQt6 import QtCore, QtGui
|
||||
|
||||
from beeref import constants
|
||||
|
||||
|
|
@ -117,6 +118,12 @@ class BeeSettings(QtCore.QSettings):
|
|||
'cast': int,
|
||||
'validate': lambda x: 0 <= x <= 200,
|
||||
},
|
||||
'Items/image_allocation_limit': {
|
||||
'default': QtGui.QImageReader.allocationLimit(),
|
||||
'cast': int,
|
||||
'validate': lambda x: x >= 0,
|
||||
'post_save_callback': QtGui.QImageReader.setAllocationLimit,
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
|
@ -132,6 +139,26 @@ class BeeSettings(QtCore.QSettings):
|
|||
constants.APPNAME,
|
||||
constants.APPNAME)
|
||||
|
||||
def on_startup(self):
|
||||
"""Settings to be applied on application startup."""
|
||||
|
||||
if os.environ.get('QT_IMAGEIO_MAXALLOC'):
|
||||
alloc = int(os.environ['QT_IMAGEIO_MAXALLOC'])
|
||||
else:
|
||||
alloc = self.valueOrDefault('Items/image_allocation_limit')
|
||||
QtGui.QImageReader.setAllocationLimit(alloc)
|
||||
|
||||
def setValue(self, key, value):
|
||||
super().setValue(key, value)
|
||||
if key in self.FIELDS and 'post_save_callback' in self.FIELDS[key]:
|
||||
self.FIELDS[key]['post_save_callback'](value)
|
||||
|
||||
def remove(self, key):
|
||||
super().remove(key)
|
||||
if key in self.FIELDS and 'post_save_callback' in self.FIELDS[key]:
|
||||
value = self.valueOrDefault(key)
|
||||
self.FIELDS[key]['post_save_callback'](value)
|
||||
|
||||
def valueOrDefault(self, key):
|
||||
"""Get the value for key, or the default value specified in FIELDS.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with BeeRef. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
IMG_LOADING_ERROR_MSG = (
|
||||
'Unknown format or too big?\n'
|
||||
'Check Settings -> Miscellaneous -> Maximum Image Size')
|
||||
|
||||
|
||||
class BeeFileIOError(Exception):
|
||||
def __init__(self, msg, filename):
|
||||
self.msg = msg
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ from PyQt6 import QtGui
|
|||
|
||||
from beeref import constants
|
||||
from beeref.items import BeePixmapItem
|
||||
from .errors import BeeFileIOError
|
||||
from .errors import BeeFileIOError, IMG_LOADING_ERROR_MSG
|
||||
from .schema import SCHEMA, USER_VERSION, MIGRATIONS, APPLICATION_ID
|
||||
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ def handle_sqlite_errors(func):
|
|||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
func(self, *args, **kwargs)
|
||||
except sqlite3.Error as e:
|
||||
except (sqlite3.Error, BeeFileIOError) as e:
|
||||
logger.exception(f'Error while reading/writing {self.filename}')
|
||||
try:
|
||||
# Try to roll back transaction if there is any
|
||||
|
|
@ -210,8 +210,15 @@ class SQLiteIO:
|
|||
}
|
||||
|
||||
if data['type'] == 'pixmap':
|
||||
data['item'] = BeePixmapItem(QtGui.QImage())
|
||||
data['item'].pixmap_from_bytes(row[9])
|
||||
item = BeePixmapItem(QtGui.QImage())
|
||||
item.pixmap_from_bytes(row[9])
|
||||
if item.pixmap().isNull():
|
||||
item = data['data']['text'] = (
|
||||
f'Image could not be loaded: {item.filename}\n'
|
||||
+ IMG_LOADING_ERROR_MSG)
|
||||
data['type'] = 'text'
|
||||
data['item'] = item
|
||||
data['data']['is_editable'] = False
|
||||
|
||||
self.scene.add_item_later(data)
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ class BeePixmapItem(BeeItemMixin, QtWidgets.QGraphicsPixmapItem):
|
|||
TYPE = 'pixmap'
|
||||
CROP_HANDLE_SIZE = 15
|
||||
|
||||
def __init__(self, image, filename=None):
|
||||
def __init__(self, image, filename=None, **kwargs):
|
||||
super().__init__(QtGui.QPixmap.fromImage(image))
|
||||
self.save_id = None
|
||||
self.filename = filename
|
||||
|
|
@ -602,13 +602,13 @@ class BeeTextItem(BeeItemMixin, QtWidgets.QGraphicsTextItem):
|
|||
|
||||
TYPE = 'text'
|
||||
|
||||
def __init__(self, text=None):
|
||||
def __init__(self, text=None, is_editable=True, **kwargs):
|
||||
super().__init__(text or "Text")
|
||||
self.save_id = None
|
||||
logger.debug(f'Initialized {self}')
|
||||
self.is_image = False
|
||||
self.init_selectable()
|
||||
self.is_editable = True
|
||||
self.is_editable = is_editable
|
||||
self.edit_mode = False
|
||||
self.setDefaultTextColor(QtGui.QColor(*COLORS['Scene:Text']))
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ class MainControlsMixin:
|
|||
elif mimedata.hasImage():
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
msg = 'Attempted drop not an image'
|
||||
msg = 'Attempted drop not an image or image too big'
|
||||
logger.info(msg)
|
||||
widgets.BeeNotification(self.control_target, msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from beeref import commands
|
|||
from beeref.config import CommandlineArgs, BeeSettings, KeyboardSettings
|
||||
from beeref import constants
|
||||
from beeref import fileio
|
||||
from beeref.fileio.errors import IMG_LOADING_ERROR_MSG
|
||||
from beeref.fileio.export import exporter_registry
|
||||
from beeref import widgets
|
||||
from beeref.items import BeePixmapItem, BeeTextItem
|
||||
|
|
@ -532,13 +533,12 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
errornames = [
|
||||
f'<li>{fn}</li>' for fn in errors]
|
||||
errornames = '<ul>%s</ul>' % '\n'.join(errornames)
|
||||
msg = ('{errors} image(s) out of {total} '
|
||||
'could not be opened:'.format(
|
||||
errors=len(errors), total=len(errors)))
|
||||
num = len(errors)
|
||||
msg = f'{num} image(s) could not be opened.<br/>'
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self,
|
||||
'Problem loading images',
|
||||
msg + errornames)
|
||||
msg + IMG_LOADING_ERROR_MSG + errornames)
|
||||
self.scene.add_queued_items()
|
||||
self.scene.arrange_optimal()
|
||||
self.undo_stack.endMacro()
|
||||
|
|
@ -631,7 +631,7 @@ class BeeGraphicsView(MainControlsMixin,
|
|||
self.undo_stack.push(commands.InsertItems(self.scene, [item], pos))
|
||||
return
|
||||
|
||||
msg = 'No image data or text in clipboard'
|
||||
msg = 'No image data or text in clipboard or image too big'
|
||||
logger.info(msg)
|
||||
widgets.BeeNotification(self, msg)
|
||||
|
||||
|
|
|
|||
|
|
@ -135,6 +135,15 @@ class ArrangeGapWidget(IntegerGroup):
|
|||
MAX = 200
|
||||
|
||||
|
||||
class AllocationLimitWidget(IntegerGroup):
|
||||
TITLE = 'Maximum Image Size:'
|
||||
HELPTEXT = ('The maximum image size that can be loaded (in megabytes). '
|
||||
'Set to 0 for no limitation.')
|
||||
KEY = 'Items/image_allocation_limit'
|
||||
MIN = 0
|
||||
MAX = 10000
|
||||
|
||||
|
||||
class SettingsDialog(QtWidgets.QDialog):
|
||||
def __init__(self, parent):
|
||||
super().__init__(parent)
|
||||
|
|
@ -147,6 +156,7 @@ class SettingsDialog(QtWidgets.QDialog):
|
|||
misc.setLayout(misc_layout)
|
||||
misc_layout.addWidget(ImageStorageFormatWidget(), 0, 0)
|
||||
misc_layout.addWidget(ArrangeGapWidget(), 0, 1)
|
||||
misc_layout.addWidget(AllocationLimitWidget(), 1, 0)
|
||||
tabs.addTab(misc, '&Miscellaneous')
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from PyQt6 import QtGui
|
||||
|
||||
from beeref.config.settings import CommandlineArgs
|
||||
|
||||
|
||||
|
|
@ -35,6 +38,52 @@ def test_command_line_args_get_unknown():
|
|||
CommandlineArgs._instance = None
|
||||
|
||||
|
||||
def test_settings_on_startup_sets_alloc_from_settings(settings):
|
||||
settings.setValue('Items/image_allocation_limit', 66)
|
||||
QtGui.QImageReader.setAllocationLimit(100)
|
||||
settings.on_startup()
|
||||
assert QtGui.QImageReader.allocationLimit() == 66
|
||||
|
||||
|
||||
def test_settings_on_startup_sets_alloc_from_environment(settings):
|
||||
settings.setValue('Items/image_allocation_limit', 66)
|
||||
os.environ['QT_IMAGEIO_MAXALLOC'] = '42'
|
||||
QtGui.QImageReader.setAllocationLimit(100)
|
||||
settings.on_startup()
|
||||
assert QtGui.QImageReader.allocationLimit() == 42
|
||||
|
||||
|
||||
def test_settings_set_value_without_callback(settings):
|
||||
settings.FIELDS = {'foo/bar': {}}
|
||||
settings.setValue('foo/bar', 100)
|
||||
assert settings.value('foo/bar') == 100
|
||||
|
||||
|
||||
def test_settings_set_value_with_callback(settings):
|
||||
foo_callback = MagicMock()
|
||||
settings.FIELDS = {'foo/bar': {'post_save_callback': foo_callback}}
|
||||
settings.setValue('foo/bar', 100)
|
||||
foo_callback.assert_called_once_with(100)
|
||||
assert settings.value('foo/bar') == 100
|
||||
|
||||
|
||||
def test_settings_remove_without_callback(settings):
|
||||
settings.FIELDS = {'foo/bar': {}}
|
||||
settings.remove('foo/bar')
|
||||
assert settings.value('foo/bar') is None
|
||||
|
||||
|
||||
def test_settings_remove_with_callback(settings):
|
||||
foo_callback = MagicMock()
|
||||
settings.FIELDS = {
|
||||
'foo/bar': {'default': 66,
|
||||
'post_save_callback': foo_callback}
|
||||
}
|
||||
settings.remove('foo/bar')
|
||||
foo_callback.assert_called_once_with(66)
|
||||
assert settings.value('foo/bar') is None
|
||||
|
||||
|
||||
def test_settings_value_or_default_gets_default(settings):
|
||||
assert settings.valueOrDefault('Items/image_storage_format') == 'best'
|
||||
|
||||
|
|
|
|||
|
|
@ -120,21 +120,21 @@ def test_all_migrations(tmpfile):
|
|||
assert result[3] == b'bla'
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_application_id(tmpfile):
|
||||
def test_sqliteio_write_meta_application_id(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA application_id')
|
||||
assert result[0] == schema.APPLICATION_ID
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_user_version(tmpfile):
|
||||
def test_sqliteio_write_meta_user_version(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA user_version')
|
||||
assert result[0] == schema.USER_VERSION
|
||||
|
||||
|
||||
def test_sqliteio_ẁrite_meta_foreign_keys(tmpfile):
|
||||
def test_sqliteio_write_meta_foreign_keys(tmpfile):
|
||||
io = SQLiteIO(tmpfile, MagicMock(), create_new=True)
|
||||
io.write_meta()
|
||||
result = io.fetchone('PRAGMA foreign_keys')
|
||||
|
|
@ -463,6 +463,8 @@ def test_sqliteio_read_reads_readonly_text_item(tmpfile, view):
|
|||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeeTextItem)
|
||||
assert item.is_editable is True
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
|
|
@ -493,6 +495,7 @@ def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
|||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeePixmapItem)
|
||||
assert item.isSelected() is False
|
||||
assert item.save_id == 1
|
||||
assert item.pos().x() == 22.2
|
||||
|
|
@ -510,6 +513,30 @@ def test_sqliteio_read_reads_readonly_pixmap_item(tmpfile, view, imgdata3x3):
|
|||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
def test_sqliteio_read_reads_readonly_pixmap_item_error(tmpfile, view):
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True)
|
||||
io.create_schema_on_new()
|
||||
io.ex('INSERT INTO items '
|
||||
'(type, x, y, z, scale, rotation, flip, data) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?) ',
|
||||
('pixmap', 22.2, 33.3, 0.22, 3.4, 45, -1,
|
||||
json.dumps({'filename': 'bee.png'})))
|
||||
io.ex('INSERT INTO sqlar (item_id, data) VALUES (?, ?)',
|
||||
(1, b'not an image'))
|
||||
io.connection.commit()
|
||||
del io
|
||||
|
||||
io = SQLiteIO(tmpfile, view.scene, readonly=True)
|
||||
io.read()
|
||||
view.scene.add_queued_items()
|
||||
assert len(view.scene.items()) == 1
|
||||
item = view.scene.items()[0]
|
||||
assert isinstance(item, BeeTextItem)
|
||||
assert item.is_editable is False
|
||||
item.toPlainText().startswith('Unknown')
|
||||
assert view.scene.items_to_add.empty() is True
|
||||
|
||||
|
||||
def test_sqliteio_read_updates_progress(tmpfile, view):
|
||||
worker = MagicMock(canceled=False)
|
||||
io = SQLiteIO(tmpfile, view.scene, create_new=True,
|
||||
|
|
|
|||
|
|
@ -29,12 +29,16 @@ def test_beerefapplication_fileopenevent(open_mock, qapp, main_window):
|
|||
|
||||
@patch('beeref.__main__.BeeRefApplication')
|
||||
@patch('beeref.__main__.CommandlineArgs')
|
||||
def test_main(args_mock, app_mock, qapp):
|
||||
@patch('beeref.config.BeeSettings.on_startup')
|
||||
def test_main(startup_mock, args_mock, app_mock, qapp):
|
||||
app_mock.return_value = qapp
|
||||
args_mock.return_value.filename = None
|
||||
args_mock.return_value.loglevel = 'WARN'
|
||||
args_mock.return_value.debug_raise_error = ''
|
||||
|
||||
with patch.object(qapp, 'exec') as exec_mock:
|
||||
main()
|
||||
args_mock.assert_called_once_with(with_check=True)
|
||||
exec_mock.assert_called_once_with()
|
||||
|
||||
args_mock.assert_called_once_with(with_check=True)
|
||||
startup_mock.assert_called()
|
||||
|
|
|
|||
Loading…
Reference in a new issue