From a12be862e22bdec6a243a3f0b5f4f28d69084a2a Mon Sep 17 00:00:00 2001 From: Brianna Rainey Date: Thu, 22 Jan 2026 16:29:46 -0500 Subject: fix #89 with Image component v2.0 + 23 tests (#90) * qtbot is needed in any test that uses a QObject previously these tests would fail if they ran before qtbot was initialized by another test. I'm now running tests in a random order * add tests for drawBars, readAudioFile, BlankFrame * replace numpy.seterr with numpy.errstate * fix incorrect comment * add MockVideoWorker and imageDataSum * test further into visualization (less likely to be a false positive) * test FloodFrame function * add failing test for Image component one step towards fixing #89 * test component name CLI parsing * prevent log warning when 1 setting changed * correct tests to use widgets when needed * test undo and blockSignals * remove stretch_scale (use scale only) * image ignores scale if stretch checkbox checked fixes #89 * test Title Text component, ffmpeg command * Image v2: replace stretched setting with resizeMode 3 resize modes are scale, cover, and stretch. Scale only applies when resizeMode is set to scale. Cover uses ImageOps.fit() to stretch while maintaining aspect ratio. Also, spinBox_scale was moved to be underneath comboBox_resizeMode. * change transformData into staticmethod the purpose is to allow easier reuse in other components * add respondToAudio option to Image component this causes the image to scale up and down slightly based on the input audio file * cache static portion of image when animating increases rendering speed of a 1-minute video by 12 seconds (based on two manual tests anyway)--- src/avp/components/image.py | 160 +++++++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 45 deletions(-) (limited to 'src/avp/components/image.py') diff --git a/src/avp/components/image.py b/src/avp/components/image.py index 2393611..bada15f 100644 --- a/src/avp/components/image.py +++ b/src/avp/components/image.py @@ -1,29 +1,40 @@ -from PIL import Image, ImageDraw, ImageEnhance -from PyQt6 import QtGui, QtCore, QtWidgets +from PIL import Image, ImageOps, ImageEnhance +from PyQt6 import QtWidgets import os +from copy import copy from ..component import Component from ..toolkit.frame import BlankFrame +from .original import Component as Visualizer class Component(Component): name = "Image" - version = "1.0.1" + version = "2.0.0" def widget(self, *args): super().widget(*args) + + # cache a modified image object in case we are rendering beyond frame 1 + self.existingImage = None + self.page.pushButton_image.clicked.connect(self.pickImage) + self.page.comboBox_resizeMode.addItem("Scale") + self.page.comboBox_resizeMode.addItem("Cover") + self.page.comboBox_resizeMode.addItem("Stretch") + self.page.comboBox_resizeMode.setCurrentIndex(0) self.trackWidgets( { "imagePath": self.page.lineEdit_image, "scale": self.page.spinBox_scale, - "stretchScale": self.page.spinBox_scale_stretch, "rotate": self.page.spinBox_rotate, "color": self.page.spinBox_color, "xPosition": self.page.spinBox_x, "yPosition": self.page.spinBox_y, - "stretched": self.page.checkBox_stretch, + "resizeMode": self.page.comboBox_resizeMode, "mirror": self.page.checkBox_mirror, + "respondToAudio": self.page.checkBox_respondToAudio, + "sensitivity": self.page.spinBox_sensitivity, }, presetNames={ "imagePath": "image", @@ -33,11 +44,19 @@ class Component(Component): relativeWidgets=["xPosition", "yPosition", "scale"], ) + def update(self): + self.page.spinBox_sensitivity.setEnabled( + self.page.checkBox_respondToAudio.isChecked() + ) + self.page.spinBox_scale.setEnabled( + self.page.comboBox_resizeMode.currentIndex() == 0 + ) + def previewRender(self): - return self.drawFrame(self.width, self.height) + return self.drawFrame(self.width, self.height, None) def properties(self): - props = ["static"] + props = ["pcm" if self.respondToAudio else "static"] if not os.path.exists(self.imagePath): props.append("error") return props @@ -48,34 +67,106 @@ class Component(Component): if not os.path.exists(self.imagePath): return "The image selected does not exist!" + def preFrameRender(self, **kwargs): + super().preFrameRender(**kwargs) + if not self.respondToAudio: + return + + # Trigger creation of new base image + self.existingImage = None + + smoothConstantDown = 0.08 + 0 + smoothConstantUp = 0.8 - 0 + self.lastSpectrum = None + self.spectrumArray = {} + + for i in range(0, len(self.completeAudioArray), self.sampleSize): + if self.canceled: + break + self.lastSpectrum = Visualizer.transformData( + i, + self.completeAudioArray, + self.sampleSize, + smoothConstantDown, + smoothConstantUp, + self.lastSpectrum, + self.sensitivity, + ) + self.spectrumArray[i] = copy(self.lastSpectrum) + + progress = int(100 * (i / len(self.completeAudioArray))) + if progress >= 100: + progress = 100 + pStr = "Analyzing audio: " + str(progress) + "%" + self.progressBarSetText.emit(pStr) + self.progressBarUpdate.emit(int(progress)) + def frameRender(self, frameNo): - return self.drawFrame(self.width, self.height) + return self.drawFrame( + self.width, + self.height, + ( + None + if not self.respondToAudio + else self.spectrumArray[frameNo * self.sampleSize] + ), + ) - def drawFrame(self, width, height): + def drawFrame(self, width, height, dynamicScale): frame = BlankFrame(width, height) if self.imagePath and os.path.exists(self.imagePath): - scale = self.scale if not self.stretched else self.stretchScale - image = Image.open(self.imagePath) - - # Modify image's appearance - if self.color != 100: - image = ImageEnhance.Color(image).enhance(float(self.color / 100)) - if self.mirror: - image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) - if self.stretched and image.size != (width, height): - image = image.resize((width, height), Image.Resampling.LANCZOS) - if scale != 100: - newHeight = int((image.height / 100) * scale) - newWidth = int((image.width / 100) * scale) - image = image.resize((newWidth, newHeight), Image.Resampling.LANCZOS) + if dynamicScale is not None and self.existingImage: + image = self.existingImage + else: + image = Image.open(self.imagePath) + # Modify static image appearance + if self.color != 100: + image = ImageEnhance.Color(image).enhance(float(self.color / 100)) + if self.mirror: + image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + if self.resizeMode == 1: # Cover + image = ImageOps.fit( + image, (width, height), Image.Resampling.LANCZOS + ) + elif self.resizeMode == 2: # Stretch + image = image.resize((width, height), Image.Resampling.LANCZOS) + elif self.scale != 100: # Scale + newHeight = int((image.height / 100) * self.scale) + newWidth = int((image.width / 100) * self.scale) + image = image.resize( + (newWidth, newHeight), Image.Resampling.LANCZOS + ) + self.existingImage = image + + # Respond to audio + scale = 0 + if dynamicScale is not None: + scale = dynamicScale[36 * 4] / 4 + image = ImageOps.contain( + image, + ( + image.width + int(scale / 2), + image.height + int(scale / 2), + ), + Image.Resampling.LANCZOS, + ) # Paste image at correct position - frame.paste(image, box=(self.xPosition, self.yPosition)) + frame.paste( + image, + box=( + self.xPosition - (0 if not self.respondToAudio else int(scale / 2)), + self.yPosition - (0 if not self.respondToAudio else int(scale / 2)), + ), + ) if self.rotate != 0: frame = frame.rotate(self.rotate) return frame + def postFrameRender(self): + self.existingImage = None + def pickImage(self): imgDir = self.settings.value("componentDir", os.path.expanduser("~")) filename, _ = QtWidgets.QFileDialog.getOpenFileName( @@ -106,24 +197,3 @@ class Component(Component): def commandHelp(self): print("Load an image:\n path=/filepath/to/image.png") - - def savePreset(self): - # Maintain the illusion that the scale spinbox is one widget - scaleBox = self.page.spinBox_scale - stretchScaleBox = self.page.spinBox_scale_stretch - if self.page.checkBox_stretch.isChecked(): - scaleBox.setValue(stretchScaleBox.value()) - else: - stretchScaleBox.setValue(scaleBox.value()) - return super().savePreset() - - def update(self): - # Maintain the illusion that the scale spinbox is one widget - scaleBox = self.page.spinBox_scale - stretchScaleBox = self.page.spinBox_scale_stretch - if self.page.checkBox_stretch.isChecked(): - scaleBox.setVisible(False) - stretchScaleBox.setVisible(True) - else: - scaleBox.setVisible(True) - stretchScaleBox.setVisible(False) -- cgit v1.2.3