diff --git a/doc/example/report.md b/doc/example/report.md
index ff16297..03c470b 100644
--- a/doc/example/report.md
+++ b/doc/example/report.md
@@ -1,8 +1,8 @@

|name|origin|size|remaining|collisions
|:-|:-|:-|:-|:-|
-|kernel|0x10|0x50|-0x10|{'rootfs': '0x50'}|
-|uboot|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}|
-|rootfs|0x50|0x30|0x10|{'kernel': '0x50'}|
-|dtb|0x90|0x30|0x10|{}|
-|uboot-scr|0x110|0x30|0x50|{'uboot': '0x110'}|
+|kernel|0x10|0x50|-0x10|{'rootfs': '0x50'}|
+|uboot|0xD0|0x50|-0x10|{'uboot-scr': '0x110'}|
+|rootfs|0x50|0x30|0x10|{'kernel': '0x50'}|
+|dtb|0x90|0x30|0x10|{}|
+|uboot-scr|0x110|0x30|0x50|{'uboot': '0x110'}|
diff --git a/doc/example/report_cropped.png b/doc/example/report_cropped.png
new file mode 100644
index 0000000..fd4e377
Binary files /dev/null and b/doc/example/report_cropped.png differ
diff --git a/doc/example/report_full.png b/doc/example/report_full.png
new file mode 100644
index 0000000..fd4e377
Binary files /dev/null and b/doc/example/report_full.png differ
diff --git a/mm/diagram.py b/mm/diagram.py
index 49543c6..bcc3b85 100644
--- a/mm/diagram.py
+++ b/mm/diagram.py
@@ -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"""
@@ -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:
@@ -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:
diff --git a/mm/types.py b/mm/types.py
index 452c4d9..3557437 100644
--- a/mm/types.py
+++ b/mm/types.py
@@ -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):
@@ -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
-
-
-
diff --git a/tests/test_args.py b/tests/test_args.py
index 0570901..a879e0c 100644
--- a/tests/test_args.py
+++ b/tests/test_args.py
@@ -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():
@@ -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
diff --git a/tests/test_scaling.py b/tests/test_scaling.py
index 50f43c5..9b757cf 100644
--- a/tests/test_scaling.py
+++ b/tests/test_scaling.py
@@ -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',
@@ -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',
@@ -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)
diff --git a/tests/test_skip_regions.py b/tests/test_skip_regions.py
new file mode 100644
index 0000000..137191e
--- /dev/null
+++ b/tests/test_skip_regions.py
@@ -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