How-to: Integrar o EdgePy Viewer com um app Django¶
Utilize este guia caso deseje integrar o EdgePy Viewer em uma aplicação Django que necessite de visualização de imagens médicas, RTSTRUCT e mapas de dose. O EdgePy Viewer é um pacote JS/CSS estático que roda no navegador e baixa somente os assets autorizados pelo host.
Objetivo da integração: Django continua responsável por autenticação, autorização, persistência e entrega dos arquivos clínicos. O EdgePy Viewer é servido como JS/CSS estático, roda no navegador e baixa somente os assets autorizados pelo host.
Resultado esperado¶
Ao final da integração, o app Django terá:
- uma página autenticada que renderiza o viewer em
/studies/<id>/viewer/; - o pacote hosted em
static/clinical-viewer/; - um endpoint
viewer-assets/que retornaHostAssetManifest; - endpoints autenticados para baixar ZIPs, DICOMs, RTSTRUCT, máscaras e dose;
- um fluxo pós-simulação que publica ZIPs MHD/RAW como overlays de dose;
- botão
Sairno rail lateral usandoui.exitUrl; - debug oculto por padrão e habilitado somente com
?debug=1.
Visão da arquitetura¶
O caminho principal e recomendado é same-origin. Não use iframe ou postMessage cross-origin como integração oficial, a menos que seja um fluxo experimental separado.
Fluxo de runtime¶
sequenceDiagram
participant U as Usuário
participant D as Django
participant V as EdgePy Viewer
participant S as Storage clínico
U->>D: Abre /studies/<id>/viewer/
D->>U: Retorna HTML autenticado
U->>V: Carrega JS/CSS hosted
V->>D: GET /api/studies/<id>/viewer-assets/
D->>V: HostAssetManifest
V->>D: GET URLs autorizadas do manifesto
D->>S: Lê arquivos do estudo
S->>D: ZIPs, DICOMs, RTSTRUCT, dose
D->>V: Streams autenticados
V->>U: Renderiza viewport e overlays
Step-by-step de integração¶
1. Baixar e instalar o pacote hosted
Baixe edgepy-viewer-hosted-0.1.0.zip na página Downloads e descompacte-o em um diretório de arquivos estáticos do seu projeto Django. O ZIP já contém a pasta clinical-viewer/ na raiz.
2. Criar página do viewer
Renderize um template autenticado, defina window.PYCOM_VIEWER_CONFIG e window.PYCOM_CLINICAL_VIEWER_CONFIG, e só depois carregue o JS hosted.
3. Implementar manifesto
Retorne HostAssetManifest com path lógico e url autenticada para cada asset clínico.
4. Servir arquivos
Use FileResponse, sessão Django e checagem de permissão por estudo e por arquivo.
5. Publicar dose
Após simulação, salve o ZIP MHD/RAW e recarregue o manifesto.
6. Validar no browser
Confira Network, viewport, overlays, segurança e dose beta/gamma.
1. Instale os assets hosted¶
Para apps Django, use o .zip hosted pronto. O servidor Django de produção não precisa de Node.js: ele apenas serve JS/CSS estáticos e arquivos clínicos autenticados.
Baixe o arquivo da página Downloads:
Antes de instalar, confira a estrutura do ZIP:
O ZIP deve conter a pasta clinical-viewer/ na raiz:
clinical-viewer/
edgepy-viewer-hosted.iife.js
edgepy-viewer-hosted.css
assets/
*.js
agents/ # opcional/reservado; não é necessário para a integração padrão
Instale o pacote em um diretório de arquivos estáticos versionado pelo seu app Django, por exemplo static/ na raiz do projeto:
Depois da descompactação, estes caminhos devem existir no seu projeto:
Se o seu projeto usa STATICFILES_DIRS, inclua esse diretório de origem em settings.py:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
Em produção, rode o fluxo normal do seu projeto para copiar os arquivos para STATIC_ROOT:
Confirme no navegador ou com curl que o Django resolve estes assets:
/static/clinical-viewer/edgepy-viewer-hosted.css
/static/clinical-viewer/edgepy-viewer-hosted.iife.js
Não descompacte dentro de static/clinical-viewer/
O ZIP já inclui a pasta clinical-viewer/. Se você fizer unzip ... -d static/clinical-viewer, os arquivos ficarão em static/clinical-viewer/clinical-viewer/ e {% static 'clinical-viewer/edgepy-viewer-hosted.iife.js' %} retornará 404.
2. Crie a página Django do viewer¶
Template recomendado:
{% load static %}
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EdgePy Viewer | {{ study.patient_name }}</title>
<link
rel="stylesheet"
href="{% static 'clinical-viewer/edgepy-viewer-hosted.css' %}?v=EDGEpy_VERSION"
/>
<style>
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
#pycom-clinical-viewer {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
</style>
</head>
<body>
<section id="pycom-clinical-viewer" data-pycom-clinical-viewer></section>
<script>
const edgepyViewerDebug =
new URLSearchParams(window.location.search).get("debug") === "1";
window.PYCOM_CLINICAL_VIEWER_CONFIG = {
mode: "hosted",
ui: {
displayMode: "workstation",
debug: edgepyViewerDebug,
exitUrl: "{% url 'study-wizard' study.pk %}",
},
};
window.PYCOM_VIEWER_CONFIG = {
viewerAssetsUrl: "{% url 'viewer-assets' study.pk %}",
};
</script>
<script src="{% static 'clinical-viewer/edgepy-viewer-hosted.iife.js' %}?v=EDGEpy_VERSION"></script>
</body>
</html>
Contratos importantes:
window.PYCOM_VIEWER_CONFIG.viewerAssetsUrlaponta para o manifesto do estudo.window.PYCOM_CLINICAL_VIEWER_CONFIG.ui.exitUrlmostra a açãoSairno rodapé do rail lateral.?debug=1habilita o botão/painel de debug; em uso normal, o debug fica oculto.data-pycom-clinical-vieweré o elemento de montagem do viewer.- O mount precisa ter altura real e
display: flex.
3. Defina as rotas Django¶
Use nomes finais compatíveis com o app real. Um formato simples é:
from django.urls import path
from . import views
urlpatterns = [
path("studies/<int:study_id>/viewer/", views.study_viewer, name="study-viewer"),
path("api/studies/<int:study_id>/viewer-assets/", views.viewer_assets, name="viewer-assets"),
path("api/studies/<int:study_id>/archives/<int:archive_id>/download/", views.download_archive, name="download-archive"),
]
4. Retorne o HostAssetManifest¶
O manifesto descreve os arquivos que o viewer pode baixar. Use paths lógicos, nunca paths do filesystem.
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
@login_required
def study_viewer(request, study_id):
study = get_object_or_404(DosimetryStudy, pk=study_id)
assert_user_can_access(request.user, study)
return render(request, "viewer/study_viewer.html", {"study": study})
@login_required
def viewer_assets(request, study_id):
study = get_object_or_404(DosimetryStudy, pk=study_id)
assert_user_can_access(request.user, study)
assets = []
for archive in study.archives.all():
assets.append({
"path": logical_path_for_archive(archive),
"url": reverse("download-archive", args=[study.pk, archive.pk]),
"kind": archive.viewer_kind,
"contentType": archive.content_type,
"size": archive.file.size,
})
return JsonResponse({
"studyUid": f"irdose-study-{study.pk}",
"label": str(study),
"assets": assets,
})
Exemplo de resposta:
{
"studyUid": "irdose-study-42",
"label": "Paciente exemplo",
"assets": [
{
"path": "acquisitions/scan1/ct/ct.zip",
"url": "/api/studies/42/archives/10/download/",
"kind": "archive",
"contentType": "application/zip",
"size": 31572947
},
{
"path": "acquisitions/scan1/spect/spect.zip",
"url": "/api/studies/42/archives/11/download/",
"kind": "archive",
"contentType": "application/zip"
},
{
"path": "targets/rtstruct/liver.zip",
"url": "/api/studies/42/archives/12/download/",
"kind": "archive",
"contentType": "application/zip"
}
]
}
Regras do manifesto:
pathé lógico e relativo.urldeve ser same-origin autenticada ou URL assinada de curta duração.- use
kind: "archive"para ZIPs de CT/SPECT/PET/máscaras/RTSTRUCT; - use
kind: "dose"para ZIPs de dose MHD/RAW; - não envie milhares de DICOMs individualmente quando já há ZIPs de aquisição;
- não use Base64 para DICOM, ZIP, máscara ou dose.
5. Sirva os arquivos clínicos com permissão¶
Exemplo de download autenticado:
from django.contrib.auth.decorators import login_required
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404
@login_required
def download_archive(request, study_id, archive_id):
study = get_object_or_404(DosimetryStudy, pk=study_id)
assert_user_can_access(request.user, study)
archive = get_object_or_404(study.archives, pk=archive_id)
if not archive.file:
raise Http404("Archive not found")
response = FileResponse(archive.file.open("rb"), content_type="application/zip")
response["Content-Disposition"] = f'attachment; filename="{archive.file.name.rsplit("/", 1)[-1]}"'
response["X-Content-Type-Options"] = "nosniff"
return response
6. Publique dose MHD/RAW pós-simulação¶
Quando a aplicação terminar a simulação, salve o ZIP de dose no estudo e faça o manifesto incluir um asset kind: "dose".
{
"path": "dose/lu177/guirey_None_Lu-177_files.zip",
"url": "/api/studies/42/archives/88/download/",
"kind": "dose",
"contentType": "application/zip",
"metadata": {
"role": "dose",
"label": "Lu-177 dose maps"
}
}
Formato esperado dentro do ZIP:
O .mhd deve apontar para o RAW correspondente:
ObjectType = Image
NDims = 3
DimSize = 256 256 199
ElementSpacing = 1.95313 1.95313 1.95313
Offset = -249.023 -410.023 1156.93
ElementType = MET_FLOAT
ElementDataFile = patient_beta--Dose.raw
Fluxo pós-simulação:
sequenceDiagram
participant I as IRDose
participant D as Django
participant V as EdgePy Viewer
I->>D: Entrega ZIP MHD/RAW
D->>D: Salva arquivo no estudo
D->>D: Atualiza viewer-assets/
D-->>V: edgepy:reload-host-manifest
V->>D: GET viewer-assets/ atualizado
V->>D: GET ZIP de dose
V->>V: Agrupa pares .mhd/.raw
V->>V: Renderiza Beta- dose e Gamma dose
Disparo no browser:
window.dispatchEvent(
new CustomEvent("edgepy:reload-host-manifest", {
detail: { manifestUrl: "/api/studies/42/viewer-assets/" },
}),
);
Se manifestUrl for omitido, o viewer reutiliza window.PYCOM_VIEWER_CONFIG.viewerAssetsUrl.
Labels esperados para o pacote validado:
7. Valide a integração¶
No backend Django:
No navegador:
- Faça login no Django.
- Abra
/studies/<id>/viewer/. - Confirme que
edgepy-viewer-hosted.csseedgepy-viewer-hosted.iife.jsretornam200. - Confirme que
viewer-assets/retorna200e JSON válido. - Confirme que cada
urldo manifesto retorna200para usuário autorizado. - Confirme que as mesmas URLs retornam login,
403ou404para usuário não autorizado. - Confirme que CT/SPECT/PET renderizam.
- Confirme que RTSTRUCT/máscaras aparecem em
Overlays. - Confirme que o botão
Sairno rail volta para a página definida emexitUrl. - Publique manualmente ou gere por simulação o ZIP de dose.
- Confirme que
viewer-assets/atualizado incluikind: "dose". - Dispare
edgepy:reload-host-manifest. - Confirme que
Beta- doseeGamma doseaparecem no painelOverlays. - Confirme visualmente o alinhamento da dose com a CT de referência.
Checklist de segurança¶
- Exigir login na página do viewer.
- Exigir login no endpoint
viewer-assets/. - Exigir login nos downloads.
- Verificar que o usuário pode acessar o estudo.
- Verificar que cada arquivo pertence ao estudo solicitado.
- Rejeitar paths lógicos absolutos, com
..ou segmentos vazios. - Não retornar paths absolutos do servidor.
- Retornar
X-Content-Type-Options: nosniffem respostas de arquivo. - Preferir sessão same-origin.
Solução de problemas¶
Se o viewer aparece, mas a imagem não renderiza:
- verifique no Network se
clinical-viewer/assets/*.jsretorna200; - verifique se o mount tem altura real e
display: flex; - confirme que
viewer-assets/retornakind: "archive"para ZIPs; - confirme que os ZIPs contêm DICOMs válidos;
- abra o Console e procure mensagens do Cornerstone ou dos workers.
Se o manifest retorna 302 para login, a sessão Django não está autenticada no navegador atual.
Se o mapa de dose não aparece como camada:
- confirme que o asset está no manifesto com
kind: "dose"; - confirme que o path lógico contém
dose/ou um nome com-dosequando o ZIP é expandido internamente; - confirme que o ZIP contém pares
.mhde.rawcom o mesmo nome base; - confirme que o
.mhdusaElementType = MET_FLOATe apontaElementDataFilepara o.rawcorreto; - confirme que
Offset,ElementSpacingeDimSizesão compatíveis com a CT usada como referência.
Se a interface ainda carrega uma versão antiga do viewer:
- confirme que o template referencia
edgepy-viewer-hosted.iife.jseedgepy-viewer-hosted.css; - use cache-bust nos assets estáticos durante validações, por exemplo
?v=20260626-rail-exit-status; - faça hard reload do navegador e verifique no Network se o JS/CSS novo retornou
200.
