[Swift] Lecture d'une image jpeg à partir d'un fichier RTFD
J'ai voulu accéder à une image jpeg contenue dans un fichier RTFD. A priori, c'est facile, il suffit de récupérer le NSTextAttachment correspondant et de lire sa propriété .image. ça marche très bien, sauf que là .. non !
Après quelques tâtonnements, j'ai compris que NSTextAttachment fonctionne différemment selon que le texte soit créé en mémoire, ou lu à partir d'un fichier RTFD. Petit programme d'exemple, bricolé dans l'urgence pour tenter de comprendre le truc :
//
// ViewController.swift
// LoadImageRTFD
//
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView(frame: self.view.frame)
self.view.addSubview(textView)
let textSansImageRTFD = loadRTFD("textSansImage")
let ajout = NSTextAttachment()
ajout.image = UIImage(named: "unchat")
textSansImageRTFD.appendAttributedString(NSAttributedString(attachment: ajout))
let textAvecImageRTFD = self.loadRTFD("textAvecImage")
// let test = textSansImageRTFD
let test = textAvecImageRTFD
self.ajusterImage(test)
textView.attributedText = test
}
func ajusterImage(texte:NSMutableAttributedString) {
texte.enumerateAttribute(NSAttachmentAttributeName, inRange: NSMakeRange(0, texte.length), options: NSAttributedStringEnumerationOptions(0)) { (value, range, stop) -> Void in
if let textAttachement = value as? NSTextAttachment {
let image = textAttachement.image
if image == nil {
println("image nil")
println(textAttachement.fileType)
} else {
println(image!.size)
println(textAttachement.fileType)
let newImage = self.resizeImage(image!, reduction: 0.2)
let att = NSTextAttachment()
att.image = newImage
texte.removeAttribute(NSAttachmentAttributeName, range: range)
texte.addAttribute(NSAttachmentAttributeName, value: att, range: range)
}
}
}
}
func loadRTFD(nom:String) -> NSMutableAttributedString {
var error:NSErrorPointer = NSErrorPointer()
let urlFichier = NSBundle.mainBundle().URLForResource(nom, withExtension: "rtfd")
let monRTF = NSMutableAttributedString(fileURL: urlFichier,
options: [NSDocumentTypeDocumentAttribute:NSRTFDTextDocumentType],
documentAttributes: nil,
error: error)
return monRTF!
}
func resizeImage(image: UIImage, reduction: CGFloat) -> UIImage {
let newSize = CGSizeMake(image.size.width*reduction, image.size.height*reduction)
let rect = CGRectMake(0, 0, newSize.width, newSize.height)
UIGraphicsBeginImageContext(newSize)
image.drawInRect(rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
J'ai deux NSMutableAttributedString, l'une avec une image (un chat) ajoutée par le code, et l'autre avec une image(un bonhomme de neige) incluse directement dans le fichier RTFD.
La méthode ajusterImage() est censé parcourir la chaà®ne pour modifier la taille des images rencontrés.
textSansImageRTFD => ajusterImage() trouve bien l'image du chat. fileType contient nil.
textAvecImageRTFD => l'image vaut nil, et fileType contient Optionnel("public.jpeg"). Pourtant le fichier s'affiche correctement dans le UITextView, avec la bonne image (le bonhomme de neige).
Si j'ai bien compris, il faut utiliser fileWrapper pour récupérer l'image, mais je ne trouve pas la bonne manière de s'en servir, depuis .. outch .. 17 heures de l'après-midi !
L'idée est d'ajuster la taille de l'image en fonction du device. Il y a peut-être un moyen plus simple de faire ça. Quelqu'un a une suggestion ?
Allliiiiiiii au secours !
Réponses
Perso je ne suis pas sûr de l'avoir utilisé un jour, mais la doc semble laisser penser que ça ne doit pas être bien sorcier.
En fait, un fichier RTFD n'est rien d'autre qu'un bundle (tu peux faire "Afficher le contenu du paquet" via un clic droit dans le Finder, tu verras), contenant à la fois le texte (fichier "TXT.rtf") et les pièces jointes (images dans des fichiers JPG, etc).
Donc quand tu lis un fichier RTFD dans un NSAttributedString, du coup les NSTextAttachment ne sont que des pointeurs vers les pièces jointes dans le bundle du paquet du "RTFD". C'est ce que sont les NSFileWrapper, d'après ce que je lis dans la doc, ce sont des sortes de "pointeurs vers un emplacement sur disque" (soit un fichier, soit un dossier, soit un lien symbolique, cf le paragraphe d'introduction dans la doc de NSFileWrapper)
Du coup si tu regardes la propriété "fileWrapper" de ton NSTextAttachment, tu devrais y voir un objet NSFileWrapper. Cet objet NSFileWrapper est très certainement un FileWrapper de type "regular file" " puisqu'il va à priori pointer vers un fichier (JPG), et non vers un dossier ou un lien symbolique " dont la propriété regularFile (Bool) va donc certainement retourner true, la propriété "filename" pointera vers le fichier "12345.jpg" qui se trouve dans ton bundle ".rtfd", et la propriété "regularFileContents" devrait alors te permettre d'accéder au contenu (NSData) de ce fichier joint (JPG).
Usage:
PS : Idéalement on devrait même tester si "UTTypeConformsTo(textAttachment.fileType!, kUTTypeImage)" retourne true (NB : il faut "import MobileCoreService" pour pouvoir appeler la fonction UTTypeCOnformsTo) pour s'assurer que le NSTextAttachment est bien un type compatible avec le type "image" (que ce soit du JPEG, PNG, etc)... avant d'essayer d'en faire une UIImage.
Voir cette page de la doc Apple pour + d'infos sur les types UTI.
Bon, au pire, "UIImage(data: fileData)" étant un failable initializer, il retournera nil s'il n'a pas réussi à créer l'image à partir des NSData (mais bon c'est un peu bête d'aller lire tout les NSData de la pièce jointe si on sait d'avance via le fileType que ce n'est pas une image donc tester le fileType peut être une bonne optimisation quand même)
---
PS : Présentation alternative : utiliser une extension pour étendre le type NSTextAttachment et lui rajouter une propriété imageFromFile (plutôt que d'avoir une méthode privée dans ta classe)
Ce qui permet en + ensuite d'utiliser l'opérateur ternaire "??" (équivalent de "?:" en C et Objective-C), ainsi la fonction privée "image(fromAttachment: )" deviendrait tout simplement :
---
Note : aucun de ce code tapé ici n'a été testé, je te laisse ajuster en cas d'erreur de frappe ou autre.
Merci, je regarde ça demain. Là , dodo ..
Du coup, je suis également intéressé par la solution, plus par curiosité qu'autre chose.
On avait déjà parlé ici d'un soucis avec les NSTextAttachment dont les infos étaient différentes en fonction de si elle étaients loadées manuellement ou depuis un URL (via du HTML to NSAttributedString par example).
Une solution un peu plus clean (mais en Objective-C avec des crochets qui te font cauchemarder Draken) sur SO.
Donc ton problème relève peut-être d'une nouvelle subtilité dans la récupération des image sur un NSAttributedString, donc ça titille ma curiosité tout ça.
ça marche !
Y compris quand l'image est du .png . C'était logique en regardant le code, mais rien ne vaut un vrai test en situation réelle.
Question subsidiaire : y a-t-il un moyen de lire la taille de l'image sans la charger entièrement ? j'ai trouvé le moyen d'ajuster la taille d'un NSTextAttachment sans redimensionner l'image, en jouant sur la propriété bounds.
Cet exemple ne marche correctement qu'avec des images ayant un rapport largeur/hauteur de 1,5. Pour que cela fonctionne avec des images de n'importe quelle dimension j'ai besoin de connaà®tre leurs tailles. D'où ma question: est-il possible de lire la taille d'un fichier image sans devoir le charger entièrement en mémoire ?
J'ai écrit une extension pour récupérer la taille d'un NSTextAttachment.
Ce qui allége considérablement le code pour ajuster l'affichage du NSTextAttachment :
Pourquoi donc ne pas avoir utilisé l'extension de NSTextAttachment que j'ai proposé quelques posts plus haut (à la fin du #2) pour récupérer la "UIImage?" ? Et ensuite tu as la UIImage, tu peux en faire ce que tu veux, dont récupérer sa CGSize (image?.size) mais aussi avoir directement l'image pour la redimensionner.
Car sinon avec ton extension tu lis l'image sur disque juste pour en extraire la CGSize, alors qu'ailleurs tu vas avoir besoin de l'image elle-même et va être obligé d'aller la relire sur disque, c'est un peu dommage !
Rome ne s'est pas construite en un jour. ça avance petit à petit. La preuve :
Un petit pas pour l'homme, un grand pour moi. Bon, reste plus qu'à connecter ça à un NSTextAttachment.
Finish ! * croise les doigts *
J'ai passé un temps fou à essayer de récupérer l'url du NSTextAttachment, alors qu'il suffisait de créer une imageSource à partir de fileWrapper.regularFileContents.
Note pour les personnes voulant utiliser ce code: ne pas oublier d'importer le framework imageIO.
1) Comme dit plus haut, c'est pas malin de récupérer fileWrapper.regularFileContents (qui va aller lire le contenu du fichier sur disque, donc charger en mémoire l'intégralité du NSData représentant le contenu du disque) dans la méthode "size" au lieu de l'exposer séparément, car là ton code ne calcule que la taille, et donc quand ailleurs tu vas avoir ton code qui va avoir vraiment besoin de l'image (pour la redimensionner, etc), tu vas devoir aller la relire sur le disque !
Pourquoi ne pas segmenter tout ça pour permettre d'accéder à chaque propriété, en particulier à chaque étape intermédiaire, celle qui récupère la NSData, celle qui en extrait la size, et celle qui en extrait l'UIImage, pour qu'ainsi tu n'aies pas à dupliquer le code qui va lire le NSData depuis le disque dans plusieurs méthodes (une pour avoir la CGSize, l'autre pour avoir la UIImage) ?
2) Pourquoi ne pas faire des lazy vars, qui sont en plus carrément tout à fait adapté à cet usage, et vont permettre d'éviter de réexécuter le code qui va lire le contenu sur le disque à chaque fois, alors que quand tu l'as lu une fois c'est pas la peine d'aller le relire à chaque fois que tu vas accéder à la propriété sinon (Bon ceci dit, j'ai un doute, je ne suis pas sûr qu'on puisse faire une "lazy var"... dans une "extension", à vérifier)
[EDIT] Bon je viens de tester, et effectivement on ne peut pas faire de "lazy vars" dans une "extension", ce qui semble normal finalement car cela consisterait à rajouter une stored property sous le capot, ce qu'on ne peut pas faire avec les extensions (tout comme on ne peut pas rajouter de variable d'instance à une catégorie en ObjC)
3) Quel est l'intérêt d'utiliser ImageIO si c'est pour lire les NSData depuis le disque ? L'intérêt d'utiliser ImageIO (plutôt que d'aller lire les NSData sur le disque, la transformer en UIImage et extraire sa size), c'est justement de ne pas à avoir à lire TOUT le fichier JPEG ou PNG ni à le charger *entièrement* en mémoire juste pour déterminer la taille ! Or là c'est justement ce que tu fais, tu charges toute la NSData en mémoire, du coup utiliser ImageIO derrière (comparativement à "UIImage(data:...)?.size") perd totalement de son intérêt !
Dans ton post #10, le seul truc c'est que tu construis ton URL avec "NSBundle.mainBundle().URLForResource(...)"... comme si le fichier en question était dans les ressources de ton MainBundle (donc dans les ressources de ton propre projet), alors que le fichier que tu cherches à lire c'est un fichier qui est à l'intérieur du paquet ".rtfd", rien à voir avec le bundle de ton app.
Au lieu de ça, il suffit d'utiliser la propriété "filename" du NSFileWrapper associé à ton NSTextAttachment), qui te donne un chemin sur le disque (String), et de le transformer en NSURL via la méthode de classe NSURL.fileURLWithPath().
Utilisation :
Je ne m'occupe pas de l'affichage, je balance un NSAttributedString à un UITextView qui se débrouille tout seul, avec ces propres optimisations. Je présume (peut-être naà¯vement) qu'il est préférable de donner les NSTextAttachment sous forme de référence vers des images disque, que sous forme d'une UIImage. L'idée est d'afficher un long texte dans un UITextView avec plusieurs images.
J'avais cru comprendre qu'une CGImageSource était une référence vers un fichier, et non le fichier complet. Ce n'est peut-être le cas que pour CGImageSourceCreateWithURL. Je n'avais pas compris que fileWrapper.regularFileContents chargeait l'intégralité du fichier en mémoire.
J'ai testé les deux techniques sur mon iPhone 4. Il n'y a aucune différence visible entre les deux méthodes de chargement, même avec une quinzaine d'images dans un texte copieux.
Si tu penses que ce n'est pas une bonne idée, je laisse tomber pour suivre ton conseil et convertir les images en UIImage. J'espérais vraiment arriver à lire la taille d'un NSTextAttachment sans charger toute l'image, pour laisser le UITextView s'occuper du chargement à sa sauce, forcément plus optimisé que la mienne, mais tant pis. Je ne vais pas perdre des jours sur ce problème.
EDIT : Tu as posté alors que j'écrivais ma réponse. J'ai essayé d'utiliser filename, mais il semblais ne donner qu'un nom interne au package RTFD et non un path complet. D'ailleurs, les posts traitant du sujet dénichés par Google semblent arriver à la même conclusion que moi. Manifestement, nous, les noobs, sommes légions sur le net.
Merci d'avoir répondu à mon problème. Je ne serais jamais à l'aise avec la programmation système, c'est évident.
Ca ne m'étonne pas trop en pratique, car même sur un iPhone 4, la lecture sur disque est certainement rapide dans l'ensemble. Par contre je serait curieux de voir ce que ça donne si tu lis un fichier RTFD... qui contient des *grosses* images (surtout si elles sont en JPG, qui est moins optimisé sur iOS que le PNG à ma connaissance, qui lui est le format de prédilection pour iOS) ; genre un RTFD dont les fichiers images qui se trouvent dans son paquet font plusieurs Mo (et qu'il y en a plusieurs, de surcroit).
Après, ça doit pas non plus arriver tous les jours et du coup vu que dans la plupart des cas les fichiers image à lire sont de taille modérée et que la lecture sur disque reste pas non plus si longue que ça, ça ne doit pas non plus se ressentir un max si tu relis le fichier du disque en entier à chaque fois. Mais dans ce cas je ne vois pas l'intérêt d'aller se compliquer la vie avec ImageIO (alors que le code pour utiliser ImageIO est un peu plus dur à appréhender que directement utiliser UIImage), alors que le but de passer à ImageIO était justement d'éviter d'aller lire tout le fichier sur disque justement !
Bah justement non, c'est une bonne idée d'essayer de lire la taille de l'image sans avoir à charger toute l'image, mais ce que je disais c'est que justement là ton code précédent bah il chargeait toute l'image. Si tu veux éviter de charger toute l'image pour avoir sa taille, il ne faut ni utiliser UIImage ni CGImageSourceCreateWithData, mais utiliser CGImageSourceCreateWithURL.
Ah. En effet, j'avoue ne pas du tout avoir testé le code que j'ai écrit ici.
Bon, bah en effet faut le savoir que filename n'est que le nom du fichier et pas son path " en même temps j'aurais quand même pu mieux lire la doc et surtout m'en douter, la propriété s'appelle "filename" (soit "nom du fichier") et pas filepath, après tout.
Mais qu'à cela ne tienne, maintenant qu'on sait ça, il suffit de construire la NSURL en conséquence qui va pointer sur ce fichier. C'est donc la NSURL pointant sur ton fichier RTFD (genre "NSURL.fileURLWithPath(pathToRTFDFile)"), à laquelle tu concatènes ("URLByAppendingPathComponent(...)") ce fameux "filename".
Comme ça tu obtiens une NSURL (qui est une URL représentant un chemin sur ton disque, donc sans doute du type "file:///Users/toi/Documents/...") qui pointe vers le fichier nommé "filename" à l'intérieur du bundle ".rtfd" en question, et tu peux ensuite utiliser avec CGImageSourceCreateWithURL.
Je viens de m'y remettre. ça marche en ajoutant le path du fichier RTFD "container" au nom interne dans le paquet. Merci Ali !