__init__.py 19 KB

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