wenjun99 commited on
Commit
e43da75
Β·
verified Β·
1 Parent(s): 9eee5e2

Update src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +220 -189
src/app.py CHANGED
@@ -48,44 +48,65 @@ B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
48
  # =========================
49
  # Encoding Functions
50
  # =========================
51
- def encode_to_binary(text: str, scheme: str) -> tuple[list[int], list[str]]:
52
  """
53
- Returns (flat_bits, display_units).
54
- display_units is a list of labels for each chunk (character, byte, or Base64 symbol).
 
55
  """
56
  if scheme == "Voyager 6-bit":
57
  bits = []
58
  for char in text:
59
  val = reverse_voyager_table.get(char.upper(), 0)
60
  bits.extend([(val >> b) & 1 for b in range(5, -1, -1)])
61
- return bits, list(text.upper())
 
62
 
63
  elif scheme == "ASCII (7-bit)":
64
  bits = []
65
  for c in text:
66
  val = ord(c) & 0x7F
67
  bits.extend([(val >> b) & 1 for b in range(6, -1, -1)])
68
- return bits, list(text)
 
69
 
70
  elif scheme == "UTF-8 (8-bit)":
71
  raw = text.encode("utf-8")
72
  bits = []
73
  for byte in raw:
74
  bits.extend([(byte >> b) & 1 for b in range(7, -1, -1)])
75
- # For display: show hex byte value and the character it belongs to
76
  labels = [f"0x{b:02X}" for b in raw]
77
- return bits, labels
 
 
 
 
 
78
 
79
  elif scheme == "Base64 (6-bit)":
80
- b64_str = base64.b64encode(text.encode("utf-8")).decode("ascii")
 
81
  bits = []
82
  clean = b64_str.rstrip("=")
83
  for c in clean:
84
  val = B64_ALPHABET.index(c)
85
  bits.extend([(val >> b) & 1 for b in range(5, -1, -1)])
86
- return bits, list(clean)
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- return [], []
89
 
90
 
91
  # =========================
@@ -131,7 +152,6 @@ def decode_from_binary(bits: list[int], scheme: str) -> str:
131
  val = sum(b << (5 - j) for j, b in enumerate(chunk))
132
  chars.append(B64_ALPHABET[val])
133
  b64_str = ''.join(chars)
134
- # Add Base64 padding
135
  while len(b64_str) % 4 != 0:
136
  b64_str += '='
137
  try:
@@ -145,7 +165,7 @@ def decode_from_binary(bits: list[int], scheme: str) -> str:
145
  # =========================
146
  # Tabs
147
  # =========================
148
- tab1, tab2, tab3, tab4, tab5 = st.tabs(["Encoding", "Decoding", "Image Preview", "Data Analytics", "Writing"])
149
 
150
  # --------------------------------------------------
151
  # TAB 1: Text/Image β†’ Binary
@@ -191,12 +211,11 @@ with tab1:
191
  group_size = custom_cols
192
 
193
  if user_input:
194
- binary_labels, display_units = encode_to_binary(user_input, encoding_scheme)
195
  binary_concat = ''.join(map(str, binary_labels))
196
 
197
- unit_label = "Character"
198
- st.markdown(f"### Output 1 – Binary Labels per {unit_label}")
199
- st.caption(f"Encoding: **{encoding_scheme}** β€” {bits_per} bits per {unit_label.lower()}")
200
 
201
  grouped_bits = [binary_labels[i:i + bits_per] for i in range(0, len(binary_labels), bits_per)]
202
  scroll_html = (
@@ -204,17 +223,28 @@ with tab1:
204
  "padding:6px; border:1px solid #ccc;'>"
205
  )
206
  for i, bits in enumerate(grouped_bits):
207
- label = display_units[i] if i < len(display_units) else "?"
208
- scroll_html += f"<div>'{label}' β†’ {bits}</div>"
 
 
 
 
 
 
209
  scroll_html += "</div>"
210
  st.markdown(scroll_html, unsafe_allow_html=True)
211
 
212
  per_char_lines = []
213
  for i, bits in enumerate(grouped_bits):
214
- label = display_units[i] if i < len(display_units) else "?"
215
- per_char_lines.append(f"'{label}' β†’ {''.join(map(str, bits))}")
 
 
 
 
 
216
  st.download_button(
217
- f"⬇️ Download Binary per {unit_label} (.txt)",
218
  data='\n'.join(per_char_lines),
219
  file_name="binary_per_unit.txt",
220
  mime="text/plain",
@@ -322,7 +352,7 @@ with tab1:
322
  )
323
 
324
  # Output as matrix with width = target_width
325
- st.markdown("### Output 2 – Binary Matrix (rows = pixel rows)")
326
  columns = [f"Position {i+1}" for i in range(target_width)]
327
  df_img = pd.DataFrame(binary_matrix, columns=columns)
328
  df_img.insert(0, "Sample", range(1, len(df_img) + 1))
@@ -337,7 +367,7 @@ with tab1:
337
  )
338
 
339
  # Also offer custom grouping (same as text mode)
340
- st.markdown("### Output 3 – Custom Grouped Matrix")
341
  col1, col2 = st.columns([2, 1])
342
  with col1:
343
  img_group_size = st.slider(
@@ -375,190 +405,191 @@ with tab1:
375
  st.info("πŸ‘† Upload an image to encode it as binary.")
376
 
377
  # --------------------------------------------------
378
- # TAB 2: Binary β†’ Text
379
  # --------------------------------------------------
380
  with tab2:
381
  st.markdown("""
382
- Convert binary data back into readable text.
383
- Upload either:
384
- - `.csv` file with 0/1 values (any number of columns/rows)
385
- - `.xlsx` Excel file
386
- - `.txt` file containing a concatenated binary string (e.g. `010101...`)
387
  """)
388
 
389
- decode_scheme = st.selectbox(
390
- "Decoding scheme (must match the encoding used):",
391
- ENCODING_OPTIONS,
392
- index=0,
393
- key="dec_scheme",
394
- help="Select the same encoding scheme that was used to produce the binary data."
395
- )
396
 
397
- uploaded_decode = st.file_uploader(
398
- "Upload your file (.csv, .xlsx, or .txt):",
399
- type=["csv", "xlsx", "txt"],
400
- key="decode_uploader"
401
- )
402
-
403
- if uploaded_decode is not None:
404
- try:
405
- if uploaded_decode.name.endswith(".csv"):
406
- df = pd.read_csv(uploaded_decode)
407
- bits = df.values.flatten().astype(int).tolist()
408
- elif uploaded_decode.name.endswith(".xlsx"):
409
- df = pd.read_excel(uploaded_decode)
410
- bits = df.values.flatten().astype(int).tolist()
411
- elif uploaded_decode.name.endswith(".txt"):
412
- content = uploaded_decode.read().decode().strip()
413
- bits = [int(b) for b in content if b in ['0', '1']]
414
- else:
415
- bits = []
416
-
417
- if not bits:
418
- st.warning("No binary data detected.")
419
- else:
420
- recovered_text = decode_from_binary(bits, decode_scheme)
421
- st.success(f"βœ… Conversion complete using **{decode_scheme}**!")
422
- st.markdown("**Recovered text:**")
423
- st.text_area("Output", recovered_text, height=150)
424
-
425
- st.download_button(
426
- "⬇️ Download Recovered Text (.txt)",
427
- data=recovered_text,
428
- file_name="recovered_text.txt",
429
- mime="text/plain",
430
- key="download_recovered"
431
- )
432
- except Exception as e:
433
- st.error(f"Error reading or converting file: {e}")
434
- else:
435
- st.info("πŸ‘† Upload a file to start the reverse conversion.")
436
 
437
- # --------------------------------------------------
438
- # TAB 3: Image Preview
439
- # --------------------------------------------------
440
- with tab3:
441
- st.header("πŸ–ΌοΈ Image Preview")
442
- st.markdown("""
443
- Render binary data (0/1) as a **black & white image**.
444
- Upload a binary matrix CSV (rows Γ— positions) or a concatenated binary `.txt` string.
445
- """)
446
 
447
- img_preview_file = st.file_uploader(
448
- "πŸ“€ Upload binary data file (.csv, .xlsx, or .txt):",
449
- type=["csv", "xlsx", "txt"],
450
- key="img_preview_uploader"
451
- )
452
 
453
- if img_preview_file is not None:
454
- try:
455
- # --- Load binary data ---
456
- if img_preview_file.name.endswith(".csv"):
457
- idf = pd.read_csv(img_preview_file)
458
- # Drop Sample column if present
459
- if "Sample" in idf.columns or "sample" in idf.columns:
460
- idf = idf.drop(columns=[c for c in idf.columns if c.lower() == "sample"])
461
- bits_matrix = idf.values.flatten().astype(int)
462
- detected_width = len(idf.columns)
463
- elif img_preview_file.name.endswith(".xlsx"):
464
- idf = pd.read_excel(img_preview_file)
465
- if "Sample" in idf.columns or "sample" in idf.columns:
466
- idf = idf.drop(columns=[c for c in idf.columns if c.lower() == "sample"])
467
- bits_matrix = idf.values.flatten().astype(int)
468
- detected_width = len(idf.columns)
469
- elif img_preview_file.name.endswith(".txt"):
470
- content = img_preview_file.read().decode().strip()
471
- bits_matrix = np.array([int(b) for b in content if b in ['0', '1']])
472
- detected_width = None
473
- else:
474
- bits_matrix = np.array([])
475
- detected_width = None
476
 
477
- if len(bits_matrix) == 0:
478
- st.warning("No binary data detected.")
479
- else:
480
- total_bits = len(bits_matrix)
481
- st.success(f"βœ… Loaded **{total_bits:,}** bits.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
- # --- Width control ---
484
- st.markdown("#### βš™οΈ Image Dimensions")
 
 
 
 
 
 
 
 
 
 
 
 
485
 
486
- if detected_width and detected_width > 1:
487
- default_w = detected_width
488
- st.caption(f"Auto-detected width from columns: **{detected_width}**")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  else:
490
- # Guess a square-ish default
491
- default_w = max(1, int(np.sqrt(total_bits)))
492
-
493
- img_width = st.number_input(
494
- "Image width (pixels / positions per row):",
495
- min_value=1, max_value=total_bits, value=default_w, step=1,
496
- key="img_preview_width"
497
- )
498
- img_height = int(np.ceil(total_bits / img_width))
499
- st.caption(f"Image size: **{img_width} Γ— {img_height}** = **{img_width * img_height:,}** pixels "
500
- f"({total_bits:,} bits, {img_width * img_height - total_bits} padded)")
501
-
502
- # Pad to fill the last row
503
- padded = np.zeros(img_width * img_height, dtype=int)
504
- padded[:total_bits] = bits_matrix[:total_bits]
505
- img_data = padded.reshape((img_height, img_width))
506
-
507
- # Render: 1 = black (0), 0 = white (255)
508
- img_render = ((1 - img_data) * 255).astype(np.uint8)
509
- pil_img = Image.fromarray(img_render, mode="L")
510
-
511
- st.markdown("### πŸ–ΌοΈ Rendered Image")
512
- # Use nearest-neighbor scaling for crisp pixels
513
- display_scale = max(1, 256 // img_width)
514
- display_w = img_width * display_scale
515
- display_h = img_height * display_scale
516
- pil_display = pil_img.resize((display_w, display_h), Image.NEAREST)
517
- st.image(pil_display, caption=f"Binary image β€” {img_width}Γ—{img_height} (1=black, 0=white)")
518
-
519
- # Stats
520
- ones = int(bits_matrix.sum())
521
- st.markdown(
522
- f"- **Black pixels (1):** {ones:,} ({100*ones/total_bits:.1f}%) \n"
523
- f"- **White pixels (0):** {total_bits - ones:,} ({100*(total_bits-ones)/total_bits:.1f}%)"
524
- )
525
 
526
- # Download rendered image as PNG
527
- buf = io.BytesIO()
528
- pil_img.save(buf, format="PNG")
529
- st.download_button(
530
- "⬇️ Download as PNG",
531
- data=buf.getvalue(),
532
- file_name=f"binary_image_{img_width}x{img_height}.png",
533
- mime="image/png",
534
- key="download_preview_png"
535
- )
536
 
537
- # Also offer a high-res version
538
- buf_hr = io.BytesIO()
539
- pil_display.save(buf_hr, format="PNG")
540
- st.download_button(
541
- "⬇️ Download Scaled PNG (for viewing)",
542
- data=buf_hr.getvalue(),
543
- file_name=f"binary_image_{display_w}x{display_h}_scaled.png",
544
- mime="image/png",
545
- key="download_preview_png_scaled"
546
- )
547
 
548
- except Exception as e:
549
- st.error(f"❌ Error processing file: {e}")
550
- import traceback
551
- st.code(traceback.format_exc())
552
- else:
553
- st.info("πŸ‘† Upload a binary data file (CSV or TXT) to render as an image.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
  # --------------------------------------------------
556
- # TAB 4: Data Analytics
557
  # --------------------------------------------------
558
- with tab4:
559
  st.header("πŸ“Š Data Analytics")
560
  st.markdown("""
561
- Upload your sample data file (Excel or CSV) for a quick exploratory assessment.
562
  The file should contain samples as rows and position columns with editing values.
563
  This tab provides visualizations **before** any binary labelling.
564
  """)
@@ -768,9 +799,9 @@ with tab4:
768
  st.info("πŸ‘† Upload a data file (CSV or Excel) to start exploring.")
769
 
770
  # --------------------------------------------------
771
- # TAB 5: Pipetting Command Generator
772
  # --------------------------------------------------
773
- with tab5:
774
  from math import ceil
775
 
776
  st.header("πŸ§ͺ Pipetting Command Generator for Eppendorf epMotion liquid handler")
@@ -1099,4 +1130,4 @@ with tab5:
1099
  except Exception as e:
1100
  st.error(f"❌ Error processing file: {e}")
1101
  else:
1102
- st.info("πŸ‘† Upload an Excel/CSV/TXT file to start.")
 
48
  # =========================
49
  # Encoding Functions
50
  # =========================
51
+ def encode_to_binary(text: str, scheme: str) -> tuple[list[int], list[str], list[str]]:
52
  """
53
+ Returns (flat_bits, display_units, source_chars).
54
+ - display_units: the encoded representation (Base64 symbol, hex byte, ASCII code, Voyager char)
55
+ - source_chars: the original text character each chunk maps to
56
  """
57
  if scheme == "Voyager 6-bit":
58
  bits = []
59
  for char in text:
60
  val = reverse_voyager_table.get(char.upper(), 0)
61
  bits.extend([(val >> b) & 1 for b in range(5, -1, -1)])
62
+ labels = list(text.upper())
63
+ return bits, labels, list(text)
64
 
65
  elif scheme == "ASCII (7-bit)":
66
  bits = []
67
  for c in text:
68
  val = ord(c) & 0x7F
69
  bits.extend([(val >> b) & 1 for b in range(6, -1, -1)])
70
+ labels = [f"0x{ord(c) & 0x7F:02X}" for c in text]
71
+ return bits, labels, list(text)
72
 
73
  elif scheme == "UTF-8 (8-bit)":
74
  raw = text.encode("utf-8")
75
  bits = []
76
  for byte in raw:
77
  bits.extend([(byte >> b) & 1 for b in range(7, -1, -1)])
 
78
  labels = [f"0x{b:02X}" for b in raw]
79
+ # Map each byte back to its source character
80
+ source = []
81
+ for ch in text:
82
+ n_bytes = len(ch.encode("utf-8"))
83
+ source.extend([ch] * n_bytes)
84
+ return bits, labels, source
85
 
86
  elif scheme == "Base64 (6-bit)":
87
+ raw_bytes = text.encode("utf-8")
88
+ b64_str = base64.b64encode(raw_bytes).decode("ascii")
89
  bits = []
90
  clean = b64_str.rstrip("=")
91
  for c in clean:
92
  val = B64_ALPHABET.index(c)
93
  bits.extend([(val >> b) & 1 for b in range(5, -1, -1)])
94
+ labels = list(clean)
95
+ # Map each Base64 symbol to its primary source character
96
+ byte_to_char = []
97
+ for ch in text:
98
+ n_bytes = len(ch.encode("utf-8"))
99
+ byte_to_char.extend([ch] * n_bytes)
100
+ source = []
101
+ for j in range(len(clean)):
102
+ byte_idx = (j * 6) // 8
103
+ if byte_idx < len(byte_to_char):
104
+ source.append(byte_to_char[byte_idx])
105
+ else:
106
+ source.append("?")
107
+ return bits, labels, source
108
 
109
+ return [], [], []
110
 
111
 
112
  # =========================
 
152
  val = sum(b << (5 - j) for j, b in enumerate(chunk))
153
  chars.append(B64_ALPHABET[val])
154
  b64_str = ''.join(chars)
 
155
  while len(b64_str) % 4 != 0:
156
  b64_str += '='
157
  try:
 
165
  # =========================
166
  # Tabs
167
  # =========================
168
+ tab1, tab2, tab3, tab4 = st.tabs(["Encoding", "Decoding", "Data Analytics", "Writing"])
169
 
170
  # --------------------------------------------------
171
  # TAB 1: Text/Image β†’ Binary
 
211
  group_size = custom_cols
212
 
213
  if user_input:
214
+ binary_labels, display_units, source_chars = encode_to_binary(user_input, encoding_scheme)
215
  binary_concat = ''.join(map(str, binary_labels))
216
 
217
+ st.markdown("### Output 1 – Binary Labels per Character")
218
+ st.caption(f"Encoding: **{encoding_scheme}** β€” {bits_per} bits per unit")
 
219
 
220
  grouped_bits = [binary_labels[i:i + bits_per] for i in range(0, len(binary_labels), bits_per)]
221
  scroll_html = (
 
223
  "padding:6px; border:1px solid #ccc;'>"
224
  )
225
  for i, bits in enumerate(grouped_bits):
226
+ src = source_chars[i] if i < len(source_chars) else "?"
227
+ enc = display_units[i] if i < len(display_units) else "?"
228
+ if encoding_scheme == "Voyager 6-bit":
229
+ # Voyager: direct char β†’ binary (encoded label = uppercase of same char)
230
+ scroll_html += f"<div>'{src}' β†’ {bits}</div>"
231
+ else:
232
+ # Show original β†’ encoded representation β†’ binary
233
+ scroll_html += f"<div>'{src}' β†’ '{enc}' β†’ {bits}</div>"
234
  scroll_html += "</div>"
235
  st.markdown(scroll_html, unsafe_allow_html=True)
236
 
237
  per_char_lines = []
238
  for i, bits in enumerate(grouped_bits):
239
+ src = source_chars[i] if i < len(source_chars) else "?"
240
+ enc = display_units[i] if i < len(display_units) else "?"
241
+ bit_str = ''.join(map(str, bits))
242
+ if encoding_scheme == "Voyager 6-bit":
243
+ per_char_lines.append(f"'{src}' β†’ {bit_str}")
244
+ else:
245
+ per_char_lines.append(f"'{src}' β†’ '{enc}' β†’ {bit_str}")
246
  st.download_button(
247
+ "⬇️ Download Binary per Character (.txt)",
248
  data='\n'.join(per_char_lines),
249
  file_name="binary_per_unit.txt",
250
  mime="text/plain",
 
352
  )
353
 
354
  # Output as matrix with width = target_width
355
+ st.markdown("### Output 2 – Binary Matrix by dimension (Samples x Positions))")
356
  columns = [f"Position {i+1}" for i in range(target_width)]
357
  df_img = pd.DataFrame(binary_matrix, columns=columns)
358
  df_img.insert(0, "Sample", range(1, len(df_img) + 1))
 
367
  )
368
 
369
  # Also offer custom grouping (same as text mode)
370
+ st.markdown("### Output 3 – Custom Grouped Matrix by Number of Target Positions")
371
  col1, col2 = st.columns([2, 1])
372
  with col1:
373
  img_group_size = st.slider(
 
405
  st.info("πŸ‘† Upload an image to encode it as binary.")
406
 
407
  # --------------------------------------------------
408
+ # TAB 2: Decoding (Text & Image)
409
  # --------------------------------------------------
410
  with tab2:
411
  st.markdown("""
412
+ Decode binary data back into **text** or render it as a **black & white image**.
 
 
 
 
413
  """)
414
 
415
+ decode_mode = st.selectbox("Output mode:", ["Text", "Image"], key="decode_mode")
 
 
 
 
 
 
416
 
417
+ if decode_mode == "Text":
418
+ st.markdown("""
419
+ Upload either:
420
+ - `.csv` file with 0/1 values (any number of columns/rows)
421
+ - `.xlsx` Excel file
422
+ - `.txt` file containing a concatenated binary string (e.g. `010101...`)
423
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
+ decode_scheme = st.selectbox(
426
+ "Decoding scheme (must match the encoding used):",
427
+ ENCODING_OPTIONS,
428
+ index=0,
429
+ key="dec_scheme",
430
+ help="Select the same encoding scheme that was used to produce the binary data."
431
+ )
 
 
432
 
433
+ uploaded_decode = st.file_uploader(
434
+ "Upload your file (.csv, .xlsx, or .txt):",
435
+ type=["csv", "xlsx", "txt"],
436
+ key="decode_uploader"
437
+ )
438
 
439
+ if uploaded_decode is not None:
440
+ try:
441
+ if uploaded_decode.name.endswith(".csv"):
442
+ df = pd.read_csv(uploaded_decode)
443
+ bits = df.values.flatten().astype(int).tolist()
444
+ elif uploaded_decode.name.endswith(".xlsx"):
445
+ df = pd.read_excel(uploaded_decode)
446
+ bits = df.values.flatten().astype(int).tolist()
447
+ elif uploaded_decode.name.endswith(".txt"):
448
+ content = uploaded_decode.read().decode().strip()
449
+ bits = [int(b) for b in content if b in ['0', '1']]
450
+ else:
451
+ bits = []
 
 
 
 
 
 
 
 
 
 
452
 
453
+ if not bits:
454
+ st.warning("No binary data detected.")
455
+ else:
456
+ recovered_text = decode_from_binary(bits, decode_scheme)
457
+ st.success(f"βœ… Conversion complete using **{decode_scheme}**!")
458
+ st.markdown("**Recovered text:**")
459
+ st.text_area("Output", recovered_text, height=150)
460
+
461
+ st.download_button(
462
+ "⬇️ Download Recovered Text (.txt)",
463
+ data=recovered_text,
464
+ file_name="recovered_text.txt",
465
+ mime="text/plain",
466
+ key="download_recovered"
467
+ )
468
+ except Exception as e:
469
+ st.error(f"Error reading or converting file: {e}")
470
+ else:
471
+ st.info("πŸ‘† Upload a file to start the reverse conversion.")
472
 
473
+ # =====================================================
474
+ # IMAGE DECODE MODE
475
+ # =====================================================
476
+ else:
477
+ st.markdown("""
478
+ Render binary data (0/1) as a **black & white image**.
479
+ Upload a binary matrix CSV (rows Γ— positions) or a concatenated binary `.txt` string.
480
+ """)
481
+
482
+ img_preview_file = st.file_uploader(
483
+ "πŸ“€ Upload binary data file (.csv, .xlsx, or .txt):",
484
+ type=["csv", "xlsx", "txt"],
485
+ key="img_preview_uploader"
486
+ )
487
 
488
+ if img_preview_file is not None:
489
+ try:
490
+ # --- Load binary data ---
491
+ if img_preview_file.name.endswith(".csv"):
492
+ idf = pd.read_csv(img_preview_file)
493
+ if "Sample" in idf.columns or "sample" in idf.columns:
494
+ idf = idf.drop(columns=[c for c in idf.columns if c.lower() == "sample"])
495
+ bits_matrix = idf.values.flatten().astype(int)
496
+ detected_width = len(idf.columns)
497
+ elif img_preview_file.name.endswith(".xlsx"):
498
+ idf = pd.read_excel(img_preview_file)
499
+ if "Sample" in idf.columns or "sample" in idf.columns:
500
+ idf = idf.drop(columns=[c for c in idf.columns if c.lower() == "sample"])
501
+ bits_matrix = idf.values.flatten().astype(int)
502
+ detected_width = len(idf.columns)
503
+ elif img_preview_file.name.endswith(".txt"):
504
+ content = img_preview_file.read().decode().strip()
505
+ bits_matrix = np.array([int(b) for b in content if b in ['0', '1']])
506
+ detected_width = None
507
  else:
508
+ bits_matrix = np.array([])
509
+ detected_width = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
 
511
+ if len(bits_matrix) == 0:
512
+ st.warning("No binary data detected.")
513
+ else:
514
+ total_bits = len(bits_matrix)
515
+ st.success(f"βœ… Loaded **{total_bits:,}** bits.")
 
 
 
 
 
516
 
517
+ # --- Width control ---
518
+ st.markdown("#### βš™οΈ Image Dimensions")
 
 
 
 
 
 
 
 
519
 
520
+ if detected_width and detected_width > 1:
521
+ default_w = detected_width
522
+ st.caption(f"Auto-detected width from columns: **{detected_width}**")
523
+ else:
524
+ default_w = max(1, int(np.sqrt(total_bits)))
525
+
526
+ img_width = st.number_input(
527
+ "Image width (pixels / positions per row):",
528
+ min_value=1, max_value=total_bits, value=default_w, step=1,
529
+ key="img_preview_width"
530
+ )
531
+ img_height = int(np.ceil(total_bits / img_width))
532
+ st.caption(f"Image size: **{img_width} Γ— {img_height}** = **{img_width * img_height:,}** pixels "
533
+ f"({total_bits:,} bits, {img_width * img_height - total_bits} padded)")
534
+
535
+ # Pad to fill the last row
536
+ padded = np.zeros(img_width * img_height, dtype=int)
537
+ padded[:total_bits] = bits_matrix[:total_bits]
538
+ img_data = padded.reshape((img_height, img_width))
539
+
540
+ # Render: 1 = black (0), 0 = white (255)
541
+ img_render = ((1 - img_data) * 255).astype(np.uint8)
542
+ pil_img = Image.fromarray(img_render, mode="L")
543
+
544
+ st.markdown("### πŸ–ΌοΈ Rendered Image")
545
+ display_scale = max(1, 256 // img_width)
546
+ display_w = img_width * display_scale
547
+ display_h = img_height * display_scale
548
+ pil_display = pil_img.resize((display_w, display_h), Image.NEAREST)
549
+ st.image(pil_display, caption=f"Binary image β€” {img_width}Γ—{img_height} (1=black, 0=white)")
550
+
551
+ # Stats
552
+ ones = int(bits_matrix.sum())
553
+ st.markdown(
554
+ f"- **Black pixels (1):** {ones:,} ({100*ones/total_bits:.1f}%) \n"
555
+ f"- **White pixels (0):** {total_bits - ones:,} ({100*(total_bits-ones)/total_bits:.1f}%)"
556
+ )
557
+
558
+ # Download rendered image as PNG
559
+ buf = io.BytesIO()
560
+ pil_img.save(buf, format="PNG")
561
+ st.download_button(
562
+ "⬇️ Download as PNG",
563
+ data=buf.getvalue(),
564
+ file_name=f"binary_image_{img_width}x{img_height}.png",
565
+ mime="image/png",
566
+ key="download_preview_png"
567
+ )
568
+
569
+ buf_hr = io.BytesIO()
570
+ pil_display.save(buf_hr, format="PNG")
571
+ st.download_button(
572
+ "⬇️ Download Scaled PNG (for viewing)",
573
+ data=buf_hr.getvalue(),
574
+ file_name=f"binary_image_{display_w}x{display_h}_scaled.png",
575
+ mime="image/png",
576
+ key="download_preview_png_scaled"
577
+ )
578
+
579
+ except Exception as e:
580
+ st.error(f"❌ Error processing file: {e}")
581
+ import traceback
582
+ st.code(traceback.format_exc())
583
+ else:
584
+ st.info("πŸ‘† Upload a binary data file (CSV or TXT) to render as an image.")
585
 
586
  # --------------------------------------------------
587
+ # TAB 3: Data Analytics
588
  # --------------------------------------------------
589
+ with tab3:
590
  st.header("πŸ“Š Data Analytics")
591
  st.markdown("""
592
+ Upload your sample data file (Excel or CSV) for a quick exploratory assessment of the editing rates distribution.
593
  The file should contain samples as rows and position columns with editing values.
594
  This tab provides visualizations **before** any binary labelling.
595
  """)
 
799
  st.info("πŸ‘† Upload a data file (CSV or Excel) to start exploring.")
800
 
801
  # --------------------------------------------------
802
+ # TAB 4: Pipetting Command Generator
803
  # --------------------------------------------------
804
+ with tab4:
805
  from math import ceil
806
 
807
  st.header("πŸ§ͺ Pipetting Command Generator for Eppendorf epMotion liquid handler")
 
1130
  except Exception as e:
1131
  st.error(f"❌ Error processing file: {e}")
1132
  else:
1133
+ st.info("πŸ‘† Upload an Excel/CSV/TXT file to start.")