diff --git a/fotobox/README.md b/fotobox/README.md new file mode 100644 index 00000000..a3bf24b5 --- /dev/null +++ b/fotobox/README.md @@ -0,0 +1,75 @@ +# IBM Fotobox + +This is the IBM Fotobox. Deploy your own Fotobox straight to the IBM Cloud Access it directly from any device with browser and camera. Take Pictures and view them all from your device. + +The solution is based on: +- The frontend a Svelte Single Page Application running as a App on Code Engine able to scale to 0 in oder to optimise cost. +- The Upload function which generates a thumbnail of the image and stores both in COS written in Python and running as a Function on Code Engine. +- The Downloader a Go programm designed to serve the images stored in COS or download all at one go if you are the operator of the fotobox. +- All Images are stored in IBM Cloud Object Storage to ensure security and scalability. + +## Setup + +If you use your own machine you'll need to install the following (if not +already installed) and make sure you have a IBM Cloud Account: + +- [IBM Cloud command line (`ibmcloud`)](https://cloud.ibm.com/docs/cli/reference/ibmcloud?topic=cloud-cli-getting-started) +- [Code Engine plugin (`ce`)](https://cloud.ibm.com/codeengine/cli) +- [Terraform ](https://developer.hashicorp.com/terraform/install) +- [jq](https://jqlang.org/) + + +## Automated Setup + +In order to make the setup as convinent as possible we probide you with a setupscript which uses teraform and the IBM Cloud CLI + +1. configure the `terraform.auto.tfvars` with your apikey and your resource group id + +2. Run the `setup.sh `and it will deploy all the required components and done + +## Manual setup + +1. Setup COS with cos bucket this can be done over the UI or using the CLI + note dont the bucket name and API credentials + +2. Deploy the Upload Function using the CLI + ```bash + ibmcloud ce fn create --name fotobox-cos-upload --runtime python --build-source upload-function + ``` + +3. Deploy the Download App using the CLI + ```bash + ibmcloud ce app create --name fotobox-get-pics --build-dockerfile Dockerfile --build-source download-app + ``` + +4. Create a Secret map containing the following values. use the following command to create the password + ```bash + echo -n "password" | sha256 + 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8 + + ``` + Secrets/environment variables + ``` + apikey="cos api key mus upload und download können" + resource_instance_id="cos credential" + bucket="mybucket" + imageprefix="my-event-" + passwords="5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" + ``` + +5. Reference the full secret to the function and the app + +6. inside the the [stores.js](frontend-app/src/stores.js) replace the URLs to the upload function and download app + + +```javascript +export const uploadURL = "" +export const downloadURL = " To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/fotobox/frontend-app/package.json b/fotobox/frontend-app/package.json new file mode 100644 index 00000000..9a1de3d9 --- /dev/null +++ b/fotobox/frontend-app/package.json @@ -0,0 +1,29 @@ +{ + "name": "photobooth", + "version": "0.0.1", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview" + }, + "devDependencies": { + "@fontsource/fira-mono": "^5.0.0", + "@neoconfetti/svelte": "^2.0.0", + "@sveltejs/adapter-auto": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "carbon-components-svelte": "^0.85.2", + "svelte": "^4.2.7", + "vite": "^5.0.3" + }, + "type": "module", + "dependencies": { + "@sveltejs/adapter-node": "^5.2.7", + "carbon-icons-svelte": "^12.13.0", + "mermaid": "^11.4.0", + "qrcode": "^1.5.4" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.9.5" + } +} diff --git a/fotobox/frontend-app/robots.txt b/fotobox/frontend-app/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/fotobox/frontend-app/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/fotobox/frontend-app/src/app.css b/fotobox/frontend-app/src/app.css new file mode 100644 index 00000000..1441d940 --- /dev/null +++ b/fotobox/frontend-app/src/app.css @@ -0,0 +1,107 @@ +@import '@fontsource/fira-mono'; + +:root { + --font-body: Arial, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, + Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + --font-mono: 'Fira Mono', monospace; + --color-bg-0: rgb(202, 216, 228); + --color-bg-1: hsl(209, 36%, 86%); + --color-bg-2: hsl(224, 44%, 95%); + --color-theme-1: #ff3e00; + --color-theme-2: #4075a6; + --color-text: rgba(0, 0, 0, 0.7); + --column-width: 42rem; + --column-margin-top: 4rem; + font-family: var(--font-body); + color: var(--color-text); +} + +body { + min-height: 100vh; + margin: 0; + background-attachment: fixed; + background-color: var(--color-bg-1); + background-size: 100vw 100vh; + background-image: radial-gradient( + 50% 50% at 50% 50%, + rgba(255, 255, 255, 0.75) 0%, + rgba(255, 255, 255, 0) 100% + ), + linear-gradient(180deg, var(--color-bg-0) 0%, var(--color-bg-1) 15%, var(--color-bg-2) 50%); +} + +h1, +h2, +p { + font-weight: 400; +} + +p { + line-height: 1.5; +} + +a { + color: var(--color-theme-1); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +h1 { + font-size: 2rem; + text-align: center; +} + +h2 { + font-size: 1rem; +} + +pre { + font-size: 16px; + font-family: var(--font-mono); + background-color: rgba(255, 255, 255, 0.45); + border-radius: 3px; + box-shadow: 2px 2px 6px rgb(255 255 255 / 25%); + padding: 0.5em; + overflow-x: auto; + color: var(--color-text); +} + +.text-column { + display: flex; + max-width: 48rem; + flex: 0.6; + flex-direction: column; + justify-content: center; + margin: 0 auto; +} + +input, +button { + font-size: inherit; + font-family: inherit; +} + +button:focus:not(:focus-visible) { + outline: none; +} + +@media (min-width: 720px) { + h1 { + font-size: 2.4rem; + } +} + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} diff --git a/fotobox/frontend-app/src/app.html b/fotobox/frontend-app/src/app.html new file mode 100644 index 00000000..77a5ff52 --- /dev/null +++ b/fotobox/frontend-app/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/fotobox/frontend-app/src/lib/images/github.svg b/fotobox/frontend-app/src/lib/images/github.svg new file mode 100644 index 00000000..bc5d249d --- /dev/null +++ b/fotobox/frontend-app/src/lib/images/github.svg @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/fotobox/frontend-app/src/lib/images/svelte-logo.svg b/fotobox/frontend-app/src/lib/images/svelte-logo.svg new file mode 100644 index 00000000..49492a83 --- /dev/null +++ b/fotobox/frontend-app/src/lib/images/svelte-logo.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/fotobox/frontend-app/src/lib/images/svelte-welcome.png b/fotobox/frontend-app/src/lib/images/svelte-welcome.png new file mode 100644 index 00000000..fe7d2d6b Binary files /dev/null and b/fotobox/frontend-app/src/lib/images/svelte-welcome.png differ diff --git a/fotobox/frontend-app/src/lib/images/svelte-welcome.webp b/fotobox/frontend-app/src/lib/images/svelte-welcome.webp new file mode 100644 index 00000000..6ec1a28d Binary files /dev/null and b/fotobox/frontend-app/src/lib/images/svelte-welcome.webp differ diff --git a/fotobox/frontend-app/src/routes/+layout.svelte b/fotobox/frontend-app/src/routes/+layout.svelte new file mode 100644 index 00000000..7357eaf2 --- /dev/null +++ b/fotobox/frontend-app/src/routes/+layout.svelte @@ -0,0 +1,153 @@ + + +
+ + + + + + + + + + {#if $page.url.pathname === '/slideshow'} + + + + Grid Cycle {slideshowToggleTimer ? "on": "off"} + Show Download {imageToggleDownload ? "on": "off"} + Download All + {#if downloadingAll} + + {/if} + + + + {/if} +
+ + + + + + + + {#if $page.url.pathname === '/slideshow'} + + + + + {#if downloadingAll} + + {/if} + + {/if} + + + + + + + + + {#if showDeveloperCard} + + {/if} + + diff --git a/fotobox/frontend-app/src/routes/+page.js b/fotobox/frontend-app/src/routes/+page.js new file mode 100644 index 00000000..a72419a6 --- /dev/null +++ b/fotobox/frontend-app/src/routes/+page.js @@ -0,0 +1,3 @@ +// since there's no dynamic data here, we can prerender +// it so that it gets served as a static asset in production +export const prerender = true; diff --git a/fotobox/frontend-app/src/routes/+page.svelte b/fotobox/frontend-app/src/routes/+page.svelte new file mode 100644 index 00000000..e8025a8b --- /dev/null +++ b/fotobox/frontend-app/src/routes/+page.svelte @@ -0,0 +1,87 @@ + + + + + + + + + +

IBM Girls Day 2025

+

The easy way to save a snapshot of the Event

+
+
+
+ + + +

+ Disclamer: By using this photobox, you agree that your dazzling or hilariously awkward photos may be displayed for all to admire and stored for posterity. + Think of it as your ticket to instant fame—or at least a great laugh at the office party. + If you prefer to stay mysterious, maybe skip the photobox... but where's the fun in that? +

+
+
+
+
+ + +
+ {#if qrCodeData} + QR Code + {/if} +
+
+
+
+ + + + + + + + + +
\ No newline at end of file diff --git a/fotobox/frontend-app/src/routes/Alert.svelte b/fotobox/frontend-app/src/routes/Alert.svelte new file mode 100644 index 00000000..c41e224f --- /dev/null +++ b/fotobox/frontend-app/src/routes/Alert.svelte @@ -0,0 +1,68 @@ + + + + + + {#if showNotification} +
+ {#if data.success} + { + timeout = undefined; + dispatch("resetAlert"); + }} + /> + {:else} + { + timeout = undefined; + dispatch("resetAlert"); + }} + /> + {/if} + + +
+ {/if} + +
+
+ + + diff --git a/fotobox/frontend-app/src/routes/DevInfoCard.svelte b/fotobox/frontend-app/src/routes/DevInfoCard.svelte new file mode 100644 index 00000000..a2c0eef4 --- /dev/null +++ b/fotobox/frontend-app/src/routes/DevInfoCard.svelte @@ -0,0 +1,46 @@ + + + + +
+ Developer +
+

Luke Roy

+

Created the Foto Box. Slack me for more info.

+
+
diff --git a/fotobox/frontend-app/src/routes/architecture/+page.svelte b/fotobox/frontend-app/src/routes/architecture/+page.svelte new file mode 100644 index 00000000..fe32549b --- /dev/null +++ b/fotobox/frontend-app/src/routes/architecture/+page.svelte @@ -0,0 +1,147 @@ + + +
+ +
+ + + +

Fotobox Architecture Diagram

+
+
+ + + + + +

Components

+
+ + {#each componentsInfo as comp } + {comp["name"]} + {/each} + +
+ +
+
{componentsInfo[selectedIndex].name}
+
+ Language/Framework: {componentsInfo[selectedIndex].lang} +
+
+ Type: {componentsInfo[selectedIndex].type} +
+
+ Description: {componentsInfo[selectedIndex].description} +
+
+
+
+
+
+ \ No newline at end of file diff --git a/fotobox/frontend-app/src/routes/camera/+page.svelte b/fotobox/frontend-app/src/routes/camera/+page.svelte new file mode 100644 index 00000000..10c81314 --- /dev/null +++ b/fotobox/frontend-app/src/routes/camera/+page.svelte @@ -0,0 +1,226 @@ + + + + + +
+ {#if dataURL == ""} + + + + + + {:else} + + Your Photo + {/if} +
+ + + +
+
+ + + + + + + +
+ + + + + + +

Disclamer: Images are Displayed and Stored

+
+ {#if dataURL == ""} + + + + + + {:else} + + + {/if} + + +
+
+ alertInfo={}}> +
+ + + \ No newline at end of file diff --git a/fotobox/frontend-app/src/routes/slideshow/+page.svelte b/fotobox/frontend-app/src/routes/slideshow/+page.svelte new file mode 100644 index 00000000..8fbc11bd --- /dev/null +++ b/fotobox/frontend-app/src/routes/slideshow/+page.svelte @@ -0,0 +1,370 @@ + + + + + + +{#if !passwordSet} + (open = false)} + on:open + on:close + on:submit={getImages} + > + + + +{/if} + +{#if gridviewToggle} +
+ {#each paginatedImages($images, currentPage, itemsPerPage) as image} +
+ + + + + {#if showNames || showImageDownloads} + saveOriginalImage(image)} >{image.original} + {/if} + +
+ {/each} +
+ + (currentPage = event.detail.page)} +/> +{:else} +

NO

+{/if} + + + + + diff --git a/fotobox/frontend-app/src/stores.js b/fotobox/frontend-app/src/stores.js new file mode 100644 index 00000000..82e9d7a0 --- /dev/null +++ b/fotobox/frontend-app/src/stores.js @@ -0,0 +1,8 @@ +import { writable } from 'svelte/store'; + +// Create a writable store +export const toggelGridTimer = writable(false); +export const toggleDownload = writable(false) + +export let uploadURL = "https://gd-25-fotobox-cos-upload.8kyziehrspg.eu-de.codeengine.appdomain.cloud" +export let downloadURL = "https://gd-25-fotobox-get-pics.8kyziehrspg.eu-de.codeengine.appdomain.cloud" \ No newline at end of file diff --git a/fotobox/frontend-app/static/favicon.png b/fotobox/frontend-app/static/favicon.png new file mode 100644 index 00000000..825b9e65 Binary files /dev/null and b/fotobox/frontend-app/static/favicon.png differ diff --git a/fotobox/frontend-app/static/luke.jpg b/fotobox/frontend-app/static/luke.jpg new file mode 100644 index 00000000..445ef5d3 Binary files /dev/null and b/fotobox/frontend-app/static/luke.jpg differ diff --git a/fotobox/frontend-app/static/robots.txt b/fotobox/frontend-app/static/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/fotobox/frontend-app/static/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/fotobox/frontend-app/svelte.config.js b/fotobox/frontend-app/svelte.config.js new file mode 100644 index 00000000..37d06a30 --- /dev/null +++ b/fotobox/frontend-app/svelte.config.js @@ -0,0 +1,13 @@ +import adapter from '@sveltejs/adapter-node'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/fotobox/frontend-app/vite.config.js b/fotobox/frontend-app/vite.config.js new file mode 100644 index 00000000..bbf8c7da --- /dev/null +++ b/fotobox/frontend-app/vite.config.js @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/fotobox/setup.sh b/fotobox/setup.sh new file mode 100755 index 00000000..5e3bf906 --- /dev/null +++ b/fotobox/setup.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +pushd cos-setup + +terraform init + +terraform apply && sleep 10 + +popd + +echo "create upload-function and download-application in code engine" + +ibmcloud login --apikey $1 +ibmcloud target -g $(ibmcloud resource groups --default --output JSON | jq -r '.[0].id') + +ibmcloud ce project select -n codeengine-fotobox-project # name of the project that was setup in terraform script + +function_url=$(ibmcloud ce fn create --name fotobox-cos-upload --runtime python --build-source upload-function --env-from-configmap fotobox-config --env-from-secret fotobox-secret -o jsonpath={.endpoint}) + +app_url=$(ibmcloud ce app create --name fotobox-get-pics --build-dockerfile Dockerfile --build-source download-app --env-from-configmap fotobox-config --env-from-secret fotobox-secret -o jsonpath={.status.url}) + +echo "import { writable } from 'svelte/store'; + +// Create a writable store +export const toggelGridTimer = writable(false); +export const toggleDownload = writable(false) + +export const uploadURL = \"$function_url\" +export const downloadURL = \"$app_url\" +" > ./frontend-app/src/stores.js + +ibmcloud ce app create --name fotobox-frontend --build-dockerfile Dockerfile --build-source frontend-app + + + diff --git a/fotobox/upload-function/__main__.py b/fotobox/upload-function/__main__.py new file mode 100644 index 00000000..d8674924 --- /dev/null +++ b/fotobox/upload-function/__main__.py @@ -0,0 +1,116 @@ +import base64 +import os +import datetime +import ibm_boto3 +from ibm_botocore.client import Config +import string +import random +from PIL import Image, ImageOps + +COS_ENDPOINT = os.environ.get('endpointURL', "https://s3.us-south.cloud-object-storage.appdomain.cloud") +COS_API_KEY = os.environ.get('apikey', "apikey") +COS_INSTANCE_CRN =os.environ.get('resource_instance_id', "resource_instance_id") +COS_BUCKET_NAME = os.environ.get('bucket', "fotobox") + +IMG_PREFIX = os.environ.get("imageprefix","fotobox-prefix-") + + +alphabet = string.ascii_lowercase + string.digits +def random_choice(): + return ''.join(random.choices(alphabet, k=8)) + +def process_image(params): + # Extract the base64 string from the 'image' field in the params dictionary + image_base64 = params.get('image', '') + + if not image_base64: + raise ValueError("No image data provided.") + + # Decode the base64 string + image_data = base64.b64decode(image_base64) + + datetime_str = datetime.datetime.fromtimestamp(int(datetime.datetime.now().timestamp())).strftime("%Y-%m-%d-%H-%M-%S") + + + # Specify the file path where the image will be saved + image_file_path = f'{IMG_PREFIX}{datetime_str}-{random_choice()}.png' + + # Save the decoded image data to a file + with open(image_file_path, 'wb') as image_file: + image_file.write(image_data) + + return image_file_path + + +def create_thumbnail_with_padding(input_path, size=(384, 216), background_color=(255, 255, 255)): + """ + Creates a thumbnail from a PNG image with padding to ensure a consistent aspect ratio. + + Parameters: + - input_path (str): The file path of the input PNG image. + - size (tuple): The target size of the thumbnail (width, height). + - background_color (tuple): RGB color for padding, default is white (255, 255, 255). + """ + output_path = f"thumbnail-{input_path}" + try: + # Open the input image + with Image.open(input_path) as img: + # Resize the image, maintaining the aspect ratio + img.thumbnail(size, Image.LANCZOS) + + # Create a new image with the desired size and background color + padded_img = Image.new("RGB", size, background_color) + # Calculate position to center the resized image on the background + offset = ((size[0] - img.width) // 2, (size[1] - img.height) // 2) + # Paste the resized image onto the background + padded_img.paste(img, offset) + + # Save the padded thumbnail + padded_img.save(output_path, "PNG") + print(f"Thumbnail with padding saved to {output_path}") + return output_path + except Exception as e: + print(f"An error occurred: {e}") + return "" + +def upload_file_to_cos(file_path, object_name): + try: + # Create a client for IBM COS + cos = ibm_boto3.client( + 's3', + ibm_api_key_id=COS_API_KEY, + ibm_service_instance_id=COS_INSTANCE_CRN, + config=Config(signature_version='oauth'), + endpoint_url=COS_ENDPOINT + ) + + # Upload file + with open(file_path, 'rb') as file_data: + cos.upload_fileobj(file_data, COS_BUCKET_NAME, object_name) + + print(f"File {object_name} uploaded successfully to bucket {COS_BUCKET_NAME}") + return True + except Exception as e: + print(f"Unable to upload file: {e}") + return False + + +def main(params): + + image_path = process_image(params) + thumbnail_path = create_thumbnail_with_padding(image_path) + success = upload_file_to_cos(image_path, image_path) + if thumbnail_path != "" and success: + success = upload_file_to_cos(thumbnail_path, thumbnail_path) + + + rspstatus = 200 + if not success: + rspstatus = 500 + return { + "headers": { + "Content-Type": "application/json", + }, + "statusCode": rspstatus, + "body": {"image": image_path, "thumbnail":thumbnail_path} + } \ No newline at end of file diff --git a/fotobox/upload-function/requirements.txt b/fotobox/upload-function/requirements.txt new file mode 100644 index 00000000..cae718d1 --- /dev/null +++ b/fotobox/upload-function/requirements.txt @@ -0,0 +1,2 @@ +ibm-cos-sdk +pillow \ No newline at end of file