عنوان:

‫طراحی و چاپ دقیق اسناد A4 در وب با ASP.NET Core MVC


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۷/۱۵ ۱۳:۳۵
آدرس: www.dntips.ir
چکیده: چاپ یک سند از طریق مرورگر، در نگاه اول ساده به نظر می‌رسد؛ اما زمانی که خروجی باید با ابعاد فیزیکی مشخص، روی کاغذ A4 و با دقت مناسب چاپ شود، موضوع پیچیده‌تر می‌شود. تفاوت میان اندازه‌های CSS و ابعاد فیزیکی کاغذ، حاشیه‌های مرورگر و چاپگر، page-break، نحوه محاسبه width و height، padding و border و همچنین تنظیمات چاپگر می‌تواند باعث شود خروجی نهایی با چیزی که روی صفحه نمایش دیده می‌شود تفاوت قابل‌توجهی داشته باشد. در این مقاله، روشی برای طراحی یک خروجی چاپی چهارقسمتی روی کاغذ A4 با استفاده از ASP.NET Core MVC بررسی می‌شود. تمرکز اصلی مقاله بر نکاتی است که برای دستیابی به چاپ دقیق و قابل پیش‌بینی اهمیت دارند؛ از جمله استفاده از واحد mm، کنترل حاشیه، مرکزچین کردن محتوا، جلوگیری از ایجاد صفحه سفید اضافی، مدیریت page-break و انتخاب روش مناسب برای چیدمان عناصر.

۱. مقدمه
HTML در اصل برای نمایش محتوا در مرورگر طراحی شده است، نه برای شبیه‌سازی یک کاغذ فیزیکی. به همین دلیل، وقتی یک صفحه وب را چاپ می‌کنیم، چند لایه مختلف هم‌زمان درگیر هستند:
  • ساختار HTML
  • CSS صفحه
  • CSS مخصوص چاپ (@media print)
  • تعریف صفحه چاپ با @page
  • موتور چاپ مرورگر
  • تنظیمات Print Dialog
  • درایور چاپگر
  • محدوده فیزیکی قابل چاپ چاپگر
بنابراین اگر مثلاً در CSS بنویسیم:
width: 210mm;
height: 297mm;
این به‌تنهایی تضمین نمی‌کند که خروجی فیزیکی دقیقاً بدون هیچ اختلافی روی یک کاغذ A4 چاپ شود. برای طراحی خروجی‌های معمولی، این اختلاف‌ها ممکن است اهمیت چندانی نداشته باشند؛ اما در اسنادی که باید بریده شوند، تا شوند، داخل فرم قرار بگیرند یا دقیقاً در محل مشخصی از کاغذ قرار داشته باشند، حتی چند میلی‌متر اختلاف نیز می‌تواند مشکل‌ساز شود. بنابراین طراحی چاپ باید به‌عنوان یک مسئله مستقل از طراحی صفحه نمایش در نظر گرفته شود.

۲. ابتدا باید مدل فیزیکی خروجی را مشخص کنیم
فرض کنیم قرار است در هر صفحه A4، چهار سند مستقل داشته باشیم:
┌───────────────────────────────┐
│             │                 │
│    سند ۱    │      سند ۲      │
│             │                 │
├─────────────┼─────────────────┤
│             │                 │
│    سند ۳    │      سند ۴      │
│             │                 │
└───────────────────────────────┘
هدف ما این است که این چهار قسمت پس از چاپ بتوانند از یکدیگر جدا شوند. A4 در حالت Portrait دارای ابعاد زیر است:
210mm × 297mm
اگر بخواهیم ۵ میلی‌متر حاشیه داخلی در چهار طرف داشته باشیم، فضای قابل استفاده برابر خواهد بود با:
عرض:
210 - 5 - 5 = 200mm

ارتفاع:
297 - 5 - 5 = 287mm
بنابراین هر قسمت تقریباً دارای ابعاد زیر خواهد بود:
عرض:    100mm
ارتفاع: 143.5mm
این محاسبه ساده، پایه اصلی طراحی ماست.

۳. چرا بهتر است صفحه‌بندی در سمت سرور انجام شود؟
یکی از اشتباه‌های رایج این است که تمام آیتم‌ها را در یک HTML بزرگ قرار دهیم و انتظار داشته باشیم مرورگر خودش تصمیم بگیرد هر چهار آیتم در یک صفحه قرار بگیرند. این روش ممکن است در بعضی مرورگرها کار کند، اما برای چاپ دقیق قابل اتکا نیست. روش بهتر این است که صفحه چاپ را به‌عنوان یک واحد داده‌ای مستقل در نظر بگیریم.
برای مثال:
public class DocumentItem
{
    public string Title { get; set; } = "";
    public string Description { get; set; } = "";
    public string Date { get; set; } = "";
    public string From { get; set; } = "";
    public string To { get; set; } = "";
}
و برای هر صفحه:
public class PrintPageViewModel
{
    public List<DocumentItem?> Items { get; set; } = new();
}
سپس اطلاعات را چهار تا چهار تا تقسیم می‌کنیم. یک پیاده‌سازی ساده در Controller:
var pages = new List<PrintPageViewModel>();

for (int i = 0; i < items.Count; i += 4)
{
    var page = new PrintPageViewModel();

    for (int j = 0; j < 4 && i + j < items.Count; j++)
        page.Items.Add(items[i + j]);

    while (page.Items.Count < 4)
        page.Items.Add(null);

    pages.Add(page);
}

return View(pages);
مزیت این روش این است که برنامه دقیقاً می‌داند:
هر چهار آیتم متعلق به یک صفحه چاپ هستند.
در نتیجه کنترل page-break نیز بسیار ساده‌تر می‌شود.

۴. چرا برای این نوع خروجیtableانتخاب مناسبی است؟
امروزه برای Layout معمولاً استفاده از Flexbox یا CSS Grid پیشنهاد می‌شود. اما خروجی چاپی دقیق، خصوصاً زمانی که سازگاری با مرورگرهای قدیمی نیز اهمیت داشته باشد، شرایط متفاوتی دارد. در این مثال ما یک Layout کاملاً جدولی داریم:
2 ستون × 2 ردیف
بنابراین استفاده از:
<table>
    <tr>
        <td>...</td>
        <td>...</td>
    </tr>
    <tr>
        <td>...</td>
        <td>...</td>
    </tr>
</table>
نه‌تنها ساده است، بلکه برای موتورهای قدیمی‌تر مرورگر نیز قابل پیش‌بینی‌تر است. هدف اینجا استفاده از table برای داده نیست؛ بلکه از رفتار تثبیت‌شده جدول برای یک Layout ثابت چاپی استفاده می‌کنیم.

۵. تعریف صفحه A4 در CSS
مهم‌ترین بخش CSS چاپ از اینجا شروع می‌شود:
@page {
    size: A4 portrait;
    margin: 0;
}
و سپس:
@media print {

    html,
    body {
        margin: 0;
        padding: 0;
    }

}
در اینجا یک نکته مهم وجود دارد.
@page و عنصر HTML ما دو مفهوم متفاوت هستند.
@page درباره صفحه چاپ صحبت می‌کند؛ در حالی که .print-page یک عنصر HTML است که محتوای خود را درون آن قرار می‌دهیم.

۶. چرا@page { margin: 0; }کافی نیست؟
فرض کنیم بنویسیم:
@page {
    size: A4;
    margin: 5mm;
}
ممکن است تصور کنیم حالا دقیقاً ۵ میلی‌متر فاصله از لبه کاغذ خواهیم داشت. اما این برداشت کاملاً صحیح نیست. زیرا در چاپ، حداقل سه مفهوم متفاوت داریم:
۱. حاشیه CSS صفحه
@page {
    margin: 5mm;
}
۲. حاشیه داخلی Layout
مثلاً:
.print-page {
    padding: 5mm;
}
۳. محدوده غیرقابل چاپ چاپگر
این قسمت کاملاً در اختیار برنامه وب نیست. بسیاری از چاپگرها نمی‌توانند تا لبه فیزیکی کاغذ چاپ کنند. بنابراین اگر چاپگر دارای مثلاً ۴ میلی‌متر ناحیه غیرقابل چاپ باشد، نمی‌توان با CSS آن را حذف کرد.

۷. یک روش قابل پیش‌بینی‌تر برای حاشیه داخلی
برای طراحی این نوع فرم‌ها می‌توانیم @page را بدون margin تعریف کنیم و حاشیه داخلی را خودمان کنترل کنیم:
@page {
    size: A4 portrait;
    margin: 0;
}

.print-page {
    width: 210mm;
    height: 297mm;
    padding: 5mm;
    box-sizing: border-box;
}
در این حالت:
A4
┌─────────────────────────────────┐
│  5mm                            │
│   ┌─────────────────────────┐   │
│   │                         │   │
│   │      Content Area       │   │
│   │                         │   │
│   └─────────────────────────┘   │
│                            5mm  │
└─────────────────────────────────┘
عرض فضای داخلی:
210 - 10 = 200mm
و ارتفاع:
297 - 10 = 287mm
خواهد بود.
این روش برای طراحی فرم‌های ثابت معمولاً قابل کنترل‌تر است.

۸. نقشbox-sizing: border-box
یکی از مهم‌ترین نکات در طراحی چاپی، box-sizing است. فرض کنید بنویسیم:
.card {
    width: 100mm;
    padding: 5mm;
    border: 1mm solid;
}
اگر از content-box استفاده شود، width فقط مربوط به محتوای داخلی خواهد بود. در نتیجه اندازه واقعی عنصر بیشتر از 100mm خواهد شد. برای خروجی چاپی ثابت بهتر است از:
box-sizing: border-box;
استفاده کنیم:
.card {
    width: 100mm;
    height: 143.5mm;
    padding: 5mm;
    border: 1px solid #333;
    box-sizing: border-box;
}
اکنون کل عنصر، شامل padding و border، همان:
100mm × 143.5mm
خواهد بود. این نکته کوچک می‌تواند از بسیاری از مشکلات Overflow و حتی ایجاد صفحه سفید اضافی جلوگیری کند.

۹. CSS کامل Layout چاپ
یک نمونه ساده و قابل استفاده:
@page {
    size: A4 portrait;
    margin: 0;
}

@media print {

    html,
    body {
        margin: 0;
        padding: 0;
        background: #fff;
    }

    .print-page {
        width: 210mm;
        height: 297mm;
        padding: 5mm;
        box-sizing: border-box;

        page-break-after: always;
        overflow: hidden;
    }

    .print-page:last-child {
        page-break-after: auto;
    }

    .page-table {
        width: 200mm;
        height: 287mm;

        margin-left: auto;
        margin-right: auto;

        border-collapse: collapse;
        table-layout: fixed;
    }

    .card-cell {
        width: 100mm;
        height: 143.5mm;
        padding: 0;
        vertical-align: top;
    }

    .card {
        width: 100mm;
        height: 143.5mm;

        box-sizing: border-box;

        padding: 5mm;
        border: 3px double #333;

        direction: rtl;
        text-align: right;

        overflow: hidden;
    }
}
چند ویژگی این CSS عمداً ساده نگه داشته شده‌اند. در این نمونه از مواردی مانند:
display: flex;
display: grid;
break-inside;
aspect-ratio;
استفاده نکرده‌ایم. این تصمیم برای افزایش قابلیت پیش‌بینی خروجی چاپ، خصوصاً در محیط‌های قدیمی‌تر، منطقی است.

۱۰. مرکزچین کردن Layout
یکی دیگر از مشکلات رایج این است که صفحه در مرورگر ظاهراً درست باشد، اما جدول چاپی در سمت چپ قرار بگیرد.
برای مثال:
.page-table {
    width: 200mm;
    margin: 0 auto;
}
در یک محیط استاندارد باید جدول ۲۰۰ میلی‌متری را در مرکز فضای ۲۱۰ میلی‌متری قرار دهد:
210mm
┌──────────────────────────────────┐
│ 5mm ┌────────────────────────┐   │
│     │                        │   │
│     │       200mm            │   │
│     │                        │   │
│     └────────────────────────┘   │
│                              5mm │
└──────────────────────────────────┘
نکته مهم این است که:
direction: rtl;
برای جهت متن است، نه برای مرکزچین کردن Layout. بنابراین نباید انتظار داشته باشیم direction: rtl باعث شود جدول در مرکز صفحه قرار بگیرد. برای مرورگرهای بسیار قدیمی که در برخی شرایط چاپی با margin: auto رفتار مطلوبی ندارند، می‌توان از یک wrapper جدولی نیز استفاده کرد:
<table class="print-wrapper">
    <tr>
        <td align="center">

            <table class="page-table">
                ...
            </table>

        </td>
    </tr>
</table>
این روش قدیمی است، اما در سناریوهایی که سازگاری با موتورهای قدیمی چاپ اهمیت دارد، می‌تواند مفید باشد.

۱۱. پیاده‌سازی View در ASP.NET Core MVC
فرض کنیم Controller مدل زیر را به View ارسال کرده است:
@model List<PrintPageViewModel>
یک View ساده می‌تواند چنین باشد:
@foreach (var page in Model)
{
    <div class="print-page">

        <table class="page-table">
            <tr>
                @for (var i = 0; i < 2; i++)
                {
                    <td class="card-cell">
                        @if (page.Items[i] != null)
                        {
                            <div class="card">
                                <h2>@page.Items[i]!.Title</h2>

                                <p>
                                    @page.Items[i]!.Description
                                </p>

                                <p>
                                    تاریخ:
                                    @page.Items[i]!.Date
                                </p>

                                <p>
                                    فرستنده:
                                    @page.Items[i]!.From
                                </p>

                                <p>
                                    گیرنده:
                                    @page.Items[i]!.To
                                </p>
                            </div>
                        }
                    </td>
                }
            </tr>

            <tr>
                @for (var i = 2; i < 4; i++)
                {
                    <td class="card-cell">
                        @if (page.Items[i] != null)
                        {
                            <div class="card">
                                <h2>@page.Items[i]!.Title</h2>

                                <p>
                                    @page.Items[i]!.Description
                                </p>

                                <p>
                                    تاریخ:
                                    @page.Items[i]!.Date
                                </p>

                                <p>
                                    فرستنده:
                                    @page.Items[i]!.From
                                </p>

                                <p>
                                    گیرنده:
                                    @page.Items[i]!.To
                                </p>
                            </div>
                        }
                    </td>
                }
            </tr>
        </table>

    </div>
}
برای پروژه واقعی می‌توان این View را تمیزتر کرد و Rendering کارت را به Partial View منتقل کرد؛ اما برای یک مثال آموزشی، همین ساختار رابطه بین مدل، صفحه و Layout را به‌خوبی نشان می‌دهد.

۱۲. یک نکته مهم درباره صفحه آخر
فرض کنیم تعداد آیتم‌ها ۹ عدد باشد. در این حالت سه صفحه خواهیم داشت:
صفحه ۱ → ۴ آیتم
صفحه ۲ → ۴ آیتم
صفحه ۳ → ۱ آیتم
اگر روی تمام صفحات بنویسیم:
page-break-after: always;
مرورگر ممکن است بعد از آخرین صفحه نیز یک شکست صفحه ایجاد کند. نتیجه ممکن است این باشد:
Page 1
Page 2
Page 3
Blank Page
بنابراین صفحه آخر نباید الزاماً page-break-after: always داشته باشد:
.print-page:last-child {
    page-break-after: auto;
}
در محیط‌های قدیمی‌تر، حتی بهتر است این وضعیت را به‌صورت صریح از سمت سرور مشخص کنیم. مثلاً:
public class PrintPageViewModel
{
    public List<DocumentItem?> Items { get; set; } = new();

    public bool IsLastPage { get; set; }
}
و در Razor:
<div class="print-page @(page.IsLastPage ? "last-page" : "")">
سپس:
.print-page {
    page-break-after: always;
}

.print-page.last-page {
    page-break-after: auto;
}
این روش به‌خصوص زمانی مفید است که بخواهیم رفتار چاپ را کاملاً صریح و مستقل از ساختار DOM کنترل کنیم.

۱۳. چرا صفحه سفید اضافی ایجاد می‌شود؟
صفحه سفید اضافی همیشه به معنی یک مشکل ساده در page-break نیست. چند علت متداول وجود دارد.
علت اول: شکست صفحه بعد از آخرین صفحه
page-break-after: always;
روی آخرین عنصر.

علت دوم: بزرگ‌تر شدن واقعی عنصر
برای مثال:
height: 297mm;
padding: 5mm;
بدون:
box-sizing: border-box;

علت سوم: Overflow
ممکن است ظاهراً ارتفاع محتوا مناسب باشد، اما یک عنصر داخلی چند میلی‌متر از محدوده خارج شده باشد.

علت چهارم: اندازه جدول
table، tr، td و عناصر داخلی می‌توانند در اثر border، padding و محتوای طولانی باعث افزایش اندازه واقعی Layout شوند.

علت پنجم: تنظیم Scale چاپ
اگر مرورگر محتوا را بزرگ‌تر از 100% چاپ کند، Layout فیزیکی دیگر با محاسبات CSS یکسان نخواهد بود.
بنابراین هنگام بررسی صفحه سفید، باید این موارد به‌صورت جداگانه بررسی شوند.

۱۴. محتوای طولانی؛ دشمن Layout ثابت
در طراحی چاپی یک تفاوت اساسی با صفحات معمولی وب داریم. در صفحه وب معمولی می‌توانیم اجازه دهیم ارتفاع یک عنصر با محتوا افزایش پیدا کند:
height: auto;
اما در فرم چاپی چهارقسمتی، این کار می‌تواند کل Layout را خراب کند. فرض کنید ارتفاع کارت:
143.5mm
است.
اگر Description بسیار طولانی شود، ممکن است متن از کارت خارج شود یا ارتفاع کارت افزایش یابد. برای فرم‌های ثابت معمولاً بهتر است:
.card {
    height: 143.5mm;
    overflow: hidden;
}
اما این راهکار یک ملاحظه مهم دارد:
overflow: hidden نباید برای پنهان کردن بی‌دلیل اطلاعات مهم استفاده شود.
اگر متن می‌تواند طولانی باشد، بهتر است از قبل سیاست مشخصی برای آن داشته باشیم:
  • محدود کردن طول متن
  • کوچک کردن فونت
  • کوتاه‌سازی متن
  • نمایش چند خط مشخص
  • یا تولید چند کارت/صفحه در صورت نیاز
چاپ دقیق بدون کنترل محتوای ورودی، عملاً قابل تضمین نیست.

۱۵. چرا باید برای چاپ ازmmاستفاده کنیم؟
برای طراحی روی صفحه نمایش معمولاً از:
px
استفاده می‌کنیم.
اما وقتی هدف یک خروجی فیزیکی است، واحدهایی مانند:
mm
cm
in
معنای طبیعی‌تری دارند.
مثلاً:
width: 100mm;
height: 143.5mm;
مستقیماً با ابعاد فیزیکی مورد نظر ما ارتباط دارد. این موضوع طراحی را بسیار ساده‌تر می‌کند. به‌جای اینکه ابتدا اندازه را بر اساس Pixel محاسبه کنیم و بعد آن را به ابعاد کاغذ تبدیل کنیم، مستقیماً با واحد مورد نیاز چاپ کار می‌کنیم. با این حال یک نکته بسیار مهم وجود دارد:
استفاده از mm به معنی تضمین چاپ دقیق فیزیکی نیست.
Browser و Printer همچنان می‌توانند با Scaling یا محدودیت‌های فیزیکی چاپگر روی نتیجه اثر بگذارند.

۱۶. تنظیمات Print Dialog
حتی اگر HTML و CSS کاملاً صحیح باشند، تنظیمات چاپ کاربر می‌تواند نتیجه را تغییر دهد. برای خروجی دقیق معمولاً باید موارد زیر بررسی شوند:

تنظیممقدار پیشنهادی
Paper SizeA4
OrientationPortrait
Scale100% / Actual Size
Fit to Pageخاموش
Headers/Footersخاموش، در صورت عدم نیاز
Background Graphicsدر صورت نیاز روشن
Printer Marginsمطابق قابلیت چاپگر
خصوصاً گزینه‌هایی مانند:
Fit to page
Shrink to fit
Scale to fit
می‌توانند ابعاد فیزیکی را تغییر دهند. برای مثال اگر مرورگر تشخیص دهد محتوا کمی بزرگ‌تر از محدوده قابل چاپ است، ممکن است آن را به:
97%
کاهش دهد. در این حالت تمام اندازه‌های فیزیکی نیز تغییر خواهند کرد.

۱۷. یک اشتباه رایج: مخلوط کردن Marginهای مختلف
در طراحی چاپی بهتر است بدانیم هر فاصله متعلق به کدام لایه است. مثلاً:
@page {
    margin: 0;
}
با:
.print-page {
    padding: 5mm;
}
و:
.card {
    margin: 5mm;
}
سه مفهوم کاملاً متفاوت هستند. یک مدل ذهنی مناسب این است:
Printer
   ↓
Paper
   ↓
@page
   ↓
.print-page
   ↓
.page-table
   ↓
.card-cell
   ↓
.card
   ↓
content
هرچه از بیرون به داخل حرکت کنیم، کنترل Layout بیشتر در اختیار HTML/CSS قرار می‌گیرد.

۱۸. آیا می‌توان تا لبه کاغذ چاپ کرد؟
خیر، صرفاً با CSS نمی‌توان این موضوع را تضمین کرد. مثلاً:
@page {
    margin: 0;
}
به این معنی نیست که چاپگر حتماً می‌تواند در فاصله صفر میلی‌متری از لبه کاغذ چاپ کند. این موضوع به قابلیت سخت‌افزاری Printer بستگی دارد. برخی چاپگرها قابلیت:
Borderless Printing
دارند و برخی ندارند.
بنابراین برای فرم‌های عمومی که قرار است روی چاپگرهای مختلف چاپ شوند، بهتر است یک Safe Margin منطقی در نظر گرفته شود.
برای مثال:
5mm
یا در شرایط حساس‌تر:
10mm

۱۹. سازگاری با مرورگرهای قدیمی
اگر خروجی باید در مرورگرهای قدیمی نیز قابل چاپ باشد، بهتر است CSS چاپ تا حد امکان محافظه‌کارانه نوشته شود. ویژگی‌هایی مانند:
table
width
height
padding
margin
border
overflow
page-break-after
سال‌هاست توسط مرورگرها پشتیبانی شده‌اند. در مقابل، اگر هدف سازگاری با موتورهای قدیمی است، استفاده از ویژگی‌هایی مانند:
grid
flex
break-inside
برای Layout اصلی چاپ، ضرورت چندانی ندارد. نکته مهم این است که:
«مدرن‌تر بودن CSS» الزاماً به معنی «قابل اعتمادتر بودن در چاپ» نیست.
در خروجی‌های اداری و فرم‌های ثابت، سادگی گاهی یک مزیت معماری محسوب می‌شود.

۲۰. معماری پیشنهادی برای یک پروژه ASP.NET Core MVC
یک ساختار ساده می‌تواند چنین باشد:
Controller
    │
    ▼
List<DocumentItem>
    │
    ▼
Group by 4
    │
    ▼
List<PrintPageViewModel>
    │
    ▼
Razor View
    │
    ▼
A4 Print Page
    │
    ├── Card 1
    ├── Card 2
    ├── Card 3
    └── Card 4
این تفکیک اهمیت زیادی دارد.
Controller مسئول این است که مشخص کند:
چه اطلاعاتی در هر صفحه قرار می‌گیرد؟
View مسئول این است که مشخص کند:
این اطلاعات چگونه روی کاغذ قرار می‌گیرند؟
و CSS Print مسئول این است که مشخص کند:
این Layout چگونه برای رسانه چاپی اندازه‌گذاری شود؟
این جداسازی باعث می‌شود منطق چاپ با منطق کسب‌وکار مخلوط نشود.

۲۱. چند نکته عملی برای تست نهایی
قبل از تحویل چنین خروجی‌ای به کاربر، بهتر است یک تست فیزیکی انجام شود.
مرحله اول
یک فایل با تعداد کم، مثلاً:
5 آیتم
تولید کنید. باید نتیجه:
Page 1 → 4 items
Page 2 → 1 item
باشد.

مرحله دوم
یک صفحه کامل را با خط‌کش اندازه بگیرید. بررسی کنید:
عرض کاغذ ≈ 210mm
ارتفاع کاغذ ≈ 297mm
و سپس:
حاشیه چپ ≈ 5mm
حاشیه راست ≈ 5mm

مرحله سوم
اندازه کارت را بررسی کنید:
100mm × 143.5mm

مرحله چهارم
Scale را بررسی کنید. باید:
100%
باشد.

مرحله پنجم
یک بار با Printer واقعی آزمایش کنید. زیرا PDF یا Preview مرورگر همیشه تمام رفتار Printer واقعی را شبیه‌سازی نمی‌کند.

۲۲. یک اصل مهم: Preview معیار نهایی نیست
ممکن است خروجی در مرورگر کاملاً صحیح به نظر برسد:
┌───────────────┐
│      A4       │
│               │
│  ┌─────┬────┐ │
│  │  1  │ 2  │ │
│  ├─────┼────┤ │
│  │  3  │ 4  │ │
│  └─────┴────┘ │
│               │
└───────────────┘
اما روی Printer واقعی، نتیجه چند میلی‌متر جابه‌جا شود.
علت می‌تواند یکی از این موارد باشد:
  • Printable Area
  • Printer Driver
  • Browser Scaling
  • Paper Size
  • Printer Margin
  • Borderless Printing
  • تنظیمات Page Setup
بنابراین در پروژه‌هایی که دقت فیزیکی اهمیت دارد، تست واقعی چاپ بخشی از فرآیند توسعه است، نه صرفاً مرحله نهایی.

۲۳. جمع‌بندی
چاپ دقیق یک سند HTML روی کاغذ A4، صرفاً مسئله طراحی یک صفحه وب نیست. برای دستیابی به خروجی قابل پیش‌بینی باید چند مفهوم را هم‌زمان در نظر گرفت:
  • صفحه A4 یک واحد فیزیکی است و بهتر است ابعاد آن با mm مدل شود.
  • @page مربوط به صفحه چاپ است و با حاشیه داخلی عناصر HTML تفاوت دارد.
  • برای کنترل بهتر Layout می‌توان @page را بدون حاشیه تعریف و Safe Margin را داخل .print-page ایجاد کرد.
  • استفاده از box-sizing: border-box برای جلوگیری از افزایش ناخواسته ابعاد بسیار مهم است.
  • direction: rtl جهت متن را کنترل می‌کند، نه مرکزچین شدن Layout.
  • برای Layoutهای ثابت چاپی، table می‌تواند حتی از تکنیک‌های مدرن‌تر نیز قابل پیش‌بینی‌تر باشد.
  • page-break-after باید با دقت مدیریت شود، خصوصاً در آخرین صفحه؛ تا بی‌جهت یک صفحه‌ی خالی ایجاد نشود.
  • صفحه سفید اضافی معمولاً نتیجه ترکیبی از page-break، Overflow، ابعاد واقعی عناصر و Scaling است.
  • CSS نمی‌تواند محدودیت فیزیکی چاپگر را حذف کند.
  • تنظیمات Print Dialog مانند A4، Portrait و Scale = 100% بخشی از فرآیند چاپ دقیق هستند.
  • اگر سازگاری با مرورگرهای قدیمی اهمیت دارد، استفاده از CSS ساده و تثبیت‌شده برای Layout چاپی منطقی‌تر است.
  • در نهایت، تست روی Printer واقعی تنها راه اطمینان از تطابق خروجی دیجیتال با سند فیزیکی است.
در یک معماری مناسب برای ASP.NET Core MVC، Controller صفحات چاپ را به‌صورت منطقی به گروه‌های چهار‌تایی تقسیم می‌کند، Razor View ساختار هر صفحه A4 را تولید می‌کند و CSS مخصوص چاپ ابعاد و موقعیت عناصر را کنترل می‌کند. این تفکیک، ضمن ساده نگه داشتن کد، امکان کنترل دقیق‌تر خروجی و عیب‌یابی مشکلات چاپ را فراهم می‌کند.

نتیجه نهایی
نکته اصلی این مقاله این است که چاپ وب را نباید صرفاً نسخه‌ای از نمایش HTML روی صفحه نمایش دانست. وقتی هدف، تولید یک سند فیزیکی با ابعاد مشخص است، باید از ابتدا «کاغذ» را به‌عنوان بخشی از مدل طراحی در نظر گرفت: ابعاد A4، Safe Margin، Printable Area، اندازه عناصر، Page Break و Scaling همگی بخشی از همان مسئله هستند.
این نگاه، به‌خصوص در برنامه‌های ASP.NET Core MVC که فرم‌ها، گزارش‌ها و اسناد قابل چاپ تولید می‌کنند، باعث می‌شود خروجی نهایی بسیار قابل پیش‌بینی‌تر و حرفه‌ای‌تر باشد.