MicrosoftEdge / WebView2Feedback

Feedback and discussions about Microsoft Edge WebView2
https://aka.ms/webview2
437 stars 51 forks source link

[Feature]: interaction with pdf files #4476

Open ToratEmetInWord opened 5 months ago

ToratEmetInWord commented 5 months ago

Describe the feature/enhancement you need

when loading a pdf file interaction with file through code is very limited such as setting the default page to open to or gettng the selected text in the pdf file through code

The scenario/use case where you would use this feature

i am using webview as part of a reaserch program my users need a lot of flexibilty when viewng and accesing pdfs through search or any other way

How important is this request to you?

Critical. My app's basic functions wouldn't work without it.

Suggested implementation

No response

What does your app do? Is there a pending deadline for this request?

i am using webview as part of a reaserch program my users need a lot of flexibilty when viewng and accesing pdfs through search or any other way people are getting very frustrated

victorhuangwq commented 5 months ago

Hi @pcinfogmach, what behaviors that is currently supported by the Edge Browser are you thinking of that is not currently supported in WebView2? Could you provide screenshots / more details?

ToratEmetInWord commented 5 months ago

thats the thing i am not looking for behaviours that are accesible in edge i am looking for the pdf reader to be exposed to code the same way that webview itself is exposed to code interaction through programming it should be so since webview is such an amazing tool for develpors to use

some sample behaviours are: accesing js window.find does not work setting default page on open acces to text of currently open document or at least to currently selected text

many thanks

victorhuangwq commented 5 months ago

@pcinfogmach I see. Could you elaborate further in the feature request on each of these behaviors, and why do you need that behavior? Else it will be difficult for us to track the request

ToratEmetInWord commented 4 months ago

accesing js window.find - will allow me to implement a search function even if i choose to hide the pdf toolbar. setting default page on open will allow showing indexed search results efficiently acces to text of currently open document or at least to currently selected text - wiil allow copying text dirctly from source to the research paper

these might seem trivial at first but when doing extensive reseach it simply piles uo and becomes gruesomly annoying

many thanks

perhaps a link to my app might be prudent

https://github.com/pcinfogmach/ToratEmetInWord/releases/tag/ToratEmetInWord

ToratEmetInWord commented 4 months ago

another good feature will be this https://www.google.com/url?q=https://github.com/MicrosoftEdge/WebView2Feedback/issues/4475%23event-12409835530&source=gmail&ust=1712775496208000&usg=AOvVaw0tY7191rw8l7DyRtIbdoaW very frustrating that it does not exits many thanks

trebahl commented 1 month ago

@ToratEmetInWord I found a solution. It's hackish and could break with later versions of chrome but it works at the moment.

You have to send js code to be executed into the context of the pdf renedering element. This is the context that you can see if you right-click/inspect into the displayed pdf. Using the inspector, you should be able to find how to achieve the task you intend. For instance, to jump to a page, the command would be window.viewer.currentController_.goToPage(pagenumberindexedfromzero).

The difficulty is to send the code into the right context. If you use ExecuteScriptAsync it is sent to the context of the root of the page, that contains an embed element which is a black box containing all of the pdf viewer and its ui elements. This is not the right context.

Here is how I managed to get it to work:

      class WebViewWrapper
      {
              WebView2 Viewer;
              DevToolsProtocolHelper devtools; // initialized to Viewer.CoreWebView2.GetDevToolsProtocolHelper();
              Dictionary<string, string> sessionids = new Dictionary<string, string>();
              Task<T> Invoke<T>(Func<Task<T>> f)
              {
                  Task<T> res = null;
                  Viewer.Invoke(new Action(() => res = f()));
                  return res;
              }
              public async Task<string> Eval(string command)
              {
                 var targets = await devtools.Target.GetTargetsAsync();

                  string session = null;
                  foreach (var tgt in targets)
                  {
                      // find the "target" corresponding to the pdf viewing extension
                      if (tgt.Type == "webview" && tgt.Url.Contains("edge_pdf"))
                      {
                          if (tgt.Attached)
                          {
                              session = sessionids[tgt.TargetId];
                          }
                          else
                          {
                             // recover its session id
                              session = await Invoke(async() => await devtools.Target.AttachToTargetAsync(tgt.TargetId, true));
                              sessionids[tgt.TargetId] = session;
                          }
                          break;
                      }
                  }
                  if (session == null) return null;

                  // now we can use CoreWebView2.CallDevToolsProtocolMethodForSessionAsync with the session id above

                  dynamic val = new ExpandoObject();
                  val.expression = command;
                  string parametersAsJson = JsonSerializer.Serialize<object>(val);
                  return await Invoke(() => Viewer.CoreWebView2.CallDevToolsProtocolMethodForSessionAsync(session, "Runtime.evaluate", parametersAsJson));
              }
      }
ToratEmetInWord commented 2 weeks ago

@trebahl can you please provide a more complete sample i couldnt get yours to work.

trebahl commented 2 weeks ago

@ToratEmetInWord

Here's a complete example to run as an exe. Replace "f:/tmp/a.pdf" by a path to some pdf you have, then clicking the "nextpage" button should do what its name implies.

using Microsoft.Web.WebView2.Core.DevToolsProtocolExtension;
using Microsoft.Web.WebView2.WinForms;
using System.Dynamic;
using System.Text.Json;

namespace WinFormsApp1
{
    class WebViewWrapper
    {
        public readonly WebView2 Viewer = new WebView2()
        {
            CreationProperties = new CoreWebView2CreationProperties()
            {
                UserDataFolder = System.IO.Path.Combine(Application.CommonAppDataPath, "WebView")
            }
        };

        public WebViewWrapper()
        {
            Viewer.CoreWebView2InitializationCompleted += (o, e) =>
            {
                devtools = Viewer.CoreWebView2.GetDevToolsProtocolHelper();
            };
        }

        DevToolsProtocolHelper devtools; // initialized to Viewer.CoreWebView2.GetDevToolsProtocolHelper();
        Dictionary<string, string> sessionids = new Dictionary<string, string>();
        Task<T> Invoke<T>(Func<Task<T>> f)
        {
            Task<T> res = null;
            Viewer.Invoke(new Action(() => res = f()));
            return res;
        }
        public async Task<string> Eval(string command)
        {
            var targets = await devtools.Target.GetTargetsAsync();

            string session = null;
            foreach (var tgt in targets)
            {
                // find the "target" corresponding to the pdf viewing extension
                if (tgt.Type == "webview" && tgt.Url.Contains("edge_pdf"))
                {
                    if (tgt.Attached)
                    {
                        session = sessionids[tgt.TargetId];
                    }
                    else
                    {
                        // recover its session id
                        session = await Invoke(async () => await devtools.Target.AttachToTargetAsync(tgt.TargetId, true));
                        sessionids[tgt.TargetId] = session;
                    }
                    break;
                }
            }
            if (session == null) return null;

            // now we can use CoreWebView2.CallDevToolsProtocolMethodForSessionAsync with the session id above

            dynamic val = new ExpandoObject();
            val.expression = command;
            string parametersAsJson = JsonSerializer.Serialize<object>(val);
            return await Invoke(() => Viewer.CoreWebView2.CallDevToolsProtocolMethodForSessionAsync(session, "Runtime.evaluate", parametersAsJson));
        }

        public void GoToPage(int p)
        {
            Eval($"window.viewer.currentController_.goToPage({p - 1})");
        }
    }

    internal static class Program
    {
        [STAThread]
        static void Main()
        {
            using (var f = new Form())
            {
                var wv = new WebViewWrapper();
                wv.Viewer.Dock = DockStyle.Fill;
                f.Controls.Add(wv.Viewer);
                wv.Viewer.EnsureCoreWebView2Async();
                var t = new System.Windows.Forms.Timer();
                t.Tick += (o, e) =>
                {
                    if (wv.Viewer.CoreWebView2 == null) return;
                    t.Stop();
                    wv.Viewer.Source = new Uri("file:///f:/tmp/a.pdf");
                };
                t.Interval = 1000;
                t.Start();
                var b = new Button() { Text = "nextpage", Dock = DockStyle.Top };
                f.Controls.Add(b);
                var p = 1;
                b.Click += (o, e) =>
                {
                    wv.GoToPage(++p);
                };

                Application.Run(f);
            }

        }
    }
}
trebahl commented 2 weeks ago

I have delved deeper into this since I first posted, and identified some difficulties:

You have to wait until the pdf viewer is fully initialized before you can pass commands. At the moment, my best detection is function IsPDFUIInitialized() { var psel = document.getElementById('pageselector'); return window.viewer !== undefined && document.getElementById('plugin') !== null && psel !== null && psel.value != 0; }

The toolbar doesn't show up if the window is too small at the time the pdf is loaded (it won't show up later even if the window is enlarged). The threshold dimensions are window.PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT and window.PDFViewer.TOOLBAR_WINDOW_MINWIDTH. In that case, you have to run `window.viewer.initializeToolbar();` to be able to have a toolbar. Then it will at least show up but without being pinned; I haven't yet found a fix for that.

trebahl commented 2 weeks ago

PS: here's how to examine the result of the js code you send to be executed.

var t = wv.Eval("...."); t.ContinueWith(tt => { / check for tt.Exception and if null you should have access to tt.Result; beware that this is run on another thread / });

trebahl commented 2 weeks ago

PPS: I've ran into a nightmare of conflicts of versions of dependencies while using WebView2.Core.DevToolsProtocolExtension. I ended up using CoreWebView2.CallDevToolsProtocolMethodForSessionAsync directly instead, using Newtonsoft.Json for the serialization/deserialization. (The code above still uses DevToolsProtocolExtension for simplicity.)

ToratEmetInWord commented 2 weeks ago

@trebahl apoligies i am but a begginner in coding and i still cant get it going here is my (wpf) code:

using Microsoft.Web.WebView2.Wpf;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace Webview2.Pdf
{
    class WebViewWrapper : ContentControl
    {
        public readonly WebView2 Viewer = new WebView2()
        {
            CreationProperties = new CoreWebView2CreationProperties()
            {
                UserDataFolder = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "WebView")
            }
        };

        public WebViewWrapper()
        {
            Viewer.CoreWebView2InitializationCompleted += async (o, e) =>
            {
                var devTools = Viewer.CoreWebView2.GetDevToolsProtocolHelper();
                // Initialization logic if necessary
            };
            Content = Viewer;
            intializePdf();
        }

        async void intializePdf()
        {
            await WaitForPDFInitialization();
            await EnsureToolbarVisible();
        }

        Dictionary<string, string> sessionIds = new Dictionary<string, string>();

        Task<T> Invoke<T>(Func<Task<T>> f)
        {
            TaskCompletionSource<T> tcs = new TaskCompletionSource<T>();
            Viewer.Dispatcher.Invoke(async () =>
            {
                try
                {
                    T result = await f();
                    tcs.SetResult(result);
                }
                catch (Exception ex)
                {
                    tcs.SetException(ex);
                }
            });
            return tcs.Task;
        }

        public async Task<string> Eval(string command)
        {
            var targetsJson = await Viewer.CoreWebView2.CallDevToolsProtocolMethodAsync("Target.getTargets", "{}");
            var targets = JsonConvert.DeserializeObject<JObject>(targetsJson)["targetInfos"];

            string session = null;
            foreach (var tgt in targets)
            {
                if (tgt["type"].ToString() == "webview" && tgt["url"].ToString().Contains("edge_pdf"))
                {
                    var targetId = tgt["targetId"].ToString();

                    if (sessionIds.ContainsKey(targetId))
                    {
                        session = sessionIds[targetId];
                    }
                    else
                    {
                        var attachParams = new { targetId = targetId, flatten = true };
                        var attachResponse = await Viewer.CoreWebView2.CallDevToolsProtocolMethodAsync("Target.attachToTarget", JsonConvert.SerializeObject(attachParams));
                        session = JsonConvert.DeserializeObject<JObject>(attachResponse)["sessionId"].ToString();
                        sessionIds[targetId] = session;
                    }
                    break;
                }
            }
            if (session == null) return null;

            var evalParams = new { expression = command };
            return await Viewer.CoreWebView2.CallDevToolsProtocolMethodForSessionAsync(session, "Runtime.evaluate", JsonConvert.SerializeObject(evalParams));
        }

        public async Task WaitForPDFInitialization()
        {
            while (true)
            {
                string initialized = await Eval("IsPDFUIInitialized()");
                if (initialized.Contains("\"result\":{\"value\":true}"))
                    break;
                await Task.Delay(100); // Check every 100ms
            }
        }

        public async Task EnsureToolbarVisible()
        {
            string script = @"
                if (window.innerHeight < window.PDFViewer.TOOLBAR_WINDOW_MIN_HEIGHT || 
                    window.innerWidth < window.PDFViewer.TOOLBAR_WINDOW_MIN_WIDTH) {
                    window.viewer.initializeToolbar_();
                }
            ";
            await Eval(script);
        }

        public async Task GoToPage(int p)
        {
            await Eval($"window.viewer.currentController_.goToPage({p - 1})");
        }
    }
}