Merge pull request #1303 from PicPeak/fix/1300-remove-fragmentation

fix: remove the image-fragmentation surface
This commit is contained in:
Paul Nothaft
2026-09-05 23:37:14 +02:00
committed by GitHub
20 changed files with 9 additions and 281 deletions
@@ -12,9 +12,6 @@ interface ProtectedImageProps extends React.CanvasHTMLAttributes<HTMLCanvasEleme
alt: string;
protectionLevel?: ProtectionLevel;
watermarkText?: string;
fragmentGrid?: boolean;
gridSize?: number;
scrambleFragments?: boolean;
invisibleWatermark?: boolean;
onProtectionViolation?: (violationType: string) => void;
fallbackSrc?: string;
@@ -26,9 +23,6 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
alt,
protectionLevel = 'standard',
watermarkText,
fragmentGrid = false,
gridSize = 4,
scrambleFragments = false,
invisibleWatermark = false,
onProtectionViolation,
fallbackSrc,
@@ -120,52 +114,6 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
ctx.shadowOffsetY = 0;
}, []);
// Fragment and scramble image for maximum protection
const renderFragmentedImage = useCallback((
ctx: CanvasRenderingContext2D,
img: HTMLImageElement,
width: number,
height: number
) => {
const fragmentWidth = width / gridSize;
const fragmentHeight = height / gridSize;
const fragments: Array<{ x: number; y: number; destX: number; destY: number }> = [];
// Create fragment map
for (let row = 0; row < gridSize; row++) {
for (let col = 0; col < gridSize; col++) {
fragments.push({
x: col * fragmentWidth,
y: row * fragmentHeight,
destX: col * fragmentWidth,
destY: row * fragmentHeight,
});
}
}
// Scramble fragments if requested
if (scrambleFragments) {
for (let i = fragments.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
const temp = fragments[i].destX;
const tempY = fragments[i].destY;
fragments[i].destX = fragments[j].destX;
fragments[i].destY = fragments[j].destY;
fragments[j].destX = temp;
fragments[j].destY = tempY;
}
}
// Draw fragments
fragments.forEach(fragment => {
ctx.drawImage(
img,
fragment.x, fragment.y, fragmentWidth, fragmentHeight,
fragment.destX, fragment.destY, fragmentWidth, fragmentHeight
);
});
}, [gridSize, scrambleFragments]);
// Main canvas rendering function - wrapped in useCallback to prevent infinite re-renders
const renderToCanvas = useCallback(() => {
if (!canvasRef.current || !imageRef.current) {
@@ -199,14 +147,9 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation
try {
if (fragmentGrid && (protectionLevel === 'enhanced' || protectionLevel === 'maximum')) {
// Render fragmented image
renderFragmentedImage(ctx, img, canvas.width, canvas.height);
} else {
// Render normal image - ensure image is valid before drawing
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
// Ensure image is valid before drawing
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
// Apply watermarks
@@ -242,7 +185,7 @@ export const ProtectedImage: React.FC<ProtectedImageProps> = ({
reportViolation('canvas_rendering_error');
setError(true);
}
}, [fragmentGrid, protectionLevel, renderFragmentedImage, watermarkText, invisibleWatermark, applyInvisibleWatermark, applyVisibleWatermark, reportViolation]);
}, [protectionLevel, watermarkText, invisibleWatermark, applyInvisibleWatermark, applyVisibleWatermark, reportViolation]);
// Set up protection event listeners
useEffect(() => {
@@ -128,25 +128,6 @@ describe('ProtectedImage', () => {
expect(mockContext.fillText).toHaveBeenCalled();
});
it('handles fragment grid rendering', async () => {
render(
<ProtectedImage
{...defaultProps}
fragmentGrid={true}
gridSize={4}
protectionLevel="enhanced"
/>
);
await waitFor(() => {
const canvas = screen.getByRole('img', { name: 'Test image' });
expect(canvas).toHaveStyle({ opacity: '1' });
});
// Verify multiple drawImage calls for fragments
expect(mockContext.drawImage).toHaveBeenCalled();
});
it('blocks interactions in maximum protection mode', async () => {
const onViolation = vi.fn();
@@ -230,25 +211,6 @@ describe('ProtectedImage', () => {
expect(mockContext.putImageData).toHaveBeenCalled();
});
it('scrambles fragments when enabled', async () => {
render(
<ProtectedImage
{...defaultProps}
fragmentGrid={true}
scrambleFragments={true}
protectionLevel="maximum"
/>
);
await waitFor(() => {
const canvas = screen.getByRole('img', { name: 'Test image' });
expect(canvas).toHaveStyle({ opacity: '1' });
});
// Fragment scrambling should result in multiple drawImage calls
expect(mockContext.drawImage).toHaveBeenCalled();
});
it('adds random noise in maximum protection', async () => {
render(
<ProtectedImage
@@ -15,7 +15,6 @@ interface ImageSecuritySettings {
max_image_requests_per_hour: number;
suspicious_activity_threshold: number;
enable_canvas_rendering: boolean;
default_fragmentation_level: number;
security_monitoring_enabled: boolean;
block_suspicious_ips: boolean;
log_security_events_to_db: boolean;
@@ -31,7 +30,6 @@ const defaultSettings: ImageSecuritySettings = {
max_image_requests_per_hour: 500,
suspicious_activity_threshold: 10,
enable_canvas_rendering: false,
default_fragmentation_level: 3,
security_monitoring_enabled: true,
block_suspicious_ips: true,
log_security_events_to_db: true,
@@ -161,21 +159,6 @@ export const ImageSecurityTab: React.FC = () => {
/>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.imageQualityHelp', '1-100, higher = better quality')}</p>
</div>
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('settings.imageSecurity.fragmentationLevel', 'Fragmentation Level')}
</label>
<input
type="number"
min="1"
max="10"
value={settings.default_fragmentation_level}
onChange={(e) => handleChange('default_fragmentation_level', parseInt(e.target.value) || 3)}
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('settings.imageSecurity.fragmentationLevelHelp', '1-10, higher = more protection')}</p>
</div>
</div>
<div className="space-y-3 pt-2">
-1
View File
@@ -20,7 +20,6 @@ interface UseImageProtectionOptions {
blockKeyboardShortcuts?: boolean;
detectPrintScreen?: boolean;
watermarkText?: string;
fragmentGrid?: boolean;
}
export const useImageProtection = (options: UseImageProtectionOptions) => {
-2
View File
@@ -1852,7 +1852,6 @@
"defaultProtectionHelp": "Diese Einstellungen gelten für alle neuen Veranstaltungen. Einzelne Veranstaltungen können diese Standardwerte überschreiben.",
"protectionLevel": "Standard-Schutzstufe",
"imageQuality": "Standard-Bildqualität",
"fragmentationLevel": "Fragmentierungsstufe",
"enableDevtools": "DevTools-Erkennung standardmäßig aktivieren",
"enableCanvas": "Canvas-Rendering standardmäßig aktivieren (erweiterter Schutz)",
"rateLimiting": "Ratenbegrenzung",
@@ -1869,7 +1868,6 @@
"infoTitle": "Über Bildschutz",
"infoText": "Diese Schutzfunktionen helfen, gelegentliches Herunterladen und Kopieren zu verhindern, können aber nicht alle Methoden blockieren. Entschlossene Benutzer finden möglicherweise trotzdem Wege, Bilder zu erfassen. Erwägen Sie die Verwendung von Wasserzeichen und rechtlichen Vereinbarungen für umfassenden Schutz.",
"imageQualityHelp": "1100, höher = bessere Qualität",
"fragmentationLevelHelp": "110, höher = mehr Schutz",
"suspiciousActivityThresholdHelp": "Verstöße, bevor als verdächtig markiert wird",
"autoBlockThresholdHelp": "Verstöße, bevor die IP automatisch gesperrt wird"
},
-2
View File
@@ -1415,7 +1415,6 @@
"defaultProtectionHelp": "These settings apply to all new events. Individual events can override these defaults.",
"protectionLevel": "Default Protection Level",
"imageQuality": "Default Image Quality",
"fragmentationLevel": "Fragmentation Level",
"enableDevtools": "Enable DevTools detection by default",
"enableCanvas": "Enable canvas rendering by default (advanced protection)",
"rateLimiting": "Rate Limiting",
@@ -1432,7 +1431,6 @@
"infoTitle": "About Image Protection",
"infoText": "These protection features help prevent casual downloading and copying but cannot block all methods. Determined users may still find ways to capture images. Consider using watermarks and legal agreements for comprehensive protection.",
"imageQualityHelp": "1-100, higher = better quality",
"fragmentationLevelHelp": "1-10, higher = more protection",
"suspiciousActivityThresholdHelp": "Violations before flagging as suspicious",
"autoBlockThresholdHelp": "Violations before auto-blocking IP"
},
-1
View File
@@ -941,7 +941,6 @@
"defaultProtectionHelp": "Estos ajustes se aplican a todos los nuevos eventos. Los eventos individuales pueden anular estos valores por defecto.",
"protectionLevel": "Nivel de protección por defecto",
"imageQuality": "Calidad de imagen por defecto",
"fragmentationLevel": "Nivel de fragmentación",
"enableDevtools": "Habilitar detección de DevTools por defecto",
"enableCanvas": "Habilitar renderizado con canvas por defecto (protección avanzada)",
"rateLimiting": "Limitación de velocidad",
-1
View File
@@ -911,7 +911,6 @@
"defaultProtectionHelp": "Ces paramètres s'appliquent à tous les nouveaux événements. Les événements individuels peuvent remplacer ces valeurs par défaut.",
"protectionLevel": "Niveau de protection par défaut",
"imageQuality": "Qualité d'image par défaut",
"fragmentationLevel": "Niveau de fragmentation",
"enableDevtools": "Activer la détection des outils DevTools par défaut",
"enableCanvas": "Activer le rendu canvas par défaut (protection avancée)",
"rateLimiting": "Limitation de débit",
-1
View File
@@ -907,7 +907,6 @@
"defaultProtectionHelp": "Deze instellingen zijn van toepassing op alle nieuwe evenementen. Individuele evenementen kunnen deze standaardwaarden overschrijven.",
"protectionLevel": "Standaard beveiligingsniveau",
"imageQuality": "Standaard beeldkwaliteit",
"fragmentationLevel": "Fragmentatieniveau",
"enableDevtools": "Standaard DevTools-detectie inschakelen",
"enableCanvas": "Standaard canvas-rendering inschakelen (geavanceerde beveiliging)",
"rateLimiting": "Snelheidsbeperking",
-1
View File
@@ -924,7 +924,6 @@
"defaultProtectionHelp": "Aplicado a novos eventos por padrão. Podem ser substituídas individualmente.",
"protectionLevel": "Nível de Proteção Padrão",
"imageQuality": "Qualidade de Imagem Padrão",
"fragmentationLevel": "Nível de Fragmentação",
"enableDevtools": "Ativar detecção de DevTools por padrão",
"enableCanvas": "Ativar renderização em Canvas por padrão",
"rateLimiting": "Limite de Requisições",
-1
View File
@@ -930,7 +930,6 @@
"defaultProtectionHelp": "Эти настройки применяются ко всем новым событиям. Отдельные события могут их переопределить.",
"protectionLevel": "Уровень защиты по умолчанию",
"imageQuality": "Качество изображений по умолчанию",
"fragmentationLevel": "Уровень фрагментации",
"enableDevtools": "Включить обнаружение DevTools по умолчанию",
"enableCanvas": "Включить рендеринг Canvas по умолчанию (расширенная защита)",
"rateLimiting": "Ограничение скорости",
-1
View File
@@ -911,7 +911,6 @@
"defaultProtectionHelp": "Te nastavitve veljajo za vse nove dogodke. Posamezni dogodki lahko te privzete nastavitve prepišejo.",
"protectionLevel": "Privzeta raven zaščite",
"imageQuality": "Privzeta kakovost slike",
"fragmentationLevel": "Raven fragmentacije",
"enableDevtools": "Privzeto omogoči zaznavanje DevTools",
"enableCanvas": "Privzeto omogoči izris prek canvas (napredna zaščita)",
"rateLimiting": "Omejevanje hitrosti",
-1
View File
@@ -290,7 +290,6 @@ export interface GalleryData {
image_quality?: number;
use_canvas_rendering?: boolean;
enable_devtools_protection?: boolean;
fragmentation_level?: number;
overlay_protection?: boolean;
// Hero logo customization fields
hero_logo_visible?: boolean | null;
-15
View File
@@ -49,7 +49,6 @@ export interface ImageProtectionOptions {
blockKeyboardShortcuts?: boolean;
detectPrintScreen?: boolean;
watermarkText?: string;
fragmentGrid?: boolean;
}
export interface ProtectedImageProps {
@@ -57,9 +56,6 @@ export interface ProtectedImageProps {
alt: string;
protectionLevel?: ProtectionLevel;
watermarkText?: string;
fragmentGrid?: boolean;
gridSize?: number;
scrambleFragments?: boolean;
invisibleWatermark?: boolean;
onProtectionViolation?: (violationType: ViolationType) => void;
fallbackSrc?: string;
@@ -83,13 +79,6 @@ export interface WatermarkConfig {
rotation: number;
}
export interface FragmentConfig {
enabled: boolean;
gridSize: number;
scramble: boolean;
randomSeed?: number;
}
export interface SteganographyConfig {
enabled: boolean;
message: string;
@@ -121,7 +110,6 @@ export interface CanvasProtectionContext {
originalImageData: ImageData;
protectedImageData: ImageData;
watermarkApplied: boolean;
fragmentsScrambled: boolean;
}
export interface PrintScreenDetectionState {
@@ -181,7 +169,6 @@ export interface ProtectionConfig {
rendering: {
canvas: {
enabled: boolean;
fragmentGrid: FragmentConfig;
watermark: WatermarkConfig;
steganography: SteganographyConfig;
noiseInjection: boolean;
@@ -231,9 +218,7 @@ export type ProtectionProps = {
export type CanvasProtectionProps = ProtectionProps & {
useCanvasRendering?: boolean;
fragmentGrid?: boolean;
watermarkText?: string;
scrambleFragments?: boolean;
invisibleWatermark?: boolean;
};