__init__.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. import ast
  2. import time
  3. from gi.repository import GLib, Gtk, Gdk, GdkPixbuf
  4. import Activity
  5. import Export
  6. import threading
  7. import cv2
  8. import numpy
  9. import getpass
  10. import os
  11. from PF2 import Histogram
  12. from PF2.Tools import BlackWhite
  13. from PF2.Tools import Colours
  14. from PF2.Tools import Contrast
  15. from PF2.Tools import Details
  16. from PF2.Tools import Tonemap
  17. from PF2.Tools import HueEqualiser
  18. class PF2(Activity.Activity):
  19. def on_init(self):
  20. self.id = "PF2"
  21. self.name = "Edit a Photo"
  22. self.subtitle = "Edit a Raster Image with PhotoFiddle"
  23. UI_FILE = "ui/PF2_Activity.glade"
  24. self.builder.add_from_file(UI_FILE)
  25. self.widget = self.builder.get_object("PF2_main")
  26. self.stack.add_titled(self.widget, self.id, self.name)
  27. self.header_widget = self.builder.get_object("PF2_header")
  28. self.header_stack.add_titled(self.header_widget, self.id, self.name)
  29. # Get all UI Components
  30. self.ui = {}
  31. components = [
  32. "control_reveal",
  33. "histogram_reveal",
  34. "layers_reveal",
  35. "upper_peak_toggle",
  36. "lower_peak_toggle",
  37. "histogram",
  38. "layer_stack",
  39. "preview",
  40. "preview_eventbox",
  41. "scroll_window",
  42. "open_button",
  43. "original_toggle",
  44. "tools",
  45. "tool_stack",
  46. "open_window",
  47. "open_header",
  48. "open_cancel_button",
  49. "open_open_button",
  50. "open_chooser",
  51. "popovermenu",
  52. "show_hist",
  53. "export_image",
  54. "undo",
  55. "redo",
  56. "zoom_toggle",
  57. "zoom_reveal",
  58. "zoom",
  59. "reset"
  60. ]
  61. for component in components:
  62. self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
  63. self.menu_popover = self.ui["popovermenu"]
  64. # Set up tools
  65. self.tools = [
  66. Contrast.Contrast(),
  67. Tonemap.Tonemap(),
  68. Details.Details(),
  69. Colours.Colours(),
  70. HueEqualiser.HueEqualiser(),
  71. BlackWhite.BlackWhite(),
  72. ]
  73. self.tool_map = {}
  74. for tool in self.tools:
  75. self.ui["tools"].add(tool.tool_button)
  76. self.ui["tool_stack"].add(tool.widget)
  77. self.tool_map[tool.tool_button] = tool
  78. tool.tool_button.connect("clicked", self.on_tool_button_clicked)
  79. tool.connect_on_change(self.on_tool_change)
  80. # Set the first tool to active
  81. self.tools[0].tool_button.set_active(True)
  82. self.ui["tools"].show_all()
  83. # Disable ui components by default
  84. self.ui["original_toggle"].set_sensitive(False)
  85. self.ui["export_image"].set_sensitive(False)
  86. # Setup editor variables
  87. self.is_editing = False
  88. self.has_loaded = False
  89. self.image_path = ""
  90. self.bit_depth = 8
  91. self.image = None
  92. self.original_image = None
  93. self.pwidth = 0
  94. self.pheight = 0
  95. self.awidth = 0
  96. self.aheight = 0
  97. self.pimage = None
  98. self.poriginal = None
  99. self.change_occurred = False
  100. self.additional_change_occurred = False
  101. self.running_stack = False
  102. self.upper_peak_on = False
  103. self.lower_peak_on = False
  104. self.is_exporting = False
  105. self.is_zooming = False
  106. self.undo_stack = []
  107. self.undo_position = 0
  108. self.undoing = False
  109. # Setup Open Dialog
  110. self.ui["open_window"].set_transient_for(self.root)
  111. self.ui["open_window"].set_titlebar(self.ui["open_header"])
  112. # Connect UI signals
  113. self.ui["open_button"].connect("clicked", self.on_open_clicked)
  114. self.ui["preview"].connect("draw", self.on_preview_draw)
  115. self.ui["original_toggle"].connect("toggled", self.on_preview_toggled)
  116. self.ui["upper_peak_toggle"].connect("toggled", self.on_upper_peak_toggled)
  117. self.ui["lower_peak_toggle"].connect("toggled", self.on_lower_peak_toggled)
  118. self.ui["show_hist"].connect("toggled", self.toggle_hist)
  119. self.ui["export_image"].connect("clicked", self.on_export_clicked)
  120. self.ui["open_open_button"].connect("clicked", self.on_file_opened)
  121. self.ui["open_cancel_button"].connect("clicked", self.on_file_canceled)
  122. self.ui["zoom_toggle"].connect("toggled", self.on_zoom_toggled)
  123. self.ui["zoom"].connect("value-changed", self.on_zoom_changed)
  124. self.ui["undo"].connect("clicked", self.on_undo)
  125. self.ui["redo"].connect("clicked", self.on_redo)
  126. self.ui["reset"].connect("clicked", self.on_reset)
  127. self.ui["preview_eventbox"].connect('motion-notify-event', self.foo)
  128. def foo(self, widget, event):
  129. print(event.x, event.y)
  130. def on_tool_button_clicked(self, sender):
  131. if(sender.get_active()):
  132. self.ui["tool_stack"].set_visible_child(self.tool_map[sender].widget)
  133. for key in self.tool_map:
  134. if(key != sender):
  135. key.set_active(False)
  136. elif(self.ui["tool_stack"].get_visible_child() == self.tool_map[sender].widget):
  137. sender.set_active(True)
  138. def on_open_clicked(self, sender):
  139. self.ui["open_window"].show_all()
  140. def on_file_opened(self, sender, path=None):
  141. self.has_loaded = False
  142. self.image = None
  143. self.undo_position = 0
  144. self.undo_stack = []
  145. self.update_undo_state()
  146. self.ui["open_window"].hide()
  147. self.ui["control_reveal"].set_reveal_child(True)
  148. self.ui["control_reveal"].set_sensitive(True)
  149. self.ui["original_toggle"].set_sensitive(True)
  150. self.ui["export_image"].set_sensitive(True)
  151. self.ui["reset"].set_sensitive(True)
  152. self.is_editing = True
  153. if(path == None):
  154. self.image_path = self.ui["open_chooser"].get_filename()
  155. else:
  156. self.image_path = path
  157. w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
  158. h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
  159. self.show_message("Loading Photo…", "Please wait while PhotoFiddle loads your photo", True)
  160. self.root.get_titlebar().set_subtitle("%s…" % (self.image_path))
  161. thread = threading.Thread(target=self.open_image,
  162. args=(w, h))
  163. thread.start()
  164. def on_zoom_toggled(self, sender):
  165. state = sender.get_active()
  166. self.ui["zoom_reveal"].set_reveal_child(state)
  167. if(not state):
  168. self.ui["zoom"].set_value(1)
  169. def on_zoom_changed(self, sender):
  170. threading.Thread(target=self.zoom_delay).start()
  171. def zoom_delay(self):
  172. if(not self.is_zooming):
  173. self.is_zooming = True
  174. time.sleep(0.5)
  175. GLib.idle_add(self.on_preview_draw, None, None)
  176. self.is_zooming = False
  177. def on_file_canceled(self, sender):
  178. self.ui["open_window"].hide()
  179. def on_preview_draw(self, sender, arg):
  180. if(self.is_editing):
  181. w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
  182. h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
  183. if(self.pheight != h) or (self.pwidth != w):
  184. thread = threading.Thread(target=self.resize_preview,
  185. args=(w, h))
  186. thread.start()
  187. def on_preview_toggled(self, sender):
  188. if(sender.get_active()):
  189. threading.Thread(target=self.draw_hist, args=(self.original_image,)).start()
  190. self.show_original()
  191. else:
  192. threading.Thread(target=self.draw_hist, args=(self.image,)).start()
  193. self.show_current()
  194. def toggle_hist(self, sender):
  195. show = sender.get_active()
  196. self.ui["histogram_reveal"].set_reveal_child(show)
  197. def on_open(self, path):
  198. self.root.get_titlebar().set_subtitle("Raster Editor")
  199. if(path != None):
  200. # We have been sent here from another
  201. # activity, clear any existing profiles
  202. self.image_path = path
  203. dataPath = self.get_data_path()
  204. if(os.path.exists(dataPath)):
  205. os.unlink(dataPath)
  206. self.on_file_opened(None, path)
  207. def on_exit(self):
  208. if(self.is_exporting):
  209. return False
  210. else:
  211. fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
  212. if (os.path.exists(fname)):
  213. os.unlink(fname)
  214. self.root.get_titlebar().set_subtitle("")
  215. self.on_init()
  216. return True
  217. def show_original(self):
  218. self.ui["preview"].set_from_pixbuf(self.poriginal)
  219. if (not self.ui["original_toggle"].get_active()):
  220. self.ui["original_toggle"].set_active(True)
  221. def show_current(self):
  222. self.ui["preview"].set_from_pixbuf(self.pimage)
  223. if (self.ui["original_toggle"].get_active()):
  224. self.ui["original_toggle"].set_active(False)
  225. def on_tool_change(self, tool, property):
  226. if(self.has_loaded):
  227. if(not self.change_occurred):
  228. self.change_occurred = True
  229. thread = threading.Thread(target=self.update_image)
  230. thread.start()
  231. else:
  232. self.additional_change_occurred = True
  233. def on_lower_peak_toggled(self, sender):
  234. self.lower_peak_on = sender.get_active()
  235. if (self.lower_peak_on):
  236. thread = threading.Thread(target=self.process_peaks, args=(True,))
  237. thread.start()
  238. else:
  239. thread = threading.Thread(target=self.update_image, args=(True,))
  240. thread.start()
  241. def on_upper_peak_toggled(self, sender):
  242. self.upper_peak_on = sender.get_active()
  243. if (self.lower_peak_on):
  244. thread = threading.Thread(target=self.process_peaks, args=(True,))
  245. thread.start()
  246. else:
  247. thread = threading.Thread(target=self.update_image, args=(True,))
  248. thread.start()
  249. def image_opened(self, depth):
  250. self.root.get_titlebar().set_subtitle("%s (%s Bit)" % (self.image_path, depth))
  251. self.hide_message()
  252. def on_export_clicked(self, sender):
  253. Export.ExportDialog(self.root, self.builder, self.awidth, self.aheight, self.get_export_image, self.on_export_complete, self.image_path)
  254. def on_export_complete(self, filename):
  255. self.is_exporting = False
  256. self.on_export_state_change()
  257. self.show_message("Export Complete!", "Your photo has been exported to '%s'" % filename)
  258. def on_export_state_change(self):
  259. self.ui["control_reveal"].set_sensitive(not self.is_exporting)
  260. self.ui["export_image"].set_sensitive(not self.is_exporting)
  261. self.ui["open_button"].set_sensitive(not self.is_exporting)
  262. def on_export_started(self):
  263. self.is_exporting = True
  264. self.on_export_state_change()
  265. def update_undo_state(self):
  266. self.ui["undo"].set_sensitive(self.undo_position > 0)
  267. self.ui["redo"].set_sensitive(len(self.undo_stack)-1 > self.undo_position)
  268. def on_undo(self, sender):
  269. self.undo_position -= 1
  270. self.update_undo_state()
  271. self.update_from_undo_stack(self.undo_stack[self.undo_position])
  272. def on_redo(self, sender):
  273. self.undo_position += 1
  274. self.update_undo_state()
  275. self.update_from_undo_stack(self.undo_stack[self.undo_position])
  276. def on_reset(self, sender):
  277. for tool in self.tools:
  278. tool.reset()
  279. ## Background Tasks ##
  280. def open_image(self, w, h):
  281. self.load_image_data()
  282. try:
  283. self.resize_preview(w, h)
  284. except:
  285. pass
  286. while(self.image == None):
  287. time.sleep(1)
  288. GLib.idle_add(self.image_opened, str(self.image.dtype).replace("uint", "").replace("float", ""))
  289. time.sleep(1)
  290. self.has_loaded = True
  291. def resize_preview(self, w, h):
  292. # Inhibit undo stack to prevent
  293. # Adding an action on resize
  294. self.undoing = True
  295. self.original_image = cv2.imread(self.image_path, 2 | 1)
  296. height, width = self.original_image.shape[:2]
  297. self.aheight = height
  298. self.awidth = width
  299. self.pheight = h
  300. self.pwidth = w
  301. # Get fitting size
  302. ratio = float(w)/width
  303. if(height*ratio > h):
  304. ratio = float(h)/height
  305. nw = width * ratio
  306. nh = height * ratio
  307. # Do quick ui resize
  308. if(self.image != None) and (os.path.exists("/tmp/phf2-preview-%s.png" % getpass.getuser())):
  309. # If we have an edited version, show that
  310. self.pimage = GdkPixbuf.Pixbuf.new_from_file_at_scale("/tmp/phf2-preview-%s.png" % getpass.getuser(),
  311. int(nw), int(nh), True)
  312. GLib.idle_add(self.show_current)
  313. self.poriginal = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.image_path,
  314. int(nw), int(nh), True)
  315. if(self.image == None):
  316. # Otherwise show the original
  317. GLib.idle_add(self.show_original)
  318. # Resize OPENCV Copy
  319. self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
  320. self.image = numpy.copy(self.original_image)
  321. # Update image
  322. if (not self.change_occurred):
  323. self.change_occurred = True
  324. self.update_image()
  325. else:
  326. self.additional_change_occurred = True
  327. def update_image(self, immediate=False):
  328. if(not immediate):
  329. time.sleep(0.5)
  330. self.additional_change_occurred = False
  331. GLib.idle_add(self.start_work)
  332. self.image = numpy.copy(self.original_image)
  333. self.image = self.run_stack(self.image)
  334. if(self.additional_change_occurred):
  335. self.update_image()
  336. else:
  337. self.save_image_data()
  338. self.draw_hist(self.image)
  339. self.process_peaks()
  340. self.update_preview()
  341. GLib.idle_add(self.stop_work)
  342. self.change_occurred = False
  343. self.undoing = False
  344. def run_stack(self, image, callback=None):
  345. if(not self.running_stack):
  346. self.running_stack = True
  347. ntools = len(self.tools)
  348. count = 1
  349. for tool in self.tools:
  350. if(callback != None):
  351. # For showing progress
  352. callback(tool.name, ntools, count-1)
  353. image = tool.on_update(image)
  354. count += 1
  355. self.running_stack = False
  356. return image
  357. else:
  358. while(self.running_stack):
  359. time.sleep(1)
  360. return self.run_stack(image, callback)
  361. def update_preview(self):
  362. fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
  363. if(os.path.exists(fname)):
  364. os.unlink(fname)
  365. cv2.imwrite(fname, self.image)
  366. self.pimage = GdkPixbuf.Pixbuf.new_from_file(fname)
  367. GLib.idle_add(self.show_current)
  368. def draw_hist(self, image):
  369. path = "/tmp/phf2-hist-%s.png" % getpass.getuser()
  370. Histogram.Histogram.draw_hist(image, path)
  371. GLib.idle_add(self.update_hist_ui, path)
  372. def update_hist_ui(self, path):
  373. try:
  374. self.ui["histogram"].set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(path))
  375. except:
  376. pass
  377. def process_peaks(self, do_update=False):
  378. bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
  379. if(self.upper_peak_on):
  380. self.image[(self.image == 2 ** bpp - 1).all(axis=2)] = 1
  381. if(self.lower_peak_on):
  382. self.image[(self.image == 0).all(axis=2)] = 2 ** bpp - 2
  383. if(do_update):
  384. self.update_preview()
  385. ## FILE STUFF ##
  386. def get_data_path(self):
  387. return "%s/.%s.pf2" % ("/".join(self.image_path.split("/")[:-1]), self.image_path.split("/")[-1:][0])
  388. def save_image_data(self):
  389. path = self.get_data_path()
  390. print(path)
  391. f = open(path, "w")
  392. toolDict = {}
  393. for tool in self.tools:
  394. toolDict[tool.id] = tool.get_properties_as_dict()
  395. if(not self.undoing) and (self.has_loaded):
  396. if(len(self.undo_stack)-1 != self.undo_position):
  397. self.undo_stack = self.undo_stack[:self.undo_position+1]
  398. if(self.undo_stack[self.undo_position] != toolDict):
  399. self.undo_stack += [toolDict,]
  400. self.undo_position = len(self.undo_stack)-1
  401. GLib.idle_add(self.update_undo_state)
  402. data = {
  403. "path":self.image_path,
  404. "format-revision":1,
  405. "layers": {
  406. "base":{
  407. "tools": toolDict
  408. }
  409. }
  410. }
  411. f.write(str(data))
  412. f.close()
  413. def load_image_data(self):
  414. path = self.get_data_path()
  415. loadDefaults = True
  416. if(os.path.exists(path)):
  417. f = open(path, 'r')
  418. sdata = f.read()
  419. try:
  420. data = ast.literal_eval(sdata)
  421. if(data["format-revision"] == 1):
  422. for tool in self.tools:
  423. if(tool.id in data["layers"]["base"]["tools"]):
  424. GLib.idle_add(tool.load_properties,data["layers"]["base"]["tools"][tool.id])
  425. self.undo_stack = [data["layers"]["base"]["tools"],]
  426. self.undo_position = 0
  427. loadDefaults = False
  428. except:
  429. GLib.idle_add(self.show_message,"Unable to load previous edits…",
  430. "The edit file for this photo is corrupted and could not be loaded.")
  431. if(loadDefaults):
  432. for tool in self.tools:
  433. GLib.idle_add(tool.reset)
  434. toolDict = {}
  435. for tool in self.tools:
  436. toolDict[tool.id] = tool.get_properties_as_dict()
  437. self.undo_stack = [toolDict, ]
  438. self.undo_position = 0
  439. GLib.idle_add(self.update_undo_state)
  440. time.sleep(2)
  441. self.undoing = False
  442. def update_from_undo_stack(self, data):
  443. self.undoing = True
  444. for tool in self.tools:
  445. if (tool.id in data):
  446. tool.load_properties(data[tool.id])
  447. def get_export_image(self, w, h):
  448. GLib.idle_add(self.on_export_started)
  449. GLib.idle_add(self.show_message, "Exporting Photo", "Please wait…", True, True)
  450. img = cv2.imread(self.image_path, 2 | 1)
  451. img = cv2.resize(img, (int(w), int(h)), interpolation=cv2.INTER_AREA)
  452. img = self.run_stack(img, self.export_progress_callback)
  453. GLib.idle_add(self.show_message, "Exporting Photo", "Saving to filesystem…", True, True)
  454. GLib.idle_add(self.update_message_progress, 1, 1)
  455. return img
  456. def export_progress_callback(self, name, count, current):
  457. GLib.idle_add(self.show_message, "Exporting Photo", "Processing: %s" % name, True, True)
  458. GLib.idle_add(self.update_message_progress, current, count)