Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

Python3 Base64 Maker

'''
When writing stories for twine2, there is no option for directly importing 
scenic pictures.  So you need to write some code, save the file, do some 
stupid stuff, etc. 

However, you can also just quickly paste base64 data becase twine2 renders 
into a browser.    This means your data files like pictures, music, etc
will load as a single file instead of trying to host it online.

When you pass your twine2 book around, the user only has 1 file to get.
This is much simpler, and likely faster too.   The downside is the file size.
You could easily have a few hundred MB book that embeds base64 data into it.

In this case it would load slow because your machine needs to load all images at once.

So, it solves some problems, but you would need to divide your book into chapters
to prevent long page load times.

You can also simply convert an image to base64, it's not ONLY for twine2 you 
would want to do this.    I wrote it for twine2 though.

Basically it creates something like this:

<img src="data:image/png;base64,[Base64 Goes Here]"/>

which you just cut and paste into your twine passage.

TODO:  add option for audio data too!

<audio autoplay loop>
  <source src="data:audio/mp3;base64,[Base64 Goes Here]" type="audio/mp3">
</audio>


'''
import sys
import tkinter as tk
import tkinter.messagebox as mb                             # to make the popup

from tkinter import *
from tkinter import Tk, Text, BOTH, W, N, E, S, DISABLED
from tkinter import filedialog                              # for file selector

from tkinter import ttk
from tkinter.ttk import Frame, Button, Label, Style, LabelFrame

# loads up default browser search
import webbrowser
import time
import base64

# some colors predefined
bgcolor = '#f0f0f0'
white  = '#FFFFFF'

dblack = '#000000'
lblack = '#444444'

dred   = '#FF0000'
lred   = '#f9dede'

dgreen = '#076d05'
lgreen = '#e0f9d9'

dblue  = '#0000FF'
lblue  = '#e2e6ff'

# globals
debug = 0
start_dir = "."
title = "Base64 Maker for Twine2 Visual Novels"

# general appearances
# todo: fix up fonts and bold stuff, hard to read
opts1 = { 'padx': 10, 'pady': 10 , 'sticky': 'nswe' } # centered
opts2 = { 'ipadx': 5, 'ipady': 5 , 'sticky': 'e' } # right justified
opts3 = { 'ipadx': 5, 'ipady': 5 , 'sticky': 'w' } # left justified
opts4 = { 'fg': 'red', 'font': 14}
opts5 = { 'fg': 'black', 'font': 14}
opts6 = { 'ipadx': 13, 'ipady': 13 , 'padx': 0, 'pady': 0, 'sticky': 'nswe' } # centered

class b64_data(tk.Tk):
    def __init__(self):
        super().__init__()
        self.initUI()

    """
    Creates the gui, buttons, fields
    """
    def initUI(self):
        # predefine variables we are using/looking for
        self.image = tk.StringVar()

        # the title bar
        self.title(title)
        
        self.style = ttk.Style(self.master)
        # Check if 'winnative' theme is available, else use a cross-platform theme
        if 'winnative' in self.style.theme_names():
            self.style.theme_use('winnative')
        else:
            # Fallback to 'clam' or another available theme
            self.style.theme_use('clam')

        self.style.configure('H.TLabel', font=('Arial', 18, 'bold'))
        self.style.configure('H.TLabel', foreground=dblack)
        self.style.configure('H.TLabel', background=bgcolor)


        self.style.configure("TLabelframe", font=('Arial', 14, 'bold'))
        self.style.configure('TLabelframe', foreground=dblue)
        self.style.configure('TLabelframe', background=bgcolor)
        self.style.configure('TLabelframe', bordercolor=dred)
        self.style.configure('TLabelframe', borderwidth = 0, highlightthickness = 0)
        #self.style.configure('TLabelframe', padx=0, pady=0, ipadx=0, ipady=0)

        self.style.configure("TLabelframe.Label", font=('Arial', 14, 'bold'))
        self.style.configure('TLabelframe.Label', foreground=dblue)
        self.style.configure('TLabelframe.Label', background=bgcolor)
        self.style.configure('TLabelframe.Label', bordercolor=dblue)
        #self.style.configure('TLabelframe.Label', padx=0, pady=0, ipadx=0, ipady=0)
        #self.style.configure('TLabelframe.Label', borderwidth = 0, highlightthickness = 0)

        self.style.configure('TLabel', font=('Arial', 12))
        self.style.configure('TLabel', foreground=dblack)
        self.style.configure('TLabel', background=bgcolor)
        #self.style.configure('TLabel', padx=0, pady=0, ipadx=0, ipady=0)
        #self.style.configure('TLabel', borderwidth = 0, highlightthickness = 0)
        
        self.style.configure('TButton', font=('Arial', 12, 'bold'))
        self.style.configure('TButton', foreground=dblack)
        self.style.configure('TButton', background=bgcolor)
        self.style.configure('TButton', padx=5, pady=5, ipadx=22, ipady=25)

        self.style.configure('TEntry', font=('Arial', 12, 'bold'))
        self.style.configure('TEntry', foreground=dblack)
        self.style.configure('TEntry', background=white)
        self.style.configure('TEntry', padx=10, pady=10, ipadx=20, ipady=20)
        
        # corners for spacing/layout
        # I wanted equal spacing on both sides of my grid layout objects
        # the solution was a space as a label todo: what is better way?
        self.label_nw = ttk.Label(self, text=" ")
        self.label_nw.grid(row=0, column=0, **opts1)
        self.label_ne = ttk.Label(self, text=" ")
        self.label_ne.grid(row=0, column=9, **opts1)

        # Instructions header
        self.label_b = ttk.Label(self, text="Select an File to Base64 Encode it",  style="H.TLabel")
        self.label_b.grid(row=1, column=1, columnspan=8, **opts1)

        # label frame for "CONTROLS"
        self.controls = ttk.LabelFrame(self, text="CONTROLS")
        self.controls.grid(row=2, column=1, columnspan=8, **opts1)
        
        # buttons
        self.btn_load_img = ttk.Button(self.controls, text=" LOAD IMAGE",command=self.load_image)
        self.btn_load_img.grid(row=3,column=1, columnspan=2)

        self.btn_load_audio = ttk.Button(self.controls, text=" LOAD AUDIO",command=self.load_audio)
        self.btn_load_audio.grid(row=3,column=3, columnspan=2)

        self.btn_view = ttk.Button(self.controls, text=" PREVIEW ",command=self.preview)
        self.btn_view.grid(row=3,column=5, columnspan=2)

        # variable where the filename/path is stored in memory
        self.loadfile = tk.StringVar()
        
        # entry where the filename is visible to us
        ttk.Entry(self.controls, textvariable=self.loadfile, width=84).grid(
            row=4, column=1, columnspan=6, **opts1)

        # Instructions header
        self.h_images = ttk.LabelFrame(self, text="IMAGE INSTRUCTIONS", style="TLabelframe")
        self.h_images.grid(row=5, column=1, columnspan=8, **opts1)

        img_instruct = """There will be a file created called b64_somefile.png.html.
Open this file with a text editor and you will see:

<img src="data:image/png;base64,[BASE64HERE]"/>
        
Paste this huge block of text directly into the twine2 passage where
you want it to appear.

It is advised to use .png images, but other formats appear to work."""

        self.img_instructions = ttk.Label(self.h_images, text=img_instruct)
        self.img_instructions.grid(row=6, column=1, columnspan=6, **opts1)

        # Instructions header
        self.h_audio = ttk.LabelFrame(self, text="AUDIO INSTRUCTIONS", style="TLabelframe")
        self.h_audio.grid(row=7, column=1, columnspan=8, **opts1)

        mp3_instruct = """There will be a file created called b64_somefile.mp3.html.
Open this file with a text editor and you will see:

<audio autoplay loop controls>
    <source src="data:audio/mp3;base64,[BASE64HERE]" type="audio/mp3">
</audio>'

You can remove loop and controls if you do not want those on your audio.
        
Paste this huge block of text directly into the twine2 passage where 
you want the audio to play.  It will not preview because autoplay
is disabled on many browsers.   It will work if you view it in twine2.

It is advisable to use mp3 only because this is universally compatible.
"""

        self.mp3_instructions = ttk.Label(self.h_audio, text=mp3_instruct)
        self.mp3_instructions.grid(row=8, column=1, columnspan=6, **opts1)


        # label frame for "DATA"
        #self.data = LabelFrame(self, text="DATA", style="B.TLabelframe")
        #self.data.grid(row=5, column=1, columnspan=8, **opts1)

        # the block of text containing the base64 data
        #self.encoded_b64_data = tk.Text(self.data, fg=dgreen,bg=lgreen, height=5)
        #self.encoded_b64_data.grid(row=6, column=1, columnspan=6, rowspan=30, **opts1)



        # some spacers at bottom
        self.label_sw = ttk.Label(self, text=" ")
        self.label_sw.grid(row=9, column=0, **opts1)
        self.label_se = ttk.Label(self, text=" ")
        self.label_se.grid(row=9, column=9, **opts1)

    """
    Erase existing data in preparation for new
    """
    def clear_all(self):
        #self.encoded_b64_data.delete("1.0", tk.END)
        self.loadfile.set("")

    """
    loads the file you choose with dialog box
    Then automatically encodes it into the DATA field
    """
    def load_image(self):

        # call the clear all definition first
        self.clear_all()

        # the file selector dialogue, each OS has a different way to list the tuples
        # https://effbot.org/tkinterbook/tkinter-file-dialogs.htm
        loadfile =  filedialog.askopenfilename(
            initialdir = start_dir,
            title = "Select file",
            filetypes = (("PNG Files","*.png"),("JPG Files","*.jpg"),("All files","*.*")))

        # put your selection in the entry field
        self.loadfile.set(loadfile)

        # this is an example of a 1 button file processor.   Modify the following
        # to do whatever you want on your file and when you load it, it will automatically
        # process it however your code is written.  My code just finds the base64 equivalent.
        
        # the entire script and GUI is for the following 4 lines of code.
        # grab the selection from entry field and do the encoding
        # add prefix and suffix so it's a complete chunk of useable data
        encoded_data = ""
        with open(self.loadfile.get(), "rb") as img_file:
            encoded_data = base64.b64encode(img_file.read()).decode('utf-8')
            encoded_data = '<img src="data:image/png;base64,' + encoded_data + '"/>'
            
            #self.encoded_b64_data.insert(tk.INSERT, encoded_data)

        print("ENCODED IMAGE: ", self.loadfile.get())

        file_path = self.loadfile.get()
        #file_data = self.encoded_b64_data.get("1.0", "end-1c")
        file_data = encoded_data

        chunks = file_path.split('/')
        file_name = chunks[-1]

        file_name = 'b64_' + file_name + '.html'

        with open(file_name, 'w') as f:
            f.write(file_data)

        print("SAVED IMAGE: ", file_name)


    def load_audio(self):

        # call the clear all definition first
        self.clear_all()

        # the file selector dialogue, each OS has a different way to list the tuples
        # https://effbot.org/tkinterbook/tkinter-file-dialogs.htm
        loadfile =  filedialog.askopenfilename(
            initialdir = start_dir,
            title = "Select file",
            filetypes = (("MP3 Files","*.mp3"),("WAV Files","*.wav"),("OGG Files", "*.ogg"),("All files","*.*")))

        # put your selection in the entry field
        self.loadfile.set(loadfile)


        # this is an example of a 1 button file processor.   Modify the following
        # to do whatever you want on your file and when you load it, it will automatically
        # process it however your code is written.  My code just finds the base64 equivalent.
        
        # the entire script and GUI is for the following 4 lines of code.
        # grab the selection from entry field and do the encoding
        # add prefix and suffix so it's a complete chunk of useable data
        encoded_data = ""
        
        with open(self.loadfile.get(), "rb") as file:
            encoded_data = base64.b64encode(file.read()).decode('utf-8')
            encoded_data = '<audio autoplay loop controls><source src="data:audio/mp3;base64,'+ encoded_data +'" type="audio/mp3"></audio>'

            #self.encoded_b64_data.insert(tk.INSERT, encoded_data)

        print("ENCODED AUDIO: ", self.loadfile.get())

        file_path = self.loadfile.get()
        #file_data = self.encoded_b64_data.get("1.0", "end-1c")
        file_data = encoded_data

        chunks = file_path.split('/')
        file_name = chunks[-1]

        file_name = 'b64_' + file_name + '.html'

        with open(file_name, 'w') as f:
            f.write(file_data)

        print("SAVED AUDIO: ", file_name)
        
    """
    You can open it in browser if you want
    """
    def preview(self):
        # take whatever data is already in the loadfile entry and use it
        file_path = self.loadfile.get()
        #file_data = self.encoded_image.get("1.0", "end-1c")

        # split the filepath and grab file name from last member or array
        chunks = file_path.split('/')
        file_name = chunks[-1]

        # rebuild filename to work with assocated OS defaults
        file_name = 'b64_' + file_name + '.html'

        # pop open the file in the default browser
        webbrowser.open(file_name)
        
        print("PREVIEW: ", file_name)


# ok, do the stuff above
if __name__ == "__main__":

    app = b64_data()
    app.mainloop()
Last updated on 20 Aug 2023
Published on 20 Aug 2023