Skip to content

Commit

Permalink
added skippable regions
Browse files Browse the repository at this point in the history
  • Loading branch information
cracked-machine committed Feb 9, 2024
1 parent 3dc3544 commit b972104
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 31 deletions.
10 changes: 5 additions & 5 deletions doc/example/report.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
![memory map diagram](report.png)
|name|origin|size|remaining|collisions
|:-|:-|:-|:-|:-|
|<span style='color:steelblue'>kernel</span>|0x10|0x50|-0x10|{'rootfs': '0x50'}|
|<span style='color:darkcyan'>uboot</span>|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}|
|<span style='color:aqua'>rootfs</span>|0x50|0x30|0x10|{'kernel': '0x50'}|
|<span style='color:cornflowerblue'>dtb</span>|0x90|0x30|0x10|{}|
|<span style='color:blue'>uboot-scr</span>|0x110|0x30|0x50|{'uboot': '0x110'}|
|<span style='color:forestgreen'>kernel</span>|0x10|0x50|-0x10|{'rootfs': '0x50'}|
|<span style='color:cornflowerblue'>uboot</span>|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}|
|<span style='color:cadetblue'>rootfs</span>|0x50|0x30|0x10|{'kernel': '0x50'}|
|<span style='color:springgreen'>dtb</span>|0x90|0x30|0x10|{}|
|<span style='color:green'>uboot-scr</span>|0x110|0x30|0x50|{'uboot': '0x110'}|
Binary file added doc/example/report_cropped.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/example/report_full.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
86 changes: 78 additions & 8 deletions mm/diagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ def __init__(self):
self.default_region_text_size: int = 12
self.fixed_legend_text_size = 12

self.skip_threshold: int = 100

self._region_list = None
"""List of region objects"""

Expand Down Expand Up @@ -74,7 +76,7 @@ def _rescale(self):
def _create_diagram(self, region_list: List[mm.types.MemoryRegion]):

# init the main image
img_main = PIL.Image.new("RGB", (self.width, self.height), color=self.bgcolour)
img_main_full = PIL.Image.new("RGB", (self.width, self.height), color=self.bgcolour)

# paste each new graphic element image to main image
for region in region_list:
Expand All @@ -83,33 +85,101 @@ def _create_diagram(self, region_list: List[mm.types.MemoryRegion]):
region.create_img(img_width=(self.width - self._legend_width), font_size=self.default_region_text_size)
if not region.img:
continue
img_main.paste(region.img, (self._legend_width + region.draw_indent, region.origin), region.img)
img_main_full.paste(region.img, (self._legend_width + region.draw_indent, region.origin), region.img)

# Origin Address Text
origin_text_label = mm.types.TextLabel(region._origin, self.fixed_legend_text_size)
img_main.paste(origin_text_label.img, (0, region.origin - origin_text_label.height + 1))
img_main_full.paste(origin_text_label.img, (0, region.origin - origin_text_label.height + 1))

# Region End Address Text
end_addr_val = region.origin + region.size
end_addr_text_label = mm.types.TextLabel(hex(end_addr_val), self.fixed_legend_text_size)
img_main.paste(end_addr_text_label.img, (0, end_addr_val - end_addr_text_label.height + 1))
img_main_full.paste(end_addr_text_label.img, (0, end_addr_val - end_addr_text_label.height + 1))

# Diagram End Address Text
diagram_end_addr_val = self.height
diagram_end_addr_label = mm.types.TextLabel(hex(diagram_end_addr_val), self.fixed_legend_text_size)
img_main_full.paste(diagram_end_addr_label.img, (0, diagram_end_addr_val - diagram_end_addr_label.height - 5))

# Dash Lines from text to region
line_width = 1
line_canvas = PIL.ImageDraw.Draw(img_main)
line_canvas = PIL.ImageDraw.Draw(img_main_full)
dash_gap = 4
dash_len = dash_gap / 2

for x in range(end_addr_text_label.width * 2, self._legend_width - 10, dash_gap):
line_canvas.line((x, end_addr_val - line_width, x + dash_len, end_addr_val - line_width), fill="black", width=line_width)

for x in range(origin_text_label.width * 2, self._legend_width - 10, dash_gap):
line_canvas.line((x, region.origin - line_width, x + dash_len, region.origin - line_width), fill="black", width=1)

for x in range(diagram_end_addr_label.width * 2, self.width - 10, dash_gap):
line_canvas.line((x, diagram_end_addr_val - 7, x + dash_len, diagram_end_addr_val - 7), fill="black", width=line_width)

self._truncate_diagram(img_main_full, region_list)

# rotate the entire diagram so the origin is at the bottom
img_main = img_main.rotate(180)
img_main_full = img_main_full.rotate(180)

# output image file
img_file = pathlib.Path(self.args.out).stem + ".png"
img_main.save(pathlib.Path(self.args.out).parent / img_file)
img_file_path = pathlib.Path(self.args.out).stem + "_full.png"
img_main_full.save(pathlib.Path(self.args.out).parent / img_file_path)

def _truncate_diagram(self, img_to_crop: PIL.Image.Image, region_list: List[mm.types.MemoryRegion]):
"""Remove large empty spaces and replace them with fixed size SkippableRegion objects"""
# gather up the clusters of regions divided by large spaces
region_cluster_list = []
address_cursor = 0
for region in region_list:
end_addr_val = region.origin + region.size
if int(region.remain, 16) > self.skip_threshold:

# dont forget the image is upside down at this stage, so upper and lower are reversed.
(left, upper, right, lower) = (0, address_cursor - 10, img_to_crop.width, end_addr_val + 10)
cropped_img = img_to_crop.crop((left, upper, right, lower))
region_cluster_list.append(cropped_img)

# move the cursor up past the end of the current region and the empty space
address_cursor = end_addr_val + int(region.remain, 16)

# join all the cropped images together into a smaller main image
skip_region = mm.types.SkippableRegion(size=hex(40))
skip_region.create_img(img_width=(self.width - 20), font_size=self.default_region_text_size)
total_cropped_height = sum(r.height for r in region_cluster_list)
total_cropped_height = total_cropped_height + (len(region_cluster_list) * skip_region.img.height) + 20
img_main_cropped = PIL.Image.new("RGB", (self.width, total_cropped_height), color=self.bgcolour)
y_pos = 0
for region_cluster in region_cluster_list:
img_main_cropped.paste(region_cluster, (0, y_pos))
y_pos = y_pos + region_cluster.height

img_main_cropped.paste(skip_region.img, (10, y_pos))
y_pos = y_pos + skip_region.img.height

# Diagram End Address Text
diagram_end_addr_val = self.height
diagram_end_addr_label = mm.types.TextLabel(hex(diagram_end_addr_val), self.fixed_legend_text_size)
img_main_cropped.paste(diagram_end_addr_label.img, (0, img_main_cropped.height - diagram_end_addr_label.height - 10))

# Diagram End Dash Line
line_width = 1
line_canvas = PIL.ImageDraw.Draw(img_main_cropped)
dash_gap = 4
dash_len = dash_gap / 2
for x in range(diagram_end_addr_label.width * 2, self.width - 10, dash_gap):
line_canvas.line((x, img_main_cropped.height - diagram_end_addr_label.height - 3,
x + dash_len,
img_main_cropped.height - diagram_end_addr_label.height - 3),
fill="black",
width=line_width)

# no large empty regions were found so just copy in the existing image
if not region_cluster_list:
img_main_cropped = img_to_crop

img_main_cropped = img_main_cropped.rotate(180)
img_file_path = pathlib.Path(self.args.out).stem + "_cropped.png"
img_main_cropped.save(pathlib.Path(self.args.out).parent / img_file_path)

def _create_markdown(self, region_list: List[mm.types.MemoryRegion]):
with open(self.args.out, "w") as f:
Expand Down
43 changes: 35 additions & 8 deletions mm/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,41 @@ def create_img(self, img_width: int, font_size: int):
self.img = region_img


@typeguard.typechecked
class SkippableRegion(Region):

def __init__(self, size: str):

self.name: str = "~~~~~ SKIPPED ~~~~~"

super().__init__(self.name, "0x0", size)

def create_img(self, img_width: int, font_size: int):

logging.info(self)
if not self.size:
logging.warning("Zero size region skipped")
return None

# MemoryRegion Blocks and text
region_img = PIL.Image.new("RGBA", (img_width + 1, self.size), color="white")
self.region_canvas = PIL.ImageDraw.Draw(region_img)

# height is -1 to avoid clipping the top border
self.region_canvas.rectangle(
(0, 0, img_width, self.size - 1),
fill="oldlace",
outline="black",
width=1,
)

# draw name text
region_w, region_h = region_img.size
region_img.paste(TextLabel(text=self.name, font_size=font_size).img, (region_w // 5, region_h // 3))

self.img = region_img


@typeguard.typechecked
class TextLabel():
def __init__(self, text: str, font_size: int):
Expand Down Expand Up @@ -207,11 +242,3 @@ def _create_img(self):

# the final diagram image will be flipped so start with the text upside down
self.img = self.img.rotate(180)


@typeguard.typechecked
class SkippableRegion(Region):
pass



8 changes: 4 additions & 4 deletions tests/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ def test_valid_default_out_arg():
'0x10',
'0x10']):
mm.diagram.MemoryMap()
assert report.exists()
assert image.exists()
assert report.is_file
assert image.is_file


def test_invalid_duplicate_name_arg():
Expand Down Expand Up @@ -98,5 +98,5 @@ def test_valid_custom_out_arg():
"-o",
str(report)]):
mm.diagram.MemoryMap()
assert report.exists()
assert image.exists()
assert report.is_file
assert image.is_file
37 changes: 31 additions & 6 deletions tests/test_scaling.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ def test_scaling_x1():
default_diagram_width = 400
requested_diagram_height = 400
requested_scale = 1

report_path = pathlib.Path(f"/tmp/pytest/{__name__}.md")
image_path = pathlib.Path(f"/tmp/pytest/{__name__}.png")
report_path.unlink(missing_ok=True)
image_path.unlink(missing_ok=True)

image_path_full = pathlib.Path(f"/tmp/pytest/{__name__}_full.png")
image_path_full.unlink(missing_ok=True)

image_path_cropped = pathlib.Path(f"/tmp/pytest/{__name__}_cropped.png")
image_path_cropped.unlink(missing_ok=True)

with unittest.mock.patch('sys.argv',
['mmap_digram.diagram',
Expand All @@ -32,21 +37,33 @@ def test_scaling_x1():
str(requested_diagram_height)]):

d = mm.diagram.MemoryMap()
outimg = PIL.Image.open(str(image_path))
assert image_path_full.is_file
outimg = PIL.Image.open(str(image_path_full))
assert d.height == requested_diagram_height * requested_scale
assert d.width == default_diagram_width * requested_scale
assert outimg.size == (d.width, d.height)

assert image_path_cropped.is_file
outimg = PIL.Image.open(str(image_path_cropped))
assert d.height == requested_diagram_height * requested_scale
assert d.width == default_diagram_width * requested_scale
assert outimg.size == (d.width, 272)


def test_scaling_x2():
""" """
default_diagram_width = 400
requested_diagram_height = 400
requested_scale = 2

report_path = pathlib.Path(f"/tmp/pytest/{__name__}.md")
image_path = pathlib.Path(f"/tmp/pytest/{__name__}.png")
report_path.unlink(missing_ok=True)
image_path.unlink(missing_ok=True)

image_path_full = pathlib.Path(f"/tmp/pytest/{__name__}_full.png")
image_path_full.unlink(missing_ok=True)

image_path_cropped = pathlib.Path(f"/tmp/pytest/{__name__}_cropped.png")
image_path_cropped.unlink(missing_ok=True)

with unittest.mock.patch('sys.argv',
['mmap_digram.diagram',
Expand All @@ -64,7 +81,15 @@ def test_scaling_x2():
"-s", str(requested_scale)]):

d = mm.diagram.MemoryMap()
outimg = PIL.Image.open(str(image_path))

assert image_path_full.is_file
outimg = PIL.Image.open(str(image_path_full))
assert d.height == requested_diagram_height * requested_scale
assert d.width == default_diagram_width * requested_scale
assert outimg.size == (d.width, d.height)

assert image_path_cropped.is_file
outimg = PIL.Image.open(str(image_path_cropped))
assert d.height == requested_diagram_height * requested_scale
assert d.width == default_diagram_width * requested_scale
assert outimg.size == (d.width, 272)
59 changes: 59 additions & 0 deletions tests/test_skip_regions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import mm.diagram
import unittest
import mm.types
import pathlib
import PIL.Image

# Check the output report at /tmp/pytest/tests.test_distance.md


def test_skip_region():
""" """

report = pathlib.Path(f"/tmp/pytest/{__name__}.md")
image_full = pathlib.Path(f"/tmp/pytest/{__name__}_full.png")
image_crop_join = pathlib.Path(f"/tmp/pytest/{__name__}_cropped.png")

report.unlink(missing_ok=True)
image_full.unlink(missing_ok=True)
image_crop_join.unlink(missing_ok=True)

diagram_height = 1000
with unittest.mock.patch('sys.argv',
['mmap_digram.diagram',
'kernel',
'0x10',
'0x30',
'rootfs',
'0x50',
'0x30',
'dtb',
'0x190',
'0x30',
"-o", str(report),
"-l", str(diagram_height)]):

d = mm.diagram.MemoryMap()
for region in d._region_list:
if region.name == "kernel":
assert region._origin == "0x10"
assert region._size == "0x30"
assert region.remain == "0x10"
if region.name == "rootfs":
assert region._origin == "0x50"
assert region._size == "0x30"
assert region.remain == "0x110"
if region.name == "dtb":
assert region._origin == "0x190"
assert region._size == "0x30"
assert region.remain == "0x228"

assert report.is_file

assert image_full.is_file
assert PIL.Image.open(image_full).width == 400
assert PIL.Image.open(image_full).height == 1000

assert image_crop_join.is_file
assert PIL.Image.open(image_crop_join).width == 400
assert PIL.Image.open(image_crop_join).height == 316

0 comments on commit b972104

Please sign in to comment.