একটি ওয়েবভিউ কখন স্ন্যাপশটের জন্য প্রস্তুত হয় ()?


9

JavaFX ডক্স রাষ্ট্র যে একটি WebViewপ্রস্তুত যখন Worker.State.SUCCEEDEDউপনিত তবে, যদি না আপনি একটি সময় (অর্থাত অপেক্ষা করুন Animation, Transition, PauseTransition, ইত্যাদি), একটি ফাঁকা পৃষ্ঠা অনুষ্ঠিত হয়।

এটি পরামর্শ দেয় যে ওয়েবভিউয়ের অভ্যন্তরে এমন একটি ইভেন্ট রয়েছে যা এটি ক্যাপচারের জন্য পড়ছে, তবে এটি কী?

আছে GitHub থেকে 7,000 ওভার কোড স্নিপেট যা ব্যবহারSwingFXUtils.fromFXImage কিন্তু তাদের অধিকাংশই হয় সম্পর্কহীন বলে মনে WebView, ইন্টারেক্টিভ (মানুষের মুখোশ race অবস্থা) অথবা নির্বিচারে স্থানান্তর (যে কোন জায়গায় 100 মিঃসে থেকে 2,000ms করার জন্য) ব্যবহার করুন।

আমি চেষ্টা করেছিলাম:

  • এর মাত্রা (উচ্চতা এবং প্রস্থের বৈশিষ্ট্যের প্রয়োগগুলি , যা এই বিষয়গুলি পর্যবেক্ষণ করতে পারে) এর changed(...)মধ্যে থেকে শুনছেনWebViewDoublePropertyObservableValue

    • - টেকসই না। কখনও কখনও, মানটি রঙের রুটিন থেকে পৃথকভাবে পরিবর্তিত হয় বলে মনে হয় আংশিক সামগ্রী।
  • অন্ধভাবে runLater(...)এফএক্স অ্যাপ্লিকেশন থ্রেডে কিছু এবং সমস্ত কিছু বলছে ।

    • Any অনেক কৌশল এটি ব্যবহার করে তবে আমার নিজস্ব ইউনিট পরীক্ষাগুলি (পাশাপাশি অন্যান্য বিকাশকারীদের কাছ থেকে কিছু দুর্দান্ত প্রতিক্রিয়া) ব্যাখ্যা করে যে ঘটনাগুলি প্রায়শই ইতিমধ্যে সঠিক থ্রেডে থাকে এবং এই কলটি অনর্থক। আমি সবচেয়ে ভাল চিন্তা করতে পারি এটি সারিগুলির মধ্যে স্থির হওয়ার জন্য যথেষ্ট পরিমাণে যুক্ত করে যা এটি কারও পক্ষে কার্যকর।
  • একটি ডিওএম শ্রোতা / ট্রিগার বা জাভাস্ক্রিপ্ট শ্রোতা / ট্রিগার যুক্ত করে WebView

    • - SUCCEEDEDফাঁকা ক্যাপচার থাকা সত্ত্বেও ডাকা হবে তখনই উভয় জাভাস্ক্রিপ্ট এবং DOM সঠিকভাবে লোড হয়েছে বলে মনে হচ্ছে । ডোম / জাভাস্ক্রিপ্ট শ্রোতাদের সহায়তা বলে মনে হচ্ছে না।
  • প্রধান এফএক্স থ্রেডটি অবরুদ্ধ না করে একটি Animationবা Transitionকার্যকরভাবে "ঘুম" ব্যবহার করা ।

    • Approach এই পদ্ধতির কাজ করে এবং যদি বিলম্ব যথেষ্ট দীর্ঘ হয় তবে ইউনিট পরীক্ষার 100% পর্যন্ত ফল পাওয়া যায়, তবে রূপান্তরের সময়টি ভবিষ্যতের কিছু মুহুর্ত বলে মনে হয় যে আমরা কেবল অনুমান এবং খারাপ নকশা করছি। পারফরম্যান্ট বা মিশন-সমালোচনামূলক অ্যাপ্লিকেশনগুলির জন্য, এটি প্রোগ্রামারকে গতি বা নির্ভরযোগ্যতার মধ্যে একটি ট্রেডঅফ করতে বাধ্য করে, এটি ব্যবহারকারীর পক্ষে সম্ভাব্য দু'টিই অভিজ্ঞতা।

কল করার ভাল সময় কখন WebView.snapshot(...)?

ব্যবহার:

SnapshotRaceCondition.initialize();
BufferedImage bufferedImage = SnapshotRaceCondition.capture("<html style='background-color: red;'><h1>TEST</h1></html>");
/**
 * Notes:
 * - The color is to observe the otherwise non-obvious cropping that occurs
 *   with some techniques, such as `setPrefWidth`, `autosize`, etc.
 * - Call this function in a loop and then display/write `BufferedImage` to
 *   to see strange behavior on subsequent calls.
 * - Recommended, modify `<h1>TEST</h1` with a counter to see content from
 *   previous captures render much later.
 */

টুকিটাকি সংকেতলিপি:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

public class SnapshotRaceCondition extends Application  {
    private static final Logger log = Logger.getLogger(SnapshotRaceCondition.class.getName());

    // self reference
    private static SnapshotRaceCondition instance = null;

    // concurrent-safe containers for flags/exceptions/image data
    private static AtomicBoolean started  = new AtomicBoolean(false);
    private static AtomicBoolean finished  = new AtomicBoolean(true);
    private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
    private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);

    // main javafx objects
    private static WebView webView = null;
    private static Stage stage = null;

    // frequency for checking fx is started
    private static final int STARTUP_TIMEOUT= 10; // seconds
    private static final int STARTUP_SLEEP_INTERVAL = 250; // millis

    // frequency for checking capture has occured 
    private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis

    /** Called by JavaFX thread */
    public SnapshotRaceCondition() {
        instance = this;
    }

    /** Starts JavaFX thread if not already running */
    public static synchronized void initialize() throws IOException {
        if (instance == null) {
            new Thread(() -> Application.launch(SnapshotRaceCondition.class)).start();
        }

        for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
            if (started.get()) { break; }

            log.fine("Waiting for JavaFX...");
            try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
        }

        if (!started.get()) {
            throw new IOException("JavaFX did not start");
        }
    }


    @Override
    public void start(Stage primaryStage) {
        started.set(true);
        log.fine("Started JavaFX, creating WebView...");
        stage = primaryStage;
        primaryStage.setScene(new Scene(webView = new WebView()));

        // Add listener for SUCCEEDED
        Worker<Void> worker = webView.getEngine().getLoadWorker();
        worker.stateProperty().addListener(stateListener);

        // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
        Platform.setImplicitExit(false);
    }

    /** Listens for a SUCCEEDED state to activate image capture **/
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();
        }
    };

    /** Listen for failures **/
    private static ChangeListener<Throwable> exceptListener = new ChangeListener<Throwable>() {
        @Override
        public void changed(ObservableValue<? extends Throwable> obs, Throwable oldExc, Throwable newExc) {
            if (newExc != null) { thrown.set(newExc); }
        }
    };

    /** Loads the specified HTML, triggering stateListener above **/
    public static synchronized BufferedImage capture(final String html) throws Throwable {
        capture.set(null);
        thrown.set(null);
        finished.set(false);

        // run these actions on the JavaFX thread
        Platform.runLater(new Thread(() -> {
            try {
                webView.getEngine().loadContent(html, "text/html");
                stage.show(); // JDK-8087569: will not capture without showing stage
                stage.toBack();
            }
            catch(Throwable t) {
                thrown.set(t);
            }
        }));

        // wait for capture to complete by monitoring our own finished flag
        while(!finished.get() && thrown.get() == null) {
            log.fine("Waiting on capture...");
            try {
                Thread.sleep(CAPTURE_SLEEP_INTERVAL);
            }
            catch(InterruptedException e) {
                log.warning(e.getLocalizedMessage());
            }
        }

        if (thrown.get() != null) {
            throw thrown.get();
        }

        return capture.get();
    }
}

সম্পর্কিত:


প্ল্যাটফর্ম.আরুনলেটর রিডান্ট্যান্ট নয়। ওয়েবভিউ এর রেন্ডারিংটি সম্পূর্ণ করার জন্য প্রয়োজনীয় মুলতুবি ইভেন্ট থাকতে পারে। প্ল্যাটফর্ম.আরুনলেটর প্রথম জিনিসটি আমি চেষ্টা করব।
ভিজিআর

প্রতিযোগিতার পাশাপাশি ইউনিট পরীক্ষাগুলি প্রস্তাব দেয় ইভেন্টগুলি মুলতুবি নয়, বরং একটি পৃথক থ্রেডে ঘটছে। Platform.runLaterপরীক্ষা করা হয়েছিল এবং এটি ঠিক করে না। আপনি যদি একমত না হন তবে নিজের জন্য এটি চেষ্টা করুন। আমি ভুল হতে পেরে খুশি হব, এটি সমস্যাটি বন্ধ করবে।
tresf

তদ্ব্যতীত, সরকারী দস্তাবেজগুলি স্টেজ মঞ্চায়িত করে SUCCEEDED(যার মধ্যে শ্রোতা এফএক্স থ্রেডে আগুন জ্বলন করে) সঠিক কৌশল। যদি সারিবদ্ধ ইভেন্টগুলি দেখানোর কোনও উপায় থাকে তবে আমি চেষ্টা করে খুশি হব। আমি ওরাকল ফোরামে মন্তব্যগুলির মাধ্যমে স্পার্স পরামর্শ এবং কিছু এসও প্রশ্নের সন্ধান পেয়েছি যা WebViewডিজাইনের মাধ্যমে নিজের থ্রেডে চালানো উচিত, তাই পরীক্ষার কয়েক দিন পরে আমি সেখানে শক্তি নিবদ্ধ করছি। যদি সেই অনুমানটি ভুল হয় তবে দুর্দান্ত। আমি যেকোন যুক্তিসঙ্গত পরামর্শের জন্য উন্মুক্ত যা নির্বিচারে অপেক্ষার সময়গুলি ছাড়াই সমস্যার সমাধান করে।
tresf

আমি আমার নিজের খুব সংক্ষিপ্ত পরীক্ষা লিখেছি এবং লোড কর্মীর রাজ্য শ্রোতার কাছে একটি ওয়েবভিউর স্ন্যাপশটটি সফলভাবে অর্জন করতে সক্ষম হয়েছি। তবে আপনার প্রোগ্রামটি আমাকে একটি ফাঁকা পৃষ্ঠা দেয়। আমি এখনও পার্থক্য বুঝতে চেষ্টা করছি।
ভিজিআর

এটি কেবলমাত্র কোনও loadContentপদ্ধতি ব্যবহার করার সময় বা কোনও ফাইলের URL লোড করার সময় ঘটেছিল appears
ভিজিআর

উত্তর:


1

দেখে মনে হচ্ছে এটি একটি বাগ যা WebEngine এর loadContentপদ্ধতি ব্যবহার করার সময় ঘটে । loadস্থানীয় ফাইল লোড করার সময় এটি ঘটে থাকে তবে সেই ক্ষেত্রে, পুনরায় লোড () কল করা তার ক্ষতিপূরণ দেবে।

এছাড়াও, যেহেতু আপনি যখন স্ন্যাপশট নেবেন তখন স্টেজটি দেখানো দরকার, show()সুতরাং সামগ্রীটি লোড করার আগে আপনাকে কল করতে হবে । যেহেতু বিষয়বস্তু অবিচ্ছিন্নভাবে লোড করা হয়েছে, এটি সম্পূর্ণরূপে সম্ভব যে কলের ডাক দেওয়ার loadবা loadContentশেষ হওয়ার পরে বিবৃতি দেওয়ার আগে এটি লোড করা হবে ।

কাজটির পরে, কোনও ফাইলটিতে বিষয়বস্তু স্থাপন করা এবং reload()ঠিক একবার ওয়েবেঞ্জিনের পদ্ধতিতে কল করা । দ্বিতীয়বার সামগ্রীটি লোড হওয়ার পরে, লোড কর্মীর রাষ্ট্রীয় সম্পত্তির শ্রোতার কাছ থেকে একটি স্ন্যাপশট সফলভাবে নেওয়া যেতে পারে।

সাধারণত, এটি সহজ হবে:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

WebEngine engine = myWebView.getEngine();
engine.getLoadWorker().stateProperty().addListener(
    new ChangeListener<Worker.State>() {
        private boolean reloaded;

        @Override
        public void changed(ObservableValue<? extends Worker.State> obs,
                            Worker.State oldState,
                            Worker.State newState) {
            if (reloaded) {
                Image image = myWebView.snapshot(null, null);
                doStuffWithImage(image);

                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                engine.reload();
            }
        }
    });


engine.load(htmlFile.toUri().toString());

তবে আপনি সমস্ত staticকিছুর জন্য ব্যবহার করছেন বলে আপনাকে কিছু ক্ষেত্র যুক্ত করতে হবে:

private static boolean reloaded;
private static volatile Path htmlFile;

এবং আপনি সেগুলি এখানে ব্যবহার করতে পারেন:

/** Listens for a SUCCEEDED state to activate image capture **/
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

এবং তারপরে আপনি যখনই সামগ্রী লোড করবেন তখন আপনাকে এটি পুনরায় সেট করতে হবে:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

Platform.runLater(new Thread(() -> {
    try {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile);
    }
    catch(Throwable t) {
        thrown.set(t);
    }
}));

নোট করুন যে মাল্টিথ্রেডেড প্রসেসিং করার আরও ভাল উপায় রয়েছে। পারমাণবিক ক্লাস ব্যবহার না করে আপনি কেবল volatileক্ষেত্রগুলি ব্যবহার করতে পারেন :

private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;

(বুলিয়ান ক্ষেত্রগুলি ডিফল্টরূপে মিথ্যা, এবং অবজেক্ট ক্ষেত্রগুলি ডিফল্টরূপে বাতিল হয় C সি প্রোগ্রামগুলির বিপরীতে, এটি জাভা দ্বারা তৈরি করা একটি কঠিন গ্যারান্টি; অবিচ্ছিন্ন মেমরির মতো কোনও জিনিস নেই))

অন্য থ্রেডে পরিবর্তিত পরিবর্তনের জন্য একটি লুপে ভোট দেওয়ার পরিবর্তে সিঙ্ক্রোনাইজেশন, একটি লক বা কাউন্টডাউনল্যাচের মতো উচ্চতর স্তরের শ্রেণি ব্যবহার করা ভাল যা এই জিনিসগুলিকে অভ্যন্তরীণভাবে ব্যবহার করে:

private static final CountDownLatch initialized = new CountDownLatch(1);
private static volatile CountDownLatch finished;
private static volatile BufferedImage capture;
private static volatile Throwable thrown;
private static boolean reloaded;

private static volatile Path htmlFile;

// main javafx objects
private static WebView webView = null;
private static Stage stage = null;

private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(null, null);
            capture = SwingFXUtils.fromFXImage(snapshot, null);
            finished.countDown();
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARNING, "Could not delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

@Override
public void start(Stage primaryStage) {
    log.fine("Started JavaFX, creating WebView...");
    stage = primaryStage;
    primaryStage.setScene(new Scene(webView = new WebView()));

    Worker<Void> worker = webView.getEngine().getLoadWorker();
    worker.stateProperty().addListener(stateListener);

    webView.getEngine().setOnError(e -> {
        thrown = e.getException();
    });

    // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
    Platform.setImplicitExit(false);

    initialized.countDown();
}

public static BufferedImage capture(String html)
throws InterruptedException,
       IOException {

    htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);

    if (initialized.getCount() > 0) {
        new Thread(() -> Application.launch(SnapshotRaceCondition2.class)).start();
        initialized.await();
    }

    finished = new CountDownLatch(1);
    thrown = null;

    Platform.runLater(() -> {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile.toUri().toString());
    });

    finished.await();

    if (thrown != null) {
        throw new IOException(thrown);
    }

    return capture;
}

reloaded অস্থির হিসাবে ঘোষিত হয় না কারণ এটি কেবল জাভাএফএক্স অ্যাপ্লিকেশন থ্রেডে অ্যাক্সেস করা হয়েছে।


1
এটি একটি খুব সুন্দর রাইটিং আপ, বিশেষত থ্রেডিং এবং volatileভেরিয়েবলগুলির চারপাশে কোডের উন্নতি । দুর্ভাগ্যক্রমে, কল করা WebEngine.reload()এবং পরবর্তীগুলির জন্য অপেক্ষা করা কার্যকর SUCCEEDEDহয় না। যদি আমি এইচটিএমএল বিষয়বস্তুতে একটি কাউন্টার রাখি তবে আমি গ্রহণ করি: 0, 0, 1, 3, 3, 5পরিবর্তে 0, 1, 2, 3, 4, 5এটি প্রস্তাবিত করে যে এটি আসলে অন্তর্নিহিত রেসের শর্তটি ঠিক করে না।
tresf

উদ্ধৃতি: "ব্যবহার করা ভাল [...] CountDownLatch"। উত্সাহ দেওয়া কারণ এই তথ্যটি সন্ধান করা সহজ ছিল না এবং এটি প্রাথমিক এফএক্স স্টার্ট-আপের সাথে কোডের গতি এবং সরলতায় সহায়তা করে।
tresf

0

পুনরায় আকার দেওয়ার পাশাপাশি অন্তর্নিহিত স্ন্যাপশট আচরণের জন্য, আমি (আমরা) নিম্নলিখিত কার্যনির্বাহী সমাধান নিয়ে এসেছি। দ্রষ্টব্য, এই পরীক্ষাগুলি চালানো হয়েছিল 2,000x (উইন্ডোজ, ম্যাকস এবং লিনাক্স) 100% সাফল্যের সাথে র্যান্ডম ওয়েবভিউ মাপ সরবরাহ করে।

প্রথমত, আমি জাভাএফএক্সের একটি ডিভস উদ্ধৃত করব। এটি একটি বেসরকারী (স্পনসরিত) বাগ রিপোর্ট থেকে উদ্ধৃত হয়েছে:

"আমি ধরে নিয়েছি যে আপনি এফএক্স অ্যাপথ্রেডে পুনরায় আকার শুরু করেছেন, এবং এটি সফল অবস্থা হওয়ার পরে সম্পন্ন হয়েছে that সেক্ষেত্রে আমার কাছে মনে হয় যে সেই মুহুর্তে, 2 টি ডালের অপেক্ষা (এফএক্স অ্যাপথ্রেডকে অবরুদ্ধ না করে) দেওয়া উচিত ওয়েবকিট বাস্তবায়নের জন্য এটি পরিবর্তন করতে পর্যাপ্ত সময় রয়েছে, যদি না জাভাএফএক্সে কিছু মাত্রা পরিবর্তিত হয়, যার ফলশ্রুতি আবার ওয়েবকিটের অভ্যন্তরে পরিবর্তিত হতে পারে।

আমি জেবিএসের আলোচনায় এই তথ্যটি কীভাবে ফিড করব সে বিষয়ে আমি ভাবছি, তবে আমি নিশ্চিত নিশ্চিতভাবেই উত্তর হবে যে "ওয়েব কম্পোনেন্টটি স্থিতিশীল হলেই আপনার স্ন্যাপশট নেওয়া উচিত"। সুতরাং এই উত্তরটি অনুমান করার জন্য, এই পদ্ধতিরটি আপনার পক্ষে কাজ করে কিনা তা দেখে ভাল হবে। অথবা, যদি এটি অন্যান্য সমস্যার কারণ হয়ে দাঁড়ায়, তবে এই সমস্যাগুলি সম্পর্কে চিন্তা করা ভাল হবে এবং কীভাবে ওপেনএফএফএক্সে সেগুলি ঠিক করা যায় কিনা তা দেখুন ""

  1. ডিফল্টরূপে, জাভাএফএক্স 8 600উচ্চতা হুবহু ডিফল্ট ব্যবহার করে 0। কোড পুনরায় ব্যবহার করা WebViewউচিত setMinHeight(1), setPrefHeight(1)এই সমস্যাটি এড়াতে। এটি নীচের কোডে নেই, তবে যে কেউ তাদের প্রকল্পে এটি খাপ খাইয়ে নেওয়ার জন্য উল্লেখযোগ্য।
  2. ওয়েবকিটের প্রস্তুতি সমন্বিত করতে, অ্যানিমেশন টাইমারের ভিতরে থেকে ঠিক দুটি ডালের জন্য অপেক্ষা করুন।
  3. স্ন্যাপশট ফাঁকা বাগ প্রতিরোধ করতে, স্ন্যাপশট কলব্যাকটি উত্তোলন করুন, এটি একটি ডালও শোনে।
// without this runlater, the first capture is missed and all following captures are offset
Platform.runLater(new Runnable() {
    public void run() {
        // start a new animation timer which waits for exactly two pulses
        new AnimationTimer() {
            int frames = 0;

            @Override
            public void handle(long l) {
                // capture at exactly two frames
                if (++frames == 2) {
                    System.out.println("Attempting image capture");
                    webView.snapshot(new Callback<SnapshotResult,Void>() {
                        @Override
                        public Void call(SnapshotResult snapshotResult) {
                            capture.set(SwingFXUtils.fromFXImage(snapshotResult.getImage(), null));
                            unlatch();
                            return null;
                        }
                    }, null, null);

                    //stop timer after snapshot
                    stop();
                }
            }
        }.start();
    }
});
আমাদের সাইট ব্যবহার করে, আপনি স্বীকার করেছেন যে আপনি আমাদের কুকি নীতি এবং গোপনীয়তা নীতিটি পড়েছেন এবং বুঝতে পেরেছেন ।
Licensed under cc by-sa 3.0 with attribution required.