From 22f004d8330107f5bb3926bdc3255e7cb5b34f6c Mon Sep 17 00:00:00 2001 From: Marat Fayzullin Date: Sat, 6 May 2023 19:13:05 -0400 Subject: [PATCH] Improved fax parser, added conversion from BMP to PNG files. --- owrx/fax.py | 211 ++++++++++++++++++++++++++---------------------- owrx/sstv.py | 3 + owrx/storage.py | 23 +++++- 3 files changed, 139 insertions(+), 98 deletions(-) diff --git a/owrx/fax.py b/owrx/fax.py index 67ebe395..cd2605ea 100644 --- a/owrx/fax.py +++ b/owrx/fax.py @@ -39,12 +39,15 @@ class FaxParser(ThreadModule): logger.debug("Deleting short bitmap file '%s'." % self.fileName) os.unlink(self.fileName) else: + # Convert file from BMP to PNG + logger.debug("Converting '%s' to PNG..." % self.fileName) + Storage().convertImage(self.fileName) # Delete excessive files from storage logger.debug("Performing storage cleanup...") Storage().cleanStoredFiles() - except Exception as exptn: - logger.debug("Exception closing file: %s" % str(exptn)) + except Exception as e: + logger.debug("Exception closing file: %s" % str(e)) self.file = None def newFile(self, fileName): @@ -54,8 +57,8 @@ class FaxParser(ThreadModule): logger.debug("Opening bitmap file '%s'..." % self.fileName) self.file = open(self.fileName, "wb") - except Exception as exptn: - logger.debug("Exception opening file: %s" % str(exptn)) + except Exception as e: + logger.debug("Exception opening file: %s" % str(e)) self.file = None def writeFile(self, data): @@ -139,11 +142,103 @@ class FaxParser(ThreadModule): out = None try: - # Parse bitmap file data (scanlines) - if self.width>0: - b = self.depth / 8 if self.depth>8 else 1 - w = self.width - if len(self.data)>=w*b: + # Pixel size, line width in pixels and bytes + b = self.depth / 8 if self.depth>8 else 1 + w = self.width + l = w * b + + # Search for BMP header and comments first, in case + # previous bitmap terminates early + ll = min(l, len(self.data)) if l>0 else len(self.data) + ph = self.data[0:ll].find(b'BM') + pc = self.data[0:ll].find(b' [') + + # + # If comment found, and we are not receiving an image... + # + if pc>=0 and (ph<0 or pc=0: + # Extract message contents + msg = self.data[2:pc].decode() + # Remove parsed data + del self.data[0:pc+1] + # Log message + logger.debug("%s says [%s]" % (self.myName(), msg)) + # If running as a service... + if self.service: + # Empty result + out = {} + else: + # Compose result + out = { + "mode": "Fax", + "message": msg, + "frequency": self.frequency + } + + # + # If BMP header ('BM ... <40> ...') found... + # + elif ph>=0 and ph+14=54+4*256: + self.width = self.data[18] + (self.data[19]<<8) + (self.data[20]<<16) + (self.data[21]<<24) + self.height = self.data[22] + (self.data[23]<<8) + (self.data[24]<<16) + (self.data[25]<<24) + self.depth = self.data[28] + (self.data[29]<<8) + # BMP height value is negative + self.height = 0x100000000 - self.height + # Fax mode is passed via reserved area at offset 6 + self.ioc = self.data[6] * 4 + self.lpm = self.data[7] + self.line = 0 + # Find total header size + headerSize = 54 + (4*256 if self.depth==8 else 0) + # 256x4 palette follows the header + if headerSize>54: + self.colors = self.data[54:headerSize] + else: + self.colors = None + # Find mode name and time + modeName = "IOC-%d %dLPM" % (self.ioc, self.lpm) + timeStamp = datetime.utcnow().strftime("%H:%M:%S") + fileName = Storage().makeFileName("FAX-{0}", self.frequency) + logger.debug("%s receiving %dx%d %s frame as '%s'." % ( + self.myName(), self.width, self.height, + modeName, fileName + )) + # If running as a service... + if self.service: + # Create a new image file and write BMP header + self.newFile(fileName) + self.writeFile(self.data[0:headerSize]) + # Empty result + out = {} + else: + # Compose result + out = { + "mode": "Fax", + "width": self.width, + "height": self.height, + "depth": self.depth, + "faxMode": modeName, + "timestamp": timeStamp, + "filename": fileName, + "frequency": self.frequency + } + # Remove parsed data + del self.data[0:headerSize] + + # + # If currently receiving image... + # + elif l>0: + if len(self.data)>=l: #logger.debug("%s got line %d of %d/%d pixels" % ( # self.myName(), self.line, w, len(self.data)/b #)) @@ -152,7 +247,7 @@ class FaxParser(ThreadModule): # If running as a service... if self.service: # Write a scanline into open image file - self.writeFile(self.data[0:w*b]) + self.writeFile(self.data[0:l]) # Close once the last scanline reached if self.line>=self.height: self.closeFile() @@ -160,7 +255,7 @@ class FaxParser(ThreadModule): out = {} else: # Compose result - #rle = self.applyRLE(self.data[0:w*b]) + #rle = self.applyRLE(self.data[0:l]) out = { "mode": "Fax", "line": self.line-1, @@ -168,7 +263,7 @@ class FaxParser(ThreadModule): "height": self.height, "depth": self.depth, "rle": False, - "pixels": base64.b64encode(self.data[0:w*b]).decode(), + "pixels": base64.b64encode(self.data[0:l]).decode(), } # If we reached the end of frame, finish scan if self.line>=self.height: @@ -180,92 +275,18 @@ class FaxParser(ThreadModule): self.lpm = 0 self.colors = None # Remove parsed data - del self.data[0:w*b] + del self.data[0:l] + # + # If not receiving anything... + # else: - # Search for the leading 'BM' or ' [' - w = self.data.find(b'BM') - d = self.data.find(b' [') - # If not found... - if w<0 and d<0: - # Skip all but last character (may have 'B') - del self.data[0:len(self.data)-1] - elif w<0 or (d>=0 and d=0: - # Extract message contents - msg = self.data[2:w].decode() - # Remove parsed data - del self.data[0:w+1] - # Log message - logger.debug("%s says [%s]" % (self.myName(), msg)) - # If running as a service... - if self.service: - # Empty result - out = {} - else: - # Compose result - out = { - "mode": "Fax", - "message": msg, - "frequency": self.frequency - } - else: - # Skip everything until 'BM' - del self.data[0:w] - # If got the entire header... - if len(self.data)>=54+4*256: - self.width = self.data[18] + (self.data[19]<<8) + (self.data[20]<<16) + (self.data[21]<<24) - self.height = self.data[22] + (self.data[23]<<8) + (self.data[24]<<16) + (self.data[25]<<24) - self.depth = self.data[28] + (self.data[29]<<8) - # BMP height value is negative - self.height = 0x100000000 - self.height - # Fax mode is passed via reserved area at offset 6 - self.ioc = self.data[6] * 4 - self.lpm = self.data[7] - self.line = 0 - # Find total header size - headerSize = 54 + (4*256 if self.depth==8 else 0) - # 256x4 palette follows the header - if headerSize>54: - self.colors = self.data[54:headerSize] - else: - self.colors = None - # Find mode name and time - modeName = "IOC-%d %dLPM" % (self.ioc, self.lpm) - timeStamp = datetime.utcnow().strftime("%H:%M:%S") - fileName = Storage().makeFileName("FAX-{0}", self.frequency) - logger.debug("%s receiving %dx%d %s frame as '%s'." % ( - self.myName(), self.width, self.height, - modeName, fileName - )) - # If running as a service... - if self.service: - # Create a new image file and write BMP header - self.newFile(fileName) - self.writeFile(self.data[0:headerSize]) - # Empty result - out = {} - else: - # Compose result - out = { - "mode": "Fax", - "width": self.width, - "height": self.height, - "depth": self.depth, - "faxMode": modeName, - "timestamp": timeStamp, - "filename": fileName, - "frequency": self.frequency - } - # Remove parsed data - del self.data[0:headerSize] + # Skip all data, but leave some since we may have 'BM ...' + l = ph if ph>=0 and ph+14>=len(self.data) else len(self.data)-1 + del self.data[0:l] - except Exception as exptn: - logger.debug("%s: Exception parsing: %s" % (self.myName(), str(exptn))) + except Exception as e: + logger.debug("%s: Exception parsing: %s" % (self.myName(), str(e))) # Return parsed result or None if no result yet return out diff --git a/owrx/sstv.py b/owrx/sstv.py index dc8a9760..65c9993e 100644 --- a/owrx/sstv.py +++ b/owrx/sstv.py @@ -82,6 +82,9 @@ class SstvParser(ThreadModule): logger.debug("Deleting short bitmap file '%s'." % self.fileName) os.unlink(self.fileName) else: + # Convert file from BMP to PNG + logger.debug("Converting '%s' to PNG..." % self.fileName) + Storage().convertImage(self.fileName) # Delete excessive files from storage logger.debug("Performing storage cleanup...") Storage().cleanStoredFiles() diff --git a/owrx/storage.py b/owrx/storage.py index db2db65e..16929bd5 100644 --- a/owrx/storage.py +++ b/owrx/storage.py @@ -2,6 +2,7 @@ from owrx.config.core import CoreConfig from owrx.config import Config from datetime import datetime +import subprocess import os import re @@ -11,7 +12,7 @@ logger = logging.getLogger(__name__) class Storage(object): def __init__(self): - self.filePattern = r'[A-Z]+-[0-9]+-[0-9]+(-[0-9]+)?\.bmp' + self.filePattern = r'[A-Z]+-[0-9]+-[0-9]+(-[0-9]+)?\.(bmp|png)' # Get file name pattern def getNamePattern(self): @@ -49,6 +50,22 @@ class Storage(object): logger.debug("Deleting stored file '%s'." % os.path.basename(f)) try: os.unlink(f) - except Exception as exptn: - logger.debug(str(exptn)) + except Exception as e: + logger.debug("cleanStoredFiles(): " + str(e)) + def convertImage(self, inFile: str): + # Adds storage path + if not inFile.startswith('/'): + inFile = self.getFilePath(inFile) + # Only converting BMP files for now + outFile = re.sub('\.bmp$', '.png', inFile) + if outFile==inFile: + return + try: + # Use ImageMagick to convert file + params = ['convert', inFile, outFile] + subprocess.check_call(params) + # If conversion was successful, delete original file + os.unlink(inFile) + except Exception as e: + logger.debug("convertImage(): " + str(e))