summaryrefslogtreecommitdiff
path: root/vcl/source/gdi/embeddedfontshelper.cxx
blob: a6a2b61676f783ca59bbdf9a4786b592fe8a1d8d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <sal/config.h>

#include <memory>
#include <config_folders.h>
#include <config_eot.h>

#include <osl/file.hxx>
#include <rtl/bootstrap.hxx>
#include <sal/log.hxx>
#include <vcl/svapp.hxx>
#include <vcl/embeddedfontshelper.hxx>
#include <com/sun/star/io/XInputStream.hpp>

#include <font/PhysicalFontFaceCollection.hxx>
#include <font/PhysicalFontCollection.hxx>
#include <salgdi.hxx>
#include <sft.hxx>


#if ENABLE_EOT
extern "C"
{
namespace libeot
{
#include <libeot/libeot.h>
} // namespace libeot
} // extern "C"
#endif

using namespace com::sun::star;
using namespace vcl;

static void clearDir( const OUString& path )
{
    osl::Directory dir( path );
    if( dir.reset() == osl::Directory::E_None )
    {
        for(;;)
        {
            osl::DirectoryItem item;
            if( dir.getNextItem( item ) != osl::Directory::E_None )
                break;
            osl::FileStatus status( osl_FileStatus_Mask_FileURL );
            if( item.getFileStatus( status ) == osl::File::E_None )
                osl::File::remove( status.getFileURL());
        }
    }
}

void EmbeddedFontsHelper::clearTemporaryFontFiles()
{
    OUString path = u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE( "bootstrap") "::UserInstallation}"_ustr;
    rtl::Bootstrap::expandMacros( path );
    path += "/user/temp/embeddedfonts/";
    clearDir( path + "fromdocs/" );
    clearDir( path + "fromsystem/" );
}

bool EmbeddedFontsHelper::addEmbeddedFont( const uno::Reference< io::XInputStream >& stream, const OUString& fontName,
    std::u16string_view extra, std::vector< unsigned char > const & key, bool eot,
    bool bSubsetted )
{
    OUString fileUrl = EmbeddedFontsHelper::fileUrlForTemporaryFont( fontName, extra );
    osl::File file( fileUrl );
    switch( file.open( osl_File_OpenFlag_Create | osl_File_OpenFlag_Write ))
    {
        case osl::File::E_None:
            break; // ok
        case osl::File::E_EXIST:
            return true; // Assume it's already been added correctly.
        default:
            SAL_WARN( "vcl.fonts", "Cannot open file for temporary font" );
            return false;
    }
    size_t keyPos = 0;
    std::vector< char > fontData;
    fontData.reserve( 1000000 );
    for(;;)
    {
        uno::Sequence< sal_Int8 > buffer;
        sal_uInt64 read = stream->readBytes( buffer, 1024 );
        auto bufferRange = asNonConstRange(buffer);
        for( sal_uInt64 pos = 0;
             pos < read && keyPos < key.size();
             ++pos )
            bufferRange[ pos ] ^= key[ keyPos++ ];
        // if eot, don't write the file out yet, since we need to unpack it first.
        if( !eot && read > 0 )
        {
            sal_uInt64 writtenTotal = 0;
            while( writtenTotal < read )
            {
                sal_uInt64 written;
                file.write( buffer.getConstArray(), read, written );
                writtenTotal += written;
            }
        }
        fontData.insert( fontData.end(), buffer.getConstArray(), buffer.getConstArray() + read );
        if( read <= 0 )
            break;
    }
    bool sufficientFontRights(false);
#if ENABLE_EOT
    if( eot )
    {
        unsigned uncompressedFontSize = 0;
        unsigned char *nakedPointerToUncompressedFont = nullptr;
        libeot::EOTMetadata eotMetadata;
        libeot::EOTError uncompressError =
            libeot::EOT2ttf_buffer( reinterpret_cast<unsigned char *>(fontData.data()), fontData.size(), &eotMetadata, &nakedPointerToUncompressedFont, &uncompressedFontSize );
        std::shared_ptr<unsigned char> uncompressedFont( nakedPointerToUncompressedFont, libeot::EOTfreeBuffer );
        if( uncompressError != libeot::EOT_SUCCESS )
        {
            SAL_WARN( "vcl.fonts", "Failed to uncompress font" );
            osl::File::remove( fileUrl );
            return false;
        }
        sal_uInt64 writtenTotal = 0;
        while( writtenTotal < uncompressedFontSize )
        {
            sal_uInt64 written;
            if( file.write( uncompressedFont.get() + writtenTotal, uncompressedFontSize - writtenTotal, written ) != osl::File::E_None )
            {
                SAL_WARN( "vcl.fonts", "Error writing temporary font file" );
                osl::File::remove( fileUrl );
                return false;
            }
            writtenTotal += written;
        }
        sufficientFontRights = libeot::EOTcanLegallyEdit( &eotMetadata );
        libeot::EOTfreeMetadata( &eotMetadata );
    }
#endif

    if( file.close() != osl::File::E_None )
    {
        SAL_WARN( "vcl.fonts", "Writing temporary font file failed" );
        osl::File::remove( fileUrl );
        return false;
    }
    if( !eot )
    {
        sufficientFontRights = sufficientTTFRights(fontData.data(), fontData.size(), FontRights::EditingAllowed);
    }
    if( !sufficientFontRights )
    {
        // It would be actually better to open the document in read-only mode in this case,
        // warn the user about this, and provide a button to drop the font(s) in order
        // to switch to editing.
        SAL_INFO( "vcl.fonts", "Ignoring embedded font that is not usable for editing" );
        osl::File::remove( fileUrl );
        return false;
    }

    if (bSubsetted)
    {
        TrueTypeFont* font;
        sal_uInt32 nGlyphs = 0;
        if (OpenTTFontBuffer(fontData.data(), fontData.size(), 0, &font) == SFErrCodes::Ok)
        {
            sal_uInt32 nGlyphCount = font->glyphCount();
            for (sal_uInt32 i = 0; i < nGlyphCount; ++i)
            {
                sal_uInt32 nOffset = font->glyphOffset(i);
                sal_uInt32 nNextOffset = font->glyphOffset(i + 1);
                if (nOffset == nNextOffset)
                {
                    // GetTTGlyphComponents() says this is an empty glyph, ignore it.
                    continue;
                }
                ++nGlyphs;
            }
            CloseTTFont(font);
        }
        // Check if it has reasonable amount of glyphs, set the limit to the number of glyphs in the
        // English alphabet (not differentiating lowercase and uppercase).
        if (nGlyphs < 26)
        {
            SAL_INFO("vcl.fonts", "Ignoring embedded font that only provides " << nGlyphs << " non-empty glyphs");
            osl::File::remove(fileUrl);
            return false;
        }
    }

    m_aAccumulatedFonts.emplace_back(std::make_pair(fontName, fileUrl));
    return true;
}

namespace
{
    struct UpdateFontsGuard
    {
        UpdateFontsGuard()
        {
            OutputDevice::ImplClearAllFontData(true);
        }

        ~UpdateFontsGuard()
        {
            OutputDevice::ImplRefreshAllFontData(true);
        }
    };
}

void EmbeddedFontsHelper::activateFonts()
{
    if (m_aAccumulatedFonts.empty())
        return;
    UpdateFontsGuard aUpdateFontsGuard;
    for (const auto& rEntry : m_aAccumulatedFonts)
        EmbeddedFontsHelper::activateFont(rEntry.first, rEntry.second);
    m_aAccumulatedFonts.clear();
}

OUString EmbeddedFontsHelper::fileUrlForTemporaryFont( const OUString& fontName, std::u16string_view extra )
{
    OUString path = u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE( "bootstrap") "::UserInstallation}"_ustr;
    rtl::Bootstrap::expandMacros( path );
    path += "/user/temp/embeddedfonts/fromdocs/";
    osl::Directory::createPath( path );
    OUString filename = fontName;
    static int uniqueCounter = 0;
    if( extra == u"?" )
        filename += OUString::number( uniqueCounter++ );
    else
        filename += extra;
    filename += ".ttf"; // TODO is it always ttf?
    return path + filename;
}

void EmbeddedFontsHelper::activateFont( const OUString& fontName, const OUString& fileUrl )
{
    OutputDevice *pDevice = Application::GetDefaultDevice();
    pDevice->AddTempDevFont(fileUrl, fontName);
}

// Check if it's (legally) allowed to embed the font file into a document
// (ttf has a flag allowing this). PhysicalFontFace::IsEmbeddable() appears
// to have a different meaning (guessing from code, IsSubsettable() might
// possibly mean it's ttf, while IsEmbeddable() might mean it's type1).
// So just try to open the data as ttf and see.
bool EmbeddedFontsHelper::sufficientTTFRights( const void* data, tools::Long size, FontRights rights )
{
    TrueTypeFont* font;
    if( OpenTTFontBuffer( data, size, 0 /*TODO*/, &font ) == SFErrCodes::Ok )
    {
        TTGlobalFontInfo info;
        GetTTGlobalFontInfo( font, &info );
        CloseTTFont( font );
        // https://www.microsoft.com/typography/otspec/os2.htm#fst
        int copyright = info.typeFlags;
        switch( rights )
        {
            case FontRights::ViewingAllowed:
                // Embedding not restricted completely.
                return ( copyright & 0x02 ) != 0x02;
            case FontRights::EditingAllowed:
                // Font is installable or editable.
                return copyright == 0 || ( copyright & 0x08 );
        }
    }
    return true; // no known restriction
}

OUString EmbeddedFontsHelper::fontFileUrl( std::u16string_view familyName, FontFamily family, FontItalic italic,
    FontWeight weight, FontPitch pitch, FontRights rights )
{
    OUString path = u"${$BRAND_BASE_DIR/" LIBO_ETC_FOLDER "/" SAL_CONFIGFILE( "bootstrap") "::UserInstallation}"_ustr;
    rtl::Bootstrap::expandMacros( path );
    path += "/user/temp/embeddedfonts/fromsystem/";
    osl::Directory::createPath( path );
    OUString filename = OUString::Concat(familyName) + "_" + OUString::number( family ) + "_" + OUString::number( italic )
        + "_" + OUString::number( weight ) + "_" + OUString::number( pitch )
        + ".ttf"; // TODO is it always ttf?
    OUString url = path + filename;
    if( osl::File( url ).open( osl_File_OpenFlag_Read ) == osl::File::E_None ) // = exists()
    {
        // File with contents of the font file already exists, assume it's been created by a previous call.
        return url;
    }
    bool ok = false;
    SalGraphics* graphics = Application::GetDefaultDevice()->GetGraphics();
    vcl::font::PhysicalFontCollection fonts;
    graphics->GetDevFontList( &fonts );
    std::unique_ptr< vcl::font::PhysicalFontFaceCollection > fontInfo( fonts.GetFontFaceCollection());
    vcl::font::PhysicalFontFace* selected = nullptr;

    // Maybe we don't find the perfect match for the font. E.G. we have fonts with the same family name
    // but not same bold or italic etc...
    // In this case we add all the fonts having the family name of the used font:
    //  - we store all these fonts in familyNameFonts during loop
    //  - if we haven't found the perfect match we store all fonts in familyNameFonts
    typedef std::vector<vcl::font::PhysicalFontFace*> FontList;
    FontList familyNameFonts;

    for( int i = 0;
         i < fontInfo->Count();
         ++i )
    {
        vcl::font::PhysicalFontFace* f = fontInfo->Get( i );
        if( f->GetFamilyName() == familyName )
        {
            // Ignore comparing text encodings, at least for now. They cannot be trivially compared
            // (e.g. UCS2 and UTF8 are technically the same characters, just have different encoding,
            // and just having a unicode font doesn't say what glyphs it actually contains).
            // It is possible that it still may be needed to do at least some checks here
            // for some encodings (can one font have more font files for more encodings?).
            if(( family == FAMILY_DONTKNOW || f->GetFamilyType() == family )
                && ( italic == ITALIC_DONTKNOW || f->GetItalic() == italic )
                && ( weight == WEIGHT_DONTKNOW || f->GetWeight() == weight )
                && ( pitch == PITCH_DONTKNOW || f->GetPitch() == pitch ))
            { // Exact match, return it immediately.
                selected = f;
                break;
            }
            if(( f->GetFamilyType() == FAMILY_DONTKNOW || family == FAMILY_DONTKNOW || f->GetFamilyType() == family )
                && ( f->GetItalic() == ITALIC_DONTKNOW || italic == ITALIC_DONTKNOW || f->GetItalic() == italic )
                && ( f->GetWeight() == WEIGHT_DONTKNOW || weight == WEIGHT_DONTKNOW || f->GetWeight() == weight )
                && ( f->GetPitch() == PITCH_DONTKNOW || pitch == PITCH_DONTKNOW || f->GetPitch() == pitch ))
            { // Some fonts specify 'DONTKNOW' for some things, still a good match, if we don't find a better one.
                selected = f;
            }
            // adding "not perfect match" to familyNameFonts vector
            familyNameFonts.push_back(f);

        }
    }

    // if we have found a perfect match we will add only "selected", otherwise all familyNameFonts
    FontList fontsToAdd = (selected ? FontList(1, selected) : std::move(familyNameFonts));

    for (vcl::font::PhysicalFontFace* f : fontsToAdd)
    {
        if (!selected) { // recalculate file not for "not perfect match"
            filename = OUString::Concat(familyName) + "_" + OUString::number(f->GetFamilyType()) + "_" +
                OUString::number(f->GetItalic()) + "_" + OUString::number(f->GetWeight()) + "_" +
                OUString::number(f->GetPitch()) + ".ttf"; // TODO is it always ttf?
            url = path + filename;
            if (osl::File(url).open(osl_File_OpenFlag_Read) == osl::File::E_None) // = exists()
            {
                // File with contents of the font file already exists, assume it's been created by a previous call.
                continue;
            }
        }
        auto aFontData(f->GetRawFontData(0));
        if (!aFontData.empty())
        {
            auto data = aFontData.data();
            auto size = aFontData.size();
            if( sufficientTTFRights( data, size, rights ))
            {
                osl::File file( url );
                if( file.open( osl_File_OpenFlag_Write | osl_File_OpenFlag_Create ) == osl::File::E_None )
                {
                    sal_uInt64 written = 0;
                    sal_uInt64 totalSize = size;
                    bool error = false;
                    while( written < totalSize && !error)
                    {
                        sal_uInt64 nowWritten;
                        switch( file.write( data + written, size - written, nowWritten ))
                        {
                            case osl::File::E_None:
                                written += nowWritten;
                                break;
                            case osl::File::E_AGAIN:
                            case osl::File::E_INTR:
                                break;
                            default:
                                error = true;
                                break;
                        }
                    }
                    file.close();
                    if( error )
                        osl::File::remove( url );
                    else
                        ok = true;
                }
            }
        }
    }
    return ok ? url : u""_ustr;
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */