Tester le recadrage iOS : de la sélection à ses limites en pixels

Tester le recadrage iOS : de la sélection à ses limites en pixels

Tester le recadrage iOS : de la sélection à ses limites en pixels

Vous affichez une image de 4000×3000 pixels dans une vue de recadrage de 300×300 points, puis sélectionnez la zone centrale. Pourtant, après l’export, le sujet apparaît décalé vers le haut. Avant de modifier le code de gestion des gestes, vérifiez les marges : l’affichage avec ajustement proportionnel laisse 37.5 points vides en haut et en bas. Une conversion fondée sur les dimensions de la vue entière fausse à la fois l’origine et la hauteur de la sélection. Sur un Mac dans le cloud, validez d’abord les coordonnées en Swift pur, puis raccordez l’interface : vous pourrez ainsi distinguer les erreurs de calcul des erreurs de rendu.

Définir un contrat entre les trois repères

Distinguez trois objets : le rectangle de l’image source en pixels, le rectangle réellement occupé par l’image dans la vue et la sélection tracée par l’utilisateur. Les deux derniers doivent être exprimés dans le même repère : l’un ne peut pas être relatif à la vue parente et l’autre au conteneur de l’image.

Cet article traite uniquement de l’affichage avec ajustement proportionnel, sans remplissage avec rognage, rotation ni perspective. L’origine se trouve en haut à gauche, l’axe horizontal pointe vers la droite et l’axe vertical vers le bas. L’image d’entrée est déjà normalisée selon son orientation d’affichage ; sa largeur et sa hauteur correspondent à ses dimensions réelles en pixels, et non aux dimensions en points de l’objet image.

Le facteur d’échelle de l’écran n’est pas le facteur d’échelle de l’image. Multiplier la sélection par le facteur de l’écran ne donne un résultat correct que pour des dimensions qui coïncident par hasard. Ce n’est pas une formule générale de recadrage.

Si l’interface comporte des vues imbriquées, utilisez d’abord les API de conversion de coordonnées pour exprimer la sélection dans le repère du conteneur de l’image, puis appelez la fonction ci-dessous. Si des gestes de zoom sont possibles, il faut également annuler au préalable les transformations de translation et de mise à l’échelle. Un rectangle transformé ne peut pas être injecté directement dans cette formule statique.

Calculer le rectangle affiché et les coordonnées en pixels

Enregistrez le contenu suivant dans CropCheck.swift. Si la chaîne d’outils Xcode complète est disponible, exécutez xcrun swift CropCheck.swift dans le terminal. Le code dépend uniquement de Foundation, fourni par le système ; il n’accède pas au réseau et ne nécessite aucun simulateur.

import Foundation

func cropPixels(
    source: CGSize,
    viewport: CGSize,
    selection: CGRect
) -> CGRect? {
    let values = [
        source.width, source.height,
        viewport.width, viewport.height,
        selection.origin.x, selection.origin.y,
        selection.width, selection.height
    ]
    guard values.allSatisfy({ $0.isFinite }),
          source.width > 0, source.height > 0,
          viewport.width > 0, viewport.height > 0,
          source.width.rounded(.down) == source.width,
          source.height.rounded(.down) == source.height,
          selection.width > 0, selection.height > 0
    else {
        return nil
    }

    let scale = min(
        viewport.width / source.width,
        viewport.height / source.height
    )
    guard scale.isFinite, scale > 0 else {
        return nil
    }

    let display = CGRect(
        x: (viewport.width - source.width * scale) / 2,
        y: (viewport.height - source.height * scale) / 2,
        width: source.width * scale,
        height: source.height * scale
    )
    let clipped = selection.intersection(display)
    guard !clipped.isNull, !clipped.isEmpty else {
        return nil
    }

    let left = max(
        0, floor((clipped.minX - display.minX) / scale)
    )
    let top = max(
        0, floor((clipped.minY - display.minY) / scale)
    )
    let right = min(
        source.width,
        ceil((clipped.maxX - display.minX) / scale)
    )
    let bottom = min(
        source.height,
        ceil((clipped.maxY - display.minY) / scale)
    )
    guard right > left, bottom > top else {
        return nil
    }

    return CGRect(
        x: left, y: top,
        width: right - left, height: bottom - top
    )
}

L’ordre des opérations est impératif : calculez d’abord l’intersection de la sélection avec la zone d’affichage de l’image, soustrayez ensuite le décalage de cette zone, puis divisez par le facteur d’échelle. Arrondir d’abord les coordonnées dans la vue amplifierait l’erreur en fonction du rapport entre les dimensions de l’image source et celles de la vue.

Les limites sont arrondies vers l’extérieur afin d’inclure tous les pixels touchés par la sélection. Cela ne garantit pas que les dimensions mathématiques de la sélection restent strictement inchangées. Si le produit exige des dimensions de sortie fixes, le redimensionnement à l’export doit constituer une étape distincte, effectuée ensuite.

Détecter les décalages avec des exemples vérifiables

Ajoutez les assertions suivantes à la suite, dans le même fichier. Une exécution réussie ne produit aucune sortie ; si une assertion échoue, le processus se termine avec un statut d’échec. N’exécutez pas ce script de validation avec des options d’optimisation qui suppriment les assertions.

let source = CGSize(width: 4000, height: 3000)
let viewport = CGSize(width: 300, height: 300)

func mapped(_ rectangle: CGRect) -> CGRect? {
    cropPixels(
        source: source,
        viewport: viewport,
        selection: rectangle
    )
}

assert(
    mapped(CGRect(x: 75, y: 75, width: 150, height: 150))
    == CGRect(x: 1000, y: 500, width: 2000, height: 2000)
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 300, height: 300))
    == CGRect(x: 0, y: 0, width: 4000, height: 3000)
)

assert(
    mapped(CGRect(x: 10, y: 0, width: 20, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 0, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 225, y: 225, width: 100, height: 100))
    == CGRect(x: 3000, y: 2500, width: 1000, height: 500)
)

L’exemple central se vérifie à la main : le facteur d’affichage vaut 0.075. Une fois la marge supérieure déduite, l’origine verticale de la sélection se situe à 37.5 points, soit une coordonnée verticale de 500 pixels dans l’image source. Le dernier exemple dépasse à la fois le bord droit et le bord inférieur. Il démontre que la fonction élimine bien toute la partie hors limites, au lieu de simplement borner l’origine.

Ajoutez des cas avec une image en orientation portrait, un conteneur non carré et une sélection à coordonnées fractionnaires. Pour tout résultat valide, vérifiez que la largeur et la hauteur sont strictement positives, que les limites restent dans l’image source et que les quatre bords ont des coordonnées entières. Ici, les entrées dont la largeur ou la hauteur est négative sont rejetées directement ; la couche d’interface doit normaliser la sélection en fonction du sens du glissement en amont.

Vérifier séparément le calcul et l’export réel

Une fois les tests mathématiques réussis, préparez une image de test aux dimensions fixes en pixels, avec une couleur différente à chaque coin et une croix au centre. Effectuez d’abord l’export dans un format sans perte, afin que la compression avec perte ne perturbe pas les assertions sur les couleurs.

Contrôlez trois résultats : les dimensions de l’image exportée correspondent à celles renvoyées par la fonction ; un recadrage du coin inférieur droit ne contient aucun repère du coin supérieur gauche ; une sélection de l’image entière n’ajoute aucune bordure transparente. Ne comparez pas uniquement les octets des fichiers : une modification des métadonnées ajoutées par l’encodeur ne signifie pas nécessairement que les pixels ont changé.

Si l’outil de recadrage travaille directement sur l’image en pixels dont l’orientation a été normalisée, transmettez-lui le rectangle en pixels calculé ici. S’il redessine l’image dans un contexte graphique, vérifiez séparément la position de l’origine, le sens des axes et le paramètre d’échelle de sortie. L’erreur supplémentaire la plus courante consiste à multiplier encore une fois par le facteur d’affichage dans le code d’export, alors que la fonction a déjà renvoyé des pixels.

Le chargement asynchrone des images doit lui aussi être testé : si une image source de dimensions différentes remplace l’image après que l’utilisateur a tracé sa sélection, il faut recalculer celle-ci ou l’invalider. Ne combinez pas le facteur d’échelle de l’ancienne image avec les dimensions en pixels de la nouvelle.

Transformer les échecs en cas reproductibles

Pour chaque échec, conservez la largeur et la hauteur en pixels de l’image source, les dimensions du conteneur en points, le rectangle de sélection, le mode d’affichage effectivement utilisé et le résultat du calcul. Les valeurs numériques suffisent ; il n’est pas nécessaire de téléverser les photos des utilisateurs. Pour reproduire le problème, remplacez-les par une image de test de mêmes dimensions.

Avant de fusionner les modifications, couvrez au minimum six catégories d’entrées : image en paysage, image en portrait, sélection de l’image entière, sélection limitée aux marges vides, dépassement des bords et limites fractionnaires. Après un changement de disposition, relisez impérativement les dimensions du conteneur au lieu de réutiliser celles mémorisées avant la rotation.

La validation finale comporte deux niveaux : la fonction pure vérifie la relation entre les coordonnées, et les tests d’export vérifient l’orientation et les dimensions des pixels. Une fois ces deux niveaux validés, contrôlez le comportement du glisser à l’aide du bureau à distance. Ainsi, les ajustements de l’interface, le remplacement de l’image source et les modifications de l’export disposent chacun d’un point de défaillance clairement identifiable.

Questions fréquentes

Faut-il multiplier les coordonnées par l’échelle de l’écran ?

Pas directement. Calculez le rapport entre les dimensions en pixels de la source et le rectangle réellement affiché. L’échelle de l’écran ne donne pas ce rapport.

Pourquoi arrondir les limites vers l’extérieur ?

Arrondir les minima vers le bas et les maxima vers le haut couvre les pixels touchés par la sélection. Limitez ensuite le résultat aux dimensions de la source.

Exécutez vos projets sur un Mac physique dédié

Utilisez un Mac dans le cloud pour compiler, tester et travailler à distance sur le bureau. Choisissez la configuration et la période de facturation adaptées à votre projet. La disponibilité effective est indiquée en temps réel dans la console.

Pour la suite, vérifiez votre environnement

Comparez d’abord les configurations, puis choisissez votre mode d’accès. La ligne de commande SSH et le bureau VNC conviennent respectivement à la compilation et au débogage interactif.