Skip to content

Instantly share code, notes, and snippets.

@maglub
Last active April 17, 2024 09:29
Show Gist options
  • Save maglub/1954a916844c7b0118d9c16f1a6d3e9b to your computer and use it in GitHub Desktop.
Save maglub/1954a916844c7b0118d9c16f1a6d3e9b to your computer and use it in GitHub Desktop.
Python script for masking the Swiss cross in a QRCode in an image or PDF read from <stdin>. Output is a PNG formatted image on <stdout>

Introduction

This script is basically a workaround for the bug in zbarimg that it cannot read QRCodes with the Swiss Cross in them (as every QR Bill in Switzerland has). See https://qr-rechnung.net/#/

Usage:

cat file.jpg | ./mask_swiss_cross.py > masked.jpg
cat file.pdf | ./mask_swiss_cross.py > masked.png

#--- decode with zbarimg with the bad encoding detection
cat file.jpg | ./mask_swiss_cross.py | zbarimg -

#--- decode with zbarimg raw
cat file.jpg | ./mask_swiss_cross.py | zbarimg -q --raw --nodbus -Sqr.binary -

Script mask_swiss_cross.md

#!/usr/bin/env python3
#web####pythoncode.py#####

import numpy as np
import sys, os
import io
import cv2
import logging

from pdf2image import convert_from_bytes as PDF_convert_from_bytes

#============================================================
# Description
#
# Note, the output is always PNG if the input is PDF. Otherwise it keeps the jpg/png formatting
# 
# usage:
#
# cat file.jpg | ./mask_swiss_cross.py > masked.jpg
# cat file.pdf | ./mask_swiss_cross.py > masked.png
#--- decode with zbarimg with the bad encoding detection
# cat file.jpg | ./mask_swiss_cross.py | zbarimg -
#--- decode with zbarimg raw
# cat file.jpg | ./mask_swiss_cross.py | zbarimg -q --raw --nodbus -Sqr.binary -
#
#============================================================
#============================================================
# Functions
#============================================================
def identify_file_format(byte_stream):
    logging.info("  - identify_file_format")
    # Read the first few bytes of the stream
    start = byte_stream[:8]  # Read more bytes to ensure we cover different formats
    # Convert to hexadecimal for easier matching
    #    start_hex = start.hex()
    start_hex = ''.join(f'{byte:02x}' for byte in start)


    logging.info("  - Checking the bytes")
    # Check the signatures
    if start_hex.startswith('25504446'):  # %PDF in hex
        return 'PDF'
    elif start_hex.startswith('ffd8ff'):
        return 'JPEG'
    elif start_hex.startswith('89504e470d0a1a0a'):
        return 'PNG'
    else:
        return 'other image'

def mask_swiss_cross(img):
  #-----------------------------------------
  # Since most QR decoders have issues with the swiss cros in the middle of the qrcode
  #-----------------------------------------
  logging.info(f"  - Masking swiss cross")
  qcd = cv2.QRCodeDetector()
  retval, points = qcd.detect(img)

  logging.debug(points)

  poly_points = points[0]
  
  # Calculate the centroid of the polygon
  centroid = np.mean(poly_points, axis=0).astype(int)
  
  
  #--- draw edges around the qrcode
  #img = cv2.polylines(img, points.astype(int), True, (0, 255, 0), 3)
  
  
  # Calculate the total width spanned by the poly-points
  min_x = np.min(poly_points[:, 0])
  max_x = np.max(poly_points[:, 0])
  total_width = max_x - min_x
  
  # Calculate the rectangle's width as 1/8 of the total width
  #rect_width = total_width / 8
  rect_width = total_width / 10
  
  # Decide on the rectangle's height (arbitrary decision or based on specific criteria)
  # For demonstration, let's say we keep the height equal to the rectangle's width for a square shape
  rect_height = rect_width
  
  # Calculate the top-left and bottom-right points of the rectangle
  top_left = (int(centroid[0] - rect_width / 2), int(centroid[1] - rect_height / 2))
  bottom_right = (int(centroid[0] + rect_width / 2), int(centroid[1] + rect_height / 2))
    
  # Draw the filled rectangle in black
  cv2.rectangle(img, top_left, bottom_right, (0, 0, 0), -1) # -1 thickness fills the rectangle
  return img

def getFileBytesFromStream(file):
    image_stream = io.BytesIO(file)
    image_stream.seek(0)
    file_bytes = np.asarray(bytearray(image_stream.read()), dtype=np.uint8)

    return file_bytes
 
def identify_file_format(byte_stream):
    logging.info("  - identify_file_format")
    # Read the first few bytes of the stream
    start = byte_stream[:8]  # Read more bytes to ensure we cover different formats
    # Convert to hexadecimal for easier matching
#    start_hex = start.hex()
    start_hex = ''.join(f'{byte:02x}' for byte in start)


    logging.info("  - Checking the bytes")
    # Check the signatures
    if start_hex.startswith('25504446'):  # %PDF in hex
        return 'PDF'
    elif start_hex.startswith('ffd8ff'):
        return 'JPEG'
    elif start_hex.startswith('89504e470d0a1a0a'):
        return 'PNG'
    else:
        return 'other image'

#============================================================
# MAIN
#============================================================

if __name__ == "__main__":
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
  # Read image from stdin
  file_bytes = sys.stdin.buffer.read()
  format = identify_file_format(file_bytes)
  logging.info("Format: " + format)
  
  if format == "PDF":
    images = PDF_convert_from_bytes(file_bytes, 300)  # 300 is the dpi (dots per inch)
    image_np = np.array(images[0])
    img = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
    outputFormat = ".png"
  else:
    img = cv2.imdecode(np.frombuffer(file_bytes, np.uint8), cv2.IMREAD_COLOR)
    outputFormat = "." + format
#    img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
  
  img_masked = mask_swiss_cross(img)
  
  #_, buffer = cv2.imencode('.' + format, img)

  _, buffer = cv2.imencode(outputFormat, img)
  sys.stdout.buffer.write(buffer)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment