Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Attach diagrams as images instead of base64 embedding #19

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
54 changes: 12 additions & 42 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 Down Expand Up @@ -122,13 +111,20 @@ 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)
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 +490,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
Loading