diff --git a/app/src/main/java/com/example/nerbos/MainActivity.kt b/app/src/main/java/com/example/nerbos/MainActivity.kt
index 6d8eeeff1eb15860623002c357be67c0256fa461..7e20fe6bb5a6d5518bf8d2ec0cc7b3ee1ed679c2 100644
--- a/app/src/main/java/com/example/nerbos/MainActivity.kt
+++ b/app/src/main/java/com/example/nerbos/MainActivity.kt
@@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 import com.example.nerbos.databinding.ActivityMainBinding
 import com.example.nerbos.fragments.scan.ScanFragment
+import com.example.nerbos.fragments.twibbon.TwibbonFragment
 import com.example.nerbos.fragments.statistic.StatisticFragment
 import com.example.nerbos.fragments.transaction.TransactionFragment
 import com.example.nerbos.fragments.user.UserFragment
@@ -18,12 +19,14 @@ class MainActivity : AppCompatActivity() {
     private val fragments = mapOf(
         R.id.transaction to TransactionFragment(),
         R.id.scan to ScanFragment(),
+        R.id.twibbon to TwibbonFragment(),
         R.id.statistic to StatisticFragment(),
         R.id.user to UserFragment()
     )
     private val fragmentTitles = mapOf(
         R.id.transaction to R.string.navbar_transaction,
         R.id.scan to R.string.navbar_scan,
+        R.id.twibbon to R.string.navbar_twibbon,
         R.id.statistic to R.string.navbar_statistic,
         R.id.user to R.string.navbar_user
     )
diff --git a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt
index f4c1d9c548598f2a0ce589070244d120c0c90651..81bedaa9f664cf07f7ec62551c6b819aba39ffb5 100644
--- a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt
+++ b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt
@@ -2,7 +2,6 @@ package com.example.nerbos.fragments.scan
 
 import android.Manifest
 import android.app.Activity
-import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.graphics.Bitmap
@@ -68,23 +67,11 @@ class ScanFragment : Fragment() {
     private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
     private lateinit var geocoder: Geocoder
     private var networkManagerService: NetworkManagerService = NetworkManagerService()
-    private var fragmentContext: Context? = null
-
     private val requestCameraPermissionCode = Manifest.permission.CAMERA
     private val uploadURL: String by lazy {
         requireContext().getString(R.string.backend_api_scan)
     }
 
-    override fun onAttach(context: Context) {
-        super.onAttach(context)
-        fragmentContext = context
-    }
-
-    override fun onDetach() {
-        super.onDetach()
-        fragmentContext = null
-    }
-
     override fun onCreateView(
         inflater: LayoutInflater, container: ViewGroup?,
         savedInstanceState: Bundle?
diff --git a/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt b/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1cd5e7ecadeb8b0dd4bae53fe2eae5887b9444f4
--- /dev/null
+++ b/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt
@@ -0,0 +1,349 @@
+package com.example.nerbos.fragments.twibbon
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.MediaStore
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.EditText
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.PreviewView
+import androidx.core.content.ContextCompat
+import androidx.core.net.toUri
+import androidx.exifinterface.media.ExifInterface
+import androidx.fragment.app.Fragment
+import com.example.nerbos.R
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class TwibbonFragment : Fragment() {
+
+    private lateinit var previewView: PreviewView
+    private lateinit var cameraExecutor: ExecutorService
+    private lateinit var imageCapture: ImageCapture
+    private lateinit var twibbonImageView: ImageView
+    private var currentTwibbonIndex = 0
+    private val twibbonTemplates = arrayOf(
+        R.drawable.twibbon_template_1,
+        R.drawable.twibbon_template_2,
+        R.drawable.twibbon_template_3,
+        R.drawable.twibbon_template_4,
+        R.drawable.twibbon_template_5
+    )
+    private val requestCameraPermissionCode = Manifest.permission.CAMERA
+
+    override fun onCreateView(
+        inflater: LayoutInflater, container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        // Inflate the layout for this fragment
+        val view = inflater.inflate(R.layout.fragment_twibbon, container, false)
+        previewView = view.findViewById(R.id.previewView)
+        twibbonImageView = view.findViewById(R.id.twibbonImageView)
+        return view
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        cameraExecutor = Executors.newSingleThreadExecutor()
+
+        if (allPermissionsGranted()) {
+            startCamera()
+        } else {
+            requestCameraPermission.launch(requestCameraPermissionCode)
+        }
+
+        view.findViewById<ImageButton>(R.id.captureButton).setOnClickListener {
+            takePicture()
+        }
+
+        view.findViewById<ImageButton>(R.id.leftButton).setOnClickListener {
+            currentTwibbonIndex = (currentTwibbonIndex - 1 + twibbonTemplates.size) % twibbonTemplates.size
+            updateTwibbonTemplate()
+        }
+
+        view.findViewById<ImageButton>(R.id.rightButton).setOnClickListener {
+            currentTwibbonIndex = (currentTwibbonIndex + 1) % twibbonTemplates.size
+            updateTwibbonTemplate()
+        }
+    }
+
+    private fun updateTwibbonTemplate() {
+        twibbonImageView.setImageResource(twibbonTemplates[currentTwibbonIndex])
+    }
+
+    private fun allPermissionsGranted() = ContextCompat.checkSelfPermission(
+        requireContext(),
+        Manifest.permission.CAMERA
+    ) == PackageManager.PERMISSION_GRANTED
+
+    private fun startCamera() {
+        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
+
+        cameraProviderFuture.addListener({
+            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
+
+            val preview = Preview.Builder()
+                .build()
+                .also {
+                    it.setSurfaceProvider(previewView.surfaceProvider)
+                }
+
+            imageCapture = ImageCapture.Builder().build()
+
+            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+            try {
+                cameraProvider.unbindAll()
+                cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+
+        }, ContextCompat.getMainExecutor(requireContext()))
+    }
+
+    private fun takePicture() {
+        val imageCapture = imageCapture
+
+        // Create output file to hold the captured image
+        val externalFilesDirs = requireContext().getExternalFilesDirs(null)
+        val photoFile = File(externalFilesDirs.firstOrNull(), "${System.currentTimeMillis()}.jpg")
+
+        // Setup image capture metadata
+        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
+
+        // Capture the image
+        imageCapture.takePicture(
+            outputOptions,
+            ContextCompat.getMainExecutor(requireContext()),
+            object : ImageCapture.OnImageSavedCallback {
+                override fun onError(exc: ImageCaptureException) {
+                    Toast.makeText(requireContext(), "Error capturing image: ${exc.message}", Toast.LENGTH_SHORT).show()
+                }
+
+                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
+                    // Image captured successfully, read orientation metadata
+                    val savedUri = photoFile.toUri()
+                    val cameraBitmap = BitmapFactory.decodeFile(savedUri.path)
+                    val rotatedCameraBitmap = savedUri.path?.let {
+                        rotateImageIfRequired(cameraBitmap,
+                            it
+                        )
+                    }
+                    val twibbonBitmap = BitmapFactory.decodeResource(resources, twibbonTemplates[currentTwibbonIndex])
+
+                    val combinedBitmap = overlayBitmap(rotatedCameraBitmap!!, twibbonBitmap)
+                    showImagePopup(combinedBitmap)
+                }
+            })
+    }
+
+    private fun overlayBitmap(cameraBitmap: Bitmap, twibbonBitmap: Bitmap): Bitmap {
+        // Determine the side length of the square image
+        val sideLength = minOf(cameraBitmap.width, cameraBitmap.height)
+
+        // Calculate the position to draw the twibbon image to center it
+        val left = (cameraBitmap.width - sideLength) / 2
+        val top = (cameraBitmap.height - sideLength) / 2
+
+        // Crop or resize the camera image to make it square
+        val croppedCameraBitmap = Bitmap.createBitmap(cameraBitmap, 0, 0, sideLength, sideLength)
+
+        // Resize the twibbon image to match the dimensions of the cropped camera image
+        val resizedTwibbonBitmap = Bitmap.createScaledBitmap(twibbonBitmap, sideLength, sideLength, true)
+
+        // Create a new bitmap with the combined images
+        val combinedBitmap = Bitmap.createBitmap(cameraBitmap.width, cameraBitmap.height, Bitmap.Config.ARGB_8888)
+        val canvas = Canvas(combinedBitmap)
+        val paint = Paint(Paint.FILTER_BITMAP_FLAG)
+
+        // Draw the cropped camera image onto the canvas
+        canvas.drawBitmap(croppedCameraBitmap, Matrix(), paint)
+
+        // Draw the resized twibbon image onto the canvas at the calculated position
+        canvas.drawBitmap(resizedTwibbonBitmap, left.toFloat(), top.toFloat(), paint)
+
+        return combinedBitmap
+    }
+
+    private fun rotateImageIfRequired(bitmap: Bitmap, path: String): Bitmap {
+        val ei = ExifInterface(path)
+        val orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+
+        return when (orientation) {
+            ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f)
+            ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f)
+            ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f)
+            else -> bitmap
+        }
+    }
+
+    private fun rotateImage(source: Bitmap, angle: Float): Bitmap {
+        val matrix = Matrix()
+        matrix.postRotate(angle)
+        return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true)
+    }
+
+    private fun showImagePopup(imageBitmap: Bitmap?) {
+        if (imageBitmap != null) {
+            MaterialAlertDialogBuilder(requireContext())
+                .setTitle("Save this picture?")
+                .setPositiveButton("Yes") { dialog, _ ->
+                    showFilenameInputDialog(imageBitmap)
+                    dialog.dismiss()
+                }
+                .setNegativeButton("No") { dialog, _ ->
+                    dialog.dismiss()
+                }
+                .setView(ImageView(requireContext()).apply {
+                    setImageBitmap(imageBitmap)
+                    adjustViewBounds = true
+                })
+                .show()
+                .apply {
+                    getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(
+                        ContextCompat.getColor(
+                            requireContext(),
+                            R.color.white
+                        )
+                    )
+                    getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(
+                        ContextCompat.getColor(
+                            requireContext(),
+                            R.color.white
+                        )
+                    )
+                }
+        } else {
+            showToastOnUIThread("Failed to load image")
+        }
+    }
+
+    private fun showFilenameInputDialog(imageBitmap: Bitmap) {
+        val inputView = LayoutInflater.from(requireContext()).inflate(R.layout.twibbon_filename, null)
+        val filenameEditText = inputView.findViewById<EditText>(R.id.filenameInput)
+
+        MaterialAlertDialogBuilder(requireContext())
+            .setTitle("Save Image")
+            .setView(inputView)
+            .setPositiveButton("Save") { dialog, _ ->
+                val filename = filenameEditText.text.toString().trim()
+                if (filename.isNotEmpty()) {
+                    saveImage(imageBitmap, filename)
+                } else {
+                    Toast.makeText(requireContext(), "Please enter a filename", Toast.LENGTH_SHORT).show()
+                    showFilenameInputDialog(imageBitmap)
+                }
+                dialog.dismiss()
+            }
+            .setNegativeButton("Cancel") { dialog, _ ->
+                dialog.dismiss()
+            }
+            .show()
+            .apply {
+                getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(
+                    ContextCompat.getColor(
+                        requireContext(),
+                        R.color.white
+                    )
+                )
+                getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(
+                    ContextCompat.getColor(
+                        requireContext(),
+                        R.color.white
+                    )
+                )
+            }
+    }
+
+    private fun saveImage(imageBitmap: Bitmap, filename: String) {
+        // Crop the image to a square (1:1 aspect ratio)
+        val croppedImage = if (imageBitmap.width >= imageBitmap.height) {
+            Bitmap.createBitmap(
+                imageBitmap,
+                (imageBitmap.width - imageBitmap.height) / 2,
+                0,
+                imageBitmap.height,
+                imageBitmap.height
+            )
+        } else {
+            Bitmap.createBitmap(
+                imageBitmap,
+                0,
+                (imageBitmap.height - imageBitmap.width) / 2,
+                imageBitmap.width,
+                imageBitmap.width
+            )
+        }
+
+        // Prepare values for saving the image
+        val contentValues = ContentValues().apply {
+            put(MediaStore.Images.Media.DISPLAY_NAME, "$filename.jpg")
+            put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
+        }
+
+        // Insert the image into MediaStore and get its URI
+        val resolver = requireContext().contentResolver
+        val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+
+        try {
+            // Save the image to the specified URI
+            if (uri != null) {
+                resolver.openOutputStream(uri)?.use { outputStream ->
+                    croppedImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
+                    outputStream.close()
+                    Toast.makeText(requireContext(), "Twibbon saved successfully", Toast.LENGTH_SHORT).show()
+                }
+            } else {
+                showToastOnUIThread("Failed to save image")
+            }
+        } catch (e: IOException) {
+            e.printStackTrace()
+            showToastOnUIThread("Failed to save image")
+        }
+    }
+
+    private fun showToastOnUIThread(message: String) {
+        Handler(Looper.getMainLooper()).post {
+            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
+        }
+    }
+
+    private val requestCameraPermission =
+        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
+            if (isGranted) {
+                startCamera()
+            } else {
+                // Handle the case where the user denied the permission
+                Toast.makeText(requireContext(), "Camera permission denied", Toast.LENGTH_SHORT).show()
+            }
+        }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        cameraExecutor.shutdown()
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/twibbon_filename.xml b/app/src/main/res/layout/twibbon_filename.xml
new file mode 100644
index 0000000000000000000000000000000000000000..b149e2a22d81053dba6a40198bd65da98ffa4511
--- /dev/null
+++ b/app/src/main/res/layout/twibbon_filename.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:padding="16dp">
+
+    <EditText
+        android:id="@+id/filenameInput"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:hint="@string/name"
+        android:inputType="text"
+        android:autofillHints="Twibbon Filename" />
+
+</LinearLayout>
\ No newline at end of file