-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathgraphgrabber.py
More file actions
185 lines (131 loc) · 4.22 KB
/
graphgrabber.py
File metadata and controls
185 lines (131 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import sark.qt
from PIL import Image, ImageChops
import idaapi
from io import BytesIO
# Those are a bit of voodoo.
# When a trimming an axis from both ends, the size on that axis is set to the
# trimmed size to speed up the capture. The margins are added because
# it sometimes fails without them.
HEIGHT_MARGIN = 10
WIDTH_MARGIN = 10
# Those 3 are safety values to keep IDA from freezing indefinitely.
# Feel free to change if needed (results are trimmed).
MAX_ITERATIONS = 30
MAX_WIDTH = 100000
MAX_HEIGHT = 100000
# Used to increment the size when needed. Higher values may speed up capture.
HEIGHT_INCREMENT = 1000
WIDTH_INCREMENT = 1000
def trim(im, bg=None):
if bg is None:
bg = get_bg(im)
diff = ImageChops.difference(im, bg)
bbox = diff.getbbox(alpha_only=False)
if bbox:
return im.crop(bbox), bbox
def get_bg(im):
return Image.new(im.mode, im.size, im.getpixel((0, 0)))
def center_graph(widget):
widget.setFocus()
graph_zoom_fit()
graph_zoom_100()
def graph_zoom_100():
if not idaapi.process_ui_action("GraphZoom100"):
raise RuntimeError("Failed to zoom 100%")
def graph_zoom_fit():
if not idaapi.process_ui_action("GraphZoomFit"):
raise RuntimeError("Failed to zoom to fit")
def get_ida_graph_widget(idaview_name="IDA View-A"):
widget = sark.qt.get_widget(idaview_name).children()[0]
try:
# IDA < 7.0
widget = widget.children()[0]
except IndexError:
pass
return widget
def grab_graph():
widget = get_ida_graph_widget()
width = widget.width()
height = widget.height()
graph_image = None
for iteration in range(MAX_ITERATIONS):
if height >= MAX_HEIGHT or width >= MAX_WIDTH:
break
print(f"Iteration: {iteration}")
sark.qt.resize_widget(widget, width, height)
center_graph(widget)
image = grab_image(widget)
try:
trimmed, (left, upper, right, lower) = trim(image)
except TypeError as e:
print(e)
width += WIDTH_INCREMENT
height += HEIGHT_INCREMENT
continue
print("Desired:", width, height)
print("Image:", image.width, image.height)
print("Trimmed:", trimmed.width, trimmed.height)
print("Bounds:", left, upper, right, lower)
resize = False
if left == 0 or right == image.width:
print("Increase width")
width += WIDTH_INCREMENT
resize = True
else: # speedup
width = trimmed.width + WIDTH_MARGIN
if upper == 0 or lower == image.height:
print("Increase height")
height += HEIGHT_INCREMENT
resize = True
else: # speedup
height = trimmed.height + HEIGHT_MARGIN
graph_image = trimmed
if not resize:
break
return graph_image
def grab_image(widget):
image_data = sark.qt.capture_widget(widget)
image = Image.open(BytesIO(image_data))
return image
def show(w):
image_data = sark.qt.capture_widget(w)
image = Image.open(BytesIO(image_data))
image.show()
def capture_graph(path=None):
if not path:
path = idaapi.ask_file(1, "graph.png", "Save Graph...")
if not path:
return
image = grab_graph()
try:
image.save(path, format="PNG")
except:
import traceback
traceback.print_exc()
class GraphGrabber(idaapi.plugin_t):
flags = idaapi.PLUGIN_PROC
comment = "GraphGrabber"
help = "Automatically grab full-res images of graphs"
wanted_name = "GraphGrabber"
wanted_hotkey = "Ctrl+Alt+G"
def init(self):
return idaapi.PLUGIN_KEEP
def run(self, arg):
capture_graph()
def term(self):
pass
def PLUGIN_ENTRY():
return GraphGrabber()
def is_script_file():
"""Check if executing as plugin or script.
Only works with Sark's plugin loader.
:return: bool
"""
# TODO: Make it work regardless of Sark's plugin loader.
import traceback
stack = traceback.extract_stack()
_filename, _line_number, function_name, _text = stack[0]
return function_name == "IDAPython_ExecScript"
if __name__ == "__main__":
if is_script_file():
capture_graph()