__init__.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  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, Layer
  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. "tool_box_stack",
  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. "mask_draw_toggle",
  61. "mask_erase_toggle",
  62. "mask_brush_size",
  63. "layers_list",
  64. ]
  65. for component in components:
  66. self.ui[component] = self.builder.get_object("%s_%s" % (self.id, component))
  67. self.menu_popover = self.ui["popovermenu"]
  68. # Set up tools
  69. self.tools = [
  70. Contrast.Contrast,
  71. Tonemap.Tonemap,
  72. Details.Details,
  73. Colours.Colours,
  74. HueEqualiser.HueEqualiser,
  75. BlackWhite.BlackWhite
  76. ]
  77. # Setup layers
  78. self.layers = []
  79. self.base_layer = self.create_layer("base", True)
  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.preview_dragged)
  128. self.ui["mask_draw_toggle"].connect("toggled", self.mask_draw_toggled)
  129. self.ui["mask_erase_toggle"].connect("toggled", self.mask_erase_toggled)
  130. def on_open_clicked(self, sender):
  131. self.ui["open_window"].show_all()
  132. def on_file_opened(self, sender, path=None):
  133. self.has_loaded = False
  134. self.image = None
  135. self.undo_position = 0
  136. self.undo_stack = []
  137. self.update_undo_state()
  138. self.ui["open_window"].hide()
  139. self.ui["control_reveal"].set_reveal_child(True)
  140. self.ui["control_reveal"].set_sensitive(True)
  141. self.ui["original_toggle"].set_sensitive(True)
  142. self.ui["export_image"].set_sensitive(True)
  143. self.ui["reset"].set_sensitive(True)
  144. self.is_editing = True
  145. if(path == None):
  146. self.image_path = self.ui["open_chooser"].get_filename()
  147. else:
  148. self.image_path = path
  149. w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
  150. h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
  151. self.show_message("Loading Photo…", "Please wait while PhotoFiddle loads your photo", True)
  152. self.root.get_titlebar().set_subtitle("%s…" % (self.image_path))
  153. thread = threading.Thread(target=self.open_image,
  154. args=(w, h))
  155. thread.start()
  156. def on_zoom_toggled(self, sender):
  157. state = sender.get_active()
  158. self.ui["zoom_reveal"].set_reveal_child(state)
  159. if(not state):
  160. self.ui["zoom"].set_value(1)
  161. def on_zoom_changed(self, sender):
  162. threading.Thread(target=self.zoom_delay).start()
  163. def zoom_delay(self):
  164. if(not self.is_zooming):
  165. self.is_zooming = True
  166. time.sleep(0.5)
  167. GLib.idle_add(self.on_preview_draw, None, None)
  168. self.is_zooming = False
  169. def on_file_canceled(self, sender):
  170. self.ui["open_window"].hide()
  171. def on_preview_draw(self, sender, arg):
  172. if(self.is_editing):
  173. w = (self.ui["scroll_window"].get_allocated_width() - 12) * self.ui["zoom"].get_value()
  174. h = (self.ui["scroll_window"].get_allocated_height() - 12) * self.ui["zoom"].get_value()
  175. if(self.pheight != h) or (self.pwidth != w):
  176. thread = threading.Thread(target=self.resize_preview,
  177. args=(w, h))
  178. thread.start()
  179. def on_preview_toggled(self, sender):
  180. if(sender.get_active()):
  181. threading.Thread(target=self.draw_hist, args=(self.original_image,)).start()
  182. self.show_original()
  183. else:
  184. threading.Thread(target=self.draw_hist, args=(self.image,)).start()
  185. self.show_current()
  186. def toggle_hist(self, sender):
  187. show = sender.get_active()
  188. self.ui["histogram_reveal"].set_reveal_child(show)
  189. def on_open(self, path):
  190. self.root.get_titlebar().set_subtitle("Raster Editor")
  191. if(path != None):
  192. # We have been sent here from another
  193. # activity, clear any existing profiles
  194. self.image_path = path
  195. dataPath = self.get_data_path()
  196. if(os.path.exists(dataPath)):
  197. os.unlink(dataPath)
  198. self.on_file_opened(None, path)
  199. def on_exit(self):
  200. if(self.is_exporting):
  201. return False
  202. else:
  203. fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
  204. if (os.path.exists(fname)):
  205. os.unlink(fname)
  206. self.root.get_titlebar().set_subtitle("")
  207. self.on_init()
  208. return True
  209. def show_original(self):
  210. self.ui["preview"].set_from_pixbuf(self.poriginal)
  211. if (not self.ui["original_toggle"].get_active()):
  212. self.ui["original_toggle"].set_active(True)
  213. def show_current(self):
  214. self.ui["preview"].set_from_pixbuf(self.pimage)
  215. if (self.ui["original_toggle"].get_active()):
  216. self.ui["original_toggle"].set_active(False)
  217. def on_layer_change(self, layer):
  218. if(self.has_loaded):
  219. if(not self.change_occurred):
  220. self.change_occurred = True
  221. thread = threading.Thread(target=self.update_image)
  222. thread.start()
  223. else:
  224. self.additional_change_occurred = True
  225. def on_lower_peak_toggled(self, sender):
  226. self.lower_peak_on = sender.get_active()
  227. if (self.lower_peak_on):
  228. thread = threading.Thread(target=self.process_peaks, args=(True,))
  229. thread.start()
  230. else:
  231. thread = threading.Thread(target=self.update_image, args=(True,))
  232. thread.start()
  233. def on_upper_peak_toggled(self, sender):
  234. self.upper_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 image_opened(self, depth):
  242. self.root.get_titlebar().set_subtitle("%s (%s Bit)" % (self.image_path, depth))
  243. self.hide_message()
  244. def on_export_clicked(self, sender):
  245. Export.ExportDialog(self.root, self.builder, self.awidth, self.aheight, self.get_export_image, self.on_export_complete, self.image_path)
  246. def on_export_complete(self, filename):
  247. self.is_exporting = False
  248. self.on_export_state_change()
  249. self.show_message("Export Complete!", "Your photo has been exported to '%s'" % filename)
  250. def on_export_state_change(self):
  251. self.ui["control_reveal"].set_sensitive(not self.is_exporting)
  252. self.ui["export_image"].set_sensitive(not self.is_exporting)
  253. self.ui["open_button"].set_sensitive(not self.is_exporting)
  254. def on_export_started(self):
  255. self.is_exporting = True
  256. self.on_export_state_change()
  257. def update_undo_state(self):
  258. self.ui["undo"].set_sensitive(self.undo_position > 0)
  259. self.ui["redo"].set_sensitive(len(self.undo_stack)-1 > self.undo_position)
  260. def on_undo(self, sender):
  261. self.undo_position -= 1
  262. self.update_undo_state()
  263. self.update_from_undo_stack(self.undo_stack[self.undo_position])
  264. def on_redo(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_reset(self, sender):
  269. for tool in self.tools:
  270. tool.reset()
  271. ## Background Tasks ##
  272. def open_image(self, w, h):
  273. self.load_image_data()
  274. try:
  275. self.resize_preview(w, h)
  276. except:
  277. pass
  278. while(self.image == None):
  279. time.sleep(1)
  280. GLib.idle_add(self.image_opened, str(self.image.dtype).replace("uint", "").replace("float", ""))
  281. time.sleep(1)
  282. self.has_loaded = True
  283. def resize_preview(self, w, h):
  284. # Inhibit undo stack to prevent
  285. # Adding an action on resize
  286. self.undoing = True
  287. self.original_image = cv2.imread(self.image_path, 2 | 1)
  288. height, width = self.original_image.shape[:2]
  289. self.aheight = height
  290. self.awidth = width
  291. self.pheight = h
  292. self.pwidth = w
  293. # Get fitting size
  294. ratio = float(w)/width
  295. if(height*ratio > h):
  296. ratio = float(h)/height
  297. nw = width * ratio
  298. nh = height * ratio
  299. # Do quick ui resize
  300. if(self.image != None) and (os.path.exists("/tmp/phf2-preview-%s.png" % getpass.getuser())):
  301. # If we have an edited version, show that
  302. self.pimage = GdkPixbuf.Pixbuf.new_from_file_at_scale("/tmp/phf2-preview-%s.png" % getpass.getuser(),
  303. int(nw), int(nh), True)
  304. GLib.idle_add(self.show_current)
  305. self.poriginal = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.image_path,
  306. int(nw), int(nh), True)
  307. if(self.image == None):
  308. # Otherwise show the original
  309. GLib.idle_add(self.show_original)
  310. # Resize OPENCV Copy
  311. self.original_image = cv2.resize(self.original_image, (int(nw), int(nh)), interpolation = cv2.INTER_AREA)
  312. self.image = numpy.copy(self.original_image)
  313. # Update image
  314. if (not self.change_occurred):
  315. self.change_occurred = True
  316. self.update_image()
  317. else:
  318. self.additional_change_occurred = True
  319. def update_image(self, immediate=False):
  320. if(not immediate):
  321. time.sleep(0.5)
  322. self.additional_change_occurred = False
  323. GLib.idle_add(self.start_work)
  324. self.image = numpy.copy(self.original_image)
  325. self.image = self.run_stack(self.image)
  326. if(self.additional_change_occurred):
  327. self.update_image()
  328. else:
  329. self.save_image_data()
  330. self.draw_hist(self.image)
  331. self.process_peaks()
  332. self.update_preview()
  333. GLib.idle_add(self.stop_work)
  334. self.change_occurred = False
  335. self.undoing = False
  336. def run_stack(self, image, callback=None):
  337. if(not self.running_stack):
  338. self.running_stack = True
  339. baseImage = image.copy()
  340. for layer in self.layers:
  341. print(layer)
  342. image = layer.render_layer(baseImage, image, callback)
  343. self.running_stack = False
  344. return image
  345. else:
  346. while(self.running_stack):
  347. time.sleep(1)
  348. return self.run_stack(image, callback)
  349. def update_preview(self):
  350. fname = "/tmp/phf2-preview-%s.png" % getpass.getuser()
  351. if(os.path.exists(fname)):
  352. os.unlink(fname)
  353. cv2.imwrite(fname, self.image)
  354. self.pimage = GdkPixbuf.Pixbuf.new_from_file(fname)
  355. GLib.idle_add(self.show_current)
  356. def draw_hist(self, image):
  357. path = "/tmp/phf2-hist-%s.png" % getpass.getuser()
  358. Histogram.Histogram.draw_hist(image, path)
  359. GLib.idle_add(self.update_hist_ui, path)
  360. def update_hist_ui(self, path):
  361. try:
  362. self.ui["histogram"].set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(path))
  363. except:
  364. pass
  365. def process_peaks(self, do_update=False):
  366. bpp = float(str(self.image.dtype).replace("uint", "").replace("float", ""))
  367. if(self.upper_peak_on):
  368. self.image[(self.image == 2 ** bpp - 1).all(axis=2)] = 1
  369. if(self.lower_peak_on):
  370. self.image[(self.image == 0).all(axis=2)] = 2 ** bpp - 2
  371. if(do_update):
  372. self.update_preview()
  373. ## FILE STUFF ##
  374. def get_data_path(self):
  375. return "%s/.%s.pf2" % ("/".join(self.image_path.split("/")[:-1]), self.image_path.split("/")[-1:][0])
  376. def save_image_data(self):
  377. path = self.get_data_path()
  378. print(path)
  379. f = open(path, "w")
  380. layerDict = {}
  381. for layer in self.layers:
  382. layerDict[layer.name] = layer.get_layer_dict()
  383. if(not self.undoing) and (self.has_loaded):
  384. if(len(self.undo_stack)-1 != self.undo_position):
  385. self.undo_stack = self.undo_stack[:self.undo_position+1]
  386. if(self.undo_stack[self.undo_position] != layerDict):
  387. self.undo_stack += [layerDict,]
  388. self.undo_position = len(self.undo_stack)-1
  389. GLib.idle_add(self.update_undo_state)
  390. data = {
  391. "path":self.image_path,
  392. "format-revision":1,
  393. "layers": layerDict
  394. }
  395. f.write(str(data))
  396. f.close()
  397. def load_image_data(self):
  398. path = self.get_data_path()
  399. loadDefaults = True
  400. if(os.path.exists(path)):
  401. f = open(path, 'r')
  402. sdata = f.read()
  403. if(True):
  404. #try:
  405. data = ast.literal_eval(sdata)
  406. if(data["format-revision"] == 1):
  407. for layer in data["layers"]:
  408. if(layer == "base"):
  409. self.base_layer.set_from_layer_dict(data["layers"][layer])
  410. else:
  411. ilayer = self.create_layer(layer, False)
  412. ilayer.set_from_layer_dict(data["layers"][layer])
  413. self.undo_stack = [data["layers"],]
  414. self.undo_position = 0
  415. loadDefaults = False
  416. #except:
  417. # GLib.idle_add(self.show_message,"Unable to load previous edits…",
  418. # "The edit file for this photo is corrupted and could not be loaded.")
  419. if(loadDefaults):
  420. for layer in self.layers:
  421. for tool in layer.tools:
  422. GLib.idle_add(tool.reset)
  423. layerDict = {}
  424. for layer in self.layers:
  425. layerDict[layer.name] = layer.get_layer_dict()
  426. self.undo_stack = [layerDict, ]
  427. self.undo_position = 0
  428. GLib.idle_add(self.update_undo_state)
  429. time.sleep(2)
  430. self.undoing = False
  431. def update_from_undo_stack(self, data):
  432. self.undoing = True
  433. for layer in data:
  434. if (layer == "base"):
  435. self.base_layer.set_from_layer_dict(data[layer])
  436. else:
  437. ilayer = self.create_layer(layer, False)
  438. ilayer.set_from_layer_dict(data[layer])
  439. def get_export_image(self, w, h):
  440. GLib.idle_add(self.on_export_started)
  441. GLib.idle_add(self.show_message, "Exporting Photo", "Please wait…", True, True)
  442. img = cv2.imread(self.image_path, 2 | 1)
  443. img = cv2.resize(img, (int(w), int(h)), interpolation=cv2.INTER_AREA)
  444. img = self.run_stack(img, self.export_progress_callback)
  445. GLib.idle_add(self.show_message, "Exporting Photo", "Saving to filesystem…", True, True)
  446. GLib.idle_add(self.update_message_progress, 1, 1)
  447. return img
  448. def export_progress_callback(self, name, count, current):
  449. GLib.idle_add(self.show_message, "Exporting Photo", "Processing: %s" % name, True, True)
  450. GLib.idle_add(self.update_message_progress, current, count)
  451. ## Layers Stuff ##
  452. def preview_dragged(self, widget, event):
  453. print(event.x, event.y)
  454. def mask_draw_toggled(self, widget):
  455. if(widget.get_active()):
  456. self.ui["mask_erase_toggle"].set_active(False)
  457. def mask_erase_toggled(self, widget):
  458. if(widget.get_active()):
  459. self.ui["mask_draw_toggle"].set_active(False)
  460. def create_layer(self, layer_name, is_base):
  461. layer = Layer.Layer(is_base, layer_name, self.on_layer_change)
  462. for tool in self.tools:
  463. tool_instance = tool()
  464. layer.add_tool(tool_instance)
  465. self.layers += [layer,]
  466. self.create_layer_ui(layer)
  467. return layer
  468. def create_layer_ui(self, layer):
  469. layer_box = Gtk.HBox()
  470. layer_toggle = Gtk.CheckButton()
  471. layer_toggle.set_sensitive(not layer.editable)
  472. layer_toggle.set_active(layer.enabled)
  473. layer_label = Gtk.Label()
  474. layer_label.set_label(layer.name)
  475. layer_box.add(layer_toggle)
  476. layer_box.add(layer_label)
  477. self.ui["layers_list"].add(layer_box)
  478. self.ui["tool_box_stack"].add(layer.tool_box)
  479. self.ui["tool_stack"].add(layer.tool_stack)