Life

Forking Chrome untuk mengubah HTML menjadi SVG












11 November 2022

Saya telah mengerjakan sebuah program bernama html2svg, ini mengonversi halaman web menjadi SVG. Ini didasarkan pada garpu Chromium untuk mendukung standar web modern. Posting ini menjelaskan sebagian besar tambalan.

Mengambil gambar

SkiBerkedipPDF

Chromium dibangun di atas Blink: mesin HTML bercabang dari WebKit, dan Skia: mesin 2D yang juga digunakan di Firefox dan Android.

Blink menggunakan input HTML, dan Skia menghasilkan output grafis. Komposit Chromium (cc) berada di antaranya, tetapi kami akan mengabaikannya untuk saat ini.

Untuk mendukung banyak platform dan target, Skia dibangun untuk merender ke back-end: bisa jadi itu adalah perender GPU yang disebut Ganesh, rasterizer perangkat lunaknya, atau bahkan file PDF. Beginilah cara Chromium bekerja dengan atau tanpa GPU dan mengekspor halaman web ke file PDF dengan fidelitas tinggi.

Skia juga memiliki back-end SVG eksperimental, dan itulah yang akan kami gunakan untuk membuatnya html2svg!

Untuk memulai, kita perlu menemukan cara merender halaman menjadi kanvas SVG dan memaparkannya di bawah JS API.

Dokumen Chromium menjelaskan cara mengekspor file .skp mengajukan menggunakan --enable-gpu-benchmarking bendera.

Sebuah .skp file adalah representasi biner dari file SkPicturekelas C++ yang berisi instruksi menggambar Skia yang dapat diputar ulang ke kanvas apa pun melaluinya playback() metode.

Melihat kode kita dapat menemukan yang digunakannya cc::Layer::GetPicture() untuk mendapatkan sebuah SkPicture:

// Recursively serializes the layer tree.
// Each layer in the tree is serialized into a separate skp file
// in the given directory.
void Serialize(const cc::Layer* root_layer) {
    for (auto* layer : *root_layer->layer_tree_host()) {
        sk_sp<const SkPicture> picture = layer->GetPicture();
        if (!picture)
            continue;

Mendeklarasikan sebuah fungsi

Kita akan menambahkan API JS global baru ke Chromium untuk memulai, sebut saja getPageContentsAsSVG().

Ekstensi GPU yang kami gunakan untuk menghasilkan .skp file mendaftar sendiri di content::RenderFrameImpl::DidClearWindowObject() yang masuk akal: ia memiliki akses ke data rendering, dan itu dipanggil tepat setelahnya window, objek global, dibuat. Menambahkan yang berikut di akhir metode ini sudah cukup untuk mendaftarkan fungsi global kita.

// Get access to the JS VM for this process (each tab is a process)
v8::Isolate* isolate = blink::MainThreadIsolate();
// Automatic v8::Local destruction
v8::HandleScope handle_scope(isolate);
// Get the JS context for the current tab
v8::Local<v8::Context> context = GetWebFrame()->MainWorldScriptContext();
// Automatic context entry/exit
v8::Context::Scope context_scope(context);
// Get the global object (window)
v8::Local<v8::Object> global = context->Global();

// Create a new JS function binding
v8::Local<v8::FunctionTemplate> fn = v8::FunctionTemplate::New(
    isolate,
    [](const v8::FunctionCallbackInfo<v8::Value>& args) 
        v8::Isolate* isolate = blink::MainThreadIsolate();

        args.GetReturnValue().Set(
            v8::String::NewFromUtf8(isolate, "imagine this is svg").ToLocalChecked()
        );
    
);

// Register the function as getPageContentsAsSVG()
global->Set(
    context,
    v8::String::NewFromUtf8(isolate, "getPageContentsAsSVG").ToLocalChecked(),
    fn->GetFunction(context).ToLocalChecked()
).Check();

Ayo jalankan Chromium, buka debugger dan coba.. dan berhasil! Sekarang kita perlu melakukan beberapa pekerjaan aktual di dalam fungsi.

Render ke SVG

// Get access to the main JS VM for this process (each tab is a process)
v8::Isolate* isolate = blink::MainThreadIsolate();
// Automatic v8::Local destruction
v8::HandleScope handle_scope(isolate);
// Get the WebLocalFrame for the current v8 Context
auto* frame = WebLocalFrame::FrameForCurrentContext();
// Get access to the root rendering layer
auto* root = frame->LocalRoot()->FrameWidget()->LayerTreeHost()->root_layer();

// Go over each sub-layer
for (auto* layer : *root->layer_tree_host()) 
    // Get vectorial data for this layer
    auto picture = layer->GetPicture();

    // Skip if we get there is no data
    if (!picture) 
        continue;
    

    // Create a memory stream to save the SVG content
    SkDynamicMemoryWStream stream;
    // Create an SVG canvas with the dimensions of the layer
    auto canvas = SkSVGCanvas::Make(picture->cullRect(), &stream);

    // Draw the layer data into the SVG canvas
    canvas->drawPicture(picture.get());

    // Allocate a buffer to hold the SVG data
    auto size = stream.bytesWritten();
    auto* bytes = new char[size];

    // Copy from the stream to the buffer
    stream.copyTo(static_cast<void *>(bytes));

    // Return the data to the JS world
    args.GetReturnValue().Set(
        // Copy the UTF-8 buffer into an UTF-16 JS string
        v8::String::NewFromUtf8(isolate, bytes, v8::NewStringType::kNormal, size).ToLocalChecked()
    );

    // Release the allocated data
    delete[] bytes;

    // Don't process any other layers
    break;

Ini tidak akan berhasil karena blink::WebFrameWidget::LayerTreeHost() pribadi. Ayo buat content::RenderFrameImpl kelas teman:

  // GPU benchmarking extension needs access to the LayerTreeHost
  friend class GpuBenchmarkingContext;
+ // Allow RenderFrameImpl to access the LayerTreeHost for html2svg
+ friend class content::RenderFrameImpl;

Menautkan kesalahan sekarang, kita perlu bundel SkSVGCanvas:

- # Remove unused util sources.
- sources -= [ "//third_party/skia/src/utils/SkParsePath.cpp" ]
+ # Add SVG dependencies for html2svg
+ deps += [ "//third_party/expat" ]
+ sources += [
+     "//third_party/skia/src/xml/SkDOM.cpp",
+     "//third_party/skia/src/svg/SkSVGCanvas.cpp",
+     "//third_party/skia/src/svg/SkSVGDevice.cpp",
+     "//third_party/skia/src/xml/SkXMLParser.cpp",
+     "//third_party/skia/src/xml/SkXMLWriter.cpp",
+ ]

getPageContentsAsSVG() menampilkan sesuatu yang menyerupai SVG, tetapi ada kesalahan saat membukanya: tag penutup XML tidak ada.

SkSVGCanvas menutup tag ketika destruktornya dipanggil:

SkSVGDevice::~SkSVGDevice() 
    // Pop order is important.
    while (!fClipStack.empty()) 
        fClipStack.pop_back();
    

Mari kita bungkus dalam ruang lingkup:

// Create a memory stream to save the SVG content
SkDynamicMemoryWStream stream;


    // Create an SVG canvas with the dimensions of the layer
    auto canvas = SkSVGCanvas::Make(picture->cullRect(), &stream);

    // Draw the layer data into the SVG canvas
    canvas->drawPicture(picture.get());


// Allocate a buffer to hold the SVG data
auto size = stream.bytesWritten();
auto* bytes = new char[size];

Lebih baik, tetapi teksnya dirender dengan font serif dan memiliki kerning yang aneh.

Font serif disebabkan oleh font yang hilang, kita dapat memperbaikinya dengan menambahkan fallback ke font-family atribut dari <text> elemen:

- if (!familyName.isEmpty()) 
-     this->addAttribute("font-family", familyName);
- 
+ familyName.appendf(
+     (familyName.isEmpty() ? "%s" : ", %s"),
+     "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
+ );
+
+ this->addAttribute("font-family", familyName);

Kerning aneh muncul karena posisi tiap karakter sudah diatur, kita bisa menyiasatinya dengan hanya mengatur posisi karakter pertama:

- fPosXStr.appendf("%.8g, ", position.fX);
- fPosYStr.appendf("%.8g, ", position.fY);
+ if (fPosXStr.isEmpty()) 
+     fPosXStr.appendf("%.8g", position.fX);
+ 
+
+ if (fPosYStr.isEmpty()) 
+     fPosYStr.appendf("%.8g", position.fY);
+ 

Sedikit lebih baik! Berikut beberapa hasilnya:

Perbaiki T di Twitter

Sehingga T surat hilang dari twitter.com, hanya T surat. Hmm oke aneh, begini cara Blink dan Skia menangani data font:

  1. Blink memuat font dari sistem atau dari jarak jauh dan memetakannya ke dalam SkTypeface kelas
  2. SkTypeface menggunakan back-end font secara internal: CoreText untuk macOS, DirectWrite untuk Windows, dan FreeType untuk Linux.
  3. Bagian belakang font mem-parsing file font mentah dan mengekspor larik poin kode UTF-32 yang didukung dan representasi vektornya, posisi dalam larik disebut ID mesin terbang.
  4. Blink meneruskan teks ke HarfBuzz yang mengembalikan satu set ID mesin terbang dan posisi relatifnya, mereka diteruskan ke Skia yang mengambil data vektor dan merendernya.

Langkah 4 diperlukan untuk mendukung ligatur dan beberapa bahasa. Karakter Arab misalnya, harus diterjemahkan secara berbeda berdasarkan posisinya dalam sebuah kata. Inilah mengapa HarfBuzz berada di antara Blink dan Skia: HarfBuzz membentuk string karakter unicode menjadi sekumpulan ID mesin terbang dan posisinya.

fātḥȳPenakluk

Ambil nama saya yang ditulis dalam aksara latin dan arab, tambahkan spasi di antara huruf dan karakter dibuat berbeda dalam bahasa arab. Karakter unicode tidak berubah, tetapi representasi grafisnya berubah.

Beberapa logika ini diterapkan di HarfBuzz, dan beberapa diimplementasikan dalam file font melalui tabel:

  • CMAP: itu ckarakter peta tabel, ID glif peta, dan ID penyandian platformnya
  • GSUB: itu glyph subtabel institusi, petakan satu atau lebih ID mesin terbang ke satu ID mesin terbang lainnya, di situlah ligatur dideklarasikan

Font yang mengimplementasikan ligatur untuk fi akan memiliki GSUB masuk untuk mengganti mesin terbang untuk f dan i dengan mesin terbang khusus untuk fidan mesin terbang khusus ini kemungkinan besar akan dipetakan ke karakter khusus di CMAP meja.

Dan itulah masalahnya, getGlyphToUnicodeMap() hanya melewati CMAP meja, yang T dari Twitter sangat mungkin diimplementasikan sebagai substitusi pada GSUB tabel, yang memetakan ke karakter khusus, yang tidak akan memetakan ke titik kode Unicode yang valid.

Back-end kita adalah SVG, seharusnya sudah menangani pembentukan teks, jadi kita perlu mem-bypass HarfBuzz. Pembentukan teks ditangani oleh blink::Font::DrawText()yang mengirim data ke HarfBuzz lalu menelepon blink::Font::DrawBlobs() dengan data mesin terbang. Kami akan menggunakan SkTextBlob::MakeFromString() untuk membuat gumpalan teks mesin terbang nominal dari sebuah string. Ini akan memetakan 1:1 ke titik kode unicode, memungkinkan penampil SVG untuk menangani pembentukan teks. Berikut tampilan tambalannya:

-  CachingWordShaper word_shaper(*this);
-  ShapeResultBuffer buffer;
-  word_shaper.FillResultBuffer(run_info, &buffer);
-  ShapeResultBloberizer::FillGlyphs bloberizer(
-      GetFontDescription(), device_scale_factor > 1.0f, run_info, buffer,
-      draw_type == Font::DrawType::kGlyphsOnly
-          ? ShapeResultBloberizer::Type::kNormal
-          : ShapeResultBloberizer::Type::kEmitText);
-  DrawBlobs(canvas, flags, bloberizer.Blobs(), point, node_id);
+  // Bypass HarfBuzz text shaping for html2svg
+  auto blob = SkTextBlob::MakeFromString(
+    StringView(run_info.run.ToStringView(), run_info.from, run_info.to - run_info.from).
+      ToString().
+      Utf8().
+      c_str(),
+    PrimaryFont()->
+      PlatformData().
+      CreateSkFont(false, &font_description_)
+  );
+
+  if (node_id != cc::kInvalidNodeId) 
+    canvas->drawTextBlob(blob, point.x(), point.y(), node_id, flags);
+   else 
+    canvas->drawTextBlob(blob, point.x(), point.y(), flags);
+  

Komposisi permukaan

>
 
 
 
 
 
text = main.onCreateDevice()
text.drawText("chocolatine", 0, 0)
gradient = main.onCreateDevice()
gradient.drawRect(0, 0, 500, 150)
text.drawDevice(gradient, SkBlendMode::DstIn)
main.drawDevice(text, SkBlendMode::Over)

Kanvas utamacokelat batanganPermukaan tekscokelat batangan

Pengujian mui.com Saya perhatikan beberapa elemen teks dengan efek gradien tidak ditampilkan. Hal ini karena SkSVGDevice tidak menerapkan pengomposisian permukaan: menggambar ke permukaan, lalu merender permukaan ini menggunakan operasi pengomposisian Porter-Duff.

Kita perlu menerapkan SkBaseDevice::onCreateDevice() dan SkBaseDevice::drawDevice() untuk mendukung ini. Pada perender GPU ia membuat tekstur, pada perender CPU ia mengalokasikan buffer, dan pada SVG kita akan menggunakan <g>.

Saya tidak akan membahas terlalu banyak detail tentang penerapannya karena mungkin layak untuk postingannya sendiri, tetapi pada dasarnya kami menggunakan <g> elemen untuk membuat tekstur, <use> untuk menampilkannya, dan <feComposite> untuk memadukannya.

Sebelum Setelah

Render seluruh halaman

Hanya ~6.000 piksel pertama yang dirender. Kita perlu meminta penyusun untuk menggambar seluruh halaman, dan blink::WebLocalFrame::capturePaintPreview() melakukan hal itu! Tampaknya telah diterapkan untuk pengklasifikasi phising, ada posting blog Chromium tentangnya. Dikombinasikan dengan cc::PaintRecorder kita bisa membuatnya dirender ke dalam kanvas kita.

cc::PaintRecorder recorder;
auto rect = SkRect::MakeWH(width, height);

frame->CapturePaintPreview(
    gfx::Rect(0, 0, width, height),
    recorder.beginRecording(rect),
    false,
    false
);

auto canvas = SkSVGCanvas::Make(rect, &stream);

recorder.finishRecordingAsPicture()->Playback(canvas.get());

Kami sekarang memiliki masalah lain, bilah gulir macOS juga dirender ke dalam SVG! Anehnya, itu sepenuhnya vektor. Kita dapat menyiasatinya dengan menambahkan beberapa CSS dalam kode Elektron:

const style = document.createElement('style')

style.innerHTML = `
    body::-webkit-scrollbar,
    body::-webkit-scrollbar-track,
    body::-webkit-scrollbar-thumb 
        display: none;
    
`

document.head.appendChild(style)

Dukung bayangan

Satu hal yang hilang adalah bayangan. Skia tidak secara eksplisit mendukung menggambarnya, tetapi Skia menyediakan dua bahan utama: gaussian blur dan clipping.

mengaburkan()klip()

Kita perlu menambahkan beberapa kode untuk menangani maskFilter properti dari SkPaintini berisi SkBlurMaskFilter digunakan untuk mengaburkan:

+    if (const SkMaskFilter* mf = paint.getMaskFilter()) {
+        SkMaskFilterBase::BlurRec maskBlur;
+
+        if (as_MFB(mf)->asABlur(&maskBlur) && maskBlur.fStyle == kNormal_SkBlurStyle) {
+            SkString maskfilterID = fResourceBucket->addColorFilter();
+
+            AutoElement filterElement("filter", fWriter);
+
+            filterElement.addAttribute("id", maskfilterID);
+
+            AutoElement floodElement("feGaussianBlur", fWriter);
+
+            floodElement.addAttribute("stdDeviation", maskBlur.fSigma);
+
+            resources.fMaskFilter.printf("url(#%s)", maskfilterID.c_str());

Kode kliping harus difaktorkan ulang karena bayangan memanjang di luar batas kliping standar, tetapi kami akan melewatkannya saat terlibat.

Buat vektor <canvas>

Bagaimana jika kita juga bisa membuat vektor 2D <canvas> elemen dikendalikan oleh JavaScript? Ternyata, Chromium memiliki kemampuan ini untuk mencetak:

// For 2D Canvas, there are two ways of render Canvas for printing:
// display list or image snapshot. Display list allows better PDF printing
// and we prefer this method.
// Here are the requirements for display list to be used:
//    1. We must have had a full repaint of the Canvas after beforeprint
//       event has been fired. Otherwise, we don't have a PaintRecord.
//    2. CSS property 'image-rendering' must not be 'pixelated'.

// display list rendering: we replay the last full PaintRecord, if Canvas
// has been redraw since beforeprint happened.
if (IsPrinting() && IsRenderingContext2D() && canvas2d_bridge_) {

Yang kita butuhkan hanyalah membuat IsPrinting() kembali true:

  bool HTMLCanvasElement::IsPrinting() const 
-   return GetDocument().BeforePrintingOrPrinting();
+   return true;
  

Dan begitulah, pacman SVG berdasarkan MDN <canvas> demo!

Pikiran terakhir

Itu saja untuk saat ini, html2svg sedang tayang langsung di GitHub, lihatlah!

Pengeluaran hk tercepat hari ini berasal dari website togel Data SGP pools https://sildenafilgeneric-bestrx.com/ hasil keluaran hk terkini tiap hari. Dengan kenakan rekapan bagan knowledge hk prize, Pasti mempermudah bettor dalam melihat nomor pengeluaran SGP hari ini. Di mana tiap hasil pengeluaran hk https://livinggreenwithbaby.com/ ini terkini senantiasa kami pembaharuan menjajaki result keluaran hongkong terkini berasal dari hongkongpools.com. Tujuannya sehingga para penggemar judi togel https://all-steroid.com/ di Indonesia dapat dengan mudahnya mengenali hasil hk hari ini terkini serta terlampau kilat.