Nous rendons Shrimp encore plus utile: ajoutez le transcodage d'images à d'autres formats



Depuis début 2017, notre petite équipe développe la bibliothèque RESTinio OpenSource pour embarquer un serveur HTTP dans des applications C ++. À notre grande surprise, nous recevons de temps en temps des questions de la catégorie «Et pourquoi un serveur HTTP C ++ intégré pourrait-il être nécessaire?» Malheureusement, les questions simples sont les plus difficiles à répondre. Parfois, la meilleure réponse est un exemple de code.

Il y a quelques mois, nous avons lancé un petit projet de démonstration, Shrimp , qui illustre clairement un scénario typique, dans lequel notre bibliothèque est «affûtée». Le projet de démonstration est un simple service Web qui reçoit des demandes de mise à l'échelle des images stockées sur le serveur et qui renvoie une image de la taille dont l'utilisateur a besoin.

Ce projet de démonstration est bon dans la mesure où, premièrement, il nécessite une intégration avec du code écrit et fonctionnant correctement en C ou C ++ (dans ce cas, ImageMagick). Par conséquent, il devrait être clair pourquoi il est judicieux d'incorporer le serveur HTTP dans une application C ++.

Et deuxièmement, dans ce cas, le traitement des demandes asynchrones est nécessaire pour que le serveur HTTP ne se bloque pas pendant la mise à l'échelle de l'image (et cela peut prendre des centaines de millisecondes ou même des secondes). Et nous avons commencé le développement de RESTinio précisément parce que nous ne pouvions pas trouver un serveur embarqué sain C ++ axé spécifiquement sur le traitement des demandes asynchrones.

Nous avons construit le travail sur Shrimp de manière itérative: d'abord, la version la plus simple a été faite et décrite , qui ne faisait que redimensionner les images. Ensuite, nous avons corrigé un certain nombre de lacunes de la première version et décrit cela dans le deuxième article . Enfin, nous nous sommes déplacés pour étendre encore une fois la fonctionnalité de Shrimp: la conversion d'images d'un format à un autre a été ajoutée. Comment cela a été fait et sera discuté dans cet article.

Prise en charge du format cible


Ainsi, dans la prochaine version de Shrimp, nous avons ajouté la possibilité de donner une image à l'échelle dans un format différent. Donc, si vous émettez une demande de crevettes du formulaire:

curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920" 

puis Shrimp rendra l'image au même format JPG que l'image originale.

Mais si vous ajoutez le paramètre de format cible à l'URL, Shrimp convertit l'image au format cible spécifié. Par exemple:

 curl "http://localhost:8080/my_picture.jpg?op=resize&max=1920&target-format=webp" 

Dans ce cas, Shrimp rendra l'image au format webp.

Shrimp mis à jour prend en charge cinq formats d'image: jpg, png, gif, webp et heic (également connu sous le nom de HEIF). Vous pouvez expérimenter différents formats sur une page Web spéciale :



(sur cette page, il n'y a aucun moyen de sélectionner le format heic, car les navigateurs de bureau ordinaires ne prennent pas en charge ce format par défaut).

Afin de prendre en charge le format cible dans Shrimp, il a été nécessaire de modifier légèrement le code Shrimp (ce qui nous a surpris nous-mêmes, car il y avait vraiment peu de changements). Mais d'un autre côté, j'ai dû jouer avec l'assemblage d'ImageMagick, ce qui nous a encore plus surpris, comme Plus tôt, nous avons dû composer avec cette cuisine, par un heureux hasard. Mais parlons de tout dans l'ordre.

ImageMagick doit comprendre différents formats


ImageMagick utilise des bibliothèques externes pour encoder / décoder les images: libjpeg, libpng, libgif, etc. Ces bibliothèques doivent être installées sur le système avant que ImageMagick ne soit configuré et construit.

La même chose devrait se produire pour qu'ImageMagick prenne en charge les formats webp et heic: vous devez d'abord construire et installer libwebp et libheif, puis configurer et installer ImageMagick. Et si tout est simple avec libwebp, alors autour de libheif j'ai du danser avec un tambourin. Bien qu'après un certain temps, après que tout se soit enfin réuni et ait fonctionné, ce n'était déjà pas clair: pourquoi avez-vous dû recourir à un tambourin, tout semble être trivial? ;)

En général, si quelqu'un veut se lier d'amitié avec heic et ImageMagick, vous devrez installer:


C'est dans cet ordre (vous devrez peut-être installer nasm pour que x265 fonctionne à la vitesse maximale). Ensuite, lors de l' exécution de la commande ./configure , ImageMagick pourra trouver tout ce dont il a besoin pour prendre en charge les fichiers .heic.

Prise en charge du format cible dans la chaîne de requête des demandes entrantes


Après avoir fait des amis ImageMagick avec les formats webp et heic, il est temps de modifier le code Shrimp. Tout d'abord, nous devons apprendre à reconnaître l'argument de format cible dans les requêtes HTTP entrantes.

Du point de vue de RESTinio, ce n'est pas du tout un problème. Eh bien, un autre argument est apparu dans la chaîne de requête, alors quoi? Mais du point de vue de Shrimp, la situation s'est avérée être un peu plus compliquée, donc le code de la fonction qui était responsable de l'analyse de la requête HTTP était compliqué.

Le fait est qu'avant il fallait distinguer seulement deux situations:

  • est venu une demande de la forme "/filename.ext" sans aucun autre paramètre. Il vous suffit donc de donner le fichier "filename.ext" tel quel;
  • Une demande est arrivée sous la forme "/filename.ext?op=resize & ...". Dans ce cas, vous devez redimensionner l'image à partir du fichier "filename.ext".

Mais après avoir ajouté le format cible, nous devons distinguer quatre situations:

  • est venu une demande de la forme "/filename.ext" sans aucun autre paramètre. Il vous suffit donc de donner le fichier "filename.ext" tel quel, sans mise à l'échelle et sans transcodage vers un autre format;
  • est venu une demande du formulaire "/filename.ext?target-format=fmt" sans aucun autre paramètre. Cela signifie prendre une image du fichier "filename.ext" et la transcoder au format "fmt" tout en conservant les tailles originales;
  • une demande est venue de la forme "/filename.ext?op=resize & ..." mais sans format cible. Dans ce cas, vous devez redimensionner l'image à partir du fichier «filename.ext» et la donner au format d'origine;
  • Une demande est venue du formulaire "/filename.ext?op=resize&...&target-format=fmt". Dans ce cas, vous devez effectuer une mise à l'échelle, puis transcoder le résultat au format «fmt».

Par conséquent, la fonction de détermination des paramètres de requête a pris la forme suivante :

 void add_transform_op_handler( const app_params_t & app_params, http_req_router_t & router, so_5::mbox_t req_handler_mbox ) { router.http_get( R"(/:path(.*)\.:ext(.{3,4}))", restinio::path2regex::options_t{}.strict( true ), [req_handler_mbox, &app_params]( auto req, auto params ) { if( has_illegal_path_components( req->header().path() ) ) { //     . return do_400_response( std::move( req ) ); } //   . const auto qp = restinio::parse_query( req->header().query() ); const auto target_format = qp.get_param( "target-format"sv ); //        // .   target-format,    //   .   target-format  // ,    ,  //    . const auto image_format = try_detect_target_image_format( params[ "ext" ], target_format ); if( !image_format ) { //     .   . return do_400_response( std::move( req ) ); } if( !qp.size() ) { //    ,    . return serve_as_regular_file( app_params.m_storage.m_root_dir, std::move( req ), *image_format ); } const auto operation = qp.get_param( "op"sv ); if( operation && "resize"sv != *operation ) { //    ,     resize. return do_400_response( std::move( req ) ); } if( !operation && !target_format ) { //      op=resize, //   target-format=something. return do_400_response( std::move( req ) ); } handle_resize_op_request( req_handler_mbox, *image_format, qp, std::move( req ) ); return restinio::request_accepted(); } ); } 

Dans la version précédente de Shrimp, où vous n'aviez pas besoin de transcoder l'image, travailler avec les paramètres de requête semblait un peu plus facile .

File d'attente de demande et cache d'images adaptés au format cible


Le point suivant dans l'implémentation de la prise en charge du format cible a été le travail sur la file d'attente des demandes en attente et un cache d'images prêtes à l'emploi dans l'agent a_transform_manager. Nous avons parlé de ces choses plus en détail dans l'article précédent , mais rappelons-nous un peu de quoi il s'agissait.

Lorsqu'une demande de conversion d'image arrive, il peut s'avérer que l'image finie avec de tels paramètres est déjà dans le cache. Dans ce cas, vous n'avez rien à faire, envoyez simplement l'image du cache en réponse. Si l'image doit être transformée, il peut s'avérer qu'il n'y a pas de travailleurs libres pour le moment et vous devez attendre qu'elle apparaisse. Pour ce faire, les informations de demande doivent être mises en file d'attente. Mais en même temps, il est nécessaire de vérifier l'unicité des demandes - si nous avons trois demandes identiques en attente de traitement (c'est-à-dire que nous devons convertir la même image de la même manière), nous ne devons traiter l'image qu'une seule fois et donner le résultat du traitement en réponse à ces trois demandes. C'est-à-dire Dans la file d'attente, des demandes identiques doivent être regroupées.

Plus tôt dans Shrimp, nous avons utilisé une simple clé composite pour rechercher le cache d'image et la file d'attente: une combinaison du nom de fichier d'origine + des options de redimensionnement de l'image . Maintenant, deux nouveaux facteurs devaient être pris en compte:

  • premièrement, le format d'image cible (c'est-à-dire que l'image originale peut être en jpg, et l'image résultante peut être en png);
  • deuxièmement, le fait que la mise à l'échelle de l'image ne soit pas nécessairement nécessaire. Cela se produit dans une situation où le client commande uniquement la conversion de l'image d'un format à un autre, mais avec la taille d'origine de l'image préservée.

Je dois dire qu'ici nous avons emprunté le chemin le plus simple, sans chercher à optimiser quoi que ce soit. Par exemple, on pourrait essayer de créer deux caches: l'un stockerait les images au format d'origine, mais à l'échelle à la taille souhaitée, et dans le second, les images à l'échelle seraient converties au format cible.

Pourquoi une telle double mise en cache serait-elle nécessaire? Le fait est que lors de la transformation d'images, les deux opérations les plus coûteuses en temps sont le redimensionnement et la sérialisation de l'image au format cible. Par conséquent, si nous recevions une demande de mise à l'échelle de l'image example.jpg à une taille de 1920 en largeur et de la transformer au format webp, nous pourrions stocker deux images dans notre mémoire: example_1920px_width.jpg et example_1920px_width.webp. Nous donnerions une image example_1920px_width.webp lorsque nous recevions une deuxième demande. Mais l'image example_1920px_width.jpg pourrait être utilisée lors de la réception de demandes de mise à l'échelle d'exemple.jpg à une taille de 1920 en largeur et de sa transformation au format heic. Nous pourrions ignorer l'opération de redimensionnement et faire uniquement la conversion de format (c'est-à-dire que l'image finie example_1920px_width.jpg serait transcodée au format heic).

Autre opportunité potentielle: lorsqu'une demande vient de transcoder une image dans un autre format sans redimensionnement, vous pouvez déterminer la taille réelle de l'image et utiliser cette taille à l'intérieur de la clé composite. Par exemple, laissez example.jpg avoir une taille de 3000x2000 pixels. Si nous recevons ensuite une demande de mise à l'échelle d'exemple.jpg à 2000 px de hauteur, nous pouvons immédiatement déterminer que nous avons déjà une image de cette taille.

En théorie, toutes ces considérations méritent notre attention. Mais d'un point de vue pratique, il n'est pas clair à quel point la probabilité d'une telle évolution des événements est élevée. C'est-à-dire à quelle fréquence recevrons-nous une demande de mise à l'échelle d'exemple.jpg en 1920px avec conversion en webp, puis une demande de mise à l'échelle de la même image, mais avec conversion en png? Il est difficile de dire qu'il n'y a pas de statistiques réelles. Par conséquent, nous avons décidé de ne pas compliquer nos vies dans notre projet de démonstration, mais de suivre d'abord le chemin le plus simple. Dans l'espoir que si quelqu'un a besoin de schémas de mise en cache plus avancés, cela peut être ajouté plus tard, à partir de scénarios réels et non fictifs pour l'utilisation de Shrimp.

En conséquence, dans la version mise à jour de Shrimp, nous avons légèrement développé la clé, en y ajoutant également un paramètre tel que le format cible:

 class resize_request_key_t { std::string m_path; image_format_t m_format; resize_params_t m_params; public: resize_request_key_t( std::string path, image_format_t format, resize_params_t params ) : m_path{ std::move(path) } , m_format{ format } , m_params{ params } {} [[nodiscard]] bool operator<(const resize_request_key_t & o ) const noexcept { return std::tie( m_path, m_format, m_params ) < std::tie( o.m_path, o.m_format, o.m_params ); } [[nodiscard]] const std::string & path() const noexcept { return m_path; } [[nodiscard]] image_format_t format() const noexcept { return m_format; } [[nodiscard]] resize_params_t params() const noexcept { return m_params; } }; 

C'est-à-dire demande de redimensionnement example.jpg jusqu'à 1920px avec conversion en png diffère du même redimensionnement, mais avec conversion en webp ou heic.

Mais l'objectif principal se cache dans la nouvelle implémentation de la classe resize_params_t , qui détermine les nouvelles tailles de l'image mise à l'échelle. Auparavant, cette classe prend en charge trois options: seule la largeur est définie, seule la hauteur est définie ou le côté long est défini (la hauteur ou la largeur est déterminée par la taille actuelle de l'image). En conséquence, la méthode resize_params_t :: value () a toujours renvoyé une valeur réelle (quelle valeur a été déterminée par la méthode resize_params_t :: mode () ).

Mais dans le nouveau Shrimp, un autre mode a été ajouté - keep_original, ce qui signifie que la mise à l'échelle n'est pas effectuée et que l'image est rendue dans sa taille d'origine. Pour prendre en charge ce mode, resize_params_t a dû apporter quelques modifications. Tout d'abord, la méthode resize_params_t :: make () détermine maintenant si le mode keep_original est utilisé (on considère que ce mode est utilisé si aucun des paramètres width, height et max dans la chaîne de requête de la requête entrante n'est spécifié). Cela nous a permis de ne pas réécrire la fonction handle_resize_op_request () , qui pousse la demande de mise à l'échelle de l'image à exécuter.

Deuxièmement, la méthode resize_params_t :: value () peut désormais être appelée pas toujours, mais uniquement lorsque le mode de mise à l'échelle diffère de keep_original.

Mais le plus important est que resize_params_t :: operator <() a continué de fonctionner comme prévu.

Grâce à toutes ces modifications dans a_transform_manager, le cache d'image mis à l'échelle et la file d'attente des demandes en attente sont restés les mêmes. Mais maintenant, des informations sur diverses requêtes sont stockées dans ces structures de données. Ainsi, la clé {"example.jpg", "jpg", keep_original} différera à la fois de la clé {"example.jpg", "png", keep_original} et de la clé {"example.jpg", "jpg", width = 1920 px}.

Il s'est avéré qu'après avoir un peu gâché la définition de structures de données simples telles que resize_params_t et resize_params_key_t, nous avons évité de modifier des structures plus complexes telles que le cache des images résultantes et la file d'attente des demandes en attente.

Prise en charge du format cible dans a_transformer


Eh bien, la dernière étape de la prise en charge du format cible consiste à étendre la logique de l'agent a_transformer afin que l'image, éventuellement déjà mise à l'échelle, soit ensuite convertie au format cible.

Cela s'est avéré être le plus facile à faire, tout ce qui était nécessaire était d'étendre le code de la méthode a_transform_t :: handle_resize_request () :

 [[nodiscard]] a_transform_manager_t::resize_result_t::result_t a_transformer_t::handle_resize_request( const transform::resize_request_key_t & key ) { try { m_logger->trace( "transformation started; request_key={}", key ); auto image = load_image( key.path() ); const auto resize_duration = measure_duration( [&]{ //       //    keep_original. if( transform::resize_params_t::mode_t::keep_original != key.params().mode() ) { transform::resize( key.params(), total_pixel_count, image ); } } ); m_logger->debug( "resize finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( resize_duration).count() ); image.magick( magick_from_image_format( key.format() ) ); datasizable_blob_shared_ptr_t blob; const auto serialize_duration = measure_duration( [&] { blob = make_blob( image ); } ); m_logger->debug( "serialization finished; request_key={}, time={}ms", key, std::chrono::duration_cast<std::chrono::milliseconds>( serialize_duration).count() ); return a_transform_manager_t::successful_resize_t{ std::move(blob), std::chrono::duration_cast<std::chrono::microseconds>( resize_duration), std::chrono::duration_cast<std::chrono::microseconds>( serialize_duration) }; } catch( const std::exception & x ) { return a_transform_manager_t::failed_resize_t{ x.what() }; } } 

Par rapport à la version précédente, il existe deux ajouts fondamentaux.

Tout d'abord, appeler la méthode image.magick () vraiment magique après le redimensionnement. Cette méthode indique à ImageMagick le format d'image résultant. Dans le même temps, la représentation de l'image dans la mémoire ne change pas - ImageMagick continue de la stocker à sa guise. Mais alors la valeur définie par la méthode magick () sera prise en compte lors de l'appel suivant à Image :: write ().

Deuxièmement, la version mise à jour enregistre le temps nécessaire pour sérialiser l'image au format spécifié. La nouvelle version de Shrimp fixe désormais séparément le temps consacré à la mise à l'échelle et le temps consacré à la conversion au format cible.

Le reste de l'agent a_transformer_t n'a subi aucune modification.

Parallélisation d'ImageMagick


Par défaut, ImageMagic est construit avec le support OpenMP. C'est-à-dire il est possible de paralléliser les opérations sur les images qu'ImageMagick effectue. Vous pouvez contrôler le nombre de workflows qu'ImageMagick utilise dans ce cas à l'aide de la variable d'environnement MAGICK_THREAD_LIMIT.

Par exemple, sur ma machine de test avec la valeur MAGICK_THREAD_LIMIT = 1 (c'est-à-dire sans réelle parallélisation), j'obtiens les résultats suivants:

 curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:51:24 GMT < Last-Modified: Wed, 15 Aug 2018 11:51:24 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 1323 < Shrimp-Resize-Time: 1086.72 < Shrimp-Encoding-Time: 236.276 

Le temps consacré au redimensionnement est indiqué dans l'en-tête Shrimp-Resize-Time. Dans ce cas, elle est de 1086,72 ms.

Mais si vous définissez MAGICK_THREAD_LIMIT = 3 sur la même machine et exécutez Shrimp, alors nous obtenons des valeurs différentes:

 curl "http://localhost:8080/DSC08084.jpg?op=resize&max=2400" -v > /dev/null > GET /DSC08084.jpg?op=resize&max=2400 HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.47.0 > Accept: */* > < HTTP/1.1 200 OK < Connection: keep-alive < Content-Length: 2043917 < Server: Shrimp draft server < Date: Wed, 15 Aug 2018 11:53:49 GMT < Last-Modified: Wed, 15 Aug 2018 11:53:49 GMT < Access-Control-Allow-Origin: * < Access-Control-Expose-Headers: Shrimp-Processing-Time, Shrimp-Resize-Time, Shrimp-Encoding-Time, Shrimp-Image-Src < Content-Type: image/jpeg < Shrimp-Image-Src: transform < Shrimp-Processing-Time: 779.901 < Shrimp-Resize-Time: 558.246 < Shrimp-Encoding-Time: 221.655 

C'est-à-dire le temps de redimensionnement a été réduit à 558,25 ms.

Par conséquent, comme ImageMagick offre la possibilité de paralléliser les calculs, vous pouvez utiliser cette opportunité. Mais en même temps, il est souhaitable de pouvoir contrôler le nombre de threads de travail que Shrimp prend pour lui-même. Dans les versions précédentes de Shrimp, il n'était pas possible d'influencer le nombre de flux de travail créés par Shrimp. Et dans la version mise à jour de Shrimp, cela peut être fait. Ou via des variables d'environnement, par exemple:

 SHRIMP_IO_THREADS=1 \ SHRIMP_WORKER_THREADS=3 \ MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ... 

Ou via des arguments de ligne de commande, par exemple:

 MAGICK_THREAD_LIMIT=4 \ shrimp.app -p 8080 -i ... --io-threads 1 --worker-threads 4 

Les valeurs spécifiées via la ligne de commande ont une priorité plus élevée.

Il convient de souligner que MAGICK_THREAD_LIMIT affecte uniquement les opérations qu'ImageMagick effectue lui-même. Par exemple, le redimensionnement est effectué par ImageMagick. Mais la conversion d'un format à un autre ImageMagick délègue aux bibliothèques externes. Et comment les opérations dans ces bibliothèques externes sont parallélisées est un problème distinct que nous n'avons pas compris.

Conclusion


Peut-être, dans cette version de Shrimp, nous avons amené notre projet de démonstration à un état acceptable. Ceux qui veulent voir et expérimenter peuvent trouver les textes sources de Shrimp sur BitBucket ou GitHub . Vous pouvez également y trouver le Dockerfile pour construire des crevettes pour vos expériences.

En général, nous avons atteint nos objectifs que nous nous sommes fixés en démarrant ce projet de démonstration. Un certain nombre d'idées sont apparues pour le développement ultérieur de RESTinio et de SObjectizer, et certaines d'entre elles ont déjà trouvé leur mode de réalisation. Par conséquent, si les crevettes se développeront ailleurs, cela dépend complètement des questions et des souhaits. S'il y en a, alors les crevettes peuvent se développer. Sinon, Shrimp restera un projet de démonstration et un terrain d'entraînement pour expérimenter de nouvelles versions de RESTinio et SObjectizer.

En conclusion, je voudrais remercier tout particulièrement aensidhe pour leur aide et leurs conseils, sans lesquels nos danses au tambourin seraient bien plus longues et tristes.

Source: https://habr.com/ru/post/fr420353/


All Articles