Javiai commited on
Commit
e1c625c
1 Parent(s): c8799d6

First update

Browse files
Files changed (6) hide show
  1. .gitignore +1 -0
  2. app.py +90 -0
  3. app2.py +405 -0
  4. final_image.png +0 -0
  5. requirements.txt +0 -0
  6. sidebar.html +61 -0
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ venv/
app.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import streamlit as st
3
+ import numpy as np
4
+ from PIL import Image, ImageEnhance
5
+
6
+
7
+ def adjust_image(image, brightness, contrast, saturation):
8
+ # Adjust brightness and contrast
9
+ adjusted_image = cv2.convertScaleAbs(image, alpha=contrast, beta=brightness)
10
+ adjusted_image = np.clip(adjusted_image, 0, 255)
11
+ # Adjust saturation
12
+ image_hsv = cv2.cvtColor(adjusted_image, cv2.COLOR_BGR2HSV)
13
+ image_hsv[:, :, 1] = np.clip(image_hsv[:, :, 1] * saturation, 0, 255)
14
+ adjusted_image = cv2.cvtColor(image_hsv, cv2.COLOR_HSV2BGR)
15
+
16
+ return adjusted_image
17
+
18
+ def halftone (image):
19
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
20
+
21
+ # Set the size of the halftone dots
22
+ dot_size = 1 # Adjust this value to control dot size
23
+
24
+ # Create a blank canvas for the halftone effect
25
+ halftone = np.zeros_like(gray_image)
26
+
27
+ # Apply halftone effect by thresholding the image
28
+ for i in range(0, gray_image.shape[0], dot_size):
29
+ for j in range(0, gray_image.shape[1], dot_size):
30
+ roi = gray_image[i:i+dot_size, j:j+dot_size]
31
+ mean_val = np.mean(roi)
32
+ halftone[i:i+dot_size, j:j+dot_size] = 255 if mean_val > 128 else 0
33
+
34
+ return halftone
35
+
36
+
37
+
38
+ def main():
39
+
40
+
41
+ st.title("Image processing for Screen Printing")
42
+ st.subheader("Work in progress")
43
+ # Sidebar
44
+
45
+
46
+ st.sidebar.title("Adjustment")
47
+
48
+ brightness = st.sidebar.slider("Brightness", -100, 100, 0)
49
+ contrast = st.sidebar.slider("Contrast", 0.1, 3.0, 1.0)
50
+ saturation = st.sidebar.slider("Saturation", 0.1, 3.0, 1.0)
51
+
52
+ image = st.sidebar.file_uploader("Upload an image", type=["jpg", "jpeg", "png"])
53
+
54
+
55
+
56
+ if not image:
57
+ return None
58
+
59
+
60
+
61
+
62
+ # central app
63
+
64
+
65
+
66
+ image = cv2.imdecode(np.fromstring(image.read(), np.uint8), cv2.IMREAD_COLOR)
67
+
68
+
69
+
70
+ adjusted_image = adjust_image(image, brightness, contrast, saturation)
71
+
72
+ #adjusted_image = np.array(halftoned)
73
+
74
+ col1, col2 = st.columns(2)
75
+
76
+ with col1:
77
+ st.header("Original Image")
78
+ st.image(image, caption="Original Image", use_column_width=True, channels="BGR")
79
+
80
+ with col2:
81
+ st.header("Processed Image")
82
+ st.image(adjusted_image, caption="Processed Image", use_column_width=True, channels="BGR")
83
+
84
+ st.image(halftone(image))
85
+
86
+
87
+
88
+
89
+ if __name__ == '__main__':
90
+ main()
app2.py ADDED
@@ -0,0 +1,405 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ from io import BytesIO
3
+
4
+ import numpy as np
5
+ import requests
6
+ import streamlit as st
7
+ from PIL import Image, ImageEnhance, ImageOps
8
+ from rembg import remove
9
+ from streamlit_cropper import st_cropper
10
+
11
+ VERSION = "0.7.0"
12
+
13
+ st.set_page_config(
14
+ page_title="Image WorkDesk",
15
+ page_icon="🖼️",
16
+ menu_items={
17
+ "About": f"Image WorkDesk v{VERSION} "
18
+ f"\nApp contact: [Siddhant Sadangi](mailto:siddhant.sadangi@gmail.com)",
19
+ "Report a Bug": "https://github.com/SiddhantSadangi/ImageWorkdesk/issues/new",
20
+ "Get help": None,
21
+ },
22
+ layout="wide",
23
+ )
24
+
25
+ # ---------- SIDEBAR ----------
26
+ with open("sidebar.html", "r", encoding="UTF-8") as sidebar_file:
27
+ sidebar_html = sidebar_file.read().replace("{VERSION}", VERSION)
28
+
29
+ with st.sidebar:
30
+ with st.expander("Supported operations"):
31
+ st.info(
32
+ "* Upload image, take one with your camera, or load from a URL\n"
33
+ "* Crop\n"
34
+ "* Remove background\n"
35
+ "* Mirror\n"
36
+ "* Convert to grayscale or black and white\n"
37
+ "* Rotate\n"
38
+ "* Change brightness, saturation, contrast, sharpness\n"
39
+ "* Generate random image from supplied image\n"
40
+ "* Download results"
41
+ )
42
+ st.components.v1.html(sidebar_html, height=750)
43
+
44
+ # ---------- HEADER ----------
45
+ st.title("🖼️ Welcome to Image WorkDesk!")
46
+
47
+
48
+ # ---------- FUNCTIONS ----------
49
+ def _reset(key: str) -> None:
50
+ if key == "all":
51
+ st.session_state["rotate_slider"] = 0
52
+ st.session_state["brightness_slider"] = st.session_state[
53
+ "saturation_slider"
54
+ ] = st.session_state["contrast_slider"] = 100
55
+ st.session_state["bg"] = st.session_state["crop"] = st.session_state[
56
+ "mirror"
57
+ ] = st.session_state["gray_bw"] = 0
58
+ elif key == "rotate_slider":
59
+ st.session_state["rotate_slider"] = 0
60
+ elif key == "checkboxes":
61
+ st.session_state["crop"] = st.session_state["mirror"] = st.session_state[
62
+ "gray_bw"
63
+ ] = 0
64
+ else:
65
+ st.session_state[key] = 100
66
+
67
+
68
+ def _randomize() -> None:
69
+ st.session_state["mirror"] = np.random.choice([0, 1])
70
+ st.session_state["rotate_slider"] = np.random.randint(0, 360)
71
+ st.session_state["brightness_slider"] = np.random.randint(0, 200)
72
+ st.session_state["saturation_slider"] = np.random.randint(0, 200)
73
+ st.session_state["contrast_slider"] = np.random.randint(0, 200)
74
+ st.session_state["sharpness_slider"] = np.random.randint(0, 200)
75
+
76
+
77
+ # ---------- OPERATIONS ----------
78
+
79
+ option = st.radio(
80
+ label="Upload an image, take one with your camera, or load image from a URL",
81
+ options=(
82
+ "Upload an image ⬆️",
83
+ "Take a photo with my camera 📷",
84
+ "Load image from a URL 🌐",
85
+ ),
86
+ help="Uploaded images are deleted from the server when you\n* upload another image\n* clear the file uploader\n* close the browser tab",
87
+ )
88
+
89
+ if option == "Take a photo with my camera 📷":
90
+ upload_img = st.camera_input(
91
+ label="Take a picture",
92
+ )
93
+ mode = "camera"
94
+
95
+ elif option == "Upload an image ⬆️":
96
+ upload_img = st.file_uploader(
97
+ label="Upload an image",
98
+ type=["bmp", "jpg", "jpeg", "png", "svg"],
99
+ )
100
+ mode = "upload"
101
+
102
+ elif option == "Load image from a URL 🌐":
103
+ url = st.text_input(
104
+ "Image URL",
105
+ key="url",
106
+ )
107
+ mode = "url"
108
+
109
+ if url != "":
110
+ try:
111
+ response = requests.get(url)
112
+ upload_img = Image.open(BytesIO(response.content))
113
+ except:
114
+ st.error("The URL does not seem to be valid.")
115
+
116
+ with contextlib.suppress(NameError):
117
+ if upload_img is not None:
118
+ pil_img = (
119
+ upload_img.convert("RGB")
120
+ if mode == "url"
121
+ else Image.open(upload_img).convert("RGB")
122
+ )
123
+ img_arr = np.asarray(pil_img)
124
+
125
+ # ---------- PROPERTIES ----------
126
+ st.image(img_arr, use_column_width="auto", caption="Uploaded Image")
127
+ st.text(
128
+ f"Original width = {pil_img.size[0]}px and height = {pil_img.size[1]}px"
129
+ )
130
+
131
+ st.caption("All changes are applied on top of the previous change.")
132
+
133
+ # ---------- CROP ----------
134
+ st.text("Crop image ✂️")
135
+ cropped_img = st_cropper(Image.fromarray(img_arr), should_resize_image=True)
136
+ st.text(
137
+ f"Cropped width = {cropped_img.size[0]}px and height = {cropped_img.size[1]}px"
138
+ )
139
+
140
+ with st.container():
141
+ lcol, rcol = st.columns(2)
142
+ if lcol.checkbox(
143
+ label="Use cropped Image?",
144
+ help="Select to use the cropped image in further operations",
145
+ key="crop",
146
+ ):
147
+ image = cropped_img
148
+ else:
149
+ image = Image.fromarray(img_arr)
150
+
151
+ # ---------- REMOVE BACKGROUND ----------
152
+ if lcol.checkbox(
153
+ label="Remove background?",
154
+ help="Select to remove background from the image",
155
+ key="bg",
156
+ ):
157
+ image = remove(image)
158
+
159
+ # ---------- MIRROR ----------
160
+ if lcol.checkbox(
161
+ label="Mirror image? 🪞",
162
+ help="Select to mirror the image",
163
+ key="mirror",
164
+ ):
165
+ image = ImageOps.mirror(image)
166
+
167
+ # ---------- GRAYSCALE / B&W ----------
168
+ flag = True
169
+
170
+ if lcol.checkbox(
171
+ "Convert to grayscale / black & white? 🔲",
172
+ key="gray_bw",
173
+ help="Select to convert image to grayscale or black and white",
174
+ ):
175
+ mode = "L"
176
+ if (
177
+ lcol.radio(
178
+ label="Grayscale or B&W",
179
+ options=("Grayscale", "Black & White"),
180
+ )
181
+ == "Grayscale"
182
+ ):
183
+ image = image.convert(mode)
184
+ else:
185
+ flag = False
186
+ lcol.warning(
187
+ "Some operations not available for black and white images."
188
+ )
189
+ thresh = np.array(image).mean()
190
+ fn = lambda x: 255 if x > thresh else 0
191
+ image = image.convert(mode).point(fn, mode="1")
192
+ else:
193
+ mode = "RGB"
194
+ rcol.image(
195
+ image,
196
+ use_column_width="auto",
197
+ )
198
+
199
+ if lcol.button(
200
+ "↩️ Reset",
201
+ on_click=_reset,
202
+ use_container_width=True,
203
+ kwargs={"key": "checkboxes"},
204
+ ):
205
+ lcol.success("Image reset to original!")
206
+
207
+ st.markdown("""---""")
208
+
209
+ # ---------- OTHER OPERATIONS ----------
210
+ # ---------- 1ST ROW ----------
211
+ with st.container():
212
+ lcol, mcol, rcol = st.columns(3)
213
+
214
+ # ---------- ROTATE ----------
215
+ if "rotate_slider" not in st.session_state:
216
+ st.session_state["rotate_slider"] = 0
217
+ degrees = lcol.slider(
218
+ "Drag slider to rotate image clockwise 🔁",
219
+ min_value=0,
220
+ max_value=360,
221
+ value=st.session_state["rotate_slider"],
222
+ key="rotate_slider",
223
+ )
224
+ rotated_img = image.rotate(360 - degrees)
225
+ lcol.image(
226
+ rotated_img,
227
+ use_column_width="auto",
228
+ caption=f"Rotated by {degrees} degrees clockwise",
229
+ )
230
+ if lcol.button(
231
+ "↩️ Reset Rotation",
232
+ on_click=_reset,
233
+ use_container_width=True,
234
+ kwargs={"key": "rotate_slider"},
235
+ ):
236
+ lcol.success("Rotation reset to original!")
237
+
238
+ if flag:
239
+ # ---------- BRIGHTNESS ----------
240
+ if "brightness_slider" not in st.session_state:
241
+ st.session_state["brightness_slider"] = 100
242
+ brightness_factor = mcol.slider(
243
+ "Drag slider to change brightness 💡",
244
+ min_value=0,
245
+ max_value=200,
246
+ value=st.session_state["brightness_slider"],
247
+ key="brightness_slider",
248
+ )
249
+ brightness_img = np.asarray(
250
+ ImageEnhance.Brightness(rotated_img).enhance(
251
+ brightness_factor / 100
252
+ )
253
+ )
254
+ mcol.image(
255
+ brightness_img,
256
+ use_column_width="auto",
257
+ caption=f"Brightness: {brightness_factor}%",
258
+ )
259
+ if mcol.button(
260
+ "↩️ Reset Brightness",
261
+ on_click=_reset,
262
+ use_container_width=True,
263
+ kwargs={"key": "brightness_slider"},
264
+ ):
265
+ mcol.success("Brightness reset to original!")
266
+
267
+ # ---------- SATURATION ----------
268
+ if "saturation_slider" not in st.session_state:
269
+ st.session_state["saturation_slider"] = 100
270
+ saturation_factor = rcol.slider(
271
+ "Drag slider to change saturation",
272
+ min_value=0,
273
+ max_value=200,
274
+ value=st.session_state["saturation_slider"],
275
+ key="saturation_slider",
276
+ )
277
+ saturation_img = np.asarray(
278
+ ImageEnhance.Color(Image.fromarray(brightness_img)).enhance(
279
+ saturation_factor / 100
280
+ )
281
+ )
282
+ rcol.image(
283
+ saturation_img,
284
+ use_column_width="auto",
285
+ caption=f"Saturation: {saturation_factor}%",
286
+ )
287
+ if rcol.button(
288
+ "↩️ Reset Saturation",
289
+ on_click=_reset,
290
+ use_container_width=True,
291
+ kwargs={"key": "saturation_slider"},
292
+ ):
293
+ rcol.success("Saturation reset to original!")
294
+
295
+ st.markdown("""---""")
296
+
297
+ # ---------- 2ND ROW ----------
298
+ with st.container():
299
+ lcol, mcol, rcol = st.columns(3)
300
+ # ---------- CONTRAST ----------
301
+ if "contrast_slider" not in st.session_state:
302
+ st.session_state["contrast_slider"] = 100
303
+ contrast_factor = lcol.slider(
304
+ "Drag slider to change contrast",
305
+ min_value=0,
306
+ max_value=200,
307
+ value=st.session_state["contrast_slider"],
308
+ key="contrast_slider",
309
+ )
310
+ contrast_img = np.asarray(
311
+ ImageEnhance.Contrast(Image.fromarray(saturation_img)).enhance(
312
+ contrast_factor / 100
313
+ )
314
+ )
315
+ lcol.image(
316
+ contrast_img,
317
+ use_column_width="auto",
318
+ caption=f"Contrast: {contrast_factor}%",
319
+ )
320
+ if lcol.button(
321
+ "↩️ Reset Contrast",
322
+ on_click=_reset,
323
+ use_container_width=True,
324
+ kwargs={"key": "contrast_slider"},
325
+ ):
326
+ lcol.success("Contrast reset to original!")
327
+
328
+ # ---------- SHARPNESS ----------
329
+ if "sharpness_slider" not in st.session_state:
330
+ st.session_state["sharpness_slider"] = 100
331
+ sharpness_factor = mcol.slider(
332
+ "Drag slider to change sharpness",
333
+ min_value=0,
334
+ max_value=200,
335
+ value=st.session_state["sharpness_slider"],
336
+ key="sharpness_slider",
337
+ )
338
+ sharpness_img = np.asarray(
339
+ ImageEnhance.Sharpness(Image.fromarray(contrast_img)).enhance(
340
+ sharpness_factor / 100
341
+ )
342
+ )
343
+ mcol.image(
344
+ sharpness_img,
345
+ use_column_width="auto",
346
+ caption=f"Sharpness: {sharpness_factor}%",
347
+ )
348
+ if mcol.button(
349
+ "↩️ Reset Sharpness",
350
+ on_click=_reset,
351
+ use_container_width=True,
352
+ kwargs={"key": "sharpness_slider"},
353
+ ):
354
+ mcol.success("Sharpness reset to original!")
355
+
356
+ st.markdown("""---""")
357
+
358
+ # ---------- FINAL OPERATIONS ----------
359
+ st.subheader("View Results")
360
+ lcol, rcol = st.columns(2)
361
+ lcol.image(
362
+ img_arr,
363
+ use_column_width="auto",
364
+ caption=f"Original Image ({pil_img.size[0]} x {pil_img.size[1]})",
365
+ )
366
+
367
+ try:
368
+ final_image = sharpness_img
369
+ except NameError:
370
+ final_image = rotated_img
371
+
372
+ rcol.image(
373
+ final_image,
374
+ use_column_width="auto",
375
+ caption=f"Final Image ({final_image.shape[1]} x {final_image.shape[0]})"
376
+ if flag
377
+ else f"Final Image ({final_image.size[1]} x {final_image.size[0]})",
378
+ )
379
+
380
+ if flag:
381
+ Image.fromarray(final_image).save("final_image.png")
382
+ else:
383
+ final_image.save("final_image.png")
384
+
385
+ col1, col2, col3 = st.columns(3)
386
+ if col1.button(
387
+ "↩️ Reset All",
388
+ on_click=_reset,
389
+ use_container_width=True,
390
+ kwargs={"key": "all"},
391
+ ):
392
+ st.success(body="Image reset to original!", icon="↩️")
393
+ if col2.button(
394
+ "🔀 Surprise Me!",
395
+ on_click=_randomize,
396
+ use_container_width=True,
397
+ ):
398
+ st.success(body="Random image generated", icon="🔀")
399
+ with open("final_image.png", "rb") as file:
400
+ col3.download_button(
401
+ "💾Download final image",
402
+ data=file,
403
+ mime="image/png",
404
+ use_container_width=True,
405
+ )
final_image.png ADDED
requirements.txt ADDED
File without changes
sidebar.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <title>Sidebar content</title>
6
+ <meta charset="UTF-8" name="viewport">
7
+ <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet" type="text/css">
8
+ <script src="https://code.iconify.design/2/2.1.2/iconify.min.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+ <div style="text-align:center; font-size:14px; color:lightgrey; font-family: 'Source Sans Pro', sans-serif;">
13
+
14
+ <b>v{VERSION}</b><br>
15
+ ⭐ the repo to be notified of updates<br>
16
+ <iframe src="https://ghbtns.com/github-btn.html?user=SiddhantSadangi&repo=ImageWorkdesk&type=star"
17
+ frameborder="0" width="50" height="30" title="GitHub"></iframe><br>
18
+ <hr>
19
+ <br>
20
+ Made with ❤️ by <b>Siddhant Sadangi</b>
21
+ <a href="https://linkedin.com/in/siddhantsadangi">
22
+ <span class="iconify-inline" data-icon="ion:logo-linkedin"></span></a>
23
+ <a href="mailto:siddhantsadangi@gmail.com">
24
+ <span class="iconify-inline" data-icon="carbon:email"></span></a><br><br>
25
+ <script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js"
26
+ data-name="bmc-button" data-slug="siddhantsadangi" data-color="#000000" data-emoji="" data-font="Cookie"
27
+ data-text="Buy me a coffee" data-outline-color="#ffffff" data-font-color="#ffffff"
28
+ data-coffee-color="#FFDD00"></script><br>
29
+ <hr><br>
30
+ Share the ❤️ on social media<br><br>
31
+ <a href="https://www.facebook.com/sharer/sharer.php?kid_directed_site=0&sdk=joey&u=https%3A%2F%2Fimageworkdesk.streamlit.app%2F&display=popup&ref=plugin&src=share_button"
32
+ target="_blank">
33
+ <img src="https://github.com/SiddhantSadangi/SiddhantSadangi/assets/41324509/de66032a-4ff1-4505-8960-848884a3c29e"
34
+ alt="Share on Facebook" width="40" height="40">
35
+ </a>
36
+ <a href="https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fimageworkdesk.streamlit.app%2F"
37
+ target="_blank">
38
+ <img src="https://github.com/SiddhantSadangi/SiddhantSadangi/assets/41324509/78caca71-10a3-45ac-949b-961b3ebf2429"
39
+ alt="Share on LinkedIn" height="40" width="40">
40
+ </a>
41
+ <a href="https://twitter.com/intent/tweet?original_referer=http%3A%2F%2Flimageworkdesk.streamlit.app&ref_src=twsrc%5Etfw%7Ctwcamp%5Ebuttonembed%7Ctwterm%5Eshare%7Ctwgr%5E&text=Check%20out%20this%20cool%20Streamlit%20app%20%F0%9F%8E%88&url=https%3A%2F%2Fimageworkdesk.streamlit.app%2F"
42
+ target="_blank">
43
+ <img src="https://github.com/SiddhantSadangi/SiddhantSadangi/assets/41324509/3d3f7366-2f96-4456-8476-e6b319cdc328"
44
+ alt="Share on Twitter" height="40" width="40">
45
+ </a>
46
+ <hr><br>
47
+ <a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">
48
+ <img alt="Creative Commons License" style="border-width:0"
49
+ src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" />
50
+ </a><br>
51
+ <div style="font-size:12px;">
52
+ This work is licensed under a <a rel="license"
53
+ href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><b>Creative Commons
54
+ Attribution-NonCommercial-ShareAlike 4.0 International License</b></a>.<br>
55
+ You can modify and build upon this work non-commercially. All derivatives should be credited to me and
56
+ be licenced under the same terms.
57
+ </div>
58
+ </div>
59
+ </body>
60
+
61
+ </html>