File size: 6,118 Bytes
b0c3a57
 
 
 
 
0377502
 
 
 
 
 
 
 
b0c3a57
 
7f69fd6
 
 
0377502
7f69fd6
 
 
b0c3a57
 
 
 
0377502
 
b0c3a57
 
0377502
b0c3a57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0377502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7f69fd6
 
0377502
b0c3a57
0377502
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b0c3a57
 
7f69fd6
 
0377502
 
 
 
 
7f69fd6
0377502
 
7f69fd6
0377502
 
b0c3a57
 
 
 
7f69fd6
b0c3a57
0377502
 
 
 
b0c3a57
0377502
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""OphthalmoCapture — Image Protection Layer

Injects CSS and JavaScript into the Streamlit page to prevent users from
downloading, dragging, or otherwise saving the confidential medical images.

APPROACH (HF Spaces compatible):
  1. CSS via st.markdown(unsafe_allow_html=True) — Streamlit renders <style>
     natively.  Handles pointer-events, overlays, drag prevention.
  2. JS via st.html() — Streamlit >= 1.33 renders raw HTML (including
     <script>) inside a tiny srcdoc iframe.  From there we reach the real
     Streamlit DOM via window.parent.document (same-origin).
  3. Additional CSS hides the st.html wrapper divs ([data-testid="stHtml"])
     so the iframe has ZERO visual footprint — no layout shifts.

Protection layers (defence-in-depth):
  1. CSS: pointer-events:none, user-select:none on <img>.
  2. CSS: transparent ::after overlay on stImage containers.
  3. CSS: -webkit-touch-callout:none for mobile.
  4. JS:  contextmenu blocked on entire parent document.
  5. JS:  Ctrl+S / Ctrl+U / Ctrl+P / F12 / DevTools shortcuts blocked.
  6. JS:  dragstart blocked for images.
  7. JS:  MutationObserver re-applies draggable=false to new images.
"""

import streamlit as st

# ── CSS via st.markdown ──────────────────────────────────────────────────────
_PROTECTION_CSS = """
<style>
/* Layer 1: Disable ALL interaction on <img> tags */
img {
    pointer-events: none       !important;
    user-select: none          !important;
    -webkit-user-select: none  !important;
    -moz-user-select: none     !important;
    -ms-user-select: none      !important;
    -webkit-user-drag: none    !important;
    -webkit-touch-callout: none !important;
}

/* Layer 2: Transparent overlay on every Streamlit image container */
[data-testid="stImage"] {
    position: relative !important;
}
[data-testid="stImage"]::after {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0; bottom: 0;
    z-index: 10;
    background: transparent;
    pointer-events: auto !important;
    cursor: default;
}

/* Layer 3: Extra drag prevention */
[data-testid="stImage"] img {
    -webkit-user-drag: none !important;
    user-drag: none         !important;
}

/* ── Hide st.html() wrappers to prevent ANY layout shift ──────────────── */
[data-testid="stHtml"] {
    height: 0 !important;
    min-height: 0 !important;
    max-height: 0 !important;
    overflow: hidden !important;
    margin: 0 !important;
    padding: 0 !important;
    line-height: 0 !important;
    font-size: 0 !important;
    border: none !important;
}
[data-testid="stHtml"] iframe {
    height: 0 !important;
    min-height: 0 !important;
    border: none !important;
    display: block !important;
}
</style>
"""

# ── JS via st.html() — runs inside srcdoc iframe, reaches parent DOM ─────────
_PROTECTION_JS = """
<script>
(function () {
    var doc;
    try { doc = window.parent.document; } catch(e) { doc = document; }

    // Guard: only attach once per page lifecycle
    if (doc.__ophthalmo_protection__) return;
    doc.__ophthalmo_protection__ = true;

    function block(e) {
        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        return false;
    }

    // ── Block context menu (right-click) on ENTIRE page ─────────────────
    doc.addEventListener('contextmenu', function (e) {
        return block(e);
    }, true);

    // ── Block keyboard shortcuts ────────────────────────────────────────
    doc.addEventListener('keydown', function (e) {
        var dominated = false;
        var ctrl = e.ctrlKey || e.metaKey;
        var key  = e.key ? e.key.toLowerCase() : '';

        if (ctrl && key === 's') dominated = true;   // Save page
        if (ctrl && key === 'u') dominated = true;   // View source
        if (ctrl && key === 'p') dominated = true;   // Print
        if (e.keyCode === 123)   dominated = true;   // F12
        if (ctrl && e.shiftKey && key === 'i') dominated = true;  // Inspector
        if (ctrl && e.shiftKey && key === 'j') dominated = true;  // Console
        if (ctrl && e.shiftKey && key === 'c') dominated = true;  // Picker

        if (dominated) return block(e);
    }, true);

    // ── Block drag-and-drop of images ───────────────────────────────────
    doc.addEventListener('dragstart', function (e) {
        if (e.target && e.target.tagName === 'IMG') return block(e);
    }, true);

    // ── MutationObserver — lock new images as they appear ───────────────
    function lockImgs(root) {
        var imgs = root.querySelectorAll ? root.querySelectorAll('img') : [];
        for (var i = 0; i < imgs.length; i++) {
            imgs[i].setAttribute('draggable', 'false');
            imgs[i].ondragstart = function () { return false; };
            imgs[i].oncontextmenu = function () { return false; };
        }
    }
    lockImgs(doc);

    var timer = null;
    new MutationObserver(function () {
        if (timer) return;
        timer = setTimeout(function () {
            timer = null;
            lockImgs(doc);
        }, 250);
    }).observe(doc.body, { childList: true, subtree: true });

})();
</script>
"""


def inject_image_protection():
    """Inject CSS + JS image-protection layers into the page.

    - CSS via st.markdown  (always re-injected per rerun, as Streamlit requires).
    - JS via st.html       (rendered with <script> support; internal guard
      prevents duplicate listeners across reruns).
    - The CSS also hides st.html wrappers to prevent layout shifts.
    """
    # 1) CSS protection + hide st.html wrappers
    st.markdown(_PROTECTION_CSS, unsafe_allow_html=True)

    # 2) JS protection (right-click, keyboard shortcuts, drag, observer)
    st.html(_PROTECTION_JS)