C++字体库开发之fontconfig使用五

1 篇文章 0 订阅
代码 


#include <cassert>
#include <algorithm>
#include "fontconfig/fontconfig.h"
#include <stdexcept>
#include <iostream>

#define HAS_FALLBACK_CONFIGURATION

enum FontStyle : uint16_t {
    Regular = 0,
    Italic = 0x01,
    Bold = 0x02,
};

enum class FontConfigSearchFlags : uint16_t {
    None = 0,
    MatchPostScriptName =
    0x01,  ///< Match postscript font name. The default is match family name. This search may be more specific
};

struct FontConfigSearchParams {
    uint16_t Style;
    uint16_t Flags = 0;
};

FcConfig* m_FcConfig;

void createDefaultConfig()
{
#ifdef _WIN32
    const char* fontconf =
        R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>WINDOWSFONTDIR</dir>
    <dir>WINDOWSUSERFONTDIR</dir>
    <dir prefix="xdg">fonts</dir>
    <cachedir>LOCAL_APPDATA_FONTCONFIG_CACHE</cachedir>
    <cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __ANDROID__
    // On android fonts are located in /system/fonts
    const char* fontconf =
        R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>/system/fonts</dir>
    <dir prefix="xdg">fonts</dir>
    <cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __LINUX__
    const char* fontconf =
        R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>/usr/share/fonts</dir>
    <dir>/usr/local/share/fonts</dir>
    <dir>/usr/X11R6/lib/X11/fonts</dir>
    <dir>/usr/X11/lib/X11/fonts</dir>
    <dir>/usr/lib/X11/fonts</dir>
    <dir prefix="xdg">fonts</dir>
    <cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#elif __APPLE__
    // Fonts location https://stackoverflow.com/a/2557291/213871
    const char* fontconf =
        R"(<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
    <dir>/System/Library/Fonts</dir>
    <dir>/Library/Fonts</dir>
    <dir>~/Library/Fonts</dir>
    <dir>/System/Library/Assets/com_apple_MobileAsset_Font3</dir>
    <dir>/System/Library/Assets/com_apple_MobileAsset_Font4"</dir>
    <dir prefix="xdg">fonts</dir>
    <cachedir prefix="xdg">fontconfig</cachedir>
</fontconfig>
)";
#endif

#ifdef HAS_FALLBACK_CONFIGURATION
    // Implement the fallback as discussed in fontconfig mailing list
    // https://lists.freedesktop.org/archives/fontconfig/2022-February/006883.html

    auto config = FcConfigCreate();
    if (config == nullptr)
        throw std::runtime_error("Could not allocate font config");

    // Manually try to load the config to determine
    // if a system configuration exists. Tell FontConfig
    // to not complain if it doesn't
    (void)FcConfigParseAndLoad(config, nullptr, FcFalse);

    auto configFiles = FcConfigGetConfigFiles(config);
    if (FcStrListNext(configFiles) == nullptr) {
        // No system config found, supply a fallback configuration
        if (!FcConfigParseAndLoadFromMemory(config, (const FcChar8*)fontconf, true)) {
            FcConfigDestroy(config);
            throw std::runtime_error("Could not parse font config");
        }

        // Load fonts for the config
        if (!FcConfigBuildFonts(config)) {
            FcConfigDestroy(config);
            throw std::runtime_error("Could not load fonts in fontconfig");
        }

        m_FcConfig = config;
    }
    else {
        // Destroy the temporary config
        FcStrListDone(configFiles);
        FcConfigDestroy(config);
#endif
        // Default initialize a local FontConfig configuration
        // http://mces.blogspot.com/2015/05/how-to-use-custom-application-fonts.html
        m_FcConfig = FcInitLoadConfigAndFonts();
        assert(m_FcConfig != nullptr);

#ifdef HAS_FALLBACK_CONFIGURATION
    }
#endif
}

bool getFontInfo(FcPattern* font, std::string& fontFamily, std::string& fontPath, uint16_t& style)
{
    FcChar8* family = nullptr;
    FcChar8* path = nullptr;
    int      slant;
    int      weight;
    style = FontStyle::Regular;

    if (FcPatternGetString(font, FC_FAMILY, 0, &family) == FcResultMatch) {
        fontFamily = reinterpret_cast<char*>(family);
        if (FcPatternGetString(font, FC_FILE, 0, &path) == FcResultMatch) {
            fontPath = reinterpret_cast<char*>(path);

            if (FcPatternGetInteger(font, FC_SLANT, 0, &slant) == FcResultMatch) {
                if (slant == FC_SLANT_ITALIC || slant == FC_SLANT_OBLIQUE)
                    style |= FontStyle::Italic;

                if (FcPatternGetInteger(font, FC_WEIGHT, 0, &weight) == FcResultMatch) {
                    if (weight >= FC_WEIGHT_BOLD)
                        style |= FontStyle::Bold;

                    return true;
                }
            }
            // free( file );
        }
        // free( family );
    }

    return false;
}

std::string SearchFontPath(const std::string_view fontPattern, const FontConfigSearchParams& params, unsigned& faceIndex)
{
    FcPattern* pattern;
    FcPattern* matched;
    FcResult   result = FcResultMatch;
    FcValue    value;

    pattern = FcPatternCreate();
    if (pattern == nullptr)
        return ("FcPatternCreate returned NULL");

    // Build a pattern to search using postscript name, bold and italic
    if (params.Flags != (uint16_t)FontConfigSearchFlags::MatchPostScriptName)
        FcPatternAddString(pattern, FC_FAMILY, (const FcChar8*)fontPattern.data());
    else
        FcPatternAddString(pattern, FC_POSTSCRIPT_NAME, (const FcChar8*)fontPattern.data());

    if (params.Style) {
        bool isItalic = (params.Style & FontStyle::Italic) == FontStyle::Italic;
        bool isBold = (params.Style & FontStyle::Bold) == FontStyle::Bold;

        FcPatternAddInteger(pattern, FC_WEIGHT, (isBold ? FC_WEIGHT_BOLD : FC_WEIGHT_MEDIUM));
        FcPatternAddInteger(pattern, FC_SLANT, (isItalic ? FC_SLANT_ITALIC : FC_SLANT_ROMAN));
    }

    // Follow fc-match procedure which proved to be more reliable
    // https://github.com/freedesktop/fontconfig/blob/e291fda7d42e5d64379555097a066d9c2c4efce3/fc-match/fc-match.c#L188
    if (!FcConfigSubstitute(m_FcConfig, pattern, FcMatchPattern)) {
        FcPatternDestroy(pattern);
        faceIndex = 0;
        return {};
    }

    FcDefaultSubstitute(pattern);

    std::string path;
    matched = FcFontMatch(m_FcConfig, pattern, &result);
    if (result != FcResultNoMatch) {
        (void)FcPatternGet(matched, FC_FILE, 0, &value);
        path = reinterpret_cast<const char*>(value.u.s);
        (void)FcPatternGet(matched, FC_INDEX, 0, &value);
        faceIndex = (unsigned)value.u.i;
    }

    FcPatternDestroy(pattern);
    FcPatternDestroy(matched);

#if _WIN32
    // Font config in Windows returns unix conventional path
    // separator. Fix it
    std::replace(path.begin(), path.end(), '/', '\\');
#endif
    return path;
}

void testSingleFont(FcPattern* font)
{
    std::string            fontFamily;
    std::string            fontPath;
    uint16_t               style;
    FontConfigSearchParams fcParams;

    if (getFontInfo(font, fontFamily, fontPath, style)) {
        unsigned faceIndex;
        fcParams.Style = style;
        fontPath = SearchFontPath(fontFamily, fcParams, faceIndex);
        if (fontPath.length() != 0) {
            std::cout << "fontPath:" << fontPath << ",family:" << fontFamily << ",style:" << style << std::endl;
        }
    }

    // 简单打印
    FcPatternPrint(font);

    //自定义格式打印
    const FcChar8* format = (const FcChar8*)"%{=fclist}\n";
    //const FcChar8* format = (const FcChar8*)"%{=unparse}\n";
    //const FcChar8* format = (const FcChar8*)"%{=fcmatch}\n";
    auto s = FcPatternFormat(font, format);
    std::cout << s << "\n";
    FcStrFree(s);
}

// 测试字体安装目录
void listInstallFonts() {
    // Get all installed fonts
    auto pattern = FcPatternCreate();
    auto objectSet = FcObjectSetBuild(FC_FAMILY, FC_STYLE, FC_FILE, FC_SLANT, FC_WEIGHT, nullptr);
    auto fontSet = FcFontList(nullptr, pattern, objectSet);

    FcObjectSetDestroy(objectSet);
    FcPatternDestroy(pattern);

    if (fontSet == nullptr) {
        printf("Unable to search for fonts");
        return;
    }

    std::cout << "font count " << fontSet->nfont << std::endl;
    for (int i = 0; i < fontSet->nfont; i++)
        testSingleFont(fontSet->fonts[i]);

    FcFontSetDestroy(fontSet);
}


static FcStrSet* processed_dirs;

static int
scanDirs(FcStrList* list, FcConfig* config, FcBool force, FcBool really_force, FcBool verbose, FcBool error_on_no_fonts, int* changed)
{
    int		    ret = 0;
    const FcChar8* dir;
    FcStrSet* subdirs;
    FcStrList* sublist;
    FcCache* cache;
    struct stat	    statb;
    FcBool	    was_valid, was_processed = FcFalse;
    int		    i;
    const FcChar8* sysroot = FcConfigGetSysRoot(config);

    /*
     * Now scan all of the directories into separate databases
     * and write out the results
     */
    while ((dir = FcStrListNext(list)))
    {
        if (verbose)
        {
            if (sysroot)
                printf("[%s]", sysroot);
            printf("%s: ", dir);
            fflush(stdout);
        }

        if (FcStrSetMember(processed_dirs, dir))
        {
            if (verbose)
                printf(("skipping, looped directory detected\n"));
            continue;
        }

        FcChar8* rooted_dir = NULL;
        if (sysroot)
        {
            rooted_dir = FcStrPlus(sysroot, dir);
        }
        else {
            rooted_dir = FcStrCopy(dir);
        }

        if (stat((char*)rooted_dir, &statb) == -1)
        {
            switch (errno) {
            case ENOENT:
            case ENOTDIR:
                if (verbose)
                    printf(("skipping, no such directory\n"));
                break;
            default:
                fprintf(stderr, "\"%s\": ", dir);
                perror("");
                ret++;
                break;
            }
            FcStrFree(rooted_dir);
            rooted_dir = NULL;
            continue;
        }

        FcStrFree(rooted_dir);
        rooted_dir = NULL;

        if ((statb.st_mode & 0xF000) != 0x4000)
        {
            fprintf(stderr,  ("\"%s\": not a directory, skipping\n"), dir);
            continue;
        }
        was_processed = FcTrue;

        if (really_force)
        {
            FcDirCacheUnlink(dir, config);
        }

        cache = NULL;
        was_valid = FcFalse;
        if (!force) {
            //if (FcFileIsDir(arg))
            //    cache = FcDirCacheLoad(arg, config, &rooted_dir);
            //else
            //    cache = FcDirCacheLoadFile(arg, &statb);

            cache = FcDirCacheLoad(dir, config, NULL);
            if (cache)
                was_valid = FcTrue;
        }

        if (!cache)
        {
            (*changed)++;
            cache = FcDirCacheRead(dir, FcTrue, config);
            if (!cache)
            {
                fprintf(stderr,  ("\"%s\": scanning error\n"), dir);
                ret++;
                continue;
            }
        }

        if (was_valid)
        {
            if (verbose)
                printf(("skipping, existing cache is valid: %d fonts, %d dirs\n"),
                    FcCacheNumFont(cache), FcCacheNumSubdir(cache));
        }
        else
        {
            if (verbose)
                printf(("caching, new cache contents: %d fonts, %d dirs\n"),
                    FcCacheNumFont(cache), FcCacheNumSubdir(cache));

            if (!FcDirCacheValid(dir))
            {
                fprintf(stderr,  ("%s: failed to write cache\n"), dir);
                (void)FcDirCacheUnlink(dir, config);
                ret++;
            }
        }

        subdirs = FcStrSetCreate();
        if (!subdirs)
        {
            fprintf(stderr,  ("%s: Can't create subdir set\n"), dir);
            ret++;
            FcDirCacheUnload(cache);
            continue;
        }
        for (i = 0; i < FcCacheNumSubdir(cache); i++)
            FcStrSetAdd(subdirs, FcCacheSubdir(cache, i));

        FcDirCacheUnload(cache);

        sublist = FcStrListCreate(subdirs);
        FcStrSetDestroy(subdirs);
        if (!sublist)
        {
            fprintf(stderr,  ("%s: Can't create subdir list\n"), dir);
            ret++;
            continue;
        }
        FcStrSetAdd(processed_dirs, dir);
        ret += scanDirs(sublist, config, force, really_force, verbose, error_on_no_fonts, changed);
        FcStrListDone(sublist);
    }

    if (error_on_no_fonts && !was_processed)
        ret++;
    return ret;
}

static FcBool
cleanCacheDirectories(FcConfig* config, FcBool verbose)
{
    FcStrList* cache_dirs = FcConfigGetCacheDirs(config);
    FcChar8* cache_dir;
    FcBool	ret = FcTrue;

    if (!cache_dirs)
        return FcFalse;
    while ((cache_dir = FcStrListNext(cache_dirs)))
    {
        if (!FcDirCacheClean(cache_dir, verbose))
        {
            ret = FcFalse;
            break;
        }
    }
    FcStrListDone(cache_dirs);
    return ret;
}

// 测试字体缓存目录
int testFontCache(const char* fontdirs) {
    //if (!FcFileIsDir((const FCChar8*)fontdirs))
    //    return 0;

        FcStrSet* dirs;
        FcStrList* list;
        FcBool    	verbose = FcFalse;
        FcBool	force = FcFalse;
        FcBool	really_force = FcFalse;
        FcBool	systemOnly = FcFalse;
        FcBool	error_on_no_fonts = FcFalse;
        FcConfig* config;
        FcChar8* sysroot = NULL;
        int		changed;
        int		ret;

        if (systemOnly)
            FcConfigEnableHome(FcFalse);
        if (sysroot)
        {
            FcConfigSetSysRoot(NULL, sysroot);
            FcStrFree(sysroot);
            config = FcConfigGetCurrent();
        }
        else
        {
            config = FcInitLoadConfig();
        }
        if (!config)
        {
            fprintf(stderr,  ("%s: Can't initialize font config library\n"), fontdirs);
            return 1;
        }
        FcConfigSetCurrent(config);

        if (fontdirs)
        {
            dirs = FcStrSetCreate();
            if (!dirs)
            {
                fprintf(stderr,  ("%s: Can't create list of directories\n"),
                    fontdirs);
                return 1;
            }

            if (!FcStrSetAddFilename(dirs, (FcChar8*)fontdirs))
            {
                fprintf(stderr,  ("%s: Can't add directory\n"), fontdirs);
                return 1;
            }
            list = FcStrListCreate(dirs);
            FcStrSetDestroy(dirs);
        }
        else
            list = FcConfigGetFontDirs(config);

        if ((processed_dirs = FcStrSetCreate()) == NULL) {
            fprintf(stderr,  ("Out of Memory\n"));
            return 1;
        }

        if (verbose)
        {
            const FcChar8* dir;

            printf("Font directories:\n");
            while ((dir = FcStrListNext(list)))
            {
                printf("\t%s\n", dir);
            }
            FcStrListFirst(list);
        }
        changed = 0;
        ret = scanDirs(list, config, force, really_force, verbose, error_on_no_fonts, &changed);
        FcStrListDone(list);

        /*
         * Try to create CACHEDIR.TAG anyway.
         * This expects the fontconfig cache directory already exists.
         * If it doesn't, it won't be simply created.
         */
        FcCacheCreateTagFile(config);

        FcStrSetDestroy(processed_dirs);

        cleanCacheDirectories(config, verbose);

        FcConfigDestroy(config);
        FcFini();
        /*
         * Now we need to sleep a second  (or two, to be extra sure), to make
         * sure that timestamps for changes after this run of fc-cache are later
         * then any timestamps we wrote.  We don't use gettimeofday() because
         * sleep(3) can't be interrupted by a signal here -- this isn't in the
         * library, and there aren't any signals flying around here.
         */
         /* the resolution of mtime on FAT is 2 seconds */
        //if (changed)
        //    sleep(2);
        if (verbose)
            printf("%s: %s\n", fontdirs, ret ? "failed" : "succeeded");
        return ret;
}

// 配置列表
void testConfigList() {
    FcConfig* config;
    FcConfigFileInfoIter iter;
    config = FcConfigGetCurrent();
    FcConfigFileInfoIterInit(config, &iter);
    do
    {
        FcChar8* name, * desc;
        FcBool enabled;

        if (FcConfigFileInfoIterGet(config, &iter, &name, &desc, &enabled))
        {
            printf("%c %s: %s\n", enabled ? '+' : '-', name, desc);
            FcStrFree(name);
            FcStrFree(desc);
        }
    } while (FcConfigFileInfoIterNext(config, &iter));

    FcFini();
}

// 字体查找
void queryFont(const FcChar8 *fontdir) {

    auto fs = FcFontSetCreate();
    // FcFreeTypeQuery
    if (!FcFreeTypeQueryAll(fontdir, 0, NULL, NULL, fs))
    {
        fprintf(stderr, "Can't query face %u of font file %s\n", -1, fontdir);
        return;
    }

    for (int i = 0; i < fs->nfont; i++)
    {
        FcPattern* pat = fs->fonts[i];
        FcPatternPrint(pat);
        //FcPatternDel(pat, FC_CHARSET);
        //FcPatternDel(pat, FC_LANG);
    }
    FcFontSetDestroy(fs);

    FcFini();
}


// 字体目录解析
void scanFont(const FcChar8* file) {

    auto fs = FcFontSetCreate();

    if (!FcFileIsDir(file))
        FcFileScan(fs, NULL, NULL, NULL, file, FcTrue);
    else
    {
        FcStrSet* dirs = FcStrSetCreate();
        FcStrList* strlist = FcStrListCreate(dirs);
        do
        {
            FcDirScan(fs, dirs, NULL, NULL, file, FcTrue);
        } while ((file = FcStrListNext(strlist)));
        FcStrListDone(strlist);
        FcStrSetDestroy(dirs);
    }

    for (int i = 0; i < fs->nfont; i++)
    {
        FcPattern* pat = fs->fonts[i];
        FcPatternPrint(pat);
        //FcPatternDel(pat, FC_CHARSET);
        //FcPatternDel(pat, FC_LANG);
    }
    FcFontSetDestroy(fs);

    FcFini();
}

void test()
{
    //listInstallFonts();
    //testFontCache("C:\\Fonts");
    //testConfigList();
    //queryFont((const FcChar8*)"C:\\Fonts\\LiberationSans.ttc");
    //scanFont((const FcChar8*)"C:\\Fonts");
    // 待续......
    // FcConfigSetCurrent FcConfigUptoDate  FcConfigGetCache  FcConfigGetRescanInterval  FcConfigGetFonts   FcConfigAppFontAddFile   FcConfigAppFontAddDir  
    // FcConfigAppFontClear     FcDirSave   FcDirCacheRescan    FcFontSetList   FcAtomicLock    FcFontSetMatch  FcFontRenderPrepare     FcFontSetSort   
    // FcFontSetSortDestroy     FcPatternHash
}
输出


    fontPath:C:\Windows\fonts\GOTHIC.TTF,family:Century Gothic,style:0
Pattern has 5 elts (size 16)
        family: "Century Gothic"(s)
        style: "Regular"(s) "Normal"(s) "obyčejné"(s) "Standard"(s) "Κανονικά"(s) "Normaali"(s) "Normál"(s) "Normale"(s) "Standaard"(s) "Normalny"(s) "Обычный"(s) "Normálne"(s) "Navadno"(s) "Arrunta"(s)
        slant: 0(i)(s)
        weight: 80(f)(s)
        file: "C:/Windows/fonts\GOTHIC.TTF"(s)

C:/Windows/fonts\GOTHIC.TTF: Century Gothic:style=Regular,Normal,obyčejné,Standard,Κανονικά,Normaali,Normál,Normale,Standaard,Normalny,Обычный,Normálne,Navadno,Arrunta:slant=0:weight=80

Pattern has 26 elts (size 32)
    family: "Liberation Sans"(s)
    familylang: "en"(s)
    style: "Bold"(s)
    stylelang: "en"(s)
    fullname: "Liberation Sans Bold"(s)
    fullnamelang: "en"(s)
    slant: 0(i)(s)
    weight: 200(f)(s)
    width: 100(f)(s)
    foundry: "1ASC"(s)
    file: "C:\Fonts\LiberationSans-Bold.ttf"(s)
    index: 0(i)(s)
    outline: True(s)
    scalable: True(s)
    charset:
    0000: 00000000 ffffffff ffffffff 7fffffff 00000000 ffffffff ffffffff ffffffff
    0001: ffffffff ffffffff ffffffff ffffffff 00040000 00000000 00000000 fc000000
    0002: 0f000000 00000000 00000000 00000000 00000000 00000000 3f0002c0 00000000
    0003: 00000000 00000000 00000000 40000000 ffffd7f0 fffffffb 00007fff 00000000
    0004: ffffffff ffffffff ffffffff 000c0000 00030000 00000000 00000000 00000000
    001e: 00000000 00000000 00000000 00000000 0000003f 00000000 00000000 000c0000
    0020: 7fbb0000 560d0047 00000010 80000000 00000000 00001098 00000000 00000000
    0021: 00480020 00004044 78000000 00000000 003f0000 00000100 00000000 00000000
    0022: c6268044 00000a00 00000100 00000033 00000000 00000000 00000000 00000000
    0023: 00010004 00000003 00000000 00000000 00000000 00000000 00000000 00000000
    0025: 11111005 10101010 ffff0000 00001fff 000f1111 14041c03 03008c10 00000040
    0026: 00000000 1c000000 00000005 00001c69 00000000 00000000 00000000 00000000
    00f0: 00000026 00000000 00000000 00000000 00000000 00000000 00000000 00000000
    00fb: 00000006 00000000 00000000 00000000 00000000 00000000 00000000 00000000
(s)
    lang: aa|af|av|ay|be|bg|bi|br|bs|ca|ce|ch|co|cs|cy|da|de|el|en|eo|es|et|eu|fi|fj|fo|fr|fur|fy|gd|gl|gv|ho|hr|hu|ia|id|ie|ik|io|is|it|ki|kl|kum|la|lb|lez|lt|lv|mg|mh|mk|mo|mt|nb|nds|nl|nn|no|nr|nso|ny|oc|om|os|pl|pt|rm|ro|ru|se|sel|sk|sl|sma|smj|smn|so|sq|sr|ss|st|sv|sw|tk|tl|tn|tr|ts|uk|uz|vo|vot|wa|wen|wo|xh|yap|zu|an|crh|csb|fil|hsb|ht|jv|kj|ku-tr|kwm|lg|li|ms|na|ng|pap-an|pap-aw|rn|rw|sc|sg|sn|su|za(s)
    fontversion: 70123(i)(s)
    capability: "otlayout:DFLT otlayout:cyrl otlayout:grek otlayout:latn"(s)
    fontformat: "TrueType"(s)
    decorative: False(s)
    postscriptname: "LiberationSans-Bold"(s)
    color: False(s)
    symbol: False(s)
    variable: False(s)
    fonthashint: True(s)
    order: 0(i)(s)

参考

C++字体库开发之字符显示四-CSDN博客

https://github.com/freedesktop/fontconfig

https://github.com/ShiftMediaProject/fontconfig

https://github.com/benoitkugler/textlayout

https://github.com/benoitkugler/textprocessing

https://github.com/go-text/typesetting


创作不易,小小的支持一下吧!

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码力码力我爱你

创作不易,小小的支持一下吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值