Skip to content

Commit

Permalink
feat: Attach diagrams as images
Browse files Browse the repository at this point in the history
  • Loading branch information
huyenngn committed Oct 10, 2023
1 parent ee09d52 commit 98aea1f
Show file tree
Hide file tree
Showing 5 changed files with 53 additions and 73 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,6 @@ dmypy.json

# Cython debug symbols
cython_debug/

# VSCode project settings
.vscode/
20 changes: 11 additions & 9 deletions capella2polarion/elements/api_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,29 @@ def patch_work_item(
logger.error("Updating work item %r failed. %s", wi, error.args[0])


def split_and_decode_diagram(diagram: str) -> tuple[str, bytes]:
"""Split the diagram into type and decoded data."""
prefix, encoded = diagram.split(";base64,")
return prefix.replace("data:image/", ""), b64.b64decode(encoded)


def has_visual_changes(old: str, new: str) -> bool:
"""Return True if the images of the diagrams differ."""
type_old, decoded_old = split_and_decode_diagram(old)
type_new, decoded_new = split_and_decode_diagram(new)
type_old, decoded_old = _typify_and_decode_diagram(old)
type_new, decoded_new = _typify_and_decode_diagram(new)

if type_old != type_new:
return True

if type_old == "svg+xml":
if type_old == "image/svg+xml":
decoded_old = cairosvg.svg2png(bytestring=decoded_old)
decoded_new = cairosvg.svg2png(bytestring=decoded_new)

return decoded_old != decoded_new


def _typify_and_decode_diagram(diagram: str) -> tuple[str, bytes]:
"""Split the diagram into type and decoded data."""
prefix, content = diagram.split(";base64,")
mimetype = prefix.replace("data:", "")
content_decoded = b64.b64decode(content)
return mimetype, content_decoded


def handle_links(
left: cabc.Iterable[polarion_api.WorkItemLink],
right: cabc.Iterable[polarion_api.WorkItemLink],
Expand Down
43 changes: 20 additions & 23 deletions capella2polarion/elements/serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"""Objects for serialization of capella objects to workitems."""
from __future__ import annotations

import base64
import base64 as b64
import collections.abc as cabc
import logging
import mimetypes
import pathlib
import re
import typing as t

import cairosvg
import markupsafe
import polarion_rest_api_client as polarion_api
from capellambse import helpers as chelpers
Expand Down Expand Up @@ -66,35 +67,31 @@ def element(
def diagram(diag: diagr.Diagram, ctx: dict[str, t.Any]) -> CapellaWorkItem:
"""Serialize a diagram for Polarion."""
diagram_path = ctx["DIAGRAM_CACHE"] / f"{diag.uuid}.svg"
src = _decode_diagram(diagram_path)
style = "; ".join(
(f"{key}: {value}" for key, value in DIAGRAM_STYLES.items())
)
description = f'<html><p><img style="{style}" src="{src}" /></p></html>'
content = diagram_path.read_bytes()
content_svg = b64.standard_b64encode(content)
svg_attachment = attachment("image/svg+xml", f"{diag.uuid}", content_svg)
content_png = b64.b16encode(cairosvg.svg2png(content.decode("utf8")))
png_attachment = attachment("image/png", f"{diag.uuid}", content_png)
return CapellaWorkItem(
type="diagram",
title=diag.name,
description_type="text/html",
description=description,
status="open",
attachments=[svg_attachment, png_attachment],
uuid_capella=diag.uuid,
)


def _decode_diagram(diagram_path: pathlib.Path) -> str:
mime_type, _ = mimetypes.guess_type(diagram_path)
if mime_type is None:
logger.error(
"Do not understand the MIME subtype for the diagram '%s'!",
diagram_path,
)
return ""
content = diagram_path.read_bytes()
content_encoded = base64.standard_b64encode(content)
assert mime_type is not None
image_data = b"data:" + mime_type.encode() + b";base64," + content_encoded
src = image_data.decode()
return src
def attachment(
mime_type: str, name: str, content: bytes
) -> polarion_api.WorkItemAttachment:
"""Serialize an attachment for Polarion."""
return polarion_api.WorkItemAttachment(
work_item_id="",
id="",
content_bytes=content,
mime_type=mime_type,
file_name=name,
)


def generic_work_item(
Expand Down Expand Up @@ -143,7 +140,7 @@ def repair_images(node: etree._Element) -> None:
filehandler = resources[["\x00", workspace][workspace in resources]]
try:
with filehandler.open(file_path, "r") as img:
b64_img = base64.b64encode(img.read()).decode("utf8")
b64_img = b64.b64encode(img.read()).decode("utf8")
node.attrib["src"] = f"data:{mime_type};base64,{b64_img}"
except FileNotFoundError:
logger.error("Inline image can't be found from %r", file_path)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ dependencies = [
"capellambse",
"click",
"PyYAML",
"polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@feat-add-attachments-workitem-relations",
"polarion-rest-api-client @ git+https://github.com/DSD-DBS/polarion-rest-api-client.git@feat-serialize-attachments",
"requests",
"cairosvg",
]
Expand Down
58 changes: 18 additions & 40 deletions tests/test_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@
TEST_OCAP_UUID: "OperationalCapability",
TEST_WE_UUID: "Entity",
}
TEST_DIAG_DESCR = (
'<html><p><img style="max-width: 100%" src="data:image/svg+xml;base64,'
)
TEST_DIAG_DESCR = '<html><p><img style="max-width: 100%" src="attachment:'
TEST_SER_DIAGRAM: dict[str, t.Any] = {
"id": None,
"title": "[CC] Capability",
Expand All @@ -78,15 +76,6 @@ class TestAPIHelper:
pathlib.Path(__file__).parent / "data" / "svg_diff" / "example.svg"
)

def encode_svg(self, params: dict[str, str]) -> str:
svg = self.SVG_PATH.read_text()
for key, value in params.items():
svg = re.sub(f"{key}=[\"'][^\"']*[\"']", f'{key}="{value}"', svg)
content_encoded = b64.b64encode(svg.encode("utf-8"))
image_data = b"data:image/svg+xml;base64," + content_encoded
src = image_data.decode()
return src

@pytest.mark.parametrize(
"params,expected",
[
Expand All @@ -105,6 +94,11 @@ def encode_svg(self, params: dict[str, str]) -> str:
True,
id="height_changed",
),
pytest.param(
({"height": "100"}, {"height": "50"}),
True,
id="height_changed",
),
pytest.param(
({"id": "test"}, {"id": "test2"}),
False,
Expand All @@ -122,13 +116,23 @@ def encode_svg(self, params: dict[str, str]) -> str:
)
def test_image_diff(self, params, expected):
old_params, new_params = params
old_svg = self.encode_svg(old_params)
new_svg = self.encode_svg(new_params)
old_svg = self._parameterize_and_decode_svg(self.SVG_PATH, old_params)
new_svg = self._parameterize_and_decode_svg(self.SVG_PATH, new_params)

is_different = api_helper.has_visual_changes(old_svg, new_svg)

assert is_different is expected

def _parameterize_and_decode_svg(
self, diagram_path: pathlib.Path, params: dict[str, str]
) -> str:
svg = diagram_path.read_text()
for key, value in params.items():
svg = re.sub(f"{key}=[\"'][^\"']*[\"']", f'{key}="{value}"', svg)
content_encoded = b64.b64encode(svg.encode("utf-8"))
image_data = b"data:image/svg+xml;base64," + content_encoded
return image_data.decode()


class TestDiagramElements:
@staticmethod
Expand Down Expand Up @@ -494,32 +498,6 @@ def test_resolve_element_type():


class TestSerializers:
@staticmethod
def test_diagram(model: capellambse.MelodyModel):
diag = model.diagrams.by_uuid(TEST_DIAG_UUID)

serialized_diagram = serialize.diagram(
diag, {"DIAGRAM_CACHE": TEST_DIAGRAM_CACHE}
)
serialized_diagram.description = None

assert serialized_diagram == serialize.CapellaWorkItem(
type="diagram",
uuid_capella=TEST_DIAG_UUID,
title="[CC] Capability",
description_type="text/html",
status="open",
linked_work_items=[],
)

@staticmethod
def test__decode_diagram():
diagram_path = TEST_DIAGRAM_CACHE / "_APMboAPhEeynfbzU12yy7w.svg"

diagram = serialize._decode_diagram(diagram_path)

assert diagram.startswith("data:image/svg+xml;base64,")

@staticmethod
@pytest.mark.parametrize(
"uuid,expected",
Expand Down

0 comments on commit 98aea1f

Please sign in to comment.