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 @@ ![memory map diagram](report.png) |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