NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

_gradia-secure-blur.patch (5641B)


      1 --- a/gradia/backend/tool_config.py
      2 +++ b/gradia/backend/tool_config.py
      3 @@ -344,7 +344,7 @@
      4                  icon="checkerboard-big-symbolic",
      5                  column=3,
      6                  row=1,
      7 -                has_scale=False,
      8 +                has_scale=True,
      9                  has_primary_color=False,
     10              ),
     11              ToolConfig(
     12 --- a/gradia/overlay/drawing_actions.py
     13 +++ b/gradia/overlay/drawing_actions.py
     14 @@ -21,6 +21,7 @@
     15  from gi.repository import Gtk, Gdk, Gio, Pango, PangoCairo, GdkPixbuf
     16  from enum import Enum
     17  import math
     18 +import random
     19  from gradia.backend.logger import Logger
     20  from gradia.utils.colors import has_visible_color
     21  import time
     22 @@ -640,19 +641,25 @@
     23  
     24  
     25  class CensorAction(RectAction):
     26 +    # Secure blur: the area is averaged down to cells (4x the size slider, in
     27 +    # screen pixels), every cell gets random noise, then it is scaled back up
     28 +    # smoothly. Averaging throws the detail away; the noise means the cells no
     29 +    # longer equal the true averages, which is what depixelation tools match on.
     30 +    NOISE_SIGMA = 10
     31 +
     32      def __init__(self, start: tuple[int, int], end: tuple[int, int], background_pixbuf: GdkPixbuf.Pixbuf, options):
     33          super().__init__(start, end, False, options)
     34  
     35          self.original_scale = 1.0
     36 -        self.base_block_size = 8
     37          self.background_pixbuf = background_pixbuf
     38 +        self.noise_seed = random.SystemRandom().getrandbits(64)
     39  
     40      def set_original_scale(self, scale: float):
     41          self.original_scale = scale
     42  
     43      def _get_scaled_block_size(self, current_scale: float) -> float:
     44          scale_ratio = current_scale / self.original_scale
     45 -        return self.base_block_size * scale_ratio
     46 +        return max(3, self.options.size) * 4 * scale_ratio
     47  
     48      def draw(self, cr: cairo.Context, image_to_widget_coords: Callable[[int, int], tuple[float, float]], scale: float):
     49          crop = self._get_image_crop()
     50 @@ -675,37 +682,59 @@
     51              return
     52  
     53          scaled_block_size = self._get_scaled_block_size(scale)
     54 -        self._draw_pixelation(cr, crop, x, y, width, height, scaled_block_size)
     55 +        self._draw_secure_blur(cr, crop, x, y, width, height, scaled_block_size)
     56 +
     57 +    def _draw_secure_blur(self, cr: cairo.Context, crop: dict, x: float, y: float, width: float, height: float, block_size: float):
     58 +        blocks_x = max(2, round(width / block_size))
     59 +        blocks_y = max(2, round(height / block_size))
     60 +
     61 +        cells = cairo.ImageSurface(cairo.FORMAT_ARGB32, blocks_x, blocks_y)
     62 +        cells_cr = cairo.Context(cells)
     63 +        cells_cr.scale(blocks_x / crop['width'], blocks_y / crop['height'])
     64 +        cells_cr.translate(-crop['x'], -crop['y'])
     65 +        Gdk.cairo_set_source_pixbuf(cells_cr, self.background_pixbuf, 0, 0)
     66 +        cells_cr.get_source().set_filter(cairo.FILTER_GOOD)  # box-average when shrinking
     67 +        cells_cr.paint()
     68 +        cells.flush()
     69 +        self._add_noise(cells)
     70 +        cells.mark_dirty()
     71 +
     72 +        # Two smooth upscales (cells -> 4x -> area) read as a blur, not a mosaic.
     73 +        mid_w, mid_h = blocks_x * 4, blocks_y * 4
     74 +        mid = cairo.ImageSurface(cairo.FORMAT_ARGB32, mid_w, mid_h)
     75 +        mid_cr = cairo.Context(mid)
     76 +        mid_cr.scale(4, 4)
     77 +        self._set_smooth_source(mid_cr, cells)
     78 +        mid_cr.paint()
     79  
     80 -    def _draw_pixelation(self, cr: cairo.Context, crop: dict, x: float, y: float, width: float, height: float, block_size: float):
     81          cr.save()
     82          cr.rectangle(x, y, width, height)
     83          cr.clip()
     84 -
     85 -        blocks_x = max(1, int(width / block_size))
     86 -        blocks_y = max(1, int(height / block_size))
     87 -
     88 -        tiny_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, blocks_x, blocks_y)
     89 -        tiny_cr = cairo.Context(tiny_surface)
     90 -
     91 -        tiny_cr.set_operator(cairo.OPERATOR_CLEAR)
     92 -        tiny_cr.paint()
     93 -        tiny_cr.set_operator(cairo.OPERATOR_OVER)
     94 -
     95 -        tiny_cr.scale(blocks_x / crop['width'], blocks_y / crop['height'])
     96 -        tiny_cr.translate(-crop['x'], -crop['y'])
     97 -        Gdk.cairo_set_source_pixbuf(tiny_cr, self.background_pixbuf, 0, 0)
     98 -        tiny_cr.paint()
     99 -
    100          cr.translate(x, y)
    101 -        cr.scale(width / blocks_x, height / blocks_y)
    102 -        pattern = cairo.SurfacePattern(tiny_surface)
    103 -        pattern.set_filter(cairo.FILTER_NEAREST)
    104 -        cr.set_source(pattern)
    105 +        cr.scale(width / mid_w, height / mid_h)
    106 +        self._set_smooth_source(cr, mid)
    107          cr.paint()
    108 -
    109          cr.restore()
    110  
    111 +    @staticmethod
    112 +    def _set_smooth_source(cr: cairo.Context, surface: cairo.ImageSurface):
    113 +        pattern = cairo.SurfacePattern(surface)
    114 +        pattern.set_filter(cairo.FILTER_BILINEAR)
    115 +        pattern.set_extend(cairo.EXTEND_PAD)  # no see-through fringe at the edges
    116 +        cr.set_source(pattern)
    117 +
    118 +    def _add_noise(self, surface: cairo.ImageSurface):
    119 +        rng = random.Random(self.noise_seed)  # fixed per box, so it does not flicker
    120 +        data = surface.get_data()
    121 +        stride = surface.get_stride()
    122 +        for row in range(surface.get_height()):
    123 +            for col in range(surface.get_width()):
    124 +                i = row * stride + col * 4
    125 +                alpha = data[i + 3]
    126 +                shift = rng.gauss(0, self.NOISE_SIGMA)  # brightness only, so it reads as grain
    127 +                for c in range(3):  # premultiplied B, G, R stay within alpha
    128 +                    data[i + c] = min(alpha, max(0, round(data[i + c] + shift)))
    129 +
    130      def _get_image_crop(self) -> dict | None:
    131          img_w, img_h = self.background_pixbuf.get_width(), self.background_pixbuf.get_height()
    132          x1 = int(self.start[0] + img_w / 2)