diff options
author | 2017-08-30 13:56:19 -0400 | |
---|---|---|
committer | 2017-08-31 14:18:31 +0000 | |
commit | a25fbef56a4e70192b0b322b6d87b78839fab0e5 (patch) | |
tree | 6ee0cc9f28a6a2c93cc742d01869e3190c669cb1 /tools/shape | |
parent | f95b17524628bd0caad7c7e26417be985905944b (diff) |
Add bidi to SkShaper.
Change-Id: Iaab9a1d5091a3b157a582b7e8c3261a6c3283ffc
Reviewed-on: https://skia-review.googlesource.com/40778
Reviewed-by: Derek Sollenberger <djsollen@google.com>
Commit-Queue: Ben Wagner <bungeman@google.com>
Diffstat (limited to 'tools/shape')
-rw-r--r-- | tools/shape/SkShaper.h | 46 | ||||
-rw-r--r-- | tools/shape/SkShaper_harfbuzz.cpp | 190 | ||||
-rw-r--r-- | tools/shape/SkShaper_primitive.cpp | 70 | ||||
-rw-r--r-- | tools/shape/using_skia_and_harfbuzz.cpp | 218 |
4 files changed, 524 insertions, 0 deletions
diff --git a/tools/shape/SkShaper.h b/tools/shape/SkShaper.h new file mode 100644 index 0000000000..a2a301e792 --- /dev/null +++ b/tools/shape/SkShaper.h @@ -0,0 +1,46 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef SkShaper_DEFINED +#define SkShaper_DEFINED + +#include <memory> + +#include "SkPoint.h" +#include "SkTypeface.h" + +class SkPaint; +class SkTextBlobBuilder; + +/** + Shapes text using HarfBuzz and places the shaped text into a + TextBlob. + + If compiled without HarfBuzz, fall back on SkPaint::textToGlyphs. + */ +class SkShaper { +public: + SkShaper(sk_sp<SkTypeface> face); + ~SkShaper(); + + bool good() const; + SkScalar shape(SkTextBlobBuilder* dest, + const SkPaint& srcPaint, + const char* utf8text, + size_t textBytes, + bool leftToRight, + SkPoint point) const; + +private: + SkShaper(const SkShaper&) = delete; + SkShaper& operator=(const SkShaper&) = delete; + + struct Impl; + std::unique_ptr<Impl> fImpl; +}; + +#endif // SkShaper_DEFINED diff --git a/tools/shape/SkShaper_harfbuzz.cpp b/tools/shape/SkShaper_harfbuzz.cpp new file mode 100644 index 0000000000..3947406b85 --- /dev/null +++ b/tools/shape/SkShaper_harfbuzz.cpp @@ -0,0 +1,190 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include <hb-ot.h> +#include <unicode/stringpiece.h> +#include <unicode/ubidi.h> +#include <unicode/unistr.h> + +#include "SkShaper.h" +#include "SkStream.h" +#include "SkTextBlob.h" +#include "SkTypeface.h" +#include "SkUtils.h" + +static const int FONT_SIZE_SCALE = 512; + +namespace { +template <class T, void(*P)(T*)> using resource = std::unique_ptr<T, SkFunctionWrapper<void, T, P>>; +using HBBlob = resource<hb_blob_t , hb_blob_destroy >; +using HBFace = resource<hb_face_t , hb_face_destroy >; +using HBFont = resource<hb_font_t , hb_font_destroy >; +using HBBuffer = resource<hb_buffer_t, hb_buffer_destroy>; +using ICUBiDi = resource<UBiDi , ubidi_close >; + +HBBlob stream_to_blob(std::unique_ptr<SkStreamAsset> asset) { + size_t size = asset->getLength(); + HBBlob blob; + if (const void* base = asset->getMemoryBase()) { + blob.reset(hb_blob_create((char*)base, SkToUInt(size), + HB_MEMORY_MODE_READONLY, asset.release(), + [](void* p) { delete (SkStreamAsset*)p; })); + } else { + // SkDebugf("Extra SkStreamAsset copy\n"); + void* ptr = size ? sk_malloc_throw(size) : nullptr; + asset->read(ptr, size); + blob.reset(hb_blob_create((char*)ptr, SkToUInt(size), + HB_MEMORY_MODE_READONLY, ptr, sk_free)); + } + SkASSERT(blob); + hb_blob_make_immutable(blob.get()); + return blob; +} +} // namespace + +struct SkShaper::Impl { + HBFont fHarfBuzzFont; + HBBuffer fBuffer; + sk_sp<SkTypeface> fTypeface; +}; + +SkShaper::SkShaper(sk_sp<SkTypeface> tf) : fImpl(new Impl) { + fImpl->fTypeface = tf ? std::move(tf) : SkTypeface::MakeDefault(); + int index; + HBBlob blob(stream_to_blob(std::unique_ptr<SkStreamAsset>( + fImpl->fTypeface->openStream(&index)))); + HBFace face(hb_face_create(blob.get(), (unsigned)index)); + SkASSERT(face); + if (!face) { + return; + } + hb_face_set_index(face.get(), (unsigned)index); + hb_face_set_upem(face.get(), fImpl->fTypeface->getUnitsPerEm()); + + fImpl->fHarfBuzzFont.reset(hb_font_create(face.get())); + SkASSERT(fImpl->fHarfBuzzFont); + hb_font_set_scale(fImpl->fHarfBuzzFont.get(), FONT_SIZE_SCALE, FONT_SIZE_SCALE); + hb_ot_font_set_funcs(fImpl->fHarfBuzzFont.get()); + + fImpl->fBuffer.reset(hb_buffer_create()); +} + +SkShaper::~SkShaper() {} + +bool SkShaper::good() const { return fImpl->fHarfBuzzFont != nullptr; } + +SkScalar SkShaper::shape(SkTextBlobBuilder* builder, + const SkPaint& srcPaint, + const char* utf8text, + size_t textBytes, + bool leftToRight, + SkPoint point) const { + SkASSERT(builder); + UBiDiLevel bidiLevel = leftToRight ? UBIDI_DEFAULT_LTR : UBIDI_DEFAULT_RTL; + //hb_script_t script = ... + UErrorCode status = U_ZERO_ERROR; + double x = point.x(); + double y = point.y(); + + // This function only accepts utf8. + // ubidi only accepts utf16 (though internally it basically works on utf32 chars). + // Internally, harfbuzz is all utf32, but always makes a copy. + + if (!SkTFitsIn<int32_t>(textBytes)) { + SkDebugf("Bidi error: text too long"); + return (SkScalar)x; + } + icu::UnicodeString utf16 = icu::UnicodeString::fromUTF8(icu::StringPiece(utf8text, textBytes)); + + ICUBiDi bidi(ubidi_openSized(utf16.length(), 0, &status)); + if (U_FAILURE(status)) { + SkDebugf("Bidi error: %s", u_errorName(status)); + return (SkScalar)x; + } + SkASSERT(bidi); + + ubidi_setPara(bidi.get(), utf16.getBuffer(), utf16.length(), bidiLevel, nullptr, &status); + if (U_FAILURE(status)) { + SkDebugf("Bidi error: %s", u_errorName(status)); + return (SkScalar)x; + } + + int32_t runCount = ubidi_countRuns(bidi.get(), &status); + if (U_FAILURE(status)) { + SkDebugf("Bidi error: %s", u_errorName(status)); + return (SkScalar)x; + } + + for (int32_t i = 0; i < runCount; ++i) { + int32_t start; + int32_t length; + UBiDiDirection direction = ubidi_getVisualRun(bidi.get(), i, &start, &length); + + SkPaint paint(srcPaint); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + paint.setTypeface(fImpl->fTypeface); + + hb_buffer_t* buffer = fImpl->fBuffer.get(); + SkAutoTCallVProc<hb_buffer_t, hb_buffer_clear_contents> autoClearBuffer(buffer); + + // The difficulty here is the cluster mapping. + // If the hb_buffer is created with utf16, clusters will be pointing to the utf16 indexes, + // but the SkTextBlob can only take utf8 and utf8 cluster indexes. + // Instead of updating each cluster index, create the hb_buffer from the utf8. + // TODO: this is horribly inefficient. + const char* utf8textStart = utf8text; + const UChar* utf16Start = utf16.getBuffer(); + while (utf16Start < utf16.getBuffer() + start) { + SkUTF16_NextUnichar(&utf16Start); + SkUTF8_NextUnichar(&utf8textStart); + } + const char* utf8textEnd = utf8textStart; + const UChar* utf16End = utf16Start; + while (utf16End < utf16.getBuffer() + start + length) { + SkUTF16_NextUnichar(&utf16End); + SkUTF8_NextUnichar(&utf8textEnd); + } + size_t utf8runLength = utf8textEnd - utf8textStart; + if (!SkTFitsIn<int>(utf8runLength)) { + SkDebugf("Shaping error: utf8 too long"); + return (SkScalar)x; + } + hb_buffer_add_utf8(buffer, utf8textStart, utf8runLength, 0, -1); + hb_buffer_guess_segment_properties(buffer); + //hb_buffer_set_script(buffer, script); + hb_buffer_set_direction(buffer, direction ? HB_DIRECTION_RTL : HB_DIRECTION_LTR); + hb_shape(fImpl->fHarfBuzzFont.get(), buffer, nullptr, 0); + unsigned len = hb_buffer_get_length(buffer); + if (len == 0) { + continue; + } + + hb_glyph_info_t* info = hb_buffer_get_glyph_infos(buffer, nullptr); + hb_glyph_position_t* pos = hb_buffer_get_glyph_positions(buffer, nullptr); + + if (!SkTFitsIn<int>(len)) { + SkDebugf("Shaping error: too many glyphs"); + return (SkScalar)x; + } + auto runBuffer = builder->allocRunTextPos(paint, len, utf8runLength, SkString()); + memcpy(runBuffer.utf8text, utf8textStart, utf8runLength); + + double textSizeY = paint.getTextSize() / (double)FONT_SIZE_SCALE; + double textSizeX = textSizeY * paint.getTextScaleX(); + + for (unsigned i = 0; i < len; i++) { + runBuffer.glyphs[i] = info[i].codepoint; + runBuffer.clusters[i] = info[i].cluster; + reinterpret_cast<SkPoint*>(runBuffer.pos)[i] = + SkPoint::Make(SkDoubleToScalar(x + pos[i].x_offset * textSizeX), + SkDoubleToScalar(y - pos[i].y_offset * textSizeY)); + x += pos[i].x_advance * textSizeX; + y += pos[i].y_advance * textSizeY; + } + } + return (SkScalar)x; +} diff --git a/tools/shape/SkShaper_primitive.cpp b/tools/shape/SkShaper_primitive.cpp new file mode 100644 index 0000000000..750c51621e --- /dev/null +++ b/tools/shape/SkShaper_primitive.cpp @@ -0,0 +1,70 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "SkShaper.h" +#include "SkStream.h" +#include "SkTextBlob.h" +#include "SkTypeface.h" + +struct SkShaper::Impl { + sk_sp<SkTypeface> fTypeface; +}; + +SkShaper::SkShaper(sk_sp<SkTypeface> tf) : fImpl(new Impl) { + fImpl->fTypeface = tf ? std::move(tf) : SkTypeface::MakeDefault(); +} + +SkShaper::~SkShaper() {} + +bool SkShaper::good() const { return true; } + +// This example only uses public API, so we don't use SkUTF8_NextUnichar. +unsigned utf8_lead_byte_to_count(const char* ptr) { + uint8_t c = *(const uint8_t*)ptr; + SkASSERT(c <= 0xF7); + SkASSERT((c & 0xC0) != 0x80); + return (((0xE5 << 24) >> ((unsigned)c >> 4 << 1)) & 3) + 1; +} + +SkScalar SkShaper::shape(SkTextBlobBuilder* builder, + const SkPaint& srcPaint, + const char* utf8text, + size_t textBytes, + bool leftToRight, + SkPoint point) const { + sk_ignore_unused_variable(leftToRight); + + SkPaint paint(srcPaint); + paint.setTypeface(fImpl->fTypeface); + paint.setTextEncoding(SkPaint::kUTF8_TextEncoding); + int glyphCount = paint.countText(utf8text, textBytes); + if (glyphCount <= 0) { + return 0; + } + SkRect bounds; + (void)paint.measureText(utf8text, textBytes, &bounds); + paint.setTextEncoding(SkPaint::kGlyphID_TextEncoding); + const SkTextBlobBuilder::RunBuffer& runBuffer = + builder->allocRunTextPosH(paint, glyphCount, point.y(), textBytes, SkString(), &bounds); + memcpy(runBuffer.utf8text, utf8text, textBytes); + const char* txtPtr = utf8text; + for (int i = 0; i < glyphCount; ++i) { + // Each charater maps to exactly one glyph via SkGlyphCache::unicharToGlyph(). + runBuffer.clusters[i] = SkToU32(txtPtr - utf8text); + txtPtr += utf8_lead_byte_to_count(txtPtr); + SkASSERT(txtPtr <= utf8text + textBytes); + } + paint.setTextEncoding(SkPaint::kUTF8_TextEncoding); + (void)paint.textToGlyphs(utf8text, textBytes, runBuffer.glyphs); + (void)paint.getTextWidths(utf8text, textBytes, runBuffer.pos); + SkScalar x = point.x(); + for (int i = 0; i < glyphCount; ++i) { + SkScalar w = runBuffer.pos[i]; + runBuffer.pos[i] = x; + x += w; + } + return (SkScalar)x; +} diff --git a/tools/shape/using_skia_and_harfbuzz.cpp b/tools/shape/using_skia_and_harfbuzz.cpp new file mode 100644 index 0000000000..3f1515fb1d --- /dev/null +++ b/tools/shape/using_skia_and_harfbuzz.cpp @@ -0,0 +1,218 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +// This sample progam demonstrates how to use Skia and HarfBuzz to +// produce a PDF file from UTF-8 text in stdin. + +#include <cassert> +#include <cstdlib> +#include <iostream> +#include <map> +#include <sstream> +#include <string> +#include <vector> + +#include "SkCanvas.h" +#include "SkDocument.h" +#include "SkShaper.h" +#include "SkStream.h" +#include "SkTextBlob.h" +#include "SkTypeface.h" + +// Options ///////////////////////////////////////////////////////////////////// + +struct BaseOption { + std::string selector; + std::string description; + virtual void set(std::string _value) = 0; + virtual std::string valueToString() = 0; + + BaseOption(std::string _selector, std::string _description) + : selector(_selector), description(_description) {} + + virtual ~BaseOption() {} + + static void Init(const std::vector<BaseOption*> &, int argc, char **argv); +}; + +template <class T> +struct Option : BaseOption { + T value; + Option(std::string selector, std::string description, T defaultValue) + : BaseOption(selector, description), value(defaultValue) {} +}; + +void BaseOption::Init(const std::vector<BaseOption*> &option_list, + int argc, char **argv) { + std::map<std::string, BaseOption *> options; + for (BaseOption *opt : option_list) { + options[opt->selector] = opt; + } + for (int i = 1; i < argc; i++) { + std::string option_selector(argv[i]); + auto it = options.find(option_selector); + if (it != options.end()) { + if (i >= argc) { + break; + } + const char *option_value = argv[i + 1]; + it->second->set(option_value); + i++; + } else { + printf("Ignoring unrecognized option: %s.\n", argv[i]); + printf("Usage: %s {option value}\n", argv[0]); + printf("\tTakes text from stdin and produces pdf file.\n"); + printf("Supported options:\n"); + for (BaseOption *opt : option_list) { + printf("\t%s\t%s (%s)\n", opt->selector.c_str(), + opt->description.c_str(), opt->valueToString().c_str()); + } + exit(-1); + } + } +} + +struct DoubleOption : Option<double> { + virtual void set(std::string _value) { value = atof(_value.c_str()); } + virtual std::string valueToString() { + std::ostringstream stm; + stm << value; + return stm.str(); + } + DoubleOption(std::string selector, + std::string description, + double defaultValue) + : Option<double>(selector, description, defaultValue) {} +}; + +struct StringOption : Option<std::string> { + virtual void set(std::string _value) { value = _value; } + virtual std::string valueToString() { return value; } + StringOption(std::string selector, + std::string description, + std::string defaultValue) + : Option<std::string>(selector, description, defaultValue) {} +}; + +// Config ////////////////////////////////////////////////////////////////////// + +struct Config { + DoubleOption page_width = DoubleOption("-w", "Page width", 600.0f); + DoubleOption page_height = DoubleOption("-h", "Page height", 800.0f); + StringOption title = StringOption("-t", "PDF title", "---"); + StringOption author = StringOption("-a", "PDF author", "---"); + StringOption subject = StringOption("-k", "PDF subject", "---"); + StringOption keywords = StringOption("-c", "PDF keywords", "---"); + StringOption creator = StringOption("-t", "PDF creator", "---"); + StringOption font_file = StringOption("-f", ".ttf font file", ""); + DoubleOption font_size = DoubleOption("-z", "Font size", 8.0f); + DoubleOption left_margin = DoubleOption("-m", "Left margin", 20.0f); + DoubleOption line_spacing_ratio = + DoubleOption("-h", "Line spacing ratio", 1.5f); + StringOption output_file_name = + StringOption("-o", ".pdf output file name", "out-skiahf.pdf"); + + Config(int argc, char **argv) { + BaseOption::Init(std::vector<BaseOption*>{ + &page_width, &page_height, &title, &author, &subject, + &keywords, &creator, &font_file, &font_size, &left_margin, + &line_spacing_ratio, &output_file_name}, argc, argv); + } +}; + +// Placement /////////////////////////////////////////////////////////////////// + +class Placement { +public: + Placement(const Config* conf, SkDocument *doc) + : config(conf), document(doc), pageCanvas(nullptr) { + white_paint.setColor(SK_ColorWHITE); + glyph_paint.setColor(SK_ColorBLACK); + glyph_paint.setFlags(SkPaint::kAntiAlias_Flag | + SkPaint::kSubpixelText_Flag); + glyph_paint.setTextSize(SkDoubleToScalar(config->font_size.value)); + } + + void WriteLine(const SkShaper& shaper, const char *text, size_t textBytes) { + if (!pageCanvas || current_y > config->page_height.value) { + if (pageCanvas) { + document->endPage(); + } + pageCanvas = document->beginPage( + SkDoubleToScalar(config->page_width.value), + SkDoubleToScalar(config->page_height.value)); + pageCanvas->drawPaint(white_paint); + current_x = config->left_margin.value; + current_y = config->line_spacing_ratio.value * config->font_size.value; + } + SkTextBlobBuilder textBlobBuilder; + shaper.shape(&textBlobBuilder, glyph_paint, text, textBytes, true, SkPoint{0, 0}); + sk_sp<const SkTextBlob> blob = textBlobBuilder.make(); + pageCanvas->drawTextBlob( + blob.get(), SkDoubleToScalar(current_x), + SkDoubleToScalar(current_y), glyph_paint); + // Advance to the next line. + current_y += config->line_spacing_ratio.value * config->font_size.value; + } + +private: + const Config* config; + SkDocument *document; + SkCanvas *pageCanvas; + SkPaint white_paint; + SkPaint glyph_paint; + double current_x; + double current_y; +}; + +//////////////////////////////////////////////////////////////////////////////// + +static sk_sp<SkDocument> MakePDFDocument(const Config &config, + SkWStream *wStream) { + SkDocument::PDFMetadata pdf_info; + pdf_info.fTitle = config.title.value.c_str(); + pdf_info.fAuthor = config.author.value.c_str(); + pdf_info.fSubject = config.subject.value.c_str(); + pdf_info.fKeywords = config.keywords.value.c_str(); + pdf_info.fCreator = config.creator.value.c_str(); + bool pdfa = false; + #if 0 + SkTime::DateTime now; + SkTime::GetDateTime(&now); + pdf_info.fCreation.fEnabled = true; + pdf_info.fCreation.fDateTime = now; + pdf_info.fModified.fEnabled = true; + pdf_info.fModified.fDateTime = now; + pdfa = true; + #endif + return SkDocument::MakePDF(wStream, SK_ScalarDefaultRasterDPI, pdf_info, + nullptr, pdfa); +} + +int main(int argc, char **argv) { + Config config(argc, argv); + SkFILEWStream wStream(config.output_file_name.value.c_str()); + sk_sp<SkDocument> doc = MakePDFDocument(config, &wStream); + assert(doc); + Placement placement(&config, doc.get()); + + const std::string &font_file = config.font_file.value; + sk_sp<SkTypeface> typeface; + if (font_file.size() > 0) { + typeface = SkTypeface::MakeFromFile(font_file.c_str(), 0 /* index */); + } + SkShaper shaper(typeface); + assert(shaper.good()); + //SkString line("This is هذا هو الخط a line."); + for (std::string line; std::getline(std::cin, line);) { + placement.WriteLine(shaper, line.c_str(), line.size()); + } + + doc->close(); + wStream.flush(); + return 0; +} |