libjpeg-turbo介绍及测试代码
很多年之前在https://blog.csdn.net/fengbingchun/article/details/10171583 中简单介绍过libjpeg-turbo的安装,因为libjpeg-turbo一直在维护更新,较之前有了些变化,这里再次整理下,并增加更多的测试代码。
libjpeg-turbo的主页为https://libjpeg-turbo.org/ ,GitHub地址为https://github.com/libjpeg-turbo/libjpeg-turbo ,最新分布版本为2.0.2。
libjpeg-turbo是一个JPEG图像编解码器,它使用SIMD指令(MMX, SSE2, AVX2, NEON, AltiVec)来加速x86, x86-64, ARM和PowerPC系统上baseline JPEG压缩和解压缩,在x86和x86-64系统上还支持渐进式(progressive) JPEG压缩。它的速度一般是libjpeg的2至6倍。在许多情况下,libjpeg-turbo的性能可以与专有的高速JPEG编解码器相媲美。
libjpeg-turbo既实现了传统的libjpeg API功能也有一些更直接但less powerful的TurboJPEG API。libjpeg-turbo还具有颜色空间扩展的特性,允许它压缩/解压缩到32位和大端(big-endian) 像素缓冲区(RGBX, XBGR等)。libjpeg-turbo也提供了功能齐全的Java接口。
因为libjpeg-turbo中有很多汇编文件,因此需要编译器支持汇编编译:
1. 在windows下通过vs编译:
可以从https://www.nasm.us/ 中下载最新的NASM稳定版2.14.02,即nasm-2.14.02-installer-x64.exe,这里下载安装的是64位的,也有对应32位的,安装到D:\ProgramFiles\NASM目录下,并将D:\ProgramFiles\NASM添加到系统环境变量中。将此目录下的nasm.exe和ndisasm.exe拷贝到C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin中,即对应的vs版本是vs2013。
从https://www.codeproject.com/Articles/410776/Integrating-a-compiler-assembler-in-VS-Using-NASM 点击下载” Download Property,Target,XML file - 3.8 KB”,即Target_Files_Collection.zip,解压缩,然后将解压后产生的3个文件nasm.props, nasm.targets, nasm.xml拷贝到C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V120\BuildCustomizations目录下,此目录下也有对应的masm的三个文件,masm.props, masm.targets, masm.xml。此3个配置文件用于控制自定义生成规则。
配置好后,打开任意一个工程:点击右键 --> 生成依赖项(B) --> 生成自定义(B),可找到nasm(.targets),勾选nasm(.targets)。再右键 --> 属性后,会发现多了一项Netwide Assmbler,如下图所示:
新建一个空工程libjpeg-turbo,用于生成静态库,但是将汇编文件加入到该工程中一直有问题,因此先将通过CMake生成工程turbojpeg-static编译生成的静态库用于测试。
2. 在Linux下通过GCC编译:
首先在ubuntu上安装nasm,有两种方式,一种是从https://www.nasm.us/pub/nasm/releasebuilds/2.14.02/ 下载nasm-2.14.02.zip通过源码安装,一种是直接通过执行”sudo apt-get install nasm”来安装。
build.sh内容如下:
#! /bin/bashreal_path=$(realpath $0)
dir_name=`dirname "${real_path}"`
echo "real_path: ${real_path}, dir_name: ${dir_name}"data_dir="test_data"
if [ -d ${dir_name}/${data_dir} ]; thenrm -rf ${dir_name}/${data_dir}
filn -s ${dir_name}/./../../${data_dir} ${dir_name}new_dir_name=${dir_name}/build
mkdir -p ${new_dir_name}
cd ${new_dir_name}
echo "pos: ${new_dir_name}"
if [ "$(ls -A ${new_dir_name})" ]; thenecho "directory is not empty: ${new_dir_name}"rm -r *
elseecho "directory is empty: ${new_dir_name}"
fi# build libjpeg-turbo
libjpeg_turbo_path=${dir_name}/../../src/libjpeg-turbo
cd ${libjpeg_turbo_path}
mkdir build
cd build
cmake ..
make
ln -s ${libjpeg_turbo_path}/build/libturbojpeg.a ${new_dir_name}cd -cd ${new_dir_name}
cmake ..
makecd -
CMakeList.txt内容如下:
PROJECT(Libjpeg-turbo_Test)
CMAKE_MINIMUM_REQUIRED(VERSION 3.0)# support C++11
SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c11")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")
# support C++14, when gcc version > 5.1, use -std=c++14 instead of c++1y
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++1y")SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -O2")
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -O2")MESSAGE(STATUS "project source dir: ${PROJECT_SOURCE_DIR}")
SET(PATH_TEST_FILES ${PROJECT_SOURCE_DIR}/./../../demo/Libjpeg-turbo_Test)
SET(PATH_SRC_LIBJPEG_TURBO_FILES ${PROJECT_SOURCE_DIR}/./../../src/libjpeg-turbo)
MESSAGE(STATUS "path libjpeg-turbo src files: ${PATH_SRC_LIBJPEG_TURBO_FILES}")SET(PATH_OPENCV /opt/opencv3.4.2)
IF(EXISTS ${PATH_OPENCV})MESSAGE(STATUS "Found OpenCV: ${PATH_OPENCV}")
ELSE()MESSAGE(FATAL_ERROR "Can not find OpenCV in ${PATH_OPENCV}")
ENDIF()# head file search path
INCLUDE_DIRECTORIES(${PATH_TEST_FILES}${PATH_SRC_LIBJPEG_TURBO_FILES}${PATH_SRC_LIBJPEG_TURBO_FILES}/build${PATH_OPENCV}/include
)# find opencv library
FIND_LIBRARY(opencv_core NAMES opencv_core PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_imgproc NAMES opencv_imgproc PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_highgui NAMES opencv_highgui PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_imgcodecs NAMES opencv_imgcodecs PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_video NAMES opencv_video PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_videoio NAMES opencv_videoio PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_objdetect NAMES opencv_objdetect PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
FIND_LIBRARY(opencv_ml NAMES opencv_ml PATHS ${PATH_OPENCV}/lib NO_DEFAULT_PATH)
MESSAGE(STATUS "opencv libraries: ${opencv_core} ${opencv_imgproc} ${opencv_highgui} ${opencv_imgcodecs} ${opencv_video}" ${opencv_videoio} ${opencv_objdetect} ${opencv_ml})# recursive query match files :*.cpp
FILE(GLOB_RECURSE TEST_CPP_LIST ${PATH_TEST_FILES}/*.cpp)# find library
FIND_LIBRARY(libturbojpeg NAMES turbojpeg PATHS ${PROJECT_SOURCE_DIR}/build NO_DEFAULT_PATH)
MESSAGE(STATUS "image libraries: ${libturbojpeg}")# build executable program
ADD_EXECUTABLE(Libjpeg-turbo_Test ${TEST_CPP_LIST})# add dependent library: static and dynamic
TARGET_LINK_LIBRARIES(Libjpeg-turbo_Test ${libturbojpeg} ${opencv_ml} ${opencv_core} ${opencv_imgproc} ${opencv_highgui} ${opencv_imgcodecs} ${opencv_video} ${opencv_videoio} ${opencv_objdetect} pthread)
测试代码如下:
#include <string>
#include <memory>#include "funset.hpp"
#include <opencv2/opencv.hpp>int parse_jpeg_file(const char* name)
{FILE* infile = nullptr;if ((infile = fopen(name, "rb")) == nullptr) {fprintf(stderr, "can't open %s\n", name);return -1;}struct jpeg_decompress_struct cinfo;struct jpeg_error_mgr jerr;cinfo.err = jpeg_std_error(&jerr);/* Now we can initialize the JPEG decompression object. */jpeg_create_decompress(&cinfo);/* Step 2: specify data source (eg, a file) */jpeg_stdio_src(&cinfo, infile);/* Step 3: read file parameters with jpeg_read_header() */jpeg_read_header(&cinfo, TRUE);fprintf(stdout, "image_width = %d\n", cinfo.image_width);fprintf(stdout, "image_height = %d\n", cinfo.image_height);fprintf(stdout, "num_components = %d\n", cinfo.num_components);/* Step 4: set parameters for decompression */cinfo.scale_num = 2;cinfo.scale_denom = 4;/* Step 5: Start decompressor */jpeg_start_decompress(&cinfo);fprintf(stdout, "output_width = %d\n", cinfo.output_width);fprintf(stdout, "output_height = %d\n", cinfo.output_height);fprintf(stdout, "output_components = %d\n", cinfo.output_components);/* JSAMPLEs per row in output buffer */int row_stride = cinfo.output_width * cinfo.output_components;/* Make a one-row-high sample array that will go away when done with image */JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1);/* Step 6: while (scan lines remain to be read) */while (cinfo.output_scanline < cinfo.output_height) {jpeg_read_scanlines(&cinfo, buffer, 1);}/* Step 7: Finish decompression */jpeg_finish_decompress(&cinfo);/* Step 8: Release JPEG decompression object */jpeg_destroy_decompress(&cinfo);fclose(infile);return 0;
}int write_jpeg_file(const unsigned char* data, int width, int height, int channels, J_COLOR_SPACE color_space, int quality, const char* name)
{/* Step 1: allocate and initialize JPEG compression object */struct jpeg_compress_struct cinfo;struct jpeg_error_mgr jerr;cinfo.err = jpeg_std_error(&jerr);/* Now we can initialize the JPEG compression object. */jpeg_create_compress(&cinfo);/* Step 2: specify data destination (eg, a file) */FILE* outfile = nullptr;if ((outfile = fopen(name, "wb")) == nullptr) {fprintf(stderr, "can't open file: %s\n", name);return -1;}jpeg_stdio_dest(&cinfo, outfile);/* Step 3: set parameters for compression */cinfo.image_width = width;cinfo.image_height = height;cinfo.input_components = channels;cinfo.in_color_space = color_space;jpeg_set_defaults(&cinfo);jpeg_set_quality(&cinfo, quality, TRUE);/* Step 4: Start compressor */jpeg_start_compress(&cinfo, TRUE);/* Step 5: while (scan lines remain to be written) */int row_stride = width * channels;int line = 0;JSAMPROW row_pointer[1];while (line < cinfo.image_height) {row_pointer[0] = (JSAMPROW)&data[line * row_stride];jpeg_write_scanlines(&cinfo, row_pointer, 1);++line;}/* Step 6: Finish compression */jpeg_finish_compress(&cinfo);fclose(outfile);/* Step 7: release JPEG compression object */jpeg_destroy_compress(&cinfo);return 0;
}int get_jpeg_compress_data(const unsigned char* data, int width, int height, int channels, J_COLOR_SPACE color_space, int quality, unsigned char** out_buffer, unsigned long out_buffer_size, unsigned long& free_in_buffer)
{/* Step 1: allocate and initialize JPEG compression object */struct jpeg_compress_struct cinfo;struct jpeg_error_mgr jerr;cinfo.err = jpeg_std_error(&jerr);/* Now we can initialize the JPEG compression object. */jpeg_create_compress(&cinfo);/* Step 2: specify data destination (eg, a file) */jpeg_mem_dest(&cinfo, out_buffer, &out_buffer_size);/* Step 3: set parameters for compression */cinfo.image_width = width;cinfo.image_height = height;cinfo.input_components = channels;cinfo.in_color_space = color_space;jpeg_set_defaults(&cinfo);jpeg_set_quality(&cinfo, quality, TRUE);/* Step 4: Start compressor */jpeg_start_compress(&cinfo, TRUE);/* Step 5: while (scan lines remain to be written) */int row_stride = width * channels;int line = 0;JSAMPROW row_pointer[1];while (line < cinfo.image_height) {row_pointer[0] = (JSAMPROW)&data[line * row_stride];jpeg_write_scanlines(&cinfo, row_pointer, 1);++line;}/* Step 6: Finish compression */jpeg_finish_compress(&cinfo);free_in_buffer = cinfo.dest->free_in_buffer;/* Step 7: release JPEG compression object */jpeg_destroy_compress(&cinfo);return 0;
}int test_libjpeg_turbo()
{
#ifdef _MSC_VERstd::string image_path{ "E:/GitCode/OCR_Test/test_data/" };
#elsestd::string image_path{ "test_data/" };
#endifstd::string name1 = image_path + "tirg.jpg";parse_jpeg_file(name1.c_str());std::string name2 = image_path + "lena.png";std::string name3 = image_path + "lena.jpg";int quality = 80;cv::Mat mat = cv::imread(name2);if (!mat.data || mat.channels() != 3) {fprintf(stderr, "read image fail: %s\n", name2.c_str());return -1;}write_jpeg_file(mat.data, mat.cols, mat.rows, mat.channels(), JCS_EXT_BGR, quality, name3.c_str()); // bgr dataname3 = image_path + "lena2.jpg";cv::cvtColor(mat, mat, CV_BGR2RGB);write_jpeg_file(mat.data, mat.cols, mat.rows, mat.channels(), JCS_RGB, quality, name3.c_str()); // rgb dataname3 = image_path + "lena3.jpg";cv::cvtColor(mat, mat, CV_RGB2GRAY);write_jpeg_file(mat.data, mat.cols, mat.rows, mat.channels(), JCS_GRAYSCALE, quality, name3.c_str()); // gray dataint length = mat.cols * mat.rows;std::unique_ptr<unsigned char[]> data(new unsigned char[length]);unsigned char* p = data.get();unsigned long free_in_buffer;get_jpeg_compress_data(mat.data, mat.cols, mat.rows, mat.channels(), JCS_GRAYSCALE, quality, &p, length, free_in_buffer);name3 = image_path + "lena4.jpg";FILE* outfile = nullptr;if ((outfile = fopen(name3.c_str(), "wb")) == nullptr) {fprintf(stderr, "can't open file: %s\n", name3.c_str());return -1;}fwrite(data.get(), sizeof(unsigned char), length - free_in_buffer, outfile);fclose(outfile);return 0;
}
执行结果如下:
GitHub: https://github.com/fengbingchun/OCR_Test
相关文章:

高级特性-多线程,GUI
2019独角兽企业重金招聘Python工程师标准>>> 创建线程两种方式第一种,导入improt thread 模块,thread.start_new_thread(功能函数名称,(参数1,参数2...)) 后面参数为功能函数的参数第二个方式类似于java,导…

Hulu视频如何提升推荐多样性?
作者 | 余沾 整理 | 深度传送门(ID: deep_deliver)导读:本文主要介绍Hulu在NIPS 2018上发表的《Fast Greedy MAP Inference for Determinantal Point Process to Improve Recommendation Diversity》中,提出的DPP算法解决视频推荐…

UITextField长度限制的写法
1.遵循代理 UITextFieldDelegate 2.点击响应方法 userNameText.addTarget(self, action: "tappedOne:", forControlEvents: UIControlEvents.EditingChanged) 3.方法的实现 func tappedOne(textField: UITextField) { textField.text textField.text?.uppercaseS…

通过Python在Windows或Linux上快速搭建HTTP服务器
在Windows 7/10或Ubuntu上可以通过python2.x或python3.x来快速搭建一个简单的HTTP服务器。 如果python为2.x,则可执行:$ python -m SimpleHTTPServer 或 $ python2 -m SimpleHTTPServer 如果python为3.x,则可执行:$ python -m h…

NAND FLASH
NAND Flash 以Micron公司的MT29F2G08为例介绍NAND Flash原理和使用。 1. 概述 MT29F2G08使用一个高度复用的8-bit总线(I/O[7:0])来数据传输、地址、指令。5个命令脚(CLE、ALE、CE#、WE#)实现NAND命令总线接口规程。3个…

swift 中跳转web view的两种方法
首先 遵循代理 引入头文件 #import <WebKit/WebKit.h> 第一种情况 直接跳转 了解不含特殊字符的 import UIKit class NewsViewController: UIViewController,WKNavigationDelegate,UIScrollViewDelegate { var webView : WKWebView WKWebView() override func viewW…

YAML开源库yaml-cpp简介及使用
关于YAML的介绍可以参考:https://blog.csdn.net/fengbingchun/article/details/88090609 yaml-cpp是用c实现的用来解析和生成yaml文件的,源码地址在https://github.com/jbeder/yaml-cpp ,这里使用的是最新发布的稳定版0.6.2. 解析和产生yam…

数据安全引担忧?get它,让你吃一颗“定心丸”
网络购物、在线外卖、远程教育、共享单车……如今,这些数字化的消费场景在个人生活中早已司空见惯。同时,在数字化浪潮下,越来越多的企业意识到大数据资产的价值,并试图推动其数字化转型。数据经济飞速发展,带来便捷和…

ETL数据抽取策略
ETL的抽取策略本文所提到的数据加载策略为OLTP系统作为源系统,并进行ETL数据加载到OLAP系统中所采用的一般数据加载策略。依循数据仓库的工作方式,原始资料由源数据库被抽取出来后,将在中间过程被写入到”Operational Data Store”(ODS)&…

iOS下拉tableView实现上面的图片放大效果
#import "ViewController.h" #define kScreenbounds [UIScreen mainScreen].bounds #define kScreenWidth [UIScreen mainScreen].bounds.size.width #define kScreenHeight [UIScreen mainScreen].bounds.size.height // 宏定义一个高度 #define pictureHeight 200…

在Windows7/10上通过VS2013编译FFmpeg 4.1.3源码操作步骤
多年前在https://blog.csdn.net/fengbingchun/article/details/40951403 中对FFmpeg在windows下的编译过程做过说明,那时FFmpeg版本用的2.4.3, VS是2010,现在FFmpeg最新稳定版为4.1.3,通过VS2013进行编译,较之前有了些不同&#x…

GitHub标星近1万:只需5秒音源,这个网络就能实时“克隆”你的声音
作者 | Google团队 译者 | 凯隐 编辑 | Jane 出品 | AI科技大本营(ID:rgznai100)本文中,Google 团队提出了一种文本语音合成(text to speech)神经系统,能通过少量样本学习到多个不同说话者&…

entity framework 使用Mysql配置文件
2019独角兽企业重金招聘Python工程师标准>>> <?xml version"1.0" encoding"utf-8"?> <configuration><configSections><section name"entityFramework" type"System.Data.Entity.Internal.ConfigFile.En…

UIWebView、WKWebView使用详解及性能分析
一、整体介绍 UIWebView自iOS2就有,WKWebView从iOS8才有,毫无疑问WKWebView将逐步取代笨重的UIWebView。通过简单的测试即可发现UIWebView占用过多内存,且内存峰值更是夸张。WKWebView网页加载速度也有提升,但是并不像内存那样提…

FFmpeg中libavutil库简介及测试代码
libavutil是一个实用库,用于辅助多媒体编程。此库包含安全的可移植字符串函数、随机数生成器、数据结构、附加数学函数、加密和多媒体相关功能(如像素和样本格式的枚举)。libavcodec和libavformat并不依赖此库。 以下是测试代码,包括base64, aes, des, …

区块链人才月均薪酬1.6万元?
在上周,我国宣布将重点推动区块链技术的发展,这个消息无疑是为区块链开发者们打了一直强心剂,简直是喜大普奔啊 ! 因为之前区块链这个技术虽然一直在圈内很火,但是却没有得到国家的全面认可和推广,所以很多…

用最少的时间学最多的数据挖掘知识(附教程数据源)| CSDN博文精选
作者 | 宋莹来源 | 数据派THU(ID:DatapiTHU)本文为你介绍数据挖掘的知识及应用。引言最近笔者学到了一个新词,叫做“认知折叠”。就是将复杂的事物包装成最简单的样子,让大家不用关心里面的细节就能方便使用。作为数据科学领域从业…

WKWebView 的使用简介
1. navigationDelegate [objc] view plaincopy print?- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { // 类似UIWebView的 -webViewDidStartLoad: NSLog("didStartProvisionalNavigation"); [UIAppli…

FFmpeg中libswscale库简介及测试代码
libswscale库功能主要包括高度优化的图像缩放、颜色空间和像素格式转换操作。 以下是测试代码(test_ffmpeg_libswscale.cpp): #include "funset.hpp" #include <string.h> #include <iostream> #include <string> #include <memor…

FFmpeg中libswresample库简介及测试代码
libswresample库功能主要包括高度优化的音频重采样、rematrixing和样本格式转换操作。 以下是测试代码(test_ffmpeg_libswresample.cpp),对音频了解较少,测试代码是参考examples中的: #include "funset.hpp" #include <iostre…
高德地图POI搜索,附近地图搜索,类似附近的人搜索
效果图: 首先导入道德地图的SDK,导入步骤不在这里介绍 2:包含头文件: [objc] view plaincopy #import <AMapSearchKit/AMapSearchAPI.h> 3:代码 [javascript] view plaincopy property(nonatomic,strong)AMap…

手把手教你实现PySpark机器学习项目——回归算法
作者 | hecongqing 来源 | AI算法之心(ID:AIHeartForYou)【导读】PySpark作为工业界常用于处理大数据以及分布式计算的工具,特别是在算法建模时起到了非常大的作用。PySpark如何建模呢?这篇文章手把手带你入门PySpark,…

mcDropdown使用方法
最近使用了mcDropdown插件,百度一查,资料较少,只看到了mcDropdown官网的英文说明文档,所以今天就写点,以便以后使用。 第一步:引用jquery库和css jQuery v1.2.6 (or higher)*jquery.mcdropdown.js Plug-inj…

Windows上通过VLC播放器搭建rtsp流媒体测试地址操作步骤
1. 从https://www.videolan.org/index.zh.html 下载最新的windows 64bit 3.0.6版本并安装; 2. 打开VLC media player,依次点击按钮:”媒体” --> “流”,如下图所示: 3. 点击”添加”按钮,选择一个视频…

Swift - AppDelegate.swift类中默认方法的介绍
项目创建后,AppDelegate类中默认带有如下几个方法,具体功能如下: 1,应用程序第一次运行时执行这个方法只有在App第一次运行的时候被执行过一次,每次App从后台激活时都不会再执行该方法。(注:所有…

上热搜了!“学了Python6个月,竟然找不到工作!”
在编程界,Python是一种神奇的存在。有人认为,只有用Python才能优雅写代码,提高代码效率;但另一部分人恨不能把Python喷成筛子。那么,Python到底有没有用,为什么用Python找不到工作?CSDN小姐姐带…

Linux0.00内核为什么要自己设置0x80号陷阱门来调用write_char过程?
我一开始没注意这个问题,只是通过陷阱门觉得很绕弯子,为何不在3级用户代码里直接调用write_char,今天自己写程序想用call调用代码段,才发现了大问题。我写了个类似于write_char的过程,代码如下:dividing_li…
iOS支付宝(Alipay)接入详细流程,比微信支付更简单,项目实战中的问题分析
最近在项目中接入了微信支付和支付宝支付,总的来说没有那么坑,很多人都说文档不全什么的,确实没有面面 俱到,但是认真一步一步测试下还是妥妥的,再配合懂得后台,效率也是很高的,看了这篇文章&a…

LIVE555简介及在Windows上通过VS2013编译操作步骤
LIVE555是使用开放标准协议(RTP/RTCP, RTSP, SIP)形成的一组用于多媒体流C库。这些库支持的平台包括Unix(包括Linux和Mac OS X)、Windows和QNX(以及其它符号POSIX的系统)。这些库已经被用于实现的应用例如LIVE555媒体服务器、LIVE555代理服务器(RTSP服务器应用)以及vobStreamer…

GitHub App终于来了,iPhone用户可尝鲜,「同性交友」更加便捷
整理 | 夕颜出品 | AI科技大本营(ID:rgznai100)【导读】据外媒 VentureBeat 报道,在 11 月 13 日举行的 GitHub Universe 上,微软宣布了面向程序员和开发人员的一系列升级,包括针对 iOS 智能手机和 iPad 推出的 GitHub…