Skip to content

Commit 95ec4bf

Browse files
committed
add deice and autosaving
1 parent 07d5469 commit 95ec4bf

File tree

2 files changed

+102
-18
lines changed

2 files changed

+102
-18
lines changed

AnsiImage.py

+65-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
from PIL import Image
33
import copy
44
import time
5+
import os
56

67
class AnsiImage:
78
"""
89
Manages a rectangular image made of ansi character cells
910
"""
10-
def __init__(self, graphics, min_line_len = None):
11+
def __init__(self, graphics, min_line_len = None, has_autosave=False):
1112
"""
1213
Optionally allows the specification of a minimum
1314
line length, used when loading
@@ -20,6 +21,11 @@ def __init__(self, graphics, min_line_len = None):
2021
self.cursor_y = 0
2122
self.is_dirty = False
2223
self.write_allowed = [True, True, True]
24+
self.steps_since_autosave = 0
25+
self.autosave_steps = 10
26+
self.autosave_counter = 0
27+
self.autosave_max = 25
28+
self.has_autosave = has_autosave
2329

2430
self.ansi_graphics = graphics
2531
self.char_size_x = self.ansi_graphics.get_char_size()[0]
@@ -457,6 +463,7 @@ def load_ans(self, ansi_path, wide_mode = False):
457463
with open(ansi_path, "rb") as f:
458464
ansi_data = f.read()
459465
self.parse_ans(ansi_data, wide_mode)
466+
self.steps_since_autosave = 0
460467
self.is_dirty = False
461468

462469
def parse_ans(self, ansi_bytes, wide_mode = False):
@@ -763,3 +770,60 @@ def to_bitmap(self, transparent = False, cursor = False, area = None):
763770
else:
764771
return Image.fromarray((self.ansi_bitmap * 255.0).astype('int8'), mode='RGBA')
765772

773+
def deice(self):
774+
"""
775+
Try to remove iCE colors from the image using closest visual matches
776+
and flipping fg/bg colors.
777+
"""
778+
bg_color_conversion = [i if i < 8 else i % 8 for i in range(16)]
779+
780+
# A function to find the best matching non-iCE character
781+
def find_best_char(in_char, fg, bg):
782+
# An expanded lookup table for character replacements
783+
char_conversion = {
784+
32: 176, # Space to light shade
785+
176: 32, # Light shade to space
786+
177: 178, # Medium shade to dark shade
787+
178: 177, # Dark shade to medium shade
788+
219: 178, # Solid block to dark shade
789+
220: 223, # Upper half block to lower half block
790+
221: 222, # Right half block to left half block
791+
222: 221, # Left half block to right half block
792+
223: 220, # Lower half block to upper half block
793+
254: 250, # Full block to small dot
794+
249: 250, # Small dot to small dot
795+
}
796+
797+
if bg > 7:
798+
# Replace the character if necessary
799+
if in_char in char_conversion:
800+
in_char = char_conversion[in_char]
801+
# Swap fg and bg colors when replacing the character
802+
fg, bg = bg, fg
803+
804+
# Replace the background color with the closest non-iCE color
805+
bg = bg_color_conversion[bg]
806+
807+
return in_char, fg, bg
808+
809+
# Iterate through each pixel and replace iCE colors and characters
810+
for x in range(self.width):
811+
for y in range(self.height):
812+
in_char, fg, bg = self.ansi_image[y][x]
813+
self.ansi_image[y][x] = find_best_char(in_char, fg, bg)
814+
self.redraw_set.add((x, y))
815+
self.is_dirty = True
816+
817+
def do_autosave(self):
818+
"""
819+
Autosave occassionally. Call when adding/removing undo/redo steps
820+
"""
821+
if not os.path.exists("autosaves"):
822+
os.mkdir("autosaves")
823+
self.steps_since_autosave += 1
824+
if self.steps_since_autosave >= self.autosave_steps:
825+
self.autosave_counter += 1
826+
if self.autosave_counter > self.autosave_max:
827+
self.autosave_counter = 0
828+
self.save_ans(f"autosaves/autosave_{self.autosave_counter}.ans")
829+
self.steps_since_autosave = 0

MainWindow.py

+37-17
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def createMenuBar(self):
7373
self.actionSave = QtWidgets.QAction("Save", self)
7474
self.actionSaveAs = QtWidgets.QAction("Save As", self)
7575
self.actionExport = QtWidgets.QAction("Export as PNG", self)
76+
self.actionDeiCE = QtWidgets.QAction("De-iCE", self)
7677
self.actionExit = QtWidgets.QAction("Exit", self)
7778

7879
menuEdit = self.menuBar().addMenu("Edit")
@@ -129,6 +130,8 @@ def createMenuBar(self):
129130
menuFile.addAction(self.actionSaveAs)
130131
menuFile.addAction(self.actionExport)
131132
menuFile.addSeparator()
133+
menuFile.addAction(self.actionDeiCE)
134+
menuFile.addSeparator()
132135
menuFile.addAction(self.actionExit)
133136

134137
menuEdit.addAction(self.actionSize)
@@ -341,6 +344,7 @@ def connectEvents(self):
341344
self.actionSave.setShortcut(QtGui.QKeySequence.Save)
342345
self.actionSaveAs.triggered.connect(self.saveFileAs)
343346
self.actionExport.triggered.connect(self.exportPNG)
347+
self.actionDeiCE.triggered.connect(self.deiCE)
344348

345349
self.actionExit.triggered.connect(self.exit)
346350
self.actionExit.setShortcut(QtGui.QKeySequence.Quit)
@@ -717,7 +721,7 @@ def newFile(self):
717721
"""
718722
Create blank 80x24 document
719723
"""
720-
self.ansiImage = AnsiImage(self.ansiGraphics)
724+
self.ansiImage = AnsiImage(self.ansiGraphics, has_autosave=True)
721725
self.ansiImage.clear_image(80, 24)
722726

723727
self.undoStack = []
@@ -777,6 +781,13 @@ def exportPNG(self):
777781
exportFileName = QtWidgets.QFileDialog.getSaveFileName(self, caption = "Export PNG", filter="PNG File (*.png)")[0]
778782
bitmap = self.ansiImage.to_bitmap(transparent = False, cursor = False)
779783
bitmap.save(exportFileName, "PNG")
784+
785+
def deiCE(self):
786+
"""
787+
Remove iCE colors and replace them with closest non-iCE match assuming regular font
788+
"""
789+
self.ansiImage.deice()
790+
self.redisplayAnsi()
780791

781792
def exit(self):
782793
"""
@@ -888,33 +899,42 @@ def addUndo(self, operation):
888899
Add an undo step (and clean out the redo stack)
889900
"""
890901
self.undoStack.append(operation)
902+
self.ansiImage.do_autosave()
891903
self.redoStack = []
892904

893905
def undo(self):
894906
"""
895907
Undo last action
896908
"""
897-
if len(self.undoStack) != 0:
898-
undoAction = self.undoStack.pop()
899-
if undoAction[0] == -1:
900-
undoAction = undoAction[1]
901-
self.redoStack.append((-1, self.ansiImage.change_size(undoAction[0], undoAction[1], undoAction[2])))
902-
else:
903-
self.redoStack.append(self.ansiImage.paste(undoAction, x = 0, y = 0))
904-
self.redisplayAnsi()
909+
try:
910+
if len(self.undoStack) != 0:
911+
undoAction = self.undoStack.pop()
912+
if undoAction[0] == -1:
913+
undoAction = undoAction[1]
914+
self.redoStack.append((-1, self.ansiImage.change_size(undoAction[0], undoAction[1], undoAction[2])))
915+
else:
916+
self.redoStack.append(self.ansiImage.paste(undoAction, x = 0, y = 0))
917+
self.redisplayAnsi()
918+
self.ansiImage.do_autosave()
919+
except:
920+
pass
905921

906922
def redo(self):
907923
"""
908924
Undo last undo
909925
"""
910-
if len(self.redoStack) != 0:
911-
redoAction = self.redoStack.pop()
912-
if redoAction[0] == -1:
913-
redoAction = redoAction[1]
914-
self.undoStack.append((-1, self.ansiImage.change_size(redoAction[0], redoAction[1], redoAction[2])))
915-
else:
916-
self.undoStack.append(self.ansiImage.paste(redoAction, x = 0, y = 0))
917-
self.redisplayAnsi()
926+
try:
927+
if len(self.redoStack) != 0:
928+
redoAction = self.redoStack.pop()
929+
if redoAction[0] == -1:
930+
redoAction = redoAction[1]
931+
self.undoStack.append((-1, self.ansiImage.change_size(redoAction[0], redoAction[1], redoAction[2])))
932+
else:
933+
self.undoStack.append(self.ansiImage.paste(redoAction, x = 0, y = 0))
934+
self.redisplayAnsi()
935+
self.ansiImage.do_autosave()
936+
except:
937+
pass
918938

919939
def changeTransparent(self):
920940
"""

0 commit comments

Comments
 (0)