Merge changes from topic "map-summary" into integration

* changes:
  feat(memmap): add summary command to memory map script
  refactor(memmap)!: change behavioural flags to commands
  refactor(memmap): fix typing-related issues
  refactor(memmap): create common image parser interface
  refactor(memmap): represent image symbols with key-value map
  refactor(memmap): factor memory regions into their own structure
  refactor(memmap): apply additional type hints
  refactor(memmap): check ELF object type rather than flag
  refactor(memmap): remove unused functionality
  fix(memmap): ensure terminal width is respected
  style(memmap): format with Ruff
diff --git a/Makefile b/Makefile
index cbf0a8a..2d85b72 100644
--- a/Makefile
+++ b/Makefile
@@ -1832,7 +1832,7 @@
 
 memmap: all
 	$(if $(host-poetry),$(q)poetry -q install --no-root)
-	$(q)$(if $(host-poetry),poetry run )memory -sr ${BUILD_PLAT}
+	$(q)$(if $(host-poetry),poetry run )memory symbols --root ${BUILD_PLAT}
 
 tl: ${BUILD_PLAT}/tl.bin
 ${BUILD_PLAT}/tl.bin: ${HW_CONFIG}
diff --git a/docs/license.rst b/docs/license.rst
index e35b9bb..05458b9 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -119,6 +119,15 @@
    -  ``include/lib/hob/mmram.h``
    -  ``include/lib/hob/mpinfo.h``
 
+- Some source files originating from the `mbed OS`_ project.
+  These files are licensed under the Apache License, Version 2.0, which is a
+  permissive license compatible with BSD-3-Clause. Any contributions to this
+  code must also be made under the terms of `Apache License 2.0`_.
+  These files are:
+
+   -  ``tools/memory/memory/mapsummary.py``
+   -  ``tools/memory/memory/mapsummary_flamegraph.hmtl``
+
 .. _FreeBSD: http://www.freebsd.org
 .. _Linux MIT license: https://raw.githubusercontent.com/torvalds/linux/master/LICENSES/preferred/MIT
 .. _SCC: http://www.simple-cc.org/
@@ -126,3 +135,4 @@
 .. _Apache License 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
 .. _pydevicetree: https://pypi.org/project/pydevicetree/
 .. _edk2: https://github.com/tianocore/edk2
+.. _mbed OS: https://github.com/ARMmbed/mbed-os/
diff --git a/docs/tools/memory-layout-tool.rst b/docs/tools/memory-layout-tool.rst
index d9c358d..0a447c5 100644
--- a/docs/tools/memory-layout-tool.rst
+++ b/docs/tools/memory-layout-tool.rst
@@ -41,7 +41,7 @@
 
 .. code:: shell
 
-    $ poetry run memory -s
+    $ poetry run memory symbols
     build-path: build/fvp/release
     Virtual Address Map:
                +------------__BL1_RAM_END__------------+---------------------------------------+
@@ -116,14 +116,14 @@
 Memory Footprint
 ~~~~~~~~~~~~~~~~
 
-The tool enables users to view static memory consumption. When the options
-``-f``, or ``--footprint`` are provided, the script analyses the ELF binaries in
-the build path to generate a table (per memory type), showing memory allocation
-and usage. This is the default output generated by the tool.
+The tool enables users to view static memory consumption. When the ``footprint``
+command is provided, the script analyses the ELF binaries in the build path to
+generate a table (per memory type), showing memory allocation and usage. This is
+the default output generated by the tool.
 
 .. code:: shell
 
-    $ poetry run memory -f
+    $ poetry run memory footprint
     build-path: build/fvp/release
     +----------------------------------------------------------------------------+
     |                         Memory Usage (bytes) [RAM]                         |
@@ -150,13 +150,13 @@
 Memory Tree
 ~~~~~~~~~~~
 
-A hierarchical view of the memory layout can be produced by passing the option
-``-t`` or ``--tree`` to the tool. This gives the start, end, and size of each
-module, their ELF segments as well as sections.
+A hierarchical view of the memory layout can be produced by passing the ``tree``
+command to the tool. This gives the start, end, and size of each module, their
+ELF segments as well as sections.
 
 .. code:: shell
 
-    $ poetry run memory -t
+    $ poetry run memory tree
     build-path: build/fvp/release
     name                                       start        end       size
     bl1                                            0    400c000    400c000
@@ -209,7 +209,7 @@
 
 .. code::
 
-    $ poetry run memory -t --depth 2
+    $ poetry run memory tree --depth 2
     build-path: build/fvp/release
     name                          start        end       size
     bl1                               0    400c000    400c000
@@ -229,6 +229,169 @@
     ├── 00                      4003000    4010000       d000
     └── 01                      4010000    4021000      11000
 
+Memory Summary
+~~~~~~~~~~~~~~
+
+The tool provides a by-translation-unit summary of the sizes (``text``, ``bss``,
+``data``) contributed by each translation unit or group of translation units.
+For example, to print a table of an FVP build, with a path depth of 3:
+
+.. code::
+
+   $ poetry run memory summary build/fvp/debug/bl1/bl1.map  -d 3
+    | Module                                 |         .text |       .data |          .bss |
+    |----------------------------------------|---------------|-------------|---------------|
+    | [fill]                                 |   3204(+3204) |       0(+0) |       97(+97) |
+    | bl1/aem_generic.o                      |         0(+0) |       0(+0) |         0(+0) |
+    | bl1/arm_bl1_fwu.o                      |     224(+224) |     80(+80) |         0(+0) |
+    | bl1/arm_bl1_setup.o                    |     608(+608) |       0(+0) |       17(+17) |
+    | bl1/arm_common.o                       |     116(+116) |       0(+0) |         0(+0) |
+    | bl1/arm_console.o                      |     116(+116) |       0(+0) |       40(+40) |
+    | bl1/arm_dev_rotpk.o                    |         0(+0) |       0(+0) |         0(+0) |
+    | bl1/arm_dyn_cfg.o                      |     276(+276) |       0(+0) |   7184(+7184) |
+    | bl1/arm_dyn_cfg_helpers.o              |     364(+364) |       0(+0) |         0(+0) |
+    | bl1/arm_err.o                          |       12(+12) |       0(+0) |         0(+0) |
+    | bl1/arm_fconf_io.o                     |         0(+0) |   952(+952) |         0(+0) |
+    | bl1/arm_helpers.o                      |       44(+44) |       0(+0) |         0(+0) |
+    | bl1/arm_io_storage.o                   |     480(+480) |       0(+0) |       32(+32) |
+    | bl1/auth_mod.o                         |   1288(+1288) |       0(+0) |         0(+0) |
+    | bl1/backtrace.o                        |     444(+444) |       0(+0) |         0(+0) |
+    | bl1/bl1_arch_setup.o                   |       16(+16) |       0(+0) |         0(+0) |
+    | bl1/bl1_context_mgmt.o                 |     340(+340) |       0(+0) |   1392(+1392) |
+    | bl1/bl1_entrypoint.o                   |     236(+236) |       0(+0) |         0(+0) |
+    | bl1/bl1_exceptions.o                   |   2240(+2240) |       0(+0) |         0(+0) |
+    | bl1/bl1_fwu.o                          |   2188(+2188) |     44(+44) |         0(+0) |
+    | bl1/bl1_main.o                         |     620(+620) |       0(+0) |         0(+0) |
+    | bl1/bl_common.o                        |     772(+772) |       0(+0) |         4(+4) |
+    | bl1/board_arm_helpers.o                |       44(+44) |       0(+0) |         0(+0) |
+    | bl1/board_arm_trusted_boot.o           |       44(+44) |     16(+16) |         0(+0) |
+    | bl1/cache_helpers.o                    |     112(+112) |       0(+0) |         0(+0) |
+    | bl1/cci.o                              |     408(+408) |       0(+0) |       24(+24) |
+    | bl1/context.o                          |     348(+348) |       0(+0) |         0(+0) |
+    | bl1/context_mgmt.o                     |   1692(+1692) |       0(+0) |       48(+48) |
+    | bl1/cortex_a35.o                       |       96(+96) |       0(+0) |         0(+0) |
+    | bl1/cortex_a53.o                       |     248(+248) |       0(+0) |         0(+0) |
+    | bl1/cortex_a57.o                       |     384(+384) |       0(+0) |         0(+0) |
+    | bl1/cortex_a72.o                       |     356(+356) |       0(+0) |         0(+0) |
+    | bl1/cortex_a73.o                       |     304(+304) |       0(+0) |         0(+0) |
+    | bl1/cpu_helpers.o                      |     200(+200) |       0(+0) |         0(+0) |
+    | bl1/crypto_mod.o                       |     380(+380) |       0(+0) |         0(+0) |
+    | bl1/debug.o                            |     224(+224) |       0(+0) |         0(+0) |
+    | bl1/delay_timer.o                      |       64(+64) |       0(+0) |         8(+8) |
+    | bl1/enable_mmu.o                       |     112(+112) |       0(+0) |         0(+0) |
+    | bl1/errata_report.o                    |     564(+564) |       0(+0) |         0(+0) |
+    | bl1/fconf.o                            |     148(+148) |       0(+0) |         0(+0) |
+    | bl1/fconf_dyn_cfg_getter.o             |     656(+656) |     32(+32) |     144(+144) |
+    | bl1/fconf_tbbr_getter.o                |     332(+332) |       0(+0) |       24(+24) |
+    | bl1/fdt_wrappers.o                     |     452(+452) |       0(+0) |         0(+0) |
+    | bl1/fvp_bl1_setup.o                    |     168(+168) |       0(+0) |         0(+0) |
+    | bl1/fvp_common.o                       |     512(+512) |       0(+0) |         8(+8) |
+    | bl1/fvp_cpu_pwr.o                      |     136(+136) |       0(+0) |         0(+0) |
+    | bl1/fvp_err.o                          |       44(+44) |       0(+0) |         0(+0) |
+    | bl1/fvp_helpers.o                      |     148(+148) |       0(+0) |         0(+0) |
+    | bl1/fvp_io_storage.o                   |     228(+228) |       0(+0) |       16(+16) |
+    | bl1/fvp_trusted_boot.o                 |     292(+292) |       0(+0) |         0(+0) |
+    | bl1/generic_delay_timer.o              |     136(+136) |       0(+0) |       16(+16) |
+    | bl1/img_parser_mod.o                   |     588(+588) |       0(+0) |       20(+20) |
+    | bl1/io_fip.o                           |   1332(+1332) |       0(+0) |     100(+100) |
+    | bl1/io_memmap.o                        |     736(+736) |     16(+16) |       32(+32) |
+    | bl1/io_semihosting.o                   |     648(+648) |     16(+16) |         0(+0) |
+    | bl1/io_storage.o                       |   1268(+1268) |       0(+0) |     104(+104) |
+    | bl1/mbedtls_common.o                   |     208(+208) |       0(+0) |         4(+4) |
+    | bl1/mbedtls_crypto.o                   |     636(+636) |       0(+0) |         0(+0) |
+    | bl1/mbedtls_x509_parser.o              |   1588(+1588) |       0(+0) |     120(+120) |
+    | bl1/misc_helpers.o                     |     392(+392) |       0(+0) |         0(+0) |
+    | bl1/multi_console.o                    |     528(+528) |       1(+1) |         8(+8) |
+    | bl1/pl011_console.o                    |     308(+308) |       0(+0) |         0(+0) |
+    | bl1/plat_bl1_common.o                  |     208(+208) |       0(+0) |         0(+0) |
+    | bl1/plat_bl_common.o                   |       40(+40) |       0(+0) |         0(+0) |
+    | bl1/plat_common.o                      |       48(+48) |       0(+0) |         8(+8) |
+    | bl1/plat_log_common.o                  |       48(+48) |       0(+0) |         0(+0) |
+    | bl1/plat_tbbr.o                        |     128(+128) |       0(+0) |         0(+0) |
+    | bl1/platform_helpers.o                 |       12(+12) |       0(+0) |         0(+0) |
+    | bl1/platform_up_stack.o                |       16(+16) |       0(+0) |         0(+0) |
+    | bl1/semihosting.o                      |     352(+352) |       0(+0) |         0(+0) |
+    | bl1/semihosting_call.o                 |         8(+8) |       0(+0) |         0(+0) |
+    | bl1/smmu_v3.o                          |     296(+296) |       0(+0) |         0(+0) |
+    | bl1/sp805.o                            |       64(+64) |       0(+0) |         0(+0) |
+    | bl1/tbbr_cot_bl1.o                     |         0(+0) |     48(+48) |     156(+156) |
+    | bl1/tbbr_cot_common.o                  |         0(+0) |   144(+144) |     306(+306) |
+    | bl1/tbbr_img_desc.o                    |         0(+0) |   768(+768) |         0(+0) |
+    | bl1/tf_log.o                           |     200(+200) |       4(+4) |         0(+0) |
+    | bl1/xlat_tables_arch.o                 |     736(+736) |       0(+0) |         0(+0) |
+    | bl1/xlat_tables_context.o              |     192(+192) |     96(+96) |   1296(+1296) |
+    | bl1/xlat_tables_core.o                 |   2112(+2112) |       0(+0) |         0(+0) |
+    | bl1/xlat_tables_utils.o                |         8(+8) |       0(+0) |         0(+0) |
+    | lib/libc.a/assert.o                    |       48(+48) |       0(+0) |         0(+0) |
+    | lib/libc.a/exit.o                      |       64(+64) |       0(+0) |         8(+8) |
+    | lib/libc.a/memchr.o                    |       44(+44) |       0(+0) |         0(+0) |
+    | lib/libc.a/memcmp.o                    |       52(+52) |       0(+0) |         0(+0) |
+    | lib/libc.a/memcpy.o                    |       32(+32) |       0(+0) |         0(+0) |
+    | lib/libc.a/memmove.o                   |       52(+52) |       0(+0) |         0(+0) |
+    | lib/libc.a/memset.o                    |     140(+140) |       0(+0) |         0(+0) |
+    | lib/libc.a/printf.o                    |   1532(+1532) |       0(+0) |         0(+0) |
+    | lib/libc.a/snprintf.o                  |   1748(+1748) |       0(+0) |         0(+0) |
+    | lib/libc.a/strcmp.o                    |       44(+44) |       0(+0) |         0(+0) |
+    | lib/libc.a/strlen.o                    |       28(+28) |       0(+0) |         0(+0) |
+    | lib/libfdt.a/fdt.o                     |   1460(+1460) |       0(+0) |         0(+0) |
+    | lib/libfdt.a/fdt_ro.o                  |   1392(+1392) |       0(+0) |         0(+0) |
+    | lib/libfdt.a/fdt_wip.o                 |     244(+244) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/asn1parse.o           |     956(+956) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/bignum.o              |   6796(+6796) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/bignum_core.o         |   3252(+3252) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/constant_time.o       |     280(+280) |       0(+0) |         8(+8) |
+    | lib/libmbedtls.a/md.o                  |     504(+504) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/memory_buffer_alloc.o |   1264(+1264) |       0(+0) |       40(+40) |
+    | lib/libmbedtls.a/oid.o                 |     752(+752) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/pk.o                  |     872(+872) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/pk_wrap.o             |     848(+848) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/pkparse.o             |     516(+516) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/platform.o            |       92(+92) |     24(+24) |         0(+0) |
+    | lib/libmbedtls.a/platform_util.o       |       96(+96) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/rsa.o                 |   6588(+6588) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/rsa_alt_helpers.o     |   2340(+2340) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/sha256.o              |   1448(+1448) |       0(+0) |         0(+0) |
+    | lib/libmbedtls.a/x509.o                |   1028(+1028) |       0(+0) |         0(+0) |
+    | Subtotals                              | 69632(+69632) | 2241(+2241) | 11264(+11264) |
+    Total Static RAM memory (data + bss): 13505(+13505) bytes
+    Total Flash memory (text + data): 71873(+71873) bytes
+
+A delta between two images can be generated by passing the ``--old`` option with
+a path to the previous map file.
+
+For example:
+
+.. code::
+
+    $ poetry run memory summary ../maps/fvp-tbb-mbedtls/bl1.map --old ../maps/fvp-tbb-mbedtls/bl1.map.old -d 1
+    | Module    |         .text |    .data |         .bss |
+    |-----------|---------------|----------|--------------|
+    | [fill]    |    780(-2424) |    0(+0) |    321(+224) |
+    | bl1       |   32024(+108) | 2217(+0) |    11111(+0) |
+    | lib       | 45020(+10508) |   24(+0) |  1880(+1824) |
+    | Subtotals |  77824(+8192) | 2241(+0) | 13312(+2048) |
+    Total Static RAM memory (data + bss): 15553(+2048) bytes
+    Total Flash memory (text + data): 80065(+8192) bytes
+
+Note that since the old map file includes the required suffix, specifying the
+``--old`` argument is optional here.
+
+Under some circumstances, some executables are padded to meet certain
+alignments, such as a 4KB page boundary, and excluding that padding can provide
+more helpful diffs. Taking the last example, and adding the ``-e`` argument
+yields such a summary:
+
+.. code::
+
+    $ poetry run memory summary ../maps/fvp-tbb-mbedtls/bl1.map --old ../maps/fvp-tbb-mbedtls/bl1.map.old  -d 1 -e
+    | Module    |         .text |    .data |         .bss |
+    |-----------|---------------|----------|--------------|
+    | bl1       |   32024(+108) | 2217(+0) |    11111(+0) |
+    | lib       | 45020(+10508) |   24(+0) |  1880(+1824) |
+    | Subtotals | 77044(+10616) | 2241(+0) | 12991(+1824) |
+    Total Static RAM memory (data + bss): 15232(+1824) bytes
+    Total Flash memory (text + data): 79285(+10616) bytes
+
 --------------
 
 *Copyright (c) 2023-2025, Arm Limited. All rights reserved.*
diff --git a/tools/memory/poetry.lock b/tools/memory/poetry.lock
index 2747479..67641ee 100644
--- a/tools/memory/poetry.lock
+++ b/tools/memory/poetry.lock
@@ -40,6 +40,103 @@
 ]
 
 [[package]]
+name = "jinja2"
+version = "3.1.6"
+description = "A very fast and expressive template engine."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
+    {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
+]
+
+[package.dependencies]
+MarkupSafe = ">=2.0"
+
+[package.extras]
+i18n = ["Babel (>=2.7)"]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+description = "Safely add untrusted strings to HTML/XML markup."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
+    {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
+    {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
+    {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
+    {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
+    {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
+    {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
+    {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+    {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+    {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
+]
+
+[[package]]
 name = "prettytable"
 version = "3.11.0"
 description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format"
@@ -68,6 +165,53 @@
 ]
 
 [[package]]
+name = "pyright"
+version = "1.1.399"
+description = "Command line wrapper for pyright"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b"},
+    {file = "pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b"},
+]
+
+[package.dependencies]
+nodeenv = ">=1.6.0"
+typing-extensions = ">=4.1"
+
+[package.extras]
+all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"]
+dev = ["twine (>=3.4.1)"]
+nodejs = ["nodejs-wheel-binaries"]
+
+[[package]]
+name = "ruff"
+version = "0.11.2"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477"},
+    {file = "ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272"},
+    {file = "ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147"},
+    {file = "ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b"},
+    {file = "ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9"},
+    {file = "ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab"},
+    {file = "ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630"},
+    {file = "ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f"},
+    {file = "ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc"},
+    {file = "ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080"},
+    {file = "ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4"},
+    {file = "ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94"},
+]
+
+[[package]]
 name = "six"
 version = "1.17.0"
 description = "Python 2 and 3 compatibility utilities"
@@ -79,6 +223,17 @@
 ]
 
 [[package]]
+name = "typing-extensions"
+version = "4.13.2"
+description = "Backported and Experimental Type Hints for Python 3.8+"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
+    {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
+]
+
+[[package]]
 name = "wcwidth"
 version = "0.2.13"
 description = "Measures the displayed width of unicode strings in a terminal"
@@ -92,4 +247,4 @@
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8.0"
-content-hash = "d7c185b3dbfc9bba145f12146e18ce501caf081d7762f138bc5a7fde99f40543"
+content-hash = "72f05cdcfe5278c3fb4408ba76cc502c83a56615681d8307bf67fe759a9da442"
diff --git a/tools/memory/pyproject.toml b/tools/memory/pyproject.toml
index c2fdfcb..70d3de7 100644
--- a/tools/memory/pyproject.toml
+++ b/tools/memory/pyproject.toml
@@ -12,10 +12,18 @@
 prettytable = "^3.5.0"
 pyelftools = "^0.29.0"
 python = "^3.8.0"
+jinja2 = "^3.1.6"
 
 [tool.poetry.scripts]
 memory = "memory.memmap:main"
 
+[tool.poetry.group.dev]
+optional = true
+
+[tool.poetry.group.dev.dependencies]
+ruff = "^0.11.2"
+pyright = "^1.1.399"
+
 [build-system]
 requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"
diff --git a/tools/memory/src/memory/buildparser.py b/tools/memory/src/memory/buildparser.py
deleted file mode 100755
index ea417e1..0000000
--- a/tools/memory/src/memory/buildparser.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#
-# Copyright (c) 2023-2025, Arm Limited. All rights reserved.
-#
-# SPDX-License-Identifier: BSD-3-Clause
-#
-
-import re
-from pathlib import Path
-
-from memory.elfparser import TfaElfParser
-from memory.mapparser import TfaMapParser
-
-
-class TfaBuildParser:
-    """A class for performing analysis on the memory layout of a TF-A build."""
-
-    def __init__(self, path: Path, map_backend=False):
-        self._modules = dict()
-        self._path = path
-        self.map_backend = map_backend
-        self._parse_modules()
-
-    def __getitem__(self, module: str):
-        """Returns an TfaElfParser instance indexed by module."""
-        return self._modules[module]
-
-    def _parse_modules(self):
-        """Parse the build files using the selected backend."""
-        backend = TfaElfParser
-        files = list(self._path.glob("**/*.elf"))
-        io_perms = "rb"
-
-        if self.map_backend or len(files) == 0:
-            backend = TfaMapParser
-            files = self._path.glob("**/*.map")
-            io_perms = "r"
-
-        for file in files:
-            module_name = file.name.split("/")[-1].split(".")[0]
-            with open(file, io_perms) as f:
-                self._modules[module_name] = backend(f)
-
-        if not len(self._modules):
-            raise FileNotFoundError(
-                f"failed to find files to analyse in path {self._path}!"
-            )
-
-    @property
-    def symbols(self) -> list:
-        return [
-            (*sym, k) for k, v in self._modules.items() for sym in v.symbols
-        ]
-
-    @staticmethod
-    def filter_symbols(symbols: list, regex: str = None) -> list:
-        """Returns a map of symbols to modules."""
-        regex = r".*" if not regex else regex
-        return sorted(
-            filter(lambda s: re.match(regex, s[0]), symbols),
-            key=lambda s: (-s[1], s[0]),
-            reverse=True,
-        )
-
-    def get_mem_usage_dict(self) -> dict:
-        """Returns map of memory usage per memory type for each module."""
-        mem_map = {}
-        for k, v in self._modules.items():
-            mod_mem_map = v.get_memory_layout()
-            if len(mod_mem_map):
-                mem_map[k] = mod_mem_map
-        return mem_map
-
-    def get_mem_tree_as_dict(self) -> dict:
-        """Returns _tree of modules, segments and segments and their total
-        memory usage."""
-        return {
-            k: {
-                "name": k,
-                **v.get_mod_mem_usage_dict(),
-                **{"children": v.get_seg_map_as_dict()},
-            }
-            for k, v in self._modules.items()
-        }
-
-    @property
-    def module_names(self):
-        """Returns sorted list of module names."""
-        return sorted(self._modules.keys())
diff --git a/tools/memory/src/memory/elfparser.py b/tools/memory/src/memory/elfparser.py
index e6581c9..019d6da 100644
--- a/tools/memory/src/memory/elfparser.py
+++ b/tools/memory/src/memory/elfparser.py
@@ -6,9 +6,22 @@
 
 import re
 from dataclasses import asdict, dataclass
-from typing import BinaryIO
+from typing import (
+    Any,
+    BinaryIO,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Tuple,
+    Union,
+)
 
 from elftools.elf.elffile import ELFFile
+from elftools.elf.sections import Section, SymbolTableSection
+from elftools.elf.segments import Segment
+
+from memory.image import Image, Region
 
 
 @dataclass(frozen=True)
@@ -17,10 +30,10 @@
     start: int
     end: int
     size: int
-    children: list
+    children: List["TfaMemObject"]
 
 
-class TfaElfParser:
+class TfaElfParser(Image):
     """A class representing an ELF file built for TF-A.
 
     Provides a basic interface for reading the symbol table and other
@@ -28,53 +41,70 @@
     the contents an ELF file.
     """
 
-    def __init__(self, elf_file: BinaryIO):
-        self._segments = {}
-        self._memory_layout = {}
+    def __init__(self, elf_file: BinaryIO) -> None:
+        self._segments: Dict[int, TfaMemObject] = {}
+        self._memory_layout: Dict[str, Dict[str, int]] = {}
 
         elf = ELFFile(elf_file)
 
-        self._symbols = {
-            sym.name: sym.entry["st_value"]
-            for sym in elf.get_section_by_name(".symtab").iter_symbols()
+        symtab = elf.get_section_by_name(".symtab")
+        assert isinstance(symtab, SymbolTableSection)
+
+        self._symbols: Dict[str, int] = {
+            sym.name: sym.entry["st_value"] for sym in symtab.iter_symbols()
         }
 
         self.set_segment_section_map(elf.iter_segments(), elf.iter_sections())
         self._memory_layout = self.get_memory_layout_from_symbols()
-        self._start = elf["e_entry"]
+        self._start: int = elf["e_entry"]
+        self._size: int
+        self._free: int
         self._size, self._free = self._get_mem_usage()
-        self._end = self._start + self._size
+        self._end: int = self._start + self._size
+
+        self._footprint: Dict[str, Region] = {}
+
+        for mem, attrs in self._memory_layout.items():
+            self._footprint[mem] = Region(
+                attrs["start"],
+                attrs["end"],
+                attrs["length"],
+            )
 
     @property
-    def symbols(self):
-        return self._symbols.items()
+    def symbols(self) -> Dict[str, int]:
+        return self._symbols
 
     @staticmethod
-    def tfa_mem_obj_factory(elf_obj, name=None, children=None, segment=False):
+    def tfa_mem_obj_factory(
+        elf_obj: Union[Segment, Section],
+        name: Optional[str] = None,
+        children: Optional[List[TfaMemObject]] = None,
+    ) -> TfaMemObject:
         """Converts a pyelfparser Segment or Section to a TfaMemObject."""
         # Ensure each segment is provided a name since they aren't in the
         # program header.
-        assert not (
-            segment and name is None
-        ), "Attempting to make segment without a name"
-
-        if children is None:
-            children = list()
+        assert not (isinstance(elf_obj, Segment) and name is None), (
+            "Attempting to make segment without a name"
+        )
 
         # Segment and sections header keys have different prefixes.
-        vaddr = "p_vaddr" if segment else "sh_addr"
-        size = "p_memsz" if segment else "sh_size"
+        vaddr = "p_vaddr" if isinstance(elf_obj, Segment) else "sh_addr"
+        size = "p_memsz" if isinstance(elf_obj, Segment) else "sh_size"
+
+        name = name if isinstance(elf_obj, Segment) else elf_obj.name
+        assert name is not None
 
         # TODO figure out how to handle free space for sections and segments
         return TfaMemObject(
-            name if segment else elf_obj.name,
+            name,
             elf_obj[vaddr],
             elf_obj[vaddr] + elf_obj[size],
             elf_obj[size],
-            [] if not children else children,
+            children or [],
         )
 
-    def _get_mem_usage(self) -> (int, int):
+    def _get_mem_usage(self) -> Tuple[int, int]:
         """Get total size and free space for this component."""
         size = free = 0
 
@@ -89,36 +119,37 @@
 
         return size, free
 
-    def set_segment_section_map(self, segments, sections):
+    def set_segment_section_map(
+        self,
+        segments: Iterable[Segment],
+        sections: Iterable[Section],
+    ) -> None:
         """Set segment to section mappings."""
-        segments = list(
-            filter(lambda seg: seg["p_type"] == "PT_LOAD", segments)
-        )
+        segments = filter(lambda seg: seg["p_type"] == "PT_LOAD", segments)
+        segments_list = list(segments)
 
         for sec in sections:
-            for n, seg in enumerate(segments):
+            for n, seg in enumerate(segments_list):
                 if seg.section_in_segment(sec):
-                    if n not in self._segments.keys():
+                    if n not in self._segments:
                         self._segments[n] = self.tfa_mem_obj_factory(
-                            seg, name=f"{n:#02}", segment=True
+                            seg, name=f"{n:#02}"
                         )
 
-                    self._segments[n].children.append(
-                        self.tfa_mem_obj_factory(sec)
-                    )
+                    self._segments[n].children.append(self.tfa_mem_obj_factory(sec))
 
-    def get_memory_layout_from_symbols(self, expr=None) -> dict:
+    def get_memory_layout_from_symbols(self) -> Dict[str, Dict[str, int]]:
         """Retrieve information about the memory configuration from the symbol
         table.
         """
-        assert len(self._symbols), "Symbol table is empty!"
+        assert self._symbols, "Symbol table is empty!"
 
-        expr = r".*(.?R.M)_REGION.*(START|END|LENGTH)" if not expr else expr
+        expr = r".*(.?R.M)_REGION.*(START|END|LENGTH)"
         region_symbols = filter(lambda s: re.match(expr, s), self._symbols)
-        memory_layout = {}
+        memory_layout: Dict[str, Dict[str, int]] = {}
 
         for symbol in region_symbols:
-            region, _, attr = tuple(symbol.lower().strip("__").split("_"))
+            region, _, attr = symbol.lower().strip("__").split("_")
             if region not in memory_layout:
                 memory_layout[region] = {}
 
@@ -127,29 +158,30 @@
 
         return memory_layout
 
-    def get_seg_map_as_dict(self):
+    def get_seg_map_as_dict(self) -> List[Dict[str, Any]]:
         """Get a dictionary of segments and their section mappings."""
-        return [asdict(v) for k, v in self._segments.items()]
+        return [asdict(segment) for segment in self._segments.values()]
 
-    def get_memory_layout(self):
+    def get_memory_layout(self) -> Dict[str, Region]:
         """Get the total memory consumed by this module from the memory
         configuration.
-            {"rom": {"start": 0x0, "end": 0xFF, "length": ... }
         """
-        mem_dict = {}
+        mem_dict: Dict[str, Region] = {}
 
         for mem, attrs in self._memory_layout.items():
-            limit = attrs["start"] + attrs["length"]
-            mem_dict[mem] = {
-                "start": attrs["start"],
-                "limit": limit,
-                "size": attrs["end"] - attrs["start"],
-                "free": limit - attrs["end"],
-                "total": attrs["length"],
-            }
+            mem_dict[mem] = Region(
+                attrs["start"],
+                attrs["end"],
+                attrs["length"],
+            )
+
         return mem_dict
 
-    def get_mod_mem_usage_dict(self):
+    @property
+    def footprint(self) -> Dict[str, Region]:
+        return self._footprint
+
+    def get_mod_mem_usage_dict(self) -> Dict[str, int]:
         """Get the total memory consumed by the module, this combines the
         information in the memory configuration.
         """
diff --git a/tools/memory/src/memory/image.py b/tools/memory/src/memory/image.py
new file mode 100644
index 0000000..cda1d8a
--- /dev/null
+++ b/tools/memory/src/memory/image.py
@@ -0,0 +1,77 @@
+#
+# Copyright (c) 2023-2025, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import Dict, Optional
+
+
+@dataclass
+class Region:
+    """Represents a memory region."""
+
+    start: Optional[int] = None
+    """Memory address of the beginning of the region."""
+
+    end: Optional[int] = None
+    """Memory address of the end of the region."""
+
+    length: Optional[int] = None
+    """Current size of the region in bytes."""
+
+    @property
+    def limit(self) -> Optional[int]:
+        """Largest possible end memory address of the region."""
+
+        if self.start is None:
+            return None
+
+        if self.length is None:
+            return None
+
+        return self.start + self.length
+
+    @property
+    def size(self) -> Optional[int]:
+        """Maximum possible size of the region in bytes."""
+
+        if self.end is None:
+            return None
+
+        if self.start is None:
+            return None
+
+        return self.end - self.start
+
+    @property
+    def free(self) -> Optional[int]:
+        """Number of bytes that the region is permitted to further expand."""
+
+        if self.limit is None:
+            return None
+
+        if self.end is None:
+            return None
+
+        return self.limit - self.end
+
+
+class Image(ABC):
+    """An image under analysis."""
+
+    @property
+    @abstractmethod
+    def footprint(self) -> Dict[str, Region]:
+        """Get metrics about the memory regions that this image occupies."""
+
+        pass
+
+    @property
+    @abstractmethod
+    def symbols(self) -> Dict[str, int]:
+        """Get a dictionary of the image's symbols and their corresponding addresses."""
+
+        pass
diff --git a/tools/memory/src/memory/mapparser.py b/tools/memory/src/memory/mapparser.py
index 1c28e71..24ee264 100644
--- a/tools/memory/src/memory/mapparser.py
+++ b/tools/memory/src/memory/mapparser.py
@@ -4,11 +4,14 @@
 # SPDX-License-Identifier: BSD-3-Clause
 #
 
+from collections import defaultdict
 from re import match, search
-from typing import TextIO
+from typing import Dict, TextIO
+
+from memory.image import Image, Region
 
 
-class TfaMapParser:
+class TfaMapParser(Image):
     """A class representing a map file built for TF-A.
 
     Provides a basic interface for reading the symbol table. The constructor
@@ -16,17 +19,31 @@
     are supported at this stage.
     """
 
-    def __init__(self, map_file: TextIO):
-        self._symbols = self.read_symbols(map_file)
+    def __init__(self, map_file: TextIO) -> None:
+        self._symbols: Dict[str, int] = self.read_symbols(map_file)
+        assert self._symbols, "Symbol table is empty!"
+
+        self._footprint: Dict[str, Region] = defaultdict(Region)
+
+        expr = r".*(.?R.M)_REGION.*(START|END|LENGTH)"
+        for symbol in filter(lambda s: match(expr, s), self._symbols):
+            region, _, attr = symbol.lower().strip("__").split("_")
+
+            if attr == "start":
+                self._footprint[region].start = self._symbols[symbol]
+            elif attr == "end":
+                self._footprint[region].end = self._symbols[symbol]
+            if attr == "length":
+                self._footprint[region].length = self._symbols[symbol]
 
     @property
-    def symbols(self):
-        return self._symbols.items()
+    def symbols(self) -> Dict[str, int]:
+        return self._symbols
 
     @staticmethod
-    def read_symbols(file: TextIO, pattern: str = None) -> dict:
-        pattern = r"\b(0x\w*)\s*(\w*)\s=" if not pattern else pattern
-        symbols = {}
+    def read_symbols(file: TextIO) -> Dict[str, int]:
+        pattern = r"\b(0x\w*)\s*(\w*)\s="
+        symbols: Dict[str, int] = {}
 
         for line in file.readlines():
             match = search(pattern, line)
@@ -37,39 +54,6 @@
 
         return symbols
 
-    def get_memory_layout(self) -> dict:
-        """Get the total memory consumed by this module from the memory
-        configuration.
-            {"rom": {"start": 0x0, "end": 0xFF, "length": ... }
-        """
-        assert len(self._symbols), "Symbol table is empty!"
-        expr = r".*(.?R.M)_REGION.*(START|END|LENGTH)"
-        memory_layout = {}
-
-        region_symbols = filter(lambda s: match(expr, s), self._symbols)
-
-        for symbol in region_symbols:
-            region, _, attr = tuple(symbol.lower().strip("__").split("_"))
-            if region not in memory_layout:
-                memory_layout[region] = {}
-
-            memory_layout[region][attr] = self._symbols[symbol]
-
-            if "start" and "length" and "end" in memory_layout[region]:
-                memory_layout[region]["limit"] = (
-                    memory_layout[region]["start"]
-                    + memory_layout[region]["length"]
-                )
-                memory_layout[region]["free"] = (
-                    memory_layout[region]["limit"]
-                    - memory_layout[region]["end"]
-                )
-                memory_layout[region]["total"] = memory_layout[region][
-                    "length"
-                ]
-                memory_layout[region]["size"] = (
-                    memory_layout[region]["end"]
-                    - memory_layout[region]["start"]
-                )
-
-        return memory_layout
+    @property
+    def footprint(self) -> Dict[str, Region]:
+        return self._footprint
diff --git a/tools/memory/src/memory/memmap.py b/tools/memory/src/memory/memmap.py
index f46db8c..e02010b 100755
--- a/tools/memory/src/memory/memmap.py
+++ b/tools/memory/src/memory/memmap.py
@@ -1,19 +1,32 @@
-#!/usr/bin/env python3
-
 #
 # Copyright (c) 2023-2025, Arm Limited. All rights reserved.
 #
 # SPDX-License-Identifier: BSD-3-Clause
 #
 
+import re
+import shutil
+from dataclasses import dataclass
 from pathlib import Path
+from typing import Any, Dict, List, Optional
 
 import click
-from memory.buildparser import TfaBuildParser
+
+from memory.elfparser import TfaElfParser
+from memory.image import Image
+from memory.mapparser import TfaMapParser
 from memory.printer import TfaPrettyPrinter
+from memory.summary import MapParser
 
 
-@click.command()
+@dataclass
+class Context:
+    build_path: Optional[Path] = None
+    printer: Optional[TfaPrettyPrinter] = None
+
+
+@click.group()
+@click.pass_obj
 @click.option(
     "-r",
     "--root",
@@ -36,75 +49,171 @@
     type=click.Choice(["debug", "release"], case_sensitive=False),
 )
 @click.option(
-    "-f",
-    "--footprint",
-    is_flag=True,
-    show_default=True,
-    help="Generate a high level view of memory usage by memory types.",
+    "-w",
+    "--width",
+    type=int,
+    default=shutil.get_terminal_size().columns,
+    help="Column width for printing.",
 )
 @click.option(
-    "-t",
-    "--tree",
-    is_flag=True,
-    help="Generate a hierarchical view of the modules, segments and sections.",
-)
-@click.option(
-    "--depth",
-    default=3,
-    help="Generate a virtual address map of important TF symbols.",
-)
-@click.option(
-    "-s",
-    "--symbols",
-    is_flag=True,
-    help="Generate a map of important TF symbols.",
-)
-@click.option("-w", "--width", type=int, envvar="COLUMNS")
-@click.option(
     "-d",
     is_flag=True,
     default=False,
     help="Display numbers in decimal base.",
 )
+def cli(
+    obj: Context,
+    root: Optional[Path],
+    platform: str,
+    build_type: str,
+    width: int,
+    d: bool,
+):
+    obj.build_path = root if root is not None else Path("build", platform, build_type)
+    click.echo(f"build-path: {obj.build_path.resolve()}")
+
+    obj.printer = TfaPrettyPrinter(columns=width, as_decimal=d)
+
+
+@cli.command()
+@click.pass_obj
 @click.option(
     "--no-elf-images",
     is_flag=True,
     help="Analyse the build's map files instead of ELF images.",
 )
-def main(
-    root: Path,
-    platform: str,
-    build_type: str,
-    footprint: str,
-    tree: bool,
-    symbols: bool,
-    depth: int,
-    width: int,
-    d: bool,
-    no_elf_images: bool,
-):
-    build_path = root if root else Path("build/", platform, build_type)
-    click.echo(f"build-path: {build_path.resolve()}")
+def footprint(obj: Context, no_elf_images: bool):
+    """Generate a high level view of memory usage by memory types."""
 
-    parser = TfaBuildParser(build_path, map_backend=no_elf_images)
-    printer = TfaPrettyPrinter(columns=width, as_decimal=d)
+    assert obj.build_path is not None
+    assert obj.printer is not None
 
-    if footprint or not (tree or symbols):
-        printer.print_footprint(parser.get_mem_usage_dict())
+    elf_image_paths: List[Path] = (
+        [] if no_elf_images else list(obj.build_path.glob("**/*.elf"))
+    )
 
-    if tree:
-        printer.print_mem_tree(
-            parser.get_mem_tree_as_dict(), parser.module_names, depth=depth
-        )
+    map_file_paths: List[Path] = (
+        [] if not no_elf_images else list(obj.build_path.glob("**/*.map"))
+    )
 
-    if symbols:
-        expr = (
-            r"(.*)(TEXT|BSS|RO|RODATA|STACKS|_OPS|PMF|XLAT|GOT|FCONF|RELA"
-            r"|R.M)(.*)(START|UNALIGNED|END)__$"
-        )
-        printer.print_symbol_table(
-            parser.filter_symbols(parser.symbols, expr), parser.module_names
-        )
+    images: Dict[str, Image] = dict()
+
+    for elf_image_path in elf_image_paths:
+        with open(elf_image_path, "rb") as elf_image_io:
+            images[elf_image_path.stem.upper()] = TfaElfParser(elf_image_io)
+
+    for map_file_path in map_file_paths:
+        with open(map_file_path, "r") as map_file_io:
+            images[map_file_path.stem.upper()] = TfaMapParser(map_file_io)
+
+    obj.printer.print_footprint({k: v.footprint for k, v in images.items()})
+
+
+@cli.command()
+@click.pass_obj
+@click.option(
+    "--depth",
+    default=3,
+    show_default=True,
+    help="Generate a virtual address map of important TF symbols.",
+)
+def tree(obj: Context, depth: int):
+    """Generate a hierarchical view of the modules, segments and sections."""
+
+    assert obj.build_path is not None
+    assert obj.printer is not None
+
+    paths: List[Path] = list(obj.build_path.glob("**/*.elf"))
+    images: Dict[str, TfaElfParser] = dict()
+
+    for path in paths:
+        with open(path, "rb") as io:
+            images[path.stem] = TfaElfParser(io)
+
+    mtree: Dict[str, Dict[str, Any]] = {
+        k: {
+            "name": k,
+            **v.get_mod_mem_usage_dict(),
+            **{"children": v.get_seg_map_as_dict()},
+        }
+        for k, v in images.items()
+    }
+
+    obj.printer.print_mem_tree(mtree, list(mtree.keys()), depth=depth)
+
+
+@cli.command()
+@click.pass_obj
+@click.option(
+    "--no-elf-images",
+    is_flag=True,
+    help="Analyse the build's map files instead of ELF images.",
+)
+def symbols(obj: Context, no_elf_images: bool):
+    """Generate a map of important TF symbols."""
+
+    assert obj.build_path is not None
+    assert obj.printer is not None
+
+    expr: str = (
+        r"(.*)(TEXT|BSS|RO|RODATA|STACKS|_OPS|PMF|XLAT|GOT|FCONF|RELA"
+        r"|R.M)(.*)(START|UNALIGNED|END)__$"
+    )
+
+    elf_image_paths: List[Path] = (
+        [] if no_elf_images else list(obj.build_path.glob("**/*.elf"))
+    )
+
+    map_file_paths: List[Path] = (
+        [] if not no_elf_images else list(obj.build_path.glob("**/*.map"))
+    )
+
+    images: Dict[str, Image] = dict()
+
+    for elf_image_path in elf_image_paths:
+        with open(elf_image_path, "rb") as elf_image_io:
+            images[elf_image_path.stem] = TfaElfParser(elf_image_io)
+
+    for map_file_path in map_file_paths:
+        with open(map_file_path, "r") as map_file_io:
+            images[map_file_path.stem] = TfaMapParser(map_file_io)
+
+    symbols = {k: v.symbols for k, v in images.items()}
+    symbols = {
+        image: {
+            symbol: symbol_value
+            for symbol, symbol_value in symbols.items()
+            if re.match(expr, symbol)
+        }
+        for image, symbols in symbols.items()
+    }
+
+    obj.printer.print_symbol_table(symbols, list(images.keys()))
+
+
+@cli.command()
+@click.option("-o", "--old", type=click.Path(exists=True))
+@click.option("-d", "--depth", type=int, default=2)
+@click.option("-e", "--exclude-fill")
+@click.option(
+    "-t",
+    "--type",
+    type=click.Choice(MapParser.export_formats, case_sensitive=False),
+    default="table",
+)
+@click.argument("file", type=click.Path(exists=True))
+def summary(file: Path, old: Optional[Path], depth: int, exclude_fill: bool, type: str):
+    """Summarize the sizes of translation units within the resulting binary"""
+    memap = MapParser()
+
+    if not memap.parse(file, old, exclude_fill):
+        exit(1)
+
+    memap.generate_output(type, depth)
+
+
+def main():
+    cli(obj=Context())
 
 
 if __name__ == "__main__":
diff --git a/tools/memory/src/memory/printer.py b/tools/memory/src/memory/printer.py
index f797139..6debf53 100755
--- a/tools/memory/src/memory/printer.py
+++ b/tools/memory/src/memory/printer.py
@@ -4,10 +4,14 @@
 # SPDX-License-Identifier: BSD-3-Clause
 #
 
+from typing import Any, Dict, List, Optional, Tuple
+
 from anytree import RenderTree
 from anytree.importer import DictImporter
 from prettytable import PrettyTable
 
+from memory.image import Region
+
 
 class TfaPrettyPrinter:
     """A class for printing the memory layout of ELF files.
@@ -17,19 +21,29 @@
     structured and consumed.
     """
 
-    def __init__(self, columns: int = None, as_decimal: bool = False):
-        self.term_size = columns if columns and columns > 120 else 120
-        self._tree = None
-        self._footprint = None
-        self._symbol_map = None
-        self.as_decimal = as_decimal
+    def __init__(self, columns: int, as_decimal: bool = False) -> None:
+        self.term_size: int = columns
+        self._tree: Optional[List[str]] = None
+        self._symbol_map: Optional[List[str]] = None
+        self.as_decimal: bool = as_decimal
 
-    def format_args(self, *args, width=10, fmt=None):
-        if not fmt and type(args[0]) is int:
+    def format_args(
+        self,
+        *args: Any,
+        width: int = 10,
+        fmt: Optional[str] = None,
+    ) -> List[str]:
+        if not fmt and isinstance(args[0], int):
             fmt = f">{width}x" if not self.as_decimal else f">{width}"
-        return [f"{arg:{fmt}}" if fmt else arg for arg in args]
+        return [f"{arg:{fmt}}" if fmt else str(arg) for arg in args]
 
-    def format_row(self, leading, *args, width=10, fmt=None):
+    def format_row(
+        self,
+        leading: str,
+        *args: Any,
+        width: int = 10,
+        fmt: Optional[str] = None,
+    ) -> str:
         formatted_args = self.format_args(*args, width=width, fmt=fmt)
         return leading + " ".join(formatted_args)
 
@@ -39,9 +53,9 @@
         section_name: str,
         rel_pos: int,
         columns: int,
-        width: int = None,
+        width: int,
         is_edge: bool = False,
-    ):
+    ) -> str:
         empty_col = "{:{}{}}"
 
         # Some symbols are longer than the column width, truncate them until
@@ -50,28 +64,26 @@
         if len_over > 0:
             section_name = section_name[len_over:-len_over]
 
-        sec_row = f"+{section_name:-^{width-1}}+"
+        sec_row = f"+{section_name:-^{width - 1}}+"
         sep, fill = ("+", "-") if is_edge else ("|", "")
 
         sec_row_l = empty_col.format(sep, fill + "<", width) * rel_pos
-        sec_row_r = empty_col.format(sep, fill + ">", width) * (
-            columns - rel_pos - 1
-        )
+        sec_row_r = empty_col.format(sep, fill + ">", width) * (columns - rel_pos - 1)
 
         return leading + sec_row_l + sec_row + sec_row_r
 
     def print_footprint(
-        self, app_mem_usage: dict, sort_key: str = None, fields: list = None
+        self,
+        app_mem_usage: Dict[str, Dict[str, Region]],
     ):
-        assert len(app_mem_usage), "Empty memory layout dictionary!"
-        if not fields:
-            fields = ["Component", "Start", "Limit", "Size", "Free", "Total"]
+        assert app_mem_usage, "Empty memory layout dictionary!"
 
-        sort_key = fields[0] if not sort_key else sort_key
+        fields = ["Component", "Start", "Limit", "Size", "Free", "Total"]
+        sort_key = fields[0]
 
         # Iterate through all the memory types, create a table for each
         # type, rows represent a single module.
-        for mem in sorted(set(k for _, v in app_mem_usage.items() for k in v)):
+        for mem in sorted({k for v in app_mem_usage.values() for k in v}):
             table = PrettyTable(
                 sortby=sort_key,
                 title=f"Memory Usage (bytes) [{mem.upper()}]",
@@ -79,13 +91,19 @@
             )
 
             for mod, vals in app_mem_usage.items():
-                if mem in vals.keys():
+                if mem in vals:
                     val = vals[mem]
                     table.add_row(
                         [
-                            mod.upper(),
+                            mod,
                             *self.format_args(
-                                *[val[k.lower()] for k in fields[1:]]
+                                *[
+                                    val.start if val.start is not None else "?",
+                                    val.limit if val.limit is not None else "?",
+                                    val.size if val.size is not None else "?",
+                                    val.free if val.free is not None else "?",
+                                    val.length if val.length is not None else "?",
+                                ]
                             ),
                         ]
                     )
@@ -93,31 +111,34 @@
 
     def print_symbol_table(
         self,
-        symbols: list,
-        modules: list,
+        symbol_table: Dict[str, Dict[str, int]],
+        modules: List[str],
         start: int = 12,
-    ):
-        assert len(symbols), "Empty symbol list!"
+    ) -> None:
+        assert len(symbol_table), "Empty symbol list!"
         modules = sorted(modules)
-        col_width = int((self.term_size - start) / len(modules))
+        col_width = (self.term_size - start) // len(modules)
         address_fixed_width = 11
 
-        num_fmt = (
-            f"0=#0{address_fixed_width}x" if not self.as_decimal else ">10"
-        )
+        num_fmt = f"0=#0{address_fixed_width}x" if not self.as_decimal else ">10"
 
         _symbol_map = [
-            " " * start
-            + "".join(self.format_args(*modules, fmt=f"^{col_width}"))
+            " " * start + "".join(self.format_args(*modules, fmt=f"^{col_width}"))
         ]
         last_addr = None
 
-        for i, (name, addr, mod) in enumerate(symbols):
+        symbols_list: List[Tuple[str, int, str]] = [
+            (name, addr, mod)
+            for mod, syms in symbol_table.items()
+            for name, addr in syms.items()
+        ]
+
+        symbols_list.sort(key=lambda x: (-x[1], x[0]), reverse=True)
+
+        for i, (name, addr, mod) in enumerate(symbols_list):
             # Do not print out an address twice if two symbols overlap,
             # for example, at the end of one region and start of another.
-            leading = (
-                f"{addr:{num_fmt}}" + " " if addr != last_addr else " " * start
-            )
+            leading = f"{addr:{num_fmt}}" + " " if addr != last_addr else " " * start
 
             _symbol_map.append(
                 self.map_elf_symbol(
@@ -125,28 +146,30 @@
                     name,
                     modules.index(mod),
                     len(modules),
-                    width=col_width,
-                    is_edge=(not i or i == len(symbols) - 1),
+                    col_width,
+                    is_edge=(i == 0 or i == len(symbols_list) - 1),
                 )
             )
 
             last_addr = addr
 
-        self._symbol_map = ["Memory Layout:"]
-        self._symbol_map += list(reversed(_symbol_map))
+        self._symbol_map = ["Memory Layout:"] + list(reversed(_symbol_map))
         print("\n".join(self._symbol_map))
 
     def print_mem_tree(
-        self, mem_map_dict, modules, depth=1, min_pad=12, node_right_pad=12
-    ):
+        self,
+        mem_map_dict: Dict[str, Any],
+        modules: List[str],
+        depth: int = 1,
+        min_pad: int = 12,
+        node_right_pad: int = 12,
+    ) -> None:
         # Start column should have some padding between itself and its data
         # values.
         anchor = min_pad + node_right_pad * (depth - 1)
         headers = ["start", "end", "size"]
 
-        self._tree = [
-            (f"{'name':<{anchor}}" + " ".join(f"{arg:>10}" for arg in headers))
-        ]
+        self._tree = [f"{'name':<{anchor}}" + " ".join(f"{arg:>10}" for arg in headers)]
 
         for mod in sorted(modules):
             root = DictImporter().import_(mem_map_dict[mod])
diff --git a/tools/memory/src/memory/summary.py b/tools/memory/src/memory/summary.py
new file mode 100644
index 0000000..b116caa
--- /dev/null
+++ b/tools/memory/src/memory/summary.py
@@ -0,0 +1,557 @@
+#
+# Copyright (c) 2016-2025, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+import json
+import os
+import re
+from collections import defaultdict
+from copy import deepcopy
+from os.path import (
+    abspath,
+    basename,
+    commonprefix,
+    dirname,
+    join,
+    relpath,
+    splitext,
+)
+from pathlib import Path
+from sys import stdout
+from typing import IO, Any, Dict, List, Optional, Pattern, Tuple, Union
+
+from jinja2 import FileSystemLoader, StrictUndefined
+from jinja2.environment import Environment
+from prettytable import HEADER, PrettyTable
+
+ModuleStats = Dict[str, int]
+Modules = Dict[str, ModuleStats]
+
+SECTIONS: Tuple[str, ...] = (".text", ".data", ".bss", ".heap", ".stack")
+MISC_FLASH_SECTIONS: Tuple[str, ...] = (".interrupts", ".flash_config")
+OTHER_SECTIONS: Tuple[str, ...] = (
+    ".interrupts_ram",
+    ".init",
+    ".ARM.extab",
+    ".ARM.exidx",
+    ".ARM.attributes",
+    ".eh_frame",
+    ".init_array",
+    ".fini_array",
+    ".jcr",
+    ".stab",
+    ".stabstr",
+    ".ARM.exidx",
+    ".ARM",
+)
+ALL_SECTIONS: Tuple[str, ...] = (
+    SECTIONS + OTHER_SECTIONS + MISC_FLASH_SECTIONS + ("unknown", "OUTPUT")
+)
+
+
+class Parser:
+    """Internal interface for parsing"""
+
+    _RE_OBJECT_FILE: Pattern[str] = re.compile(r"^(.+\/.+\.o(bj)?)$")
+    _RE_LIBRARY_OBJECT: Pattern[str] = re.compile(
+        r"((^.+" + r"" + r"lib.+\.a)\((.+\.o(bj)?)\))$"
+    )
+    _RE_STD_SECTION: Pattern[str] = re.compile(r"^\s+.*0x(\w{8,16})\s+0x(\w+)\s(.+)$")
+    _RE_FILL_SECTION: Pattern[str] = re.compile(
+        r"^\s*\*fill\*\s+0x(\w{8,16})\s+0x(\w+).*$"
+    )
+    _RE_TRANS_FILE: Pattern[str] = re.compile(r"^(.+\/|.+\.ltrans.o(bj)?)$")
+    _OBJECT_EXTENSIONS: Tuple[str, ...] = (".o", ".obj")
+
+    _modules: Modules
+    _fill: bool
+
+    def __init__(self, fill: bool = True):
+        self._modules: Modules = {}
+        self._fill = fill
+
+    def module_add(self, object_name: str, size: int, section: str):
+        """Adds a module or section to the list
+
+        Positional arguments:
+        object_name - name of the entry to add
+        size - the size of the module being added
+        section - the section the module contributes to
+        """
+        if (
+            not object_name
+            or not size
+            or not section
+            or (not self._fill and object_name == "[fill]")
+        ):
+            return
+
+        if object_name in self._modules:
+            self._modules[object_name].setdefault(section, 0)
+            self._modules[object_name][section] += size
+            return
+
+        obj_split = os.sep + basename(object_name)
+        for module_path, contents in self._modules.items():
+            if module_path.endswith(obj_split) or module_path == object_name:
+                contents.setdefault(section, 0)
+                contents[section] += size
+                return
+
+        new_module: ModuleStats = defaultdict(int)
+        new_module[section] = size
+        self._modules[object_name] = new_module
+
+    def module_replace(self, old_object: str, new_object: str):
+        """Replaces an object name with a new one"""
+        if old_object in self._modules:
+            self._modules[new_object] = self._modules.pop(old_object)
+
+    def check_new_section(self, line: str) -> Optional[str]:
+        """Check whether a new section in a map file has been detected
+
+        Positional arguments:
+        line - the line to check for a new section
+
+        return value - A section name, if a new section was found, None
+                       otherwise
+        """
+        line_s = line.strip()
+        for i in ALL_SECTIONS:
+            if line_s.startswith(i):
+                return i
+        if line.startswith("."):
+            return "unknown"
+        else:
+            return None
+
+    def parse_object_name(self, line: str) -> str:
+        """Parse a path to object file
+
+        Positional arguments:
+        line - the path to parse the object and module name from
+
+        return value - an object file name
+        """
+        if re.match(self._RE_TRANS_FILE, line):
+            return "[misc]"
+
+        test_re_file_name = re.match(self._RE_OBJECT_FILE, line)
+
+        if test_re_file_name:
+            object_name = test_re_file_name.group(1)
+
+            return object_name
+        else:
+            test_re_obj_name = re.match(self._RE_LIBRARY_OBJECT, line)
+
+            if test_re_obj_name:
+                return join(test_re_obj_name.group(2), test_re_obj_name.group(3))
+            else:
+                if not line.startswith("LONG") and not line.startswith("linker stubs"):
+                    print("Unknown object name found in GCC map file: %s" % line)
+                return "[misc]"
+
+    def parse_section(self, line: str) -> Tuple[str, int]:
+        """Parse data from a section of gcc map file
+
+        examples:
+                        0x00004308       0x7c ./BUILD/K64F/GCC_ARM/spi_api.o
+         .text          0x00000608      0x198 ./BUILD/K64F/HAL_CM4.o
+
+        Positional arguments:
+        line - the line to parse a section from
+        """
+        is_fill = re.match(self._RE_FILL_SECTION, line)
+        if is_fill:
+            o_name: str = "[fill]"
+            o_size: int = int(is_fill.group(2), 16)
+            return o_name, o_size
+
+        is_section = re.match(self._RE_STD_SECTION, line)
+        if is_section:
+            o_size: int = int(is_section.group(2), 16)
+            if o_size:
+                o_name: str = self.parse_object_name(is_section.group(3))
+                return o_name, o_size
+
+        return "", 0
+
+    def parse_mapfile(self, file_desc: IO[str]) -> Modules:
+        """Main logic to decode gcc map files
+
+        Positional arguments:
+        file_desc - a stream object to parse as a gcc map file
+        """
+        current_section: str = "unknown"
+
+        with file_desc as infile:
+            for line in infile:
+                if line.startswith("Linker script and memory map"):
+                    current_section = "unknown"
+                    break
+
+            for line in infile:
+                next_section = self.check_new_section(line)
+
+                if next_section == "OUTPUT":
+                    break
+                elif next_section:
+                    current_section = next_section
+
+                object_name, object_size = self.parse_section(line)
+                self.module_add(object_name, object_size, current_section)
+
+        def is_obj(name: str) -> bool:
+            return not name.startswith("[") or not name.endswith("]")
+
+        common_prefix: str = dirname(
+            commonprefix([o for o in self._modules.keys() if is_obj(o)])
+        )
+        new_modules: Modules = {}
+        for name, stats in self._modules.items():
+            if is_obj(name):
+                new_modules[relpath(name, common_prefix)] = stats
+            else:
+                new_modules[name] = stats
+        return new_modules
+
+
+class MapParser(object):
+    """An object that represents parsed results, parses the memory map files,
+    and writes out different file types of memory results
+    """
+
+    print_sections: Tuple[str, ...] = (".text", ".data", ".bss")
+    delta_sections: Tuple[str, ...] = (".text-delta", ".data-delta", ".bss-delta")
+
+    # sections to print info (generic for all toolchains)
+    sections: Tuple[str, ...] = SECTIONS
+    misc_flash_sections: Tuple[str, ...] = MISC_FLASH_SECTIONS
+    other_sections: Tuple[str, ...] = OTHER_SECTIONS
+
+    modules: Modules
+    old_modules: Modules
+    short_modules: Modules
+    mem_report: List[Dict[str, Union[str, ModuleStats]]]
+    mem_summary: Dict[str, int]
+    subtotal: Dict[str, int]
+    tc_name: Optional[str]
+
+    RAM_FORMAT_STR: str = "Total Static RAM memory (data + bss): {}({:+}) bytes\n"
+    ROM_FORMAT_STR: str = "Total Flash memory (text + data): {}({:+}) bytes\n"
+
+    def __init__(self):
+        # list of all modules and their sections
+        # full list - doesn't change with depth
+        self.modules: Modules = {}
+        self.old_modules = {}
+        # short version with specific depth
+        self.short_modules: Modules = {}
+
+        # Memory report (sections + summary)
+        self.mem_report: List[Dict[str, Union[str, ModuleStats]]] = []
+
+        # Memory summary
+        self.mem_summary: Dict[str, int] = {}
+
+        # Totals of ".text", ".data" and ".bss"
+        self.subtotal: Dict[str, int] = {}
+
+        # Name of the toolchain, for better headings
+        self.tc_name = None
+
+    def reduce_depth(self, depth: Optional[int]):
+        """
+        populates the short_modules attribute with a truncated module list
+
+        (1) depth = 1:
+        main.o
+        mbed-os
+
+        (2) depth = 2:
+        main.o
+        mbed-os/test.o
+        mbed-os/drivers
+
+        """
+        if depth == 0 or depth is None:
+            self.short_modules = deepcopy(self.modules)
+        else:
+            self.short_modules = dict()
+            for module_name, v in self.modules.items():
+                split_name = module_name.split(os.sep)
+                if split_name[0] == "":
+                    split_name = split_name[1:]
+                new_name = join(*split_name[:depth])
+                self.short_modules.setdefault(new_name, defaultdict(int))
+                for section_idx, value in v.items():
+                    self.short_modules[new_name][section_idx] += value
+                    delta_name = section_idx + "-delta"
+                    self.short_modules[new_name][delta_name] += value
+
+            for module_name, v in self.old_modules.items():
+                split_name = module_name.split(os.sep)
+                if split_name[0] == "":
+                    split_name = split_name[1:]
+                new_name = join(*split_name[:depth])
+                self.short_modules.setdefault(new_name, defaultdict(int))
+                for section_idx, value in v.items():
+                    delta_name = section_idx + "-delta"
+                    self.short_modules[new_name][delta_name] -= value
+
+    export_formats: List[str] = ["json", "html", "table"]
+
+    def generate_output(
+        self,
+        export_format: str,
+        depth: Optional[int],
+        file_output: Optional[str] = None,
+    ) -> Optional[bool]:
+        """Generates summary of memory map data
+
+        Positional arguments:
+        export_format - the format to dump
+
+        Keyword arguments:
+        file_desc - descriptor (either stdout or file)
+        depth - directory depth on report
+
+        Returns: generated string for the 'table' format, otherwise Nonef
+        """
+        if depth is None or depth > 0:
+            self.reduce_depth(depth)
+        self.compute_report()
+        try:
+            if file_output:
+                file_desc = open(file_output, "w")
+            else:
+                file_desc = stdout
+        except IOError as error:
+            print("I/O error({0}): {1}".format(error.errno, error.strerror))
+            return False
+
+        to_call = {
+            "json": self.generate_json,
+            "html": self.generate_html,
+            "table": self.generate_table,
+        }[export_format]
+        to_call(file_desc)
+
+        if file_desc is not stdout:
+            file_desc.close()
+
+    @staticmethod
+    def _move_up_tree(tree: Dict[str, Any], next_module: str) -> Dict[str, Any]:
+        tree.setdefault("children", [])
+        for child in tree["children"]:
+            if child["name"] == next_module:
+                return child
+
+        new_module = {"name": next_module, "value": 0, "delta": 0}
+        tree["children"].append(new_module)
+
+        return new_module
+
+    def generate_html(self, file_desc: IO[str]):
+        """Generate a json file from a memory map for D3
+
+        Positional arguments:
+        file_desc - the file to write out the final report to
+        """
+
+        tree_text = {"name": ".text", "value": 0, "delta": 0}
+        tree_bss = {"name": ".bss", "value": 0, "delta": 0}
+        tree_data = {"name": ".data", "value": 0, "delta": 0}
+
+        def accumulate(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
+            parts = module_name.split(os.sep)
+
+            val = stats.get(size_key, 0)
+            tree_root["value"] += val
+            tree_root["delta"] += val
+
+            cur = tree_root
+            for part in parts:
+                cur = self._move_up_tree(cur, part)
+                cur["value"] += val
+                cur["delta"] += val
+
+        def subtract(tree_root: Dict[str, Any], size_key: str, stats: ModuleStats):
+            parts = module_name.split(os.sep)
+
+            cur = tree_root
+            cur["delta"] -= stats.get(size_key, 0)
+
+            for part in parts:
+                children = {c["name"]: c for c in cur.get("children", [])}
+                if part not in children:
+                    return
+
+                cur = children[part]
+                cur["delta"] -= stats.get(size_key, 0)
+
+        for module_name, dct in self.modules.items():
+            accumulate(tree_text, ".text", dct)
+            accumulate(tree_data, ".data", dct)
+            accumulate(tree_bss, ".bss", dct)
+
+        for module_name, dct in self.old_modules.items():
+            subtract(tree_text, ".text", dct)
+            subtract(tree_data, ".data", dct)
+            subtract(tree_bss, ".bss", dct)
+
+        jinja_loader = FileSystemLoader(dirname(abspath(__file__)))
+        jinja_environment = Environment(loader=jinja_loader, undefined=StrictUndefined)
+        template = jinja_environment.get_template("templates/summary-flamegraph.html")
+
+        name, _ = splitext(basename(file_desc.name))
+
+        if name.endswith("_map"):
+            name = name[:-4]
+        if self.tc_name:
+            name = f"{name} {self.tc_name}"
+
+        file_desc.write(
+            template.render(
+                {
+                    "name": name,
+                    "rom": json.dumps(
+                        {
+                            "name": "ROM",
+                            "value": tree_text["value"] + tree_data["value"],
+                            "delta": tree_text["delta"] + tree_data["delta"],
+                            "children": [tree_text, tree_data],
+                        }
+                    ),
+                    "ram": json.dumps(
+                        {
+                            "name": "RAM",
+                            "value": tree_bss["value"] + tree_data["value"],
+                            "delta": tree_bss["delta"] + tree_data["delta"],
+                            "children": [tree_bss, tree_data],
+                        }
+                    ),
+                }
+            )
+        )
+
+    def generate_json(self, file_desc: IO[str]):
+        """Generate a json file from a memory map
+
+        Positional arguments:
+        file_desc - the file to write out the final report to
+        """
+        file_desc.write(json.dumps(self.mem_report, indent=4))
+        file_desc.write("\n")
+
+    def generate_table(self, file_desc: IO[str]):
+        """Generate a table from a memory map
+
+        Returns: string of the generated table
+        """
+        # Create table
+        columns = ["Module"]
+        columns.extend(self.print_sections)
+
+        table = PrettyTable(columns, junction_char="|", hrules=HEADER)
+        table.align["Module"] = "l"
+
+        for col in self.print_sections:
+            table.align[col] = "r"
+
+        for i in sorted(self.short_modules):
+            row = [i]
+
+            for k in self.print_sections:
+                row.append(
+                    "{}({:+})".format(
+                        self.short_modules[i][k], self.short_modules[i][k + "-delta"]
+                    )
+                )
+
+            table.add_row(row)
+
+        subtotal_row = ["Subtotals"]
+        for k in self.print_sections:
+            subtotal_row.append(
+                "{}({:+})".format(self.subtotal[k], self.subtotal[k + "-delta"])
+            )
+
+        table.add_row(subtotal_row)
+
+        output = table.get_string()
+        output += "\n"
+
+        output += self.RAM_FORMAT_STR.format(
+            self.mem_summary["static_ram"], self.mem_summary["static_ram_delta"]
+        )
+        output += self.ROM_FORMAT_STR.format(
+            self.mem_summary["total_flash"], self.mem_summary["total_flash_delta"]
+        )
+        file_desc.write(output)
+
+    def compute_report(self):
+        """Generates summary of memory usage for main areas"""
+        self.subtotal = defaultdict(int)
+
+        for mod in self.modules.values():
+            for k in self.sections:
+                self.subtotal[k] += mod[k]
+                self.subtotal[k + "-delta"] += mod[k]
+
+        for mod in self.old_modules.values():
+            for k in self.sections:
+                self.subtotal[k + "-delta"] -= mod[k]
+
+        self.mem_summary = {
+            "static_ram": self.subtotal[".data"] + self.subtotal[".bss"],
+            "static_ram_delta": self.subtotal[".data-delta"]
+            + self.subtotal[".bss-delta"],
+            "total_flash": (self.subtotal[".text"] + self.subtotal[".data"]),
+            "total_flash_delta": self.subtotal[".text-delta"]
+            + self.subtotal[".data-delta"],
+        }
+
+        self.mem_report = []
+        if self.short_modules:
+            for name, sizes in sorted(self.short_modules.items()):
+                self.mem_report.append(
+                    {
+                        "module": name,
+                        "size": {
+                            k: sizes.get(k, 0)
+                            for k in (self.print_sections + self.delta_sections)
+                        },
+                    }
+                )
+
+        self.mem_report.append({"summary": self.mem_summary})
+
+    def parse(
+        self, mapfile: Path, oldfile: Optional[Path] = None, no_fill: bool = False
+    ) -> bool:
+        """Parse and decode map file depending on the toolchain
+
+        Positional arguments:
+        mapfile - the file name of the memory map file
+        toolchain - the toolchain used to create the file
+        """
+        try:
+            with open(mapfile, "r") as file_input:
+                self.modules = Parser(not no_fill).parse_mapfile(file_input)
+            try:
+                if oldfile is not None:
+                    with open(oldfile, "r") as old_input:
+                        self.old_modules = Parser(not no_fill).parse_mapfile(old_input)
+                else:
+                    self.old_modules = self.modules
+            except IOError:
+                self.old_modules = {}
+            return True
+
+        except IOError as error:
+            print("I/O error({0}): {1}".format(error.errno, error.strerror))
+            return False
diff --git a/tools/memory/src/memory/templates/summary-flamegraph.html b/tools/memory/src/memory/templates/summary-flamegraph.html
new file mode 100644
index 0000000..9ec8ecb
--- /dev/null
+++ b/tools/memory/src/memory/templates/summary-flamegraph.html
@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+
+  <link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
+    integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
+  <link rel="stylesheet" type="text/css"
+    href="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@1.0.4/dist/d3.flameGraph.min.css"
+    integrity="sha256-w762vSe6WGrkVZ7gEOpnn2Y+FSmAGlX77jYj7nhuCyY=" crossorigin="anonymous" />
+
+  <style>
+    /* Space out content a bit */
+    body {
+      padding-top: 20px;
+      padding-bottom: 20px;
+    }
+
+    /* Custom page header */
+    .header {
+      padding-bottom: 20px;
+      padding-right: 15px;
+      padding-left: 15px;
+      border-bottom: 1px solid #e5e5e5;
+    }
+
+    /* Make the masthead heading the same height as the navigation */
+    .header h3 {
+      margin-top: 0;
+      margin-bottom: 0;
+      line-height: 40px;
+    }
+  </style>
+
+  <title>{{name}} Memory Details</title>
+</head>
+
+<body>
+  <div class="container">
+    <div class="header clearfix">
+      <h3 class="text-muted">{{name}} Memory Details</h3>
+    </div>
+    <div id="chart-rom">
+    </div>
+    <hr />
+    <div id="chart-ram">
+    </div>
+    <hr />
+    <div id="details"></div>
+  </div>
+
+  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"
+    integrity="sha256-r7j1FXNTvPzHR41+V71Jvej6fIq4v4Kzu5ee7J/RitM=" crossorigin="anonymous">
+    </script>
+  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js"
+    integrity="sha256-z0A2CQF8xxCKuOJsn4sJ5HBjxiHHRAfTX8hDF4RSN5s=" crossorigin="anonymous">
+    </script>
+  <script type="text/javascript"
+    src="https://cdn.jsdelivr.net/gh/spiermar/d3-flame-graph@1.0.4/dist/d3.flameGraph.min.js"
+    integrity="sha256-I1CkrWbmjv+GWjgbulJ4i0vbzdrDGfxqdye2qNlhG3Q=" crossorigin="anonymous">
+    </script>
+
+  <script type="text/javascript">
+    var tip = d3.tip()
+      .direction("s")
+      .offset([8, 0])
+      .attr('class', 'd3-flame-graph-tip')
+      .html(function (d) { return "module: " + d.data.name + ", bytes: " + d.data.value + ", delta: " + d.data.delta; });
+    var colorizer = function (d) {
+      if (d.data.delta > 0) {
+        ratio = (d.data.value - d.data.delta) / d.data.value;
+        green = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+        blue = ("0" + (Number(ratio * 0xEE | 0).toString(16))).slice(-2).toUpperCase();
+        console.log(d.data.name, green, blue);
+        return "#EE" + green + blue
+      } else if (d.data.delta < 0) {
+        ratio = (d.data.value + d.data.delta) / d.data.value;
+        green = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+        red = ("0" + (Number(ratio * 0xFF | 0).toString(16))).slice(-2).toUpperCase();
+        console.log(d.data.name, red, green);
+        return "#" + red + green + "EE";
+      } else {
+        return "#FFFFEE";
+      }
+    }
+    var flameGraph_rom = d3.flameGraph()
+      .transitionDuration(250)
+      .transitionEase(d3.easeCubic)
+      .sort(true)
+      .color(colorizer)
+      .tooltip(tip);
+    var flameGraph_ram = d3.flameGraph()
+      .transitionDuration(250)
+      .transitionEase(d3.easeCubic)
+      .sort(true)
+      .color(colorizer)
+      .tooltip(tip);
+    var rom_elem = d3.select("#chart-rom");
+    flameGraph_rom.width(rom_elem.node().getBoundingClientRect().width);
+    rom_elem.datum({{ rom }}).call(flameGraph_rom);
+    var ram_elem = d3.select("#chart-ram");
+    flameGraph_ram.width(ram_elem.node().getBoundingClientRect().width);
+    ram_elem.datum({{ ram }}).call(flameGraph_ram);
+  </script>
+</body>
+
+</html>