簡體   English   中英

如何在java中生成html頁面的pdf

[英]How to generate pdf of html page in java

我必須生成 HTML 頁面的 pdf。 我為此編寫了一個方法,但它會產生錯誤。 請指導我在哪里我錯了。 謝謝你!

public void htmlToPdf(
    String htmlPath, 
    File pdfFile
) throws IOException, DocumentException { 
    Document document = new Document(); 
    PdfWriter writer = PdfWriter.getInstance(
        document, 
        new FileOutputStream(pdfFile)
    ); 
    document.open(); 
    XMLWorkerHelper.getInstance().parseXHtml(
        writer, 
        document, 
        new FileInputStream(htmlPath), 
        Charset.forName("UTF-8")
    ); 
    document.close(); 
}

錯誤 :

Cannot resolve method 'parseXHtml(com.lowagie.text.pdf.PdfWriter,  com.lowagie.text.Document, java.io.FileInputStream, java.nio.charset.Charset)'

所以你想用 Java 從 HTML 生成 PDF 嗎? (檢查底部的 EDIT 2020)

這是我與fly-sacer一起使用的程序。

  1. 使用 CSS 2.1 格式化您的 HTML
  2. 編寫生成PDF的過程
  3. 創建PDF生成器界面
  4. 使用自定義對象包裝帶有屬性的圖像以進行進一步格式化
  5. 使用您的 PDF 參數和圖像實現您的界面

1. 用 CSS 2.1 格式化你的 HTML

示例可以是帶有 EL 的 JSP、任何其他模板(您將能夠通過內部 POST 請求獲取帶有參數的生成 HTML),或者只是靜態 HTML。

您不能使用比例值,如emremvhvw或復雜的 CSS(如動畫)。

您可以使用<style> </style>標簽或內聯style=屬性

這是我的 web 應用程序中的 JSP 示例。

<!DOCTYPE html>
<%@ page    session="false" 
            language="java" 
            contentType="text/html; charset=UTF-8" 
            pageEncoding="UTF-8" 
            isELIgnored="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
    <head>
        <META CHARSET="UTF-8" />
        <title>My PDF</title>
        <style>
            /* you can add reset css too */
            /* stylesheet */
            body { font-family: sans-serif; }
            .someCSSClass {}
            .anotherCSSClass {}
        </style>
    </head>
    <body>
        <div class="someCSSClass">
            <p class="anotherCSSClass" style="line-height:16px;">
                ${ param.aParameter }
            </p>

2. 編寫生成帶有接口的PDF的過程

為什么要使用接口? 因為在您需要從不同模型生成其他 PDF 的情況下,您不必編寫相同的邏輯來生成每個 PDF。

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.xhtmlrenderer.pdf.ITextRenderer;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfImage;
import com.itextpdf.text.pdf.PdfIndirectObject;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfStamper;
import main.java.bean.ImagePDF;
import main.java.interface.PDFInterface;
import main.java.bean.Constants;
/**
 * PDFGenerator
 * Class to generate PDF (can implement Servlet).
 */
public class PDFGenerator {
    private static final String TMP_DIR = System.getProperty("java.io.tmpdir");
    /* 
     * May not be a GET, can be simple method call for local application or
     * whatever you need
     */ 
    @Override
    protected void goGet(
        HttpServletRequest request, 
        HttpServletResponse response
    ) throws IOException {
        PDFInterface pdfImplementation = null;
        /*
         * instance your PDF Model implementation according to this 
         * parameter (for example)
         */
        int pdfModel = Integer.parseInt(
            request.getParameter("requestedPDFModel")
        );
        switch (pdfModel) {
            case Constants.PDF_MODEL_1:
                pdfImplementation = new PDFImplementationOne();
                /* 
                 * You could get the image reference from GET request too, 
                 * or from database or from constants
                 */
                pdfImplementation.addImage(
                    "image1.png", 
                    120, 
                    50, 
                    "image_name1", 
                    request
                );
                break;
            case Constants.PDF_MODEL_2:
                pdfImplementation = new PDFImplementationTwo();
                pdfImplementation.addImage(
                    "image2.png", 
                    350, 
                    70, 
                    "image_name2", 
                    request
                );
                break;
            default :
                System.out.println("Cannot find an implementation for the requested PDF.");
                return null;
        }
        String html = null;
        /*
            Get the HTML from an URL : if your implementation returns null
            then you can for example decide to get the HTML from a file in your implementation
        */
        if (pdfImplementation.getUrl(request) != null) {
            // Send POST request to generate the HTML from a template (JSP, JSF, Thymeleaf, ...)
            URLConnection connection = new URL(
                pdfImplementation.getUrl(request)
                +pdfImplementation.getEncodedQueryString()
            ).openConnection();
            connection.setDoOutput(true); // POST : remove this to do a GET
            connection.setRequestProperty("Accept-Charset", "UTF-8");
            connection.setRequestProperty(
                "Content-Type", 
                "application/x-www-form-urlencoded;charset=UTF-8"
            );
            try (OutputStream output = connection.getOutputStream()) {
                output.write(
                    pdfImplementation
                        .getEncodedQueryString()
                        .getBytes(StandardCharsets.UTF_8)
                );
            }
            // Open an input stream on the response
            BufferedReader in = new BufferedReader(
                new InputStreamReader(connection.getInputStream())
            );
            StringBuilder sb = new StringBuilder();
            // A line in our generated HTML
            String inputLine;
            // Read all HTML lines and concatenate
            while ((inputLine = in.readLine()) != null) { 
                sb.append(inputLine); 
            }
            html = sb.toString();
            in.close();
        }
        // Get the HTML from a File
        else {
            html = String.join(
                "", 
                Files.readAllLines(pdfImplementation.getHTMLFile().toPath())
            );
        }
        // Create a temp file to make the PDF
        File tempPDFFile = new File(
            TMP_DIR + pdfImplementation.getGeneratedPDFFileName()
        );
        if (!tempPDFFile.exists()) { tempPDFFile.createNewFile(); }
        FileOutputStream fos = new FileOutputStream(tempPDFFile);
        // Output the HTML to the temp PDF file
        new ITextRenderer() {{
            setDocumentFromString(html);
            layout();
            createPDF(fos);
        }};
        fos.close();
        // Create your final PDF file
        File pdf = new File(pdfImplementation.getPDFFilename());
        // Add images if needed
        addImageToPDF(pdfImplementation, tempPDFFile, pdf);
        // Write in response if you need servlet implementation
        writePDFContentToResponse(pdf, response);
    }
    /**
     * writePDFContentToResponse
     * @param pdf : the final PDF file
     * @param response :  a HTTPServletResponse to write PDF file bytes
     * @throws IOException
     */
    void writePDFContentToResponse(
        File pdf, 
        HttpServletResponse response
    ) throws IOException {
        InputStream fis = new FileInputStream(pdf);
        String mimeType = getServlet().getServletContext()
            .getMimeType(pdf.getAbsolutePath());
        response.setContentType(
            mimeType != null ? mimeType : "application/octet-stream"
        );
        response.setContentLength((int) pdf.length());
        response.setHeader(
            "Content-Disposition", 
            "attachment; filename="+pdf.getName()+".pdf"
        );
        ServletOutputStream os = response.getOutputStream();
        byte[] bufferData = new byte[1024];
        int read = 0;
        while((read = fis.read(bufferData)) != -1) { 
            os.write(bufferData, 0, read); 
        }
        os.flush();
        os.close();
        fis.close();
        response.flushBuffer();
        Files.delete(pdf.toPath());
    }
    /**
     * addImageToPDF
     * 
     * @param pdfImplementation : the pdfImplementation to get the array of
     * custom image objects ImagePDF.
     * @param tempPDFFile : the temp PDF file with already HTML content 
     * converted.
     * @param pdf : the final PDF file which will have images stamped.
     * @throws DocumentException
     * @throws IOException
     */
    void addImageToPDF(
        PDFInterface pdfImplementation, 
        File tempPDFFile, 
        File pdf
    ) throws DocumentException, IOException {
        PdfReader reader = new PdfReader(new FileInputStream(tempPDFFile));
        PdfStamper stamper = new PdfStamper(
            reader, 
            new FileOutputStream(pdf)
        );
        for (ImagePDF img: pdfImplementation.getImages()) {
            Image image = img.getImage();
            image.scalePercent(img.getScale());
            PdfImage stream = new PdfImage(image, "", null);
            stream.put(
                new PdfName("ITXT_SpecialId"), 
                new PdfName("123456789")
            );
            PdfIndirectObject ref = stamper.getWriter().addToBody(stream);
            image.setDirectReference(ref.getIndirectReference());
            image.setAbsolutePosition(
                img.getWidthPosition(), 
                img.getHeightPosition()
            );
            PdfContentByte over = stamper.getOverContent(1);
            over.addImage(image);
        }
        stamper.close();
        reader.close();
    }
}

3.創建PDF生成器界面

import java.io.File;
import java.io.IOException;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.itextpdf.text.BadElementException;
/**
* PDFInterface
* Interface to define the behavior a PDF model has to implement.
*/
public interface PDFInterface {
    /**
     * getUrl
     * @param request the HTTPServletRequest to fetch parameters for the PDF
     * @return the URL target to make a HTTP POST request to get the generated
     * HTML (for example if you are making a HTTP POST on a JSP to generate
     * HTML dynamically.
     */
    String getUrl(HttpServletRequest request);
    /**
     * getHTMLFile
     * @return return the HTML file from the local storage to be read to get 
     * the HTML.
     */
    File getHTMLFile();
    /**
     * setParametres
     * @param object : an object or a list of objects to be encoded to the
     * query String to generate the PDF.
     */
    void setParametres(Candidat candidat);

    String getEncodedQueryString();
    /**
     * getImages
     * @return a custom ImagePDF object with needed attributes to add an image
     * after the PDF has been generated has the HTML cannot be read to get
     * image during the generation of the PDF.
     */
    List<ImagePDF> getImages();
    /**
     * addImage
     * @param url : the URL to get the image
     * @param x : the X position
     * @param y : the Y position
     * @param name : the name of the image
     * @param request : the HTTPServletRequest to generate the relative link 
     * to fetch the image.
     * @param scale : the scale of the image
     * @throws BadElementException
     * @throws IOException
     */
    void addImage(
        String url,
        float x,
        float y,
        String name,
        HttpServletRequest request,
        float scale
    ) throws BadElementException, IOException;
    /**
     * getPDFFilename
     * @return : the name of the PDF file to be generated
     */
    String getPDFFilename();
}

4. ImagePDF 對象(以防您需要向 PDF 添加圖像)

import java.io.IOException;
import com.itextpdf.text.BadElementException;
import com.itextpdf.text.Image;
/**
 * ImagePDF
 * Class for a custom ImagePDF object to fit needs to stamp an image on a
 * generated PDF (URI to get the image, scale, positions x y ...).
 */
public class ImagePDF implements java.io.Serializable {
    private static final long serialVersionUID = 1L;
    private Image image;
    private float widthPosition;
    private float heightPosition;
    private String name;
    private Float scale;
    /**
     * ImagePDF
     * @param urlImage : the URL to fetch the image
     * @param heightPosition : the y position on the PDF canvas
     * @param widthPosition : the x position on the PDF canvas
     * @param name : the name of the image
     * @param scale : the scale of the image on the PDF canvas
     * @throws BadElementException
     * @throws IOException
     */
    public ImagePDF(
        String urlImage,
        float widthPosition,
        float heightPosition,
        String name,
        Float scale
    ) throws BadElementException, IOException {
        this.image = Image.getInstance(urlImage);
        this.heightPosition = heightPosition;
        this.widthPosition = widthPosition;
        this.name = name;
        this.scale = scale;
    }
    // Getters and setters ...

5. 為您的 PDF 參數實現您的接口

(在上面的例子中使用)

/**
 * PDFImplementationOne
 * The PDFImplementation to generate a specific PDF.
 */
public class PDFImplementationOne implements PDFInterface {
    private static final String PARAM_1 = "param1";
    private static final String PARAM_2 = "param2";
    private Map<String, String> parameters;
    private List<ImagePDF> images;
    /**
     * PDFImplementationOne
     * You can pass service to add information retreival from DB or objects to 
     * pass to parameters in the constructor if needed.
     */
    public PDFImplementationOne (CustomObject aParameter) {
        this.parameters = new HashMap<>();
        this.images = new ArrayList<>();
        // in case you need parameters, passed in constructor
        setParametres(aParameter); 
    }
    /* (non-Javadoc)
     * @see main.java.interface.PDFInterface#getUrl()
     */
    @Override
    public String getUrl(HttpServletRequest request) {
        /* 
         * This is an example in case your generate your HTML from JSP with 
         * parameters, if it is from static file then return null
         */
        StringBuilder sb = new StringBuilder("http://");
        sb.append(request.getServerName());
        sb.append((request.getServerName().startsWith("127.0.0")?":8080":""));
        sb.append("/MyApp/urlToJSP");
        return sb.toString();
    }
    /*
     * (non-Javadoc)
     * @see main.java.interface.PDFInterface#addImage(
     *  java.lang.String, 
     *  float, 
     *  float, 
     *  java.lang.String, 
     *  javax.servlet.http.HttpServletRequest,
     *  float scale
     * )
     */
    @Override
    public void addImage(
        String fileName,
        float x,
        float y,
        String name,
        HttpServletRequest request
    ) {
        /* 
         * Here I get the image from a ressource server but you can read the 
         * image from local storage as well
         */
        StringBuilder url = new StringBuilder("http://");
        url.append(request.getServerName());
        url.append(request.getServerName().startsWith("127.0.0")?":8080":"");
        url.append("/MyApp/img/");
        url.append(fileName);
        try {
            ImagePDF image = new ImagePDF(url.toString(), x, y, name, scale);
            images.add(image);
        }
        catch (BadElementException | IOException e) {
            System.out.println(Cannot set image for PDF "+url.toString());
        }
    }
    /* (non-Javadoc)
     * @see main.java.interface.PDFInterface#getImages()
     */
    @Override
    public List<ImagePDF> getImages() {
        return this.images;
    }
    /* (non-Javadoc)
     * @see main.java.interface.PDFInterface#setParameters(
     *  CustomObject customObject
     * )
     */
    @Override
    public void setParametres(CustomObject customObject) {
        parametres.put(PARAM_1, customObject.getAttribute().toString());
        // may have other parameters ...
    }
    /* (non-Javadoc)
     * @see model.bean.ResultatsEcritsPDF#getEncodedQueryString()
     */
    @Override
    public String getEncodedQueryString() {
        /* 
         * Create the queryString to do further HTTP POST or GET to fetch the 
         * generated HTML with parameters
         */
        StringBuilder queryStringBuilder = new StringBuilder("?");
        parameters.entrySet().stream().forEach(e -> {
            queryStringBuilder.append(e.getKey());
            queryStringBuilder.append("=");
            try {
                queryStringBuilder.append(
                    URLEncoder.encode(
                        e.getValue() == null 
                            ? "" 
                            : e.getValue(), 
                        StandardCharsets.UTF_8.name()
                    )
                );
            }
            catch (UnsupportedEncodingException e1) {
                queryStringBuilder.append("");
            }
            queryStringBuilder.append("&");
        });
        // Remove the last &
        return queryStringBuilder.toString().substring(
            0, 
            queryStringBuilder.toString().length()-1
        );
    }
    /* (non-Javadoc)
     * @see model.bean.PDFInterface#getHTMLFile()
     */
    @Override
    public File getHTMLFile() {
        return new File("/path/to/myHTMLFile.html");
    }
    /* (non-Javadoc)
     * @see model.bean.PDFInterface#getPDFFilename()
     */
    @Override
    public String getPDFFilename() {
        return "myPDF.pdf";
    }
}

告訴我是否需要澄清。


編輯 2020

現在隨着庫的改進,事情變得簡單多了,調用 HTTP 服務器本身來生成動態 HTML 內容還不夠簡單,在某些情況下還需要額外的網絡配置。

這是新流程:

  • 使用 CSS2.1(帶有<style>標簽或inline style=" )制作 HTML 模板並包含模板表達式(EL 樣式或其他)
  • 獲取 HTML 模板作為字符串
  • 替換 HTML 中的模板表達式“${ }”
  • 用編碼的 base64 圖像替換 HTML 中的圖像,如<img src="image.png" />
  • 制作PDF文件
  • 把它寫成回應或其他任何東西

這是我正在使用的項目結構(例如):

main
 |--java
     |--bean
         |--PdfConverter.java
 |--resources
     |--pdf
         |--template.html
         |--img
              |--image.png
     

依賴項:

<dependency>
    <groupId>com.github.librepdf</groupId>
    <artifactId>openpdf</artifactId>
    <version>1.3.20</version>
</dependency>

<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-core</artifactId>
    <version>9.1.20</version>
</dependency>
    
<dependency>
    <groupId>org.xhtmlrenderer</groupId>
    <artifactId>flying-saucer-pdf-openpdf</artifactId>
    <version>9.1.20</version>
</dependency>
    
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-text</artifactId>
    <version>1.9</version>
</dependency>

HTML 模板(帶圖像):

<html>
    <head>
        <style>
            body { 
                font-family:sans-serif;
                font-size:14px;
                margin: 0 auto;
                padding: 0; 
            }
            h1 { 
                text-align:center;
                font-size:21px;
                text-transform:capitalize;
            }
        </style>    
    </head>

    <body>
        <h1>some title</h1>
        <p>Some paragraph : ${ foo }</p>
        <!-- you can style images with CSS! -->
        <img src="image.png" style="width:50px;height:50px" />
    </body>
</html>

pdf轉換器:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Map;
import java.util.Scanner;

import javax.servlet.ServletContext;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.apache.poi.util.IOUtils;
import org.springframework.http.MediaType;
import org.xhtmlrenderer.pdf.ITextRenderer;
/**
 * PdfConverter
 * Extends this to implement additional to make the map to replace template
 * expressions.
 * @author user
 * @since 28 juil. 2020
 */
public class PdfConverter {
    /**
     * Temp directory.
     */
    private static final String TMP_DIR =
        System.getProperty("java.io.tmpdir") + "/";
    /**
     * Directory to HTML templates (dedicated to PDF generation).
     */
    private static final String PDF_DIR =
        "pdf/";
    /**
     * Directory to the image folders (dedicated to PDF generation).
     */
    private static final String PDF_IMG_DIR =
        "pdf/img/";
    /**
     * Prefixes for templates expressions.
     */
    private static final String PREFIX_TEMPLATE = "${ ";
    /**
     * Suffixes for template expressions.
     */
    private static final String SUFFIX_TEMPLATE = " }";
    /**
     * Generated PDF file.
     */
    private File generatedPDF;
    /**
     * PDF file name.
     */
    private String pdfName;
    /**
     * PdfConverter
     * @param m map key, value to replace, to replace expressions in HTML
     * template.
     * @param s ServletContext to get resources from context path.
     * @param fileName desired name of the generated PDF.
     * @param template name of the HTML template to make the PDF.
     * @throws IOExceptio
     */
    public PdfConverter(
        Map<String, String> m,
        ServletContext s,
        String fileName,
        String template
    ) throws IOException {
        // Set PDF filename
        setPdfName(fileName);

        // Fetch HTML template
        @SuppressWarnings("resource")
        String html = new Scanner(
            s.getResourceAsStream(PDF_DIR+ template),
            StandardCharsets.UTF_8.toString()
        ).useDelimiter("\\A").next();

        /*
         * Replace template expressions "${ }" in HTML
         */
        StringSubstitutor sub = new StringSubstitutor(
            m,
            PREFIX_TEMPLATE,
            SUFFIX_TEMPLATE
        );
        String resolvedString = sub.replace(html);

        /*
         * Replace images like <img src="image.png" /> by
         * <img src=\"data:image/png;base64," + base64Image
         */
        String[] imgs = StringUtils.substringsBetween(
            resolvedString,
            "<img src=\"", "\""
        );
        for (String s1 : imgs) {
            String mime = Files.probeContentType(Paths.get(PDF_IMG_DIR + s1));
            resolvedString = resolvedString.replace(
                s1,
                "data:" + mime + ";base64,"
                 + Base64.getEncoder().encodeToString(
                    IOUtils.toByteArray(
                        s.getResourceAsStream(PDF_IMG_DIR + s1)
                    )
                )
            );
        }

        // Make the PDF file
        FileOutputStream fos = new FileOutputStream(TMP_DIR+getPdfName());
        ITextRenderer it = new ITextRenderer();
        it.setDocumentFromString(resolvedString);
        it.layout();
        it.createPDF(fos);
        fos.close();

        // Set the PDF generated file to this PdfConverter instance
        setGeneratedPDF(new File(TMP_DIR+getPdfName()));
    }
    /**
     * getGeneratedPDF
     *
     * @return the generatedPDF
     */
    public File getGeneratedPDF() {
        return generatedPDF;
    }
    /**
     * setGeneratedPDF
     *
     * @param generatedPDF the generatedPDF to set
     */
    public void setGeneratedPDF(File generatedPDF) {
        this.generatedPDF = generatedPDF;
    }
    /**
     * getPdfName
     *
     * @return the pdfName
     */
    public String getPdfName() {
        return pdfName;
    }
    /**
     * setPdfName
     *
     * @param pdfName the pdfName to set
     */
    public void setPdfName(String pdfName) {
        this.pdfName = pdfName;
    }
    /**
     * writePdfToResponse
     * Write the PDF file into the response and delete it from temp directory
     * afterwards.
     * @param response
     * @throws IOException
     */
    public void writePdfToResponse(
        HttpServletResponse response
    ) throws IOException {
        try (
            FileInputStream fis =
                new FileInputStream(getGeneratedPDF())
        ) {
            response.setContentType(MediaType.APPLICATION_PDF_VALUE);
            response.setHeader(
                "Content-Disposition",
                "inline; filename=" + getPdfName()
            );
            response.addHeader(
                "Content-Length",
                Long.toString(getGeneratedPDF().length())
            );
            ServletOutputStream servletOutputStream =
                response.getOutputStream();
            int read = 0;
            byte[] bytes = new byte[1024];
            while ((read = fis.read(bytes)) != -1) {
                servletOutputStream.write(bytes, 0, read);
            }
            response.flushBuffer();
        }
        catch (IOException ioe) {
            response.setContentType(MediaType.TEXT_PLAIN_VALUE);
            response.getWriter().print("Cannot render PDF file.");
            response.flushBuffer();
        }
        finally {
            // Delete generated PDF after writing it to the response
            getGeneratedPDF().delete();
        }
    }
}

以及如何在 servlet 中使用它(Spring MVC 示例):

/**
 * downloadPDF
 *
 * @param response
 * @param foo
 * @throws IOException
 */
@PostMapping("/downloadPDF")
public void downloadPDF(
    HttpServletRequest request,
    HttpServletResponse response,
    String foo
) throws IOException {
    Map<String, String> m = new HashMap<>();
    m.put("foo", "my_foo_value");
    PdfConverter pdfConverter = new PdfConverter(
        m, 
        request.getServletContext(), 
        "my_pdf", 
        "template"
    );
    pdfConverter.writePdfToResponse(response);
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM