forked from agzam/vyatsu-schedule
init
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package agzam.vyatsu.data.remote
|
||||
|
||||
import agzam.vyatsu.domain.model.DateRange
|
||||
import agzam.vyatsu.domain.model.Group
|
||||
import agzam.vyatsu.domain.model.Season
|
||||
import android.util.Log
|
||||
import org.jsoup.Jsoup
|
||||
|
||||
/**
|
||||
* Основано на https://github.com/alirzaev/vyatsu-schedule-backend/blob/master/utils/extractors.py
|
||||
* @author Agzam
|
||||
*/
|
||||
object GroupsParser {
|
||||
|
||||
private val groupPattern = Regex("""/reports/schedule/Group/(\d{4,})_([12])_(\d{8})_(\d{8})\.pdf""")
|
||||
|
||||
private val shorthands = mapOf(
|
||||
"Институт биологии и биотехнологии (факультет)(ОРУ)" to "ИББТ",
|
||||
"Институт химии и экологии (факультет) (ОРУ)" to "ИнХимЭк",
|
||||
"Факультет автоматики и вычислительной техники (ОРУ)" to "ФАВТ",
|
||||
"Факультет истории, политических наук и культурологии (ОРУ)" to "ФИПНК",
|
||||
"Факультет компьютерных и физико-математических наук (ОРУ)" to "ФКФМН",
|
||||
"Факультет лингвистики (ОРУ)" to "ФЛ",
|
||||
"Факультет менеджмента и сервиса (ОРУ)" to "ФМиС",
|
||||
"Факультет педагогики и психологии (ОРУ)" to "ФПП",
|
||||
"Факультет строительства и архитектуры (ОРУ)" to "ФСА",
|
||||
"Факультет технологий, инжиниринга и дизайна (ОРУ)" to "ФТИД",
|
||||
"Факультет физической культуры и спорта (ОРУ)" to "ФФКС",
|
||||
"Факультет филологии и медиакоммуникаций (ОРУ)" to "ФФМ",
|
||||
"Факультет экономики и финансов (ОРУ)" to "ФЭиФ",
|
||||
"Электротехнический факультет (ОРУ)" to "ЭТФ",
|
||||
"Юридический институт (факультет) (ОРУ)" to "ЮИ"
|
||||
)
|
||||
|
||||
/**
|
||||
* Извлекает список групп с HTML страницы
|
||||
*/
|
||||
fun parseGroups(html: String): List<Group> {
|
||||
val doc = Jsoup.parse(html)
|
||||
val container = doc.selectFirst("div.column-center_rasp") ?: return emptyList()
|
||||
val tables = container.select("table[style~=border: none !important]")
|
||||
|
||||
return tables.flatMap { table ->
|
||||
table.select("> tbody > tr, > tr").flatMap { row ->
|
||||
Log.v("ROW", row.html());
|
||||
val faculty = shortname(row.selectFirst("div.fak_name")?.text().orEmpty()) //
|
||||
|
||||
row.select("div.grpPeriod").map { grp ->
|
||||
Group(
|
||||
id = grp.attr("data-grp_period_id").dropLast(1),
|
||||
name = removeParenthesized(grp.text()),
|
||||
faculty = faculty
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseDateRanges(html: String): Map<String, Map<Season, List<DateRange>>> {
|
||||
val result = HashMap<String, HashMap<Season, MutableList<DateRange>>>()
|
||||
groupPattern.findAll(html).forEach { m ->
|
||||
val groupId = m.groupValues[1]
|
||||
val season = if (m.groupValues[2] == "1") Season.AUTUMN else Season.SPRING
|
||||
val range = DateRange(m.groupValues[3], m.groupValues[4])
|
||||
result
|
||||
.getOrPut(groupId) { hashMapOf(Season.AUTUMN to mutableListOf(), Season.SPRING to mutableListOf()) }
|
||||
.getOrPut(season) { mutableListOf() }
|
||||
.add(range)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun shortname(name: String): String {
|
||||
val stripped = removeParenthesized(name)
|
||||
val sh = shorthands[name]
|
||||
return if (sh != null) "$sh ($stripped)" else stripped
|
||||
}
|
||||
|
||||
private fun removeParenthesized(name: String): String = name.replace(Regex("""\(.*?\)\s?"""), "").trim()
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package agzam.vyatsu.data.remote
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* @author Agzam
|
||||
*/
|
||||
object VyatsuHttpClient {
|
||||
|
||||
private const val TAG = "HTTP"
|
||||
|
||||
private val client: OkHttpClient = OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
suspend fun getText(url: String): String = withContext(Dispatchers.IO) {
|
||||
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
|
||||
Log.d(TAG, "GET $url -> ${resp.code}")
|
||||
check(resp.isSuccessful) { "HTTP ${resp.code} for $url" }
|
||||
resp.body!!.string()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getBytes(url: String): ByteArray = withContext(Dispatchers.IO) {
|
||||
client.newCall(Request.Builder().url(url).build()).execute().use { resp ->
|
||||
Log.d(TAG, "GET $url -> ${resp.code}")
|
||||
check(resp.isSuccessful) { "HTTP ${resp.code} for $url" }
|
||||
resp.body!!.bytes()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package agzam.vyatsu.data.remote
|
||||
|
||||
import agzam.vyatsu.domain.model.DateRange
|
||||
import agzam.vyatsu.domain.model.Season
|
||||
|
||||
object VyatsuUrls {
|
||||
|
||||
const val GROUPS_PAGE = "https://www.vyatsu.ru/studentu-1/spravochnaya-informatsiya/raspisanie-zanyatiy-dlya-studentov.html"
|
||||
const val PDF_BASEURL = "https://www.vyatsu.ru/reports/schedule/Group"
|
||||
|
||||
fun groupPdfUrl(groupId: String, season: Season, range: DateRange): String = "$PDF_BASEURL/${groupId}_${season.apiKey}_${range.first}_${range.second}.pdf"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package agzam.vyatsu.data.remote.pdf
|
||||
|
||||
import android.graphics.Path
|
||||
import android.graphics.PointF
|
||||
import com.tom_roush.pdfbox.contentstream.PDFGraphicsStreamEngine
|
||||
import com.tom_roush.pdfbox.cos.COSName
|
||||
import com.tom_roush.pdfbox.pdmodel.PDPage
|
||||
import com.tom_roush.pdfbox.pdmodel.graphics.color.PDColor
|
||||
import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImage
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* - Парсит страницу PDF в набор линий
|
||||
* - Позволяет получить таблицу
|
||||
* - Позволяет конвертировать в SVG (для отладки)
|
||||
*
|
||||
* Требуется вызов [processPage] для парсинга
|
||||
* @author Agzam
|
||||
*/
|
||||
class Pdf2Table(page: PDPage) : PDFGraphicsStreamEngine(page) {
|
||||
|
||||
data class PdfLine(val x1: Float, val y1: Float, val x2: Float, val y2: Float) {
|
||||
val left: Float get() = min(x1, x2)
|
||||
val right: Float get() = max(x1, x2)
|
||||
val top: Float get() = min(y1, y2)
|
||||
val bottom: Float get() = max(y1, y2)
|
||||
}
|
||||
data class TableColumn(val x: Float, val width: Float, val cells: List<TableCell>) {
|
||||
val right: Float get() = x + width
|
||||
}
|
||||
data class TableCell(val x: Float, val y: Float, val width: Float, val height: Float) {
|
||||
val centerY: Float get() = y + height/2f
|
||||
}
|
||||
|
||||
private val _lines = mutableListOf<PdfLine>()
|
||||
|
||||
private val svgBuilder = StringBuilder()
|
||||
private val currentPath = StringBuilder()
|
||||
private val tableSvg = StringBuilder()
|
||||
|
||||
|
||||
private val currentSubPathLines = mutableListOf<PdfLine>()
|
||||
private var lastPoint = PointF(0f, 0f)
|
||||
|
||||
/**
|
||||
* Представляет страницу в SVG формате
|
||||
*/
|
||||
fun asSvg(): String {
|
||||
val mediaBox = page.mediaBox
|
||||
val width = mediaBox.width
|
||||
val height = mediaBox.height
|
||||
return """
|
||||
<svg width="$width" height="$height" viewBox="0 0 $width $height" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>${svgBuilder}</g>
|
||||
<g>${tableSvg}</g>
|
||||
</svg>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
override fun appendRectangle(rp0: PointF?, rp1: PointF?, rp2: PointF?, rp3: PointF?) {
|
||||
if (rp0 == null || rp1 == null || rp2 == null || rp3 == null) return
|
||||
val p0 = t(rp0)
|
||||
val p1 = t(rp1)
|
||||
val p2 = t(rp2)
|
||||
val p3 = t(rp3)
|
||||
currentPath.append(String.format(Locale.US, "M %.2f %.2f L %.2f %.2f L %.2f %.2f L %.2f %.2f Z ", p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y))
|
||||
currentSubPathLines.add(PdfLine(p0.x, p0.y, p1.x, p1.y))
|
||||
currentSubPathLines.add(PdfLine(p1.x, p1.y, p2.x, p2.y))
|
||||
currentSubPathLines.add(PdfLine(p2.x, p2.y, p3.x, p3.y))
|
||||
currentSubPathLines.add(PdfLine(p3.x, p3.y, p0.x, p0.y))
|
||||
lastPoint.set(p3.x, p3.y)
|
||||
}
|
||||
|
||||
override fun moveTo(rx: Float, ry: Float) {
|
||||
val x = tx(rx)
|
||||
val y = ty(ry)
|
||||
currentPath.append(String.format(Locale.US, "M %.2f %.2f ", x, y))
|
||||
lastPoint.set(x, y)
|
||||
}
|
||||
|
||||
override fun lineTo(rx: Float, ry: Float) {
|
||||
val x = tx(rx)
|
||||
val y = ty(ry)
|
||||
currentPath.append(String.format(Locale.US, "L %.2f %.2f ", x, y))
|
||||
currentSubPathLines.add(PdfLine(lastPoint.x, lastPoint.y, x, y))
|
||||
lastPoint.set(x, y)
|
||||
}
|
||||
|
||||
override fun curveTo(rcx1: Float, rcy1: Float, rcx2: Float, rcy2: Float, ex: Float, ey: Float) {
|
||||
val cx1 = tx(rcx1)
|
||||
val cy1 = ty(rcy1)
|
||||
val cx2 = tx(rcx2)
|
||||
val cy2 = ty(rcy2)
|
||||
val endX = tx(ex)
|
||||
val endY = ty(ey)
|
||||
currentPath.append(String.format(Locale.US, "C %.2f %.2f, %.2f %.2f, %.2f %.2f ", cx1, cy1, cx2, cy2, endX, endY))
|
||||
lastPoint.set(endX, endY)
|
||||
}
|
||||
|
||||
override fun getCurrentPoint(): PointF {
|
||||
return lastPoint
|
||||
}
|
||||
|
||||
override fun closePath() {
|
||||
currentPath.append("Z ")
|
||||
}
|
||||
|
||||
override fun endPath() {
|
||||
currentPath.clear()
|
||||
currentSubPathLines.clear()
|
||||
}
|
||||
|
||||
override fun strokePath() {
|
||||
val strokeColor = graphicsState.strokingColor.toRGBHex()
|
||||
val lineWidth = graphicsState.lineWidth
|
||||
if (currentPath.isNotEmpty()) {
|
||||
svgBuilder.append("<path d=\"$currentPath\" stroke=\"$strokeColor\" stroke-width=\"$lineWidth\" fill=\"none\" />\n")
|
||||
_lines.addAll(currentSubPathLines)
|
||||
}
|
||||
currentPath.clear()
|
||||
currentSubPathLines.clear()
|
||||
}
|
||||
|
||||
override fun fillPath(p0: Path.FillType?) {
|
||||
val fillColor = graphicsState.nonStrokingColor.toRGBHex()
|
||||
val rule = if (p0 == Path.FillType.EVEN_ODD) "evenodd" else "nonzero"
|
||||
if (currentPath.isNotEmpty()) {
|
||||
svgBuilder.append("<path d=\"$currentPath\" fill=\"$fillColor\" fill-rule=\"$rule\" />\n")
|
||||
_lines.addAll(currentSubPathLines)
|
||||
}
|
||||
currentPath.clear()
|
||||
currentSubPathLines.clear()
|
||||
}
|
||||
|
||||
override fun fillAndStrokePath(p0: Path.FillType?) {
|
||||
val strokeColor = graphicsState.strokingColor.toRGBHex()
|
||||
val fillColor = graphicsState.nonStrokingColor.toRGBHex()
|
||||
val lineWidth = graphicsState.lineWidth
|
||||
val rule = if (p0 == Path.FillType.EVEN_ODD) "evenodd" else "nonzero"
|
||||
if (currentPath.isNotEmpty()) {
|
||||
svgBuilder.append("<path d=\"$currentPath\" fill=\"$fillColor\" fill-rule=\"$rule\" stroke=\"$strokeColor\" stroke-width=\"$lineWidth\" />\n")
|
||||
_lines.addAll(currentSubPathLines)
|
||||
}
|
||||
currentPath.clear()
|
||||
currentSubPathLines.clear()
|
||||
}
|
||||
|
||||
override fun shadingFill(p0: COSName?) {
|
||||
currentPath.clear()
|
||||
currentSubPathLines.clear()
|
||||
}
|
||||
|
||||
private fun PDColor.toRGBHex(): String {
|
||||
return try {
|
||||
val rgb = this.toRGB()
|
||||
String.format(Locale.US, "#%06X", rgb and 0xFFFFFF)
|
||||
} catch (_: Exception) {
|
||||
"#000000"
|
||||
}
|
||||
}
|
||||
|
||||
override fun drawImage(p0: PDImage?) {}
|
||||
override fun clip(p0: Path.FillType?) {}
|
||||
|
||||
private fun tx(x: Float): Float {
|
||||
return x
|
||||
}
|
||||
private fun ty(y: Float): Float {
|
||||
return page.mediaBox.height - y
|
||||
}
|
||||
private fun t(p: PointF): PointF {
|
||||
return PointF(tx(p.x), ty(p.y))
|
||||
}
|
||||
|
||||
/**
|
||||
* Представляет страницу как набор колонок с ячейками
|
||||
*
|
||||
* Сначала делит страницу на столбики, затем, изолированно для каждого столбика делит его на ячейки
|
||||
*/
|
||||
fun asTable(): List<TableColumn> {
|
||||
val verticalLines = _lines.filter { it.x1 == it.x2 }
|
||||
val horizontalLines = _lines.filter { it.y1 == it.y2 }
|
||||
|
||||
// Список уникальных "x", чтобы разделить весь лист на колонки
|
||||
val xs = merge(verticalLines.map { it.x1 })
|
||||
|
||||
for (v in horizontalLines) {
|
||||
tableSvg.append("<rect fill='#0000ff55' x='${v.left}' y='${v.top}' width='${v.right-v.left}' height='3'/>\n")
|
||||
}
|
||||
|
||||
val columns = mutableListOf<TableColumn>()
|
||||
for (i in 0 until xs.size - 1) {
|
||||
val left = xs[i]
|
||||
val right = xs[i + 1]
|
||||
|
||||
tableSvg.append("<rect fill='${if (i % 2 == 0) "#f00" else "#0f0"}' x='$left' y='3' width='${right-left}' height='3'/>\n")
|
||||
|
||||
val colWidth = right - left
|
||||
if (colWidth <= 2f) continue
|
||||
|
||||
// Выборка горизонтальных линиий которые пересекают колонку
|
||||
val linesInColumn = horizontalLines.filter { line -> (line.left-2f <= left && right <= line.right+2f) }
|
||||
|
||||
val ys = merge(linesInColumn.map { it.y1 }) // Уникальные "y", чтобы разделить уже столбик
|
||||
|
||||
val cells = mutableListOf<TableCell>()
|
||||
for (ii in 0 until ys.size - 1) {
|
||||
val top = ys[ii]
|
||||
val bottom = ys[ii+1]
|
||||
cells.add(TableCell(left, top, right-left, bottom-top))
|
||||
|
||||
tableSvg.append("<rect fill='${if (i % 2 == 0) "#f" else "#0"}${if (ii % 2 == 0) "f0" else "00"}' x='$left' y='$top' width='${right-left}' height='${bottom-top}'/>\n")
|
||||
}
|
||||
columns.add(TableColumn(left, right-left, cells))
|
||||
}
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
/**
|
||||
* Убирает дубликаты, схлопывает близкие точки
|
||||
*/
|
||||
private fun merge(fs: List<Float>): List<Float> {
|
||||
val s = fs.distinct().sorted()
|
||||
val result = mutableListOf<Float>()
|
||||
if(s.isEmpty()) return result
|
||||
result.add(s[0])
|
||||
var pad = 0f
|
||||
for (i in 0 until s.size - 1) {
|
||||
val dst = s[i + 1] - s[i]
|
||||
if (dst <= 4f) {
|
||||
pad = dst/2f
|
||||
result[result.size-1] = result.last() + pad
|
||||
continue
|
||||
}
|
||||
result.add(s[i+1]+pad)
|
||||
pad = 0f
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package agzam.vyatsu.data.remote.pdf
|
||||
|
||||
import agzam.vyatsu.domain.model.Day
|
||||
import agzam.vyatsu.domain.model.Lesson
|
||||
import agzam.vyatsu.domain.model.TimeSlot
|
||||
import android.util.Log
|
||||
import com.tom_roush.pdfbox.pdmodel.PDDocument
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Парсинг расписания из PDF
|
||||
* @author Agzam
|
||||
*/
|
||||
object PdfScheduleParser {
|
||||
|
||||
private const val TAG = "PDF-SCHEDULE"
|
||||
private const val SVG_TAG = "PDF-SVG"
|
||||
|
||||
private val TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm")
|
||||
private val DATE_FORMAT = DateTimeFormatter.ofPattern("dd.MM.yy")
|
||||
|
||||
private fun timeFrom(timeString: String?): LocalTime? { return runCatching { LocalTime.parse(timeString, TIME_FORMAT) }.getOrNull() }
|
||||
private fun dateFrom(dateString: String?): LocalDate? { return runCatching { LocalDate.parse(dateString, DATE_FORMAT) }.getOrNull() }
|
||||
|
||||
fun parse(pdfBytes: ByteArray): List<Day> {
|
||||
Log.d(TAG, "Parsing ${pdfBytes.size} bytes")
|
||||
val doc = try {
|
||||
PDDocument.load(pdfBytes)
|
||||
} catch (t: Throwable) {
|
||||
Log.e(TAG, "PDDocument.load failed", t)
|
||||
throw t
|
||||
}
|
||||
|
||||
val days = mutableListOf<Day>()
|
||||
try {
|
||||
for (pageIndex in 0 until doc.numberOfPages) {
|
||||
Log.v(TAG, "============ Page $pageIndex ============")
|
||||
|
||||
// Парсинг ячеек таблицы
|
||||
val page = doc.getPage(pageIndex)
|
||||
val svg = Pdf2Table(page)
|
||||
svg.processPage(page)
|
||||
val cols = svg.asTable()
|
||||
|
||||
/** Вывод таблички из PDF в SVG формате + отладочные жлементы из [Pdf2Table.asTable] **/
|
||||
Log.v(SVG_TAG, "====")
|
||||
for(line in svg.asSvg().split("\n")) {
|
||||
Log.v(SVG_TAG, line)
|
||||
}
|
||||
|
||||
if(cols.size != 3) throw RuntimeException("Found columns: ${cols.size}/3")
|
||||
|
||||
// Парсинг текста
|
||||
val textStripper = PdfTextParser()
|
||||
textStripper.startPage = pageIndex + 1
|
||||
textStripper.endPage = pageIndex + 1
|
||||
textStripper.getText(doc)
|
||||
val texts = textStripper.blocks
|
||||
|
||||
val daysColumn = cols[0] // Колонка "День", имеет формат "<день> dd.mm.yy"
|
||||
val intervalsColumn = cols[1] // Колонка "Интервал", имеет формат "HH:mm-HH:mm"
|
||||
val lessonsColumn = cols[2] // Колонка с инофрмацией о паре
|
||||
|
||||
Log.v(TAG, "Days: ${cols[0].cells.size}")
|
||||
Log.v(TAG, "Times: ${cols[1].cells.size}")
|
||||
Log.v(TAG, "Types: ${cols[2].cells.size}")
|
||||
|
||||
/** Мерджит ячейки в одну строку, пробел в качестве слиятиля */
|
||||
fun join(texts: List<PdfTextParser.TextBlock>): String {
|
||||
val text = StringBuilder()
|
||||
for (t in texts) {
|
||||
if(!text.isEmpty()) text.append(' ')
|
||||
text.append(t.text)
|
||||
}
|
||||
return text.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает контент из PDF внутри ячейки
|
||||
* (Подразумевается что в ячейке нет "\n", поэтому используется пробел для слияния)
|
||||
*/
|
||||
fun content(cell: Pdf2Table.TableCell): String {
|
||||
return join(texts.filter {
|
||||
cell.x <= it.x && it.x <= cell.x + cell.width && cell.y <= it.y && it.y < cell.y + cell.height
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Выбирает все ячейки в целевой колонке, которые находятся на одном уровне (строке) с заданной ячейкой
|
||||
* @param column - целевая колонка, в которой производится поиск ячеек.
|
||||
* @param cell - ячейка-ориентир, определяющая границы строки по вертикали.
|
||||
* @return Список ячеек из [column], попавших в диапазон строки
|
||||
*/
|
||||
fun select(column: Pdf2Table.TableColumn, cell: Pdf2Table.TableCell): List<Pdf2Table.TableCell> {
|
||||
return column.cells.filter {
|
||||
cell.y <= it.centerY && it.centerY < cell.y + cell.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (dayCell in daysColumn.cells) {
|
||||
val day = content(dayCell).split(' ')
|
||||
if(day.size != 2) continue // XXX: добавить защиту от заголовка и выбрасывать исключения вместо пропуска?
|
||||
|
||||
val date = dateFrom(day[1]) ?: continue
|
||||
val intervals = mutableListOf<TimeSlot>()
|
||||
// TODO: день недели (а нужен ли?)
|
||||
|
||||
Log.v(TAG, "############################## $day ############################## ")
|
||||
for(intervalCell in select(intervalsColumn, dayCell)) { // День содержит интервалы
|
||||
val timeContent = content(intervalCell)
|
||||
val range = timeContent.split('-')
|
||||
if(range.size != 2) continue // XXX: выбрасывать ошибку?
|
||||
|
||||
val start = timeFrom(range[0]) ?: continue
|
||||
val end = timeFrom(range[1]) ?: continue
|
||||
val lessons = mutableListOf<Lesson>()
|
||||
|
||||
Log.v(TAG, "=== $timeContent ===")
|
||||
for(lessonCell in select(lessonsColumn, intervalCell)) { // Информация о паре, может быть разбита, например на подгруппы
|
||||
val content = content(lessonCell)
|
||||
Log.v(TAG, "- $content")
|
||||
// TODO: парсинг контента лекции
|
||||
lessons.add(Lesson(raw=content))
|
||||
}
|
||||
intervals.add(TimeSlot(start = start, end = end, lessons = lessons))
|
||||
}
|
||||
days.add(Day(date=date, intervals=intervals))
|
||||
}
|
||||
for (text in texts.filter { it.rotation != 0f }) {
|
||||
Log.v(TAG, "${text.x.roundToInt()}\t${text.y.roundToInt()}\t${text.text}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Parsing error", e)
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
doc.close()
|
||||
}
|
||||
Log.v(TAG, "Days extracted: $days")
|
||||
return days
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package agzam.vyatsu.data.remote.pdf
|
||||
|
||||
import com.tom_roush.pdfbox.text.PDFTextStripper
|
||||
import com.tom_roush.pdfbox.text.TextPosition
|
||||
|
||||
/**
|
||||
* Парсер блоков текста из PDF
|
||||
* @author Agzam
|
||||
*/
|
||||
class PdfTextParser : PDFTextStripper() {
|
||||
|
||||
data class TextBlock(
|
||||
val text: String,
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
val width: Float,
|
||||
val height: Float,
|
||||
val rotation: Float
|
||||
)
|
||||
|
||||
val blocks = mutableListOf<TextBlock>()
|
||||
|
||||
init {
|
||||
sortByPosition = true
|
||||
}
|
||||
|
||||
override fun writeString(string: String?, textPositions: MutableList<TextPosition>?) {
|
||||
if (!string.isNullOrBlank() && !textPositions.isNullOrEmpty()) {
|
||||
val first = textPositions.first()
|
||||
val last = textPositions.last()
|
||||
|
||||
val matrix = first.textMatrix
|
||||
val rotation = first.dir
|
||||
|
||||
val totalWidth = (last.xDirAdj + last.widthDirAdj) - first.xDirAdj
|
||||
|
||||
val x = matrix.translateX
|
||||
val y = matrix.translateY - first.heightDir
|
||||
|
||||
val height = first.heightDir
|
||||
|
||||
blocks.add(TextBlock(string.trim(), x, currentPage.mediaBox.height - y - height, totalWidth, height, rotation))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user